1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46 package zip
47
48 import (
49 "archive/zip"
50 "bytes"
51 "errors"
52 "fmt"
53 "io"
54 "io/ioutil"
55 "os"
56 "os/exec"
57 "path"
58 "path/filepath"
59 "strings"
60 "unicode"
61 "unicode/utf8"
62
63 "golang.org/x/mod/module"
64 )
65
66 const (
67
68
69
70 MaxZipFile = 500 << 20
71
72
73
74 MaxGoMod = 16 << 20
75
76
77
78 MaxLICENSE = 16 << 20
79 )
80
81
82
83 type File interface {
84
85
86 Path() string
87
88
89
90 Lstat() (os.FileInfo, error)
91
92
93
94 Open() (io.ReadCloser, error)
95 }
96
97
98
99
100
101
102
103 type CheckedFiles struct {
104
105 Valid []string
106
107
108
109 Omitted []FileError
110
111
112
113 Invalid []FileError
114
115
116
117
118 SizeError error
119 }
120
121
122
123
124
125 func (cf CheckedFiles) Err() error {
126 if cf.SizeError != nil {
127 return cf.SizeError
128 }
129 if len(cf.Invalid) > 0 {
130 return FileErrorList(cf.Invalid)
131 }
132 return nil
133 }
134
135 type FileErrorList []FileError
136
137 func (el FileErrorList) Error() string {
138 buf := &strings.Builder{}
139 sep := ""
140 for _, e := range el {
141 buf.WriteString(sep)
142 buf.WriteString(e.Error())
143 sep = "\n"
144 }
145 return buf.String()
146 }
147
148 type FileError struct {
149 Path string
150 Err error
151 }
152
153 func (e FileError) Error() string {
154 return fmt.Sprintf("%s: %s", e.Path, e.Err)
155 }
156
157 func (e FileError) Unwrap() error {
158 return e.Err
159 }
160
161 var (
162
163 errPathNotClean = errors.New("file path is not clean")
164 errPathNotRelative = errors.New("file path is not relative")
165 errGoModCase = errors.New("go.mod files must have lowercase names")
166 errGoModSize = fmt.Errorf("go.mod file too large (max size is %d bytes)", MaxGoMod)
167 errLICENSESize = fmt.Errorf("LICENSE file too large (max size is %d bytes)", MaxLICENSE)
168
169
170 errVCS = errors.New("directory is a version control repository")
171 errVendored = errors.New("file is in vendor directory")
172 errSubmoduleFile = errors.New("file is in another module")
173 errSubmoduleDir = errors.New("directory is in another module")
174 errHgArchivalTxt = errors.New("file is inserted by 'hg archive' and is always omitted")
175 errSymlink = errors.New("file is a symbolic link")
176 errNotRegular = errors.New("not a regular file")
177 )
178
179
180
181
182
183
184
185
186
187
188
189
190 func CheckFiles(files []File) (CheckedFiles, error) {
191 cf, _, _ := checkFiles(files)
192 return cf, cf.Err()
193 }
194
195
196
197
198
199
200 func checkFiles(files []File) (cf CheckedFiles, validFiles []File, validSizes []int64) {
201 errPaths := make(map[string]struct{})
202 addError := func(path string, omitted bool, err error) {
203 if _, ok := errPaths[path]; ok {
204 return
205 }
206 errPaths[path] = struct{}{}
207 fe := FileError{Path: path, Err: err}
208 if omitted {
209 cf.Omitted = append(cf.Omitted, fe)
210 } else {
211 cf.Invalid = append(cf.Invalid, fe)
212 }
213 }
214
215
216
217
218 haveGoMod := make(map[string]bool)
219 for _, f := range files {
220 p := f.Path()
221 dir, base := path.Split(p)
222 if strings.EqualFold(base, "go.mod") {
223 info, err := f.Lstat()
224 if err != nil {
225 addError(p, false, err)
226 continue
227 }
228 if info.Mode().IsRegular() {
229 haveGoMod[dir] = true
230 }
231 }
232 }
233
234 inSubmodule := func(p string) bool {
235 for {
236 dir, _ := path.Split(p)
237 if dir == "" {
238 return false
239 }
240 if haveGoMod[dir] {
241 return true
242 }
243 p = dir[:len(dir)-1]
244 }
245 }
246
247 collisions := make(collisionChecker)
248 maxSize := int64(MaxZipFile)
249 for _, f := range files {
250 p := f.Path()
251 if p != path.Clean(p) {
252 addError(p, false, errPathNotClean)
253 continue
254 }
255 if path.IsAbs(p) {
256 addError(p, false, errPathNotRelative)
257 continue
258 }
259 if isVendoredPackage(p) {
260
261 addError(p, true, errVendored)
262 continue
263 }
264 if inSubmodule(p) {
265
266 addError(p, true, errSubmoduleFile)
267 continue
268 }
269 if p == ".hg_archival.txt" {
270
271
272 addError(p, true, errHgArchivalTxt)
273 continue
274 }
275 if err := module.CheckFilePath(p); err != nil {
276 addError(p, false, err)
277 continue
278 }
279 if strings.ToLower(p) == "go.mod" && p != "go.mod" {
280 addError(p, false, errGoModCase)
281 continue
282 }
283 info, err := f.Lstat()
284 if err != nil {
285 addError(p, false, err)
286 continue
287 }
288 if err := collisions.check(p, info.IsDir()); err != nil {
289 addError(p, false, err)
290 continue
291 }
292 if info.Mode()&os.ModeType == os.ModeSymlink {
293
294 addError(p, true, errSymlink)
295 continue
296 }
297 if !info.Mode().IsRegular() {
298 addError(p, true, errNotRegular)
299 continue
300 }
301 size := info.Size()
302 if size >= 0 && size <= maxSize {
303 maxSize -= size
304 } else if cf.SizeError == nil {
305 cf.SizeError = fmt.Errorf("module source tree too large (max size is %d bytes)", MaxZipFile)
306 }
307 if p == "go.mod" && size > MaxGoMod {
308 addError(p, false, errGoModSize)
309 continue
310 }
311 if p == "LICENSE" && size > MaxLICENSE {
312 addError(p, false, errLICENSESize)
313 continue
314 }
315
316 cf.Valid = append(cf.Valid, p)
317 validFiles = append(validFiles, f)
318 validSizes = append(validSizes, info.Size())
319 }
320
321 return cf, validFiles, validSizes
322 }
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337 func CheckDir(dir string) (CheckedFiles, error) {
338
339
340 files, omitted, err := listFilesInDir(dir)
341 if err != nil {
342 return CheckedFiles{}, err
343 }
344 cf, cfErr := CheckFiles(files)
345 _ = cfErr
346
347
348
349
350 for i := range cf.Valid {
351 cf.Valid[i] = filepath.Join(dir, cf.Valid[i])
352 }
353 cf.Omitted = append(cf.Omitted, omitted...)
354 for i := range cf.Omitted {
355 cf.Omitted[i].Path = filepath.Join(dir, cf.Omitted[i].Path)
356 }
357 for i := range cf.Invalid {
358 cf.Invalid[i].Path = filepath.Join(dir, cf.Invalid[i].Path)
359 }
360 return cf, cf.Err()
361 }
362
363
364
365
366
367
368
369
370
371
372
373
374 func CheckZip(m module.Version, zipFile string) (CheckedFiles, error) {
375 f, err := os.Open(zipFile)
376 if err != nil {
377 return CheckedFiles{}, err
378 }
379 defer f.Close()
380 _, cf, err := checkZip(m, f)
381 return cf, err
382 }
383
384
385
386 func checkZip(m module.Version, f *os.File) (*zip.Reader, CheckedFiles, error) {
387
388 if vers := module.CanonicalVersion(m.Version); vers != m.Version {
389 return nil, CheckedFiles{}, fmt.Errorf("version %q is not canonical (should be %q)", m.Version, vers)
390 }
391 if err := module.Check(m.Path, m.Version); err != nil {
392 return nil, CheckedFiles{}, err
393 }
394
395
396 info, err := f.Stat()
397 if err != nil {
398 return nil, CheckedFiles{}, err
399 }
400 zipSize := info.Size()
401 if zipSize > MaxZipFile {
402 cf := CheckedFiles{SizeError: fmt.Errorf("module zip file is too large (%d bytes; limit is %d bytes)", zipSize, MaxZipFile)}
403 return nil, cf, cf.Err()
404 }
405
406
407 var cf CheckedFiles
408 addError := func(zf *zip.File, err error) {
409 cf.Invalid = append(cf.Invalid, FileError{Path: zf.Name, Err: err})
410 }
411 z, err := zip.NewReader(f, zipSize)
412 if err != nil {
413 return nil, CheckedFiles{}, err
414 }
415 prefix := fmt.Sprintf("%s@%s/", m.Path, m.Version)
416 collisions := make(collisionChecker)
417 var size int64
418 for _, zf := range z.File {
419 if !strings.HasPrefix(zf.Name, prefix) {
420 addError(zf, fmt.Errorf("path does not have prefix %q", prefix))
421 continue
422 }
423 name := zf.Name[len(prefix):]
424 if name == "" {
425 continue
426 }
427 isDir := strings.HasSuffix(name, "/")
428 if isDir {
429 name = name[:len(name)-1]
430 }
431 if path.Clean(name) != name {
432 addError(zf, errPathNotClean)
433 continue
434 }
435 if err := module.CheckFilePath(name); err != nil {
436 addError(zf, err)
437 continue
438 }
439 if err := collisions.check(name, isDir); err != nil {
440 addError(zf, err)
441 continue
442 }
443 if isDir {
444 continue
445 }
446 if base := path.Base(name); strings.EqualFold(base, "go.mod") {
447 if base != name {
448 addError(zf, fmt.Errorf("go.mod file not in module root directory"))
449 continue
450 }
451 if name != "go.mod" {
452 addError(zf, errGoModCase)
453 continue
454 }
455 }
456 sz := int64(zf.UncompressedSize64)
457 if sz >= 0 && MaxZipFile-size >= sz {
458 size += sz
459 } else if cf.SizeError == nil {
460 cf.SizeError = fmt.Errorf("total uncompressed size of module contents too large (max size is %d bytes)", MaxZipFile)
461 }
462 if name == "go.mod" && sz > MaxGoMod {
463 addError(zf, fmt.Errorf("go.mod file too large (max size is %d bytes)", MaxGoMod))
464 continue
465 }
466 if name == "LICENSE" && sz > MaxLICENSE {
467 addError(zf, fmt.Errorf("LICENSE file too large (max size is %d bytes)", MaxLICENSE))
468 continue
469 }
470 cf.Valid = append(cf.Valid, zf.Name)
471 }
472
473 return z, cf, cf.Err()
474 }
475
476
477
478
479
480
481
482
483
484
485 func Create(w io.Writer, m module.Version, files []File) (err error) {
486 defer func() {
487 if err != nil {
488 err = &zipError{verb: "create zip", err: err}
489 }
490 }()
491
492
493
494 if vers := module.CanonicalVersion(m.Version); vers != m.Version {
495 return fmt.Errorf("version %q is not canonical (should be %q)", m.Version, vers)
496 }
497 if err := module.Check(m.Path, m.Version); err != nil {
498 return err
499 }
500
501
502
503 cf, validFiles, validSizes := checkFiles(files)
504 if err := cf.Err(); err != nil {
505 return err
506 }
507
508
509 zw := zip.NewWriter(w)
510 prefix := fmt.Sprintf("%s@%s/", m.Path, m.Version)
511
512 addFile := func(f File, path string, size int64) error {
513 rc, err := f.Open()
514 if err != nil {
515 return err
516 }
517 defer rc.Close()
518 w, err := zw.Create(prefix + path)
519 if err != nil {
520 return err
521 }
522 lr := &io.LimitedReader{R: rc, N: size + 1}
523 if _, err := io.Copy(w, lr); err != nil {
524 return err
525 }
526 if lr.N <= 0 {
527 return fmt.Errorf("file %q is larger than declared size", path)
528 }
529 return nil
530 }
531
532 for i, f := range validFiles {
533 p := f.Path()
534 size := validSizes[i]
535 if err := addFile(f, p, size); err != nil {
536 return err
537 }
538 }
539
540 return zw.Close()
541 }
542
543
544
545
546
547
548
549
550
551
552
553
554 func CreateFromDir(w io.Writer, m module.Version, dir string) (err error) {
555 defer func() {
556 if zerr, ok := err.(*zipError); ok {
557 zerr.path = dir
558 } else if err != nil {
559 err = &zipError{verb: "create zip from directory", path: dir, err: err}
560 }
561 }()
562
563 files, _, err := listFilesInDir(dir)
564 if err != nil {
565 return err
566 }
567
568 return Create(w, m, files)
569 }
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586 func CreateFromVCS(w io.Writer, m module.Version, repoRoot, revision, subdir string) (err error) {
587 defer func() {
588 if zerr, ok := err.(*zipError); ok {
589 zerr.path = repoRoot
590 } else if err != nil {
591 err = &zipError{verb: "create zip from version control system", path: repoRoot, err: err}
592 }
593 }()
594
595 var filesToCreate []File
596
597 switch {
598 case isGitRepo(repoRoot):
599 files, err := filesInGitRepo(repoRoot, revision, subdir)
600 if err != nil {
601 return err
602 }
603
604 filesToCreate = files
605 default:
606 return &UnrecognizedVCSError{RepoRoot: repoRoot}
607 }
608
609 return Create(w, m, filesToCreate)
610 }
611
612
613
614 type UnrecognizedVCSError struct {
615 RepoRoot string
616 }
617
618 func (e *UnrecognizedVCSError) Error() string {
619 return fmt.Sprintf("could not find a recognized version control system at %q", e.RepoRoot)
620 }
621
622
623 func filesInGitRepo(dir, rev, subdir string) ([]File, error) {
624 stderr := bytes.Buffer{}
625 stdout := bytes.Buffer{}
626
627
628
629
630
631
632
633
634
635
636
637
638
639 cmd := exec.Command("git", "-c", "core.autocrlf=input", "-c", "core.eol=lf", "archive", "--format=zip", rev)
640 if subdir != "" {
641 cmd.Args = append(cmd.Args, subdir)
642 }
643 cmd.Dir = dir
644 cmd.Stdout = &stdout
645 cmd.Stderr = &stderr
646 if err := cmd.Run(); err != nil {
647 return nil, fmt.Errorf("error running `git archive`: %w, %s", err, stderr.String())
648 }
649
650 rawReader := bytes.NewReader(stdout.Bytes())
651 zipReader, err := zip.NewReader(rawReader, int64(stdout.Len()))
652 if err != nil {
653 return nil, err
654 }
655
656 var fs []File
657 for _, zf := range zipReader.File {
658 if !strings.HasPrefix(zf.Name, subdir) || strings.HasSuffix(zf.Name, "/") {
659 continue
660 }
661
662 n := strings.TrimPrefix(zf.Name, subdir)
663 if n == "" {
664 continue
665 }
666 n = strings.TrimPrefix(n, string(filepath.Separator))
667
668 fs = append(fs, zipFile{
669 name: n,
670 f: zf,
671 })
672 }
673
674 return fs, nil
675 }
676
677
678 func isGitRepo(dir string) bool {
679 stdout := &bytes.Buffer{}
680 cmd := exec.Command("git", "rev-parse", "--git-dir")
681 cmd.Dir = dir
682 cmd.Stdout = stdout
683 if err := cmd.Run(); err != nil {
684 return false
685 }
686 gitDir := strings.TrimSpace(string(stdout.Bytes()))
687 if !filepath.IsAbs(gitDir) {
688 gitDir = filepath.Join(dir, gitDir)
689 }
690 wantDir := filepath.Join(dir, ".git")
691 return wantDir == gitDir
692 }
693
694 type dirFile struct {
695 filePath, slashPath string
696 info os.FileInfo
697 }
698
699 func (f dirFile) Path() string { return f.slashPath }
700 func (f dirFile) Lstat() (os.FileInfo, error) { return f.info, nil }
701 func (f dirFile) Open() (io.ReadCloser, error) { return os.Open(f.filePath) }
702
703 type zipFile struct {
704 name string
705 f *zip.File
706 }
707
708 func (f zipFile) Path() string { return f.name }
709 func (f zipFile) Lstat() (os.FileInfo, error) { return f.f.FileInfo(), nil }
710 func (f zipFile) Open() (io.ReadCloser, error) { return f.f.Open() }
711
712
713
714
715
716
717
718 func isVendoredPackage(name string) bool {
719 var i int
720 if strings.HasPrefix(name, "vendor/") {
721 i += len("vendor/")
722 } else if j := strings.Index(name, "/vendor/"); j >= 0 {
723
724
725
726
727
728
729 i += len("/vendor/")
730 } else {
731 return false
732 }
733 return strings.Contains(name[i:], "/")
734 }
735
736
737
738
739
740
741
742
743
744
745 func Unzip(dir string, m module.Version, zipFile string) (err error) {
746 defer func() {
747 if err != nil {
748 err = &zipError{verb: "unzip", path: zipFile, err: err}
749 }
750 }()
751
752
753
754 if files, _ := ioutil.ReadDir(dir); len(files) > 0 {
755 return fmt.Errorf("target directory %v exists and is not empty", dir)
756 }
757
758
759 f, err := os.Open(zipFile)
760 if err != nil {
761 return err
762 }
763 defer f.Close()
764 z, cf, err := checkZip(m, f)
765 if err != nil {
766 return err
767 }
768 if err := cf.Err(); err != nil {
769 return err
770 }
771
772
773 prefix := fmt.Sprintf("%s@%s/", m.Path, m.Version)
774 if err := os.MkdirAll(dir, 0777); err != nil {
775 return err
776 }
777 for _, zf := range z.File {
778 name := zf.Name[len(prefix):]
779 if name == "" || strings.HasSuffix(name, "/") {
780 continue
781 }
782 dst := filepath.Join(dir, name)
783 if err := os.MkdirAll(filepath.Dir(dst), 0777); err != nil {
784 return err
785 }
786 w, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0444)
787 if err != nil {
788 return err
789 }
790 r, err := zf.Open()
791 if err != nil {
792 w.Close()
793 return err
794 }
795 lr := &io.LimitedReader{R: r, N: int64(zf.UncompressedSize64) + 1}
796 _, err = io.Copy(w, lr)
797 r.Close()
798 if err != nil {
799 w.Close()
800 return err
801 }
802 if err := w.Close(); err != nil {
803 return err
804 }
805 if lr.N <= 0 {
806 return fmt.Errorf("uncompressed size of file %s is larger than declared size (%d bytes)", zf.Name, zf.UncompressedSize64)
807 }
808 }
809
810 return nil
811 }
812
813
814
815
816
817
818 type collisionChecker map[string]pathInfo
819
820 type pathInfo struct {
821 path string
822 isDir bool
823 }
824
825 func (cc collisionChecker) check(p string, isDir bool) error {
826 fold := strToFold(p)
827 if other, ok := cc[fold]; ok {
828 if p != other.path {
829 return fmt.Errorf("case-insensitive file name collision: %q and %q", other.path, p)
830 }
831 if isDir != other.isDir {
832 return fmt.Errorf("entry %q is both a file and a directory", p)
833 }
834 if !isDir {
835 return fmt.Errorf("multiple entries for file %q", p)
836 }
837
838
839
840 } else {
841 cc[fold] = pathInfo{path: p, isDir: isDir}
842 }
843
844 if parent := path.Dir(p); parent != "." {
845 return cc.check(parent, true)
846 }
847 return nil
848 }
849
850
851
852
853 func listFilesInDir(dir string) (files []File, omitted []FileError, err error) {
854 err = filepath.Walk(dir, func(filePath string, info os.FileInfo, err error) error {
855 if err != nil {
856 return err
857 }
858 relPath, err := filepath.Rel(dir, filePath)
859 if err != nil {
860 return err
861 }
862 slashPath := filepath.ToSlash(relPath)
863
864
865
866
867
868 if isVendoredPackage(slashPath) {
869 omitted = append(omitted, FileError{Path: slashPath, Err: errVendored})
870 return nil
871 }
872
873 if info.IsDir() {
874 if filePath == dir {
875
876 return nil
877 }
878
879
880
881
882 switch filepath.Base(filePath) {
883 case ".bzr", ".git", ".hg", ".svn":
884 omitted = append(omitted, FileError{Path: slashPath, Err: errVCS})
885 return filepath.SkipDir
886 }
887
888
889 if goModInfo, err := os.Lstat(filepath.Join(filePath, "go.mod")); err == nil && !goModInfo.IsDir() {
890 omitted = append(omitted, FileError{Path: slashPath, Err: errSubmoduleDir})
891 return filepath.SkipDir
892 }
893 return nil
894 }
895
896
897
898 if !info.Mode().IsRegular() {
899 omitted = append(omitted, FileError{Path: slashPath, Err: errNotRegular})
900 return nil
901 }
902
903 files = append(files, dirFile{
904 filePath: filePath,
905 slashPath: slashPath,
906 info: info,
907 })
908 return nil
909 })
910 if err != nil {
911 return nil, nil, err
912 }
913 return files, omitted, nil
914 }
915
916 type zipError struct {
917 verb, path string
918 err error
919 }
920
921 func (e *zipError) Error() string {
922 if e.path == "" {
923 return fmt.Sprintf("%s: %v", e.verb, e.err)
924 } else {
925 return fmt.Sprintf("%s %s: %v", e.verb, e.path, e.err)
926 }
927 }
928
929 func (e *zipError) Unwrap() error {
930 return e.err
931 }
932
933
934
935
936
937
938
939 func strToFold(s string) string {
940
941
942 for i := 0; i < len(s); i++ {
943 c := s[i]
944 if c >= utf8.RuneSelf || 'A' <= c && c <= 'Z' {
945 goto Slow
946 }
947 }
948 return s
949
950 Slow:
951 var buf bytes.Buffer
952 for _, r := range s {
953
954
955
956 for {
957 r0 := r
958 r = unicode.SimpleFold(r0)
959 if r <= r0 {
960 break
961 }
962 }
963
964 if 'A' <= r && r <= 'Z' {
965 r += 'a' - 'A'
966 }
967 buf.WriteRune(r)
968 }
969 return buf.String()
970 }
971
View as plain text