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