1
2
3
4
5 package modfetch
6
7 import (
8 "archive/zip"
9 "bytes"
10 "context"
11 "errors"
12 "fmt"
13 "io"
14 "io/fs"
15 "os"
16 "path"
17 "path/filepath"
18 "sort"
19 "strings"
20 "time"
21
22 "cmd/go/internal/gover"
23 "cmd/go/internal/modfetch/codehost"
24
25 "golang.org/x/mod/modfile"
26 "golang.org/x/mod/module"
27 "golang.org/x/mod/semver"
28 modzip "golang.org/x/mod/zip"
29 )
30
31
32 type codeRepo struct {
33 modPath string
34
35
36 code codehost.Repo
37
38 codeRoot string
39
40
41
42 codeDir string
43
44
45
46
47
48
49 pathMajor string
50
51
52 pathPrefix string
53
54
55
56
57
58 pseudoMajor string
59 }
60
61
62
63
64
65
66 func newCodeRepo(code codehost.Repo, codeRoot, subdir, path string) (Repo, error) {
67 if !hasPathPrefix(path, codeRoot) {
68 return nil, fmt.Errorf("mismatched repo: found %s for %s", codeRoot, path)
69 }
70 pathPrefix, pathMajor, ok := module.SplitPathVersion(path)
71 if !ok {
72 return nil, fmt.Errorf("invalid module path %q", path)
73 }
74 if codeRoot == path {
75 pathPrefix = path
76 }
77 pseudoMajor := module.PathMajorPrefix(pathMajor)
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123 codeDir := ""
124 if codeRoot != path {
125 if !hasPathPrefix(pathPrefix, codeRoot) {
126 return nil, fmt.Errorf("repository rooted at %s cannot contain module %s", codeRoot, path)
127 }
128 codeDir = strings.Trim(pathPrefix[len(codeRoot):], "/")
129 }
130 if subdir != "" {
131 codeDir = filepath.ToSlash(filepath.Join(codeDir, subdir))
132 }
133
134 r := &codeRepo{
135 modPath: path,
136 code: code,
137 codeRoot: codeRoot,
138 codeDir: codeDir,
139 pathPrefix: pathPrefix,
140 pathMajor: pathMajor,
141 pseudoMajor: pseudoMajor,
142 }
143
144 return r, nil
145 }
146
147 func (r *codeRepo) ModulePath() string {
148 return r.modPath
149 }
150
151 func (r *codeRepo) CheckReuse(ctx context.Context, old *codehost.Origin) error {
152 return r.code.CheckReuse(ctx, old, r.codeDir)
153 }
154
155 func (r *codeRepo) Versions(ctx context.Context, prefix string) (*Versions, error) {
156
157
158
159 if strings.HasPrefix(r.modPath, "gopkg.in/") && strings.HasSuffix(r.modPath, "-unstable") {
160 return &Versions{}, nil
161 }
162
163 p := prefix
164 if r.codeDir != "" {
165 p = r.codeDir + "/" + p
166 }
167 tags, err := r.code.Tags(ctx, p)
168 if err != nil {
169 return nil, &module.ModuleError{
170 Path: r.modPath,
171 Err: err,
172 }
173 }
174 if tags.Origin != nil {
175 tags.Origin.Subdir = r.codeDir
176 }
177
178 var list, incompatible []string
179 for _, tag := range tags.List {
180 if !strings.HasPrefix(tag.Name, p) {
181 continue
182 }
183 v := tag.Name
184 if r.codeDir != "" {
185 v = v[len(r.codeDir)+1:]
186 }
187
188
189 if v == "" || v != semver.Canonical(v) {
190
191
192
193
194
195
196 continue
197 }
198 if module.IsPseudoVersion(v) {
199
200
201 continue
202 }
203
204 if err := module.CheckPathMajor(v, r.pathMajor); err != nil {
205 if r.codeDir == "" && r.pathMajor == "" && semver.Major(v) > "v1" {
206 incompatible = append(incompatible, v)
207 }
208 continue
209 }
210
211 list = append(list, v)
212 }
213 semver.Sort(list)
214 semver.Sort(incompatible)
215
216 return r.appendIncompatibleVersions(ctx, tags.Origin, list, incompatible)
217 }
218
219
220
221
222
223
224
225
226 func (r *codeRepo) appendIncompatibleVersions(ctx context.Context, origin *codehost.Origin, list, incompatible []string) (*Versions, error) {
227 versions := &Versions{
228 Origin: origin,
229 List: list,
230 }
231 if len(incompatible) == 0 || r.pathMajor != "" {
232
233 return versions, nil
234 }
235
236 versionHasGoMod := func(v string) (bool, error) {
237 _, err := r.code.ReadFile(ctx, v, "go.mod", codehost.MaxGoMod)
238 if err == nil {
239 return true, nil
240 }
241 if !os.IsNotExist(err) {
242 return false, &module.ModuleError{
243 Path: r.modPath,
244 Err: err,
245 }
246 }
247 return false, nil
248 }
249
250 if len(list) > 0 {
251 ok, err := versionHasGoMod(list[len(list)-1])
252 if err != nil {
253 return nil, err
254 }
255 if ok {
256
257
258
259
260
261
262
263
264
265
266 return versions, nil
267 }
268 }
269
270 var (
271 lastMajor string
272 lastMajorHasGoMod bool
273 )
274 for i, v := range incompatible {
275 major := semver.Major(v)
276
277 if major != lastMajor {
278 rem := incompatible[i:]
279 j := sort.Search(len(rem), func(j int) bool {
280 return semver.Major(rem[j]) != major
281 })
282 latestAtMajor := rem[j-1]
283
284 var err error
285 lastMajor = major
286 lastMajorHasGoMod, err = versionHasGoMod(latestAtMajor)
287 if err != nil {
288 return nil, err
289 }
290 }
291
292 if lastMajorHasGoMod {
293
294
295
296
297
298
299
300
301
302
303 continue
304 }
305 versions.List = append(versions.List, v+"+incompatible")
306 }
307
308 return versions, nil
309 }
310
311 func (r *codeRepo) Stat(ctx context.Context, rev string) (*RevInfo, error) {
312 if rev == "latest" {
313 return r.Latest(ctx)
314 }
315 codeRev := r.revToRev(rev)
316 info, err := r.code.Stat(ctx, codeRev)
317 if err != nil {
318
319 var revInfo *RevInfo
320 if info != nil {
321 revInfo = &RevInfo{
322 Origin: info.Origin,
323 Version: rev,
324 }
325 }
326 return revInfo, &module.ModuleError{
327 Path: r.modPath,
328 Err: &module.InvalidVersionError{
329 Version: rev,
330 Err: err,
331 },
332 }
333 }
334 return r.convert(ctx, info, rev)
335 }
336
337 func (r *codeRepo) Latest(ctx context.Context) (*RevInfo, error) {
338 info, err := r.code.Latest(ctx)
339 if err != nil {
340 if info != nil {
341 return &RevInfo{Origin: info.Origin}, err
342 }
343 return nil, err
344 }
345 return r.convert(ctx, info, "")
346 }
347
348
349
350
351
352
353 func (r *codeRepo) convert(ctx context.Context, info *codehost.RevInfo, statVers string) (revInfo *RevInfo, err error) {
354 defer func() {
355 if info.Origin == nil {
356 return
357 }
358 if revInfo == nil {
359 revInfo = new(RevInfo)
360 } else if revInfo.Origin != nil {
361 panic("internal error: RevInfo Origin unexpectedly already populated")
362 }
363
364 origin := *info.Origin
365 revInfo.Origin = &origin
366 origin.Subdir = r.codeDir
367
368 v := revInfo.Version
369 if module.IsPseudoVersion(v) && (v != statVers || !strings.HasPrefix(v, "v0.0.0-")) {
370
371 prefix := r.codeDir
372 if prefix != "" {
373 prefix += "/"
374 }
375 if r.pathMajor != "" {
376 prefix += r.pathMajor[1:] + "."
377 }
378 tags, tagsErr := r.code.Tags(ctx, prefix)
379 if tagsErr != nil {
380 revInfo.Origin = nil
381 if err == nil {
382 err = tagsErr
383 }
384 } else {
385 origin.TagPrefix = tags.Origin.TagPrefix
386 origin.TagSum = tags.Origin.TagSum
387 }
388 }
389 }()
390
391
392
393
394
395
396
397
398 incompatibleOk := map[string]bool{}
399 canUseIncompatible := func(v string) bool {
400 if r.codeDir != "" || r.pathMajor != "" {
401
402
403
404
405 return false
406 }
407
408 ok, seen := incompatibleOk[""]
409 if !seen {
410 _, errGoMod := r.code.ReadFile(ctx, info.Name, "go.mod", codehost.MaxGoMod)
411 ok = (errGoMod != nil)
412 incompatibleOk[""] = ok
413 }
414 if !ok {
415
416 return false
417 }
418
419
420
421
422
423
424 if v != "" && !strings.HasSuffix(statVers, "+incompatible") {
425 major := semver.Major(v)
426 ok, seen = incompatibleOk[major]
427 if !seen {
428 _, errGoModSub := r.code.ReadFile(ctx, info.Name, path.Join(major, "go.mod"), codehost.MaxGoMod)
429 ok = (errGoModSub != nil)
430 incompatibleOk[major] = ok
431 }
432 if !ok {
433 return false
434 }
435 }
436
437 return true
438 }
439
440
441
442
443
444
445 checkCanonical := func(v string) (*RevInfo, error) {
446
447
448
449
450
451
452
453
454
455
456 _, _, _, err := r.findDir(ctx, v)
457 if err != nil {
458
459
460 return nil, &module.ModuleError{
461 Path: r.modPath,
462 Err: &module.InvalidVersionError{
463 Version: v,
464 Err: notExistError{err: err},
465 },
466 }
467 }
468
469 invalidf := func(format string, args ...any) error {
470 return &module.ModuleError{
471 Path: r.modPath,
472 Err: &module.InvalidVersionError{
473 Version: v,
474 Err: fmt.Errorf(format, args...),
475 },
476 }
477 }
478
479
480
481
482
483 if v == strings.TrimSuffix(statVers, "+incompatible") {
484 v = statVers
485 }
486 base := strings.TrimSuffix(v, "+incompatible")
487 var errIncompatible error
488 if !module.MatchPathMajor(base, r.pathMajor) {
489 if canUseIncompatible(base) {
490 v = base + "+incompatible"
491 } else {
492 if r.pathMajor != "" {
493 errIncompatible = invalidf("module path includes a major version suffix, so major version must match")
494 } else {
495 errIncompatible = invalidf("module contains a go.mod file, so module path must match major version (%q)", path.Join(r.pathPrefix, semver.Major(v)))
496 }
497 }
498 } else if strings.HasSuffix(v, "+incompatible") {
499 errIncompatible = invalidf("+incompatible suffix not allowed: major version %s is compatible", semver.Major(v))
500 }
501
502 if statVers != "" && statVers == module.CanonicalVersion(statVers) {
503
504
505
506 if statBase := strings.TrimSuffix(statVers, "+incompatible"); statBase != base {
507 return nil, &module.ModuleError{
508 Path: r.modPath,
509 Err: &module.InvalidVersionError{
510 Version: statVers,
511 Err: fmt.Errorf("resolves to version %v (%s is not a tag)", v, statBase),
512 },
513 }
514 }
515 }
516
517 if errIncompatible != nil {
518 return nil, errIncompatible
519 }
520
521 return &RevInfo{
522 Name: info.Name,
523 Short: info.Short,
524 Time: info.Time,
525 Version: v,
526 }, nil
527 }
528
529
530
531 if module.IsPseudoVersion(statVers) {
532
533
534
535
536
537
538 revInfo, err = checkCanonical(statVers)
539 if err != nil {
540 return revInfo, err
541 }
542 if err := r.validatePseudoVersion(ctx, info, statVers); err != nil {
543 return nil, err
544 }
545 return revInfo, nil
546 }
547
548
549
550
551
552
553
554 tagPrefix := ""
555 if r.codeDir != "" {
556 tagPrefix = r.codeDir + "/"
557 }
558
559 isRetracted, err := r.retractedVersions(ctx)
560 if err != nil {
561 isRetracted = func(string) bool { return false }
562 }
563
564
565
566
567 tagToVersion := func(tag string) (v string, tagIsCanonical bool) {
568 if !strings.HasPrefix(tag, tagPrefix) {
569 return "", false
570 }
571 trimmed := tag[len(tagPrefix):]
572
573 if module.IsPseudoVersion(tag) {
574 return "", false
575 }
576
577 v = semver.Canonical(trimmed)
578 if v == "" || !strings.HasPrefix(trimmed, v) {
579 return "", false
580 }
581 if v == trimmed {
582 tagIsCanonical = true
583 }
584 return v, tagIsCanonical
585 }
586
587
588 if v, tagIsCanonical := tagToVersion(info.Version); tagIsCanonical {
589 if info, err := checkCanonical(v); err == nil {
590 return info, err
591 }
592 }
593
594
595
596 var (
597 highestCanonical string
598 pseudoBase string
599 )
600 for _, pathTag := range info.Tags {
601 v, tagIsCanonical := tagToVersion(pathTag)
602 if statVers != "" && semver.Compare(v, statVers) == 0 {
603
604 if tagIsCanonical {
605
606
607
608
609 return checkCanonical(v)
610 } else {
611
612
613
614
615
616
617
618
619
620
621 pseudoBase = v
622 }
623 }
624
625
626 if tagIsCanonical && semver.Compare(highestCanonical, v) < 0 && !isRetracted(v) {
627 if module.MatchPathMajor(v, r.pathMajor) || canUseIncompatible(v) {
628 highestCanonical = v
629 }
630 }
631 }
632
633
634
635 if highestCanonical != "" {
636 return checkCanonical(highestCanonical)
637 }
638
639
640
641
642
643 tagAllowed := func(tag string) bool {
644 v, _ := tagToVersion(tag)
645 if v == "" {
646 return false
647 }
648 if !module.MatchPathMajor(v, r.pathMajor) && !canUseIncompatible(v) {
649 return false
650 }
651 return !isRetracted(v)
652 }
653 if pseudoBase == "" {
654 tag, err := r.code.RecentTag(ctx, info.Name, tagPrefix, tagAllowed)
655 if err != nil && !errors.Is(err, errors.ErrUnsupported) {
656 return nil, err
657 }
658 if tag != "" {
659 pseudoBase, _ = tagToVersion(tag)
660 }
661 }
662
663 return checkCanonical(module.PseudoVersion(r.pseudoMajor, pseudoBase, info.Time, info.Short))
664 }
665
666
667
668
669
670
671
672
673
674
675 func (r *codeRepo) validatePseudoVersion(ctx context.Context, info *codehost.RevInfo, version string) (err error) {
676 defer func() {
677 if err != nil {
678 if _, ok := err.(*module.ModuleError); !ok {
679 if _, ok := err.(*module.InvalidVersionError); !ok {
680 err = &module.InvalidVersionError{Version: version, Pseudo: true, Err: err}
681 }
682 err = &module.ModuleError{Path: r.modPath, Err: err}
683 }
684 }
685 }()
686
687 rev, err := module.PseudoVersionRev(version)
688 if err != nil {
689 return err
690 }
691 if rev != info.Short {
692 switch {
693 case strings.HasPrefix(rev, info.Short):
694 return fmt.Errorf("revision is longer than canonical (expected %s)", info.Short)
695 case strings.HasPrefix(info.Short, rev):
696 return fmt.Errorf("revision is shorter than canonical (expected %s)", info.Short)
697 default:
698 return fmt.Errorf("does not match short name of revision (expected %s)", info.Short)
699 }
700 }
701
702 t, err := module.PseudoVersionTime(version)
703 if err != nil {
704 return err
705 }
706 if !t.Equal(info.Time.Truncate(time.Second)) {
707 return fmt.Errorf("does not match version-control timestamp (expected %s)", info.Time.UTC().Format(module.PseudoVersionTimestampFormat))
708 }
709
710 tagPrefix := ""
711 if r.codeDir != "" {
712 tagPrefix = r.codeDir + "/"
713 }
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731 base, err := module.PseudoVersionBase(strings.TrimSuffix(version, "+incompatible"))
732 if err != nil {
733 return err
734 }
735 if base == "" {
736 if r.pseudoMajor == "" && semver.Major(version) == "v1" {
737 return fmt.Errorf("major version without preceding tag must be v0, not v1")
738 }
739 return nil
740 } else {
741 for _, tag := range info.Tags {
742 versionOnly := strings.TrimPrefix(tag, tagPrefix)
743 if versionOnly == base {
744
745
746
747
748
749
750
751
752
753
754
755
756
757 return fmt.Errorf("tag (%s) found on revision %s is already canonical, so should not be replaced with a pseudo-version derived from that tag", tag, rev)
758 }
759 }
760 }
761
762 tags, err := r.code.Tags(ctx, tagPrefix+base)
763 if err != nil {
764 return err
765 }
766
767 var lastTag string
768 ancestorFound := false
769 for _, tag := range tags.List {
770 versionOnly := strings.TrimPrefix(tag.Name, tagPrefix)
771 if semver.Compare(versionOnly, base) == 0 {
772 lastTag = tag.Name
773 ancestorFound, err = r.code.DescendsFrom(ctx, info.Name, tag.Name)
774 if ancestorFound {
775 break
776 }
777 }
778 }
779
780 if lastTag == "" {
781 return fmt.Errorf("preceding tag (%s) not found", base)
782 }
783
784 if !ancestorFound {
785 if err != nil {
786 return err
787 }
788 rev, err := module.PseudoVersionRev(version)
789 if err != nil {
790 return fmt.Errorf("not a descendent of preceding tag (%s)", lastTag)
791 }
792 return fmt.Errorf("revision %s is not a descendent of preceding tag (%s)", rev, lastTag)
793 }
794 return nil
795 }
796
797 func (r *codeRepo) revToRev(rev string) string {
798 if semver.IsValid(rev) {
799 if module.IsPseudoVersion(rev) {
800 r, _ := module.PseudoVersionRev(rev)
801 return r
802 }
803 if semver.Build(rev) == "+incompatible" {
804 rev = rev[:len(rev)-len("+incompatible")]
805 }
806 if r.codeDir == "" {
807 return rev
808 }
809 return r.codeDir + "/" + rev
810 }
811 return rev
812 }
813
814 func (r *codeRepo) versionToRev(version string) (rev string, err error) {
815 if !semver.IsValid(version) {
816 return "", &module.ModuleError{
817 Path: r.modPath,
818 Err: &module.InvalidVersionError{
819 Version: version,
820 Err: errors.New("syntax error"),
821 },
822 }
823 }
824 return r.revToRev(version), nil
825 }
826
827
828
829
830
831 func (r *codeRepo) findDir(ctx context.Context, version string) (rev, dir string, gomod []byte, err error) {
832 rev, err = r.versionToRev(version)
833 if err != nil {
834 return "", "", nil, err
835 }
836
837
838
839 file1 := path.Join(r.codeDir, "go.mod")
840 gomod1, err1 := r.code.ReadFile(ctx, rev, file1, codehost.MaxGoMod)
841 if err1 != nil && !os.IsNotExist(err1) {
842 return "", "", nil, fmt.Errorf("reading %s/%s at revision %s: %v", r.codeRoot, file1, rev, err1)
843 }
844 mpath1 := modfile.ModulePath(gomod1)
845 found1 := err1 == nil && (isMajor(mpath1, r.pathMajor) || r.canReplaceMismatchedVersionDueToBug(mpath1))
846
847 var file2 string
848 if r.pathMajor != "" && r.codeRoot != r.modPath && !strings.HasPrefix(r.pathMajor, ".") {
849
850
851
852
853
854
855
856 dir2 := path.Join(r.codeDir, r.pathMajor[1:])
857 file2 = path.Join(dir2, "go.mod")
858 gomod2, err2 := r.code.ReadFile(ctx, rev, file2, codehost.MaxGoMod)
859 if err2 != nil && !os.IsNotExist(err2) {
860 return "", "", nil, fmt.Errorf("reading %s/%s at revision %s: %v", r.codeRoot, file2, rev, err2)
861 }
862 mpath2 := modfile.ModulePath(gomod2)
863 found2 := err2 == nil && isMajor(mpath2, r.pathMajor)
864
865 if found1 && found2 {
866 return "", "", nil, fmt.Errorf("%s/%s and ...%s/go.mod both have ...%s module paths at revision %s", r.pathPrefix, file1, r.pathMajor, r.pathMajor, rev)
867 }
868 if found2 {
869 return rev, dir2, gomod2, nil
870 }
871 if err2 == nil {
872 if mpath2 == "" {
873 return "", "", nil, fmt.Errorf("%s/%s is missing module path at revision %s", r.codeRoot, file2, rev)
874 }
875 return "", "", nil, fmt.Errorf("%s/%s has non-...%s module path %q at revision %s", r.codeRoot, file2, r.pathMajor, mpath2, rev)
876 }
877 }
878
879
880 if found1 {
881
882 return rev, r.codeDir, gomod1, nil
883 }
884 if err1 == nil {
885
886 suffix := ""
887 if file2 != "" {
888 suffix = fmt.Sprintf(" (and ...%s/go.mod does not exist)", r.pathMajor)
889 }
890 if mpath1 == "" {
891 return "", "", nil, fmt.Errorf("%s is missing module path%s at revision %s", file1, suffix, rev)
892 }
893 if r.pathMajor != "" {
894 return "", "", nil, fmt.Errorf("%s has non-...%s module path %q%s at revision %s", file1, r.pathMajor, mpath1, suffix, rev)
895 }
896 if _, _, ok := module.SplitPathVersion(mpath1); !ok {
897 return "", "", nil, fmt.Errorf("%s has malformed module path %q%s at revision %s", file1, mpath1, suffix, rev)
898 }
899 return "", "", nil, fmt.Errorf("%s has post-%s module path %q%s at revision %s", file1, semver.Major(version), mpath1, suffix, rev)
900 }
901
902 if r.codeDir == "" && (r.pathMajor == "" || strings.HasPrefix(r.pathMajor, ".")) {
903
904 return rev, "", nil, nil
905 }
906
907
908
909 if file2 != "" {
910 return "", "", nil, fmt.Errorf("missing %s/go.mod and ...%s/go.mod at revision %s", r.pathPrefix, r.pathMajor, rev)
911 }
912 return "", "", nil, fmt.Errorf("missing %s/go.mod at revision %s", r.pathPrefix, rev)
913 }
914
915
916
917
918 func isMajor(mpath, pathMajor string) bool {
919 if mpath == "" {
920
921 return false
922 }
923 _, mpathMajor, ok := module.SplitPathVersion(mpath)
924 if !ok {
925
926 return false
927 }
928 if pathMajor == "" {
929
930
931
932 switch module.PathMajorPrefix(mpathMajor) {
933 case "", "v0", "v1":
934 return true
935 default:
936 return false
937 }
938 }
939 if mpathMajor == "" {
940
941
942
943
944
945 return false
946 }
947
948
949
950
951 return pathMajor[1:] == mpathMajor[1:]
952 }
953
954
955
956
957 func (r *codeRepo) canReplaceMismatchedVersionDueToBug(mpath string) bool {
958
959
960 unversioned := r.pathMajor == ""
961 replacingGopkgIn := strings.HasPrefix(mpath, "gopkg.in/")
962 return unversioned && replacingGopkgIn
963 }
964
965 func (r *codeRepo) GoMod(ctx context.Context, version string) (data []byte, err error) {
966 if version != module.CanonicalVersion(version) {
967 return nil, fmt.Errorf("version %s is not canonical", version)
968 }
969
970 if module.IsPseudoVersion(version) {
971
972
973
974
975 _, err := r.Stat(ctx, version)
976 if err != nil {
977 return nil, err
978 }
979 }
980
981 rev, dir, gomod, err := r.findDir(ctx, version)
982 if err != nil {
983 return nil, err
984 }
985 if gomod != nil {
986 return gomod, nil
987 }
988 data, err = r.code.ReadFile(ctx, rev, path.Join(dir, "go.mod"), codehost.MaxGoMod)
989 if err != nil {
990 if os.IsNotExist(err) {
991 return LegacyGoMod(r.modPath), nil
992 }
993 return nil, err
994 }
995 return data, nil
996 }
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008 func LegacyGoMod(modPath string) []byte {
1009 return fmt.Appendf(nil, "module %s\n", modfile.AutoQuote(modPath))
1010 }
1011
1012 func (r *codeRepo) modPrefix(rev string) string {
1013 return r.modPath + "@" + rev
1014 }
1015
1016 func (r *codeRepo) retractedVersions(ctx context.Context) (func(string) bool, error) {
1017 vs, err := r.Versions(ctx, "")
1018 if err != nil {
1019 return nil, err
1020 }
1021 versions := vs.List
1022
1023 for i, v := range versions {
1024 if strings.HasSuffix(v, "+incompatible") {
1025
1026
1027
1028
1029 versions = versions[:i]
1030 break
1031 }
1032 }
1033 if len(versions) == 0 {
1034 return func(string) bool { return false }, nil
1035 }
1036
1037 var highest string
1038 for i := len(versions) - 1; i >= 0; i-- {
1039 v := versions[i]
1040 if semver.Prerelease(v) == "" {
1041 highest = v
1042 break
1043 }
1044 }
1045 if highest == "" {
1046 highest = versions[len(versions)-1]
1047 }
1048
1049 data, err := r.GoMod(ctx, highest)
1050 if err != nil {
1051 return nil, err
1052 }
1053 f, err := modfile.ParseLax("go.mod", data, nil)
1054 if err != nil {
1055 return nil, err
1056 }
1057 retractions := make([]modfile.VersionInterval, 0, len(f.Retract))
1058 for _, r := range f.Retract {
1059 retractions = append(retractions, r.VersionInterval)
1060 }
1061
1062 return func(v string) bool {
1063 for _, r := range retractions {
1064 if semver.Compare(r.Low, v) <= 0 && semver.Compare(v, r.High) <= 0 {
1065 return true
1066 }
1067 }
1068 return false
1069 }, nil
1070 }
1071
1072 func (r *codeRepo) Zip(ctx context.Context, dst io.Writer, version string) error {
1073 if version != module.CanonicalVersion(version) {
1074 return fmt.Errorf("version %s is not canonical", version)
1075 }
1076
1077 if module.IsPseudoVersion(version) {
1078
1079
1080
1081
1082 _, err := r.Stat(ctx, version)
1083 if err != nil {
1084 return err
1085 }
1086 }
1087
1088 rev, subdir, _, err := r.findDir(ctx, version)
1089 if err != nil {
1090 return err
1091 }
1092
1093 if gomod, err := r.code.ReadFile(ctx, rev, filepath.Join(subdir, "go.mod"), codehost.MaxGoMod); err == nil {
1094 goVers := gover.GoModLookup(gomod, "go")
1095 if gover.Compare(goVers, gover.Local()) > 0 {
1096 return &gover.TooNewError{What: r.ModulePath() + "@" + version, GoVersion: goVers}
1097 }
1098 } else if !errors.Is(err, fs.ErrNotExist) {
1099 return err
1100 }
1101
1102 dl, err := r.code.ReadZip(ctx, rev, subdir, codehost.MaxZipFile)
1103 if err != nil {
1104 return err
1105 }
1106 defer dl.Close()
1107 subdir = strings.Trim(subdir, "/")
1108
1109
1110 f, err := os.CreateTemp("", "go-codehost-")
1111 if err != nil {
1112 dl.Close()
1113 return err
1114 }
1115 defer os.Remove(f.Name())
1116 defer f.Close()
1117 maxSize := int64(codehost.MaxZipFile)
1118 lr := &io.LimitedReader{R: dl, N: maxSize + 1}
1119 if _, err := io.Copy(f, lr); err != nil {
1120 dl.Close()
1121 return err
1122 }
1123 dl.Close()
1124 if lr.N <= 0 {
1125 return fmt.Errorf("downloaded zip file too large")
1126 }
1127 size := (maxSize + 1) - lr.N
1128 if _, err := f.Seek(0, 0); err != nil {
1129 return err
1130 }
1131
1132
1133 zr, err := zip.NewReader(f, size)
1134 if err != nil {
1135 return err
1136 }
1137
1138 var files []modzip.File
1139 if subdir != "" {
1140 subdir += "/"
1141 }
1142 haveLICENSE := false
1143 topPrefix := ""
1144 for _, zf := range zr.File {
1145 if topPrefix == "" {
1146 i := strings.Index(zf.Name, "/")
1147 if i < 0 {
1148 return fmt.Errorf("missing top-level directory prefix")
1149 }
1150 topPrefix = zf.Name[:i+1]
1151 }
1152 var name string
1153 var found bool
1154 if name, found = strings.CutPrefix(zf.Name, topPrefix); !found {
1155 return fmt.Errorf("zip file contains more than one top-level directory")
1156 }
1157
1158 if name, found = strings.CutPrefix(name, subdir); !found {
1159 continue
1160 }
1161
1162 if name == "" || strings.HasSuffix(name, "/") {
1163 continue
1164 }
1165 files = append(files, zipFile{name: name, f: zf})
1166 if name == "LICENSE" {
1167 haveLICENSE = true
1168 }
1169 }
1170
1171 if !haveLICENSE && subdir != "" {
1172 data, err := r.code.ReadFile(ctx, rev, "LICENSE", codehost.MaxLICENSE)
1173 if err == nil {
1174 files = append(files, dataFile{name: "LICENSE", data: data})
1175 }
1176 }
1177
1178 return modzip.Create(dst, module.Version{Path: r.modPath, Version: version}, files)
1179 }
1180
1181 type zipFile struct {
1182 name string
1183 f *zip.File
1184 }
1185
1186 func (f zipFile) Path() string { return f.name }
1187 func (f zipFile) Lstat() (fs.FileInfo, error) { return f.f.FileInfo(), nil }
1188 func (f zipFile) Open() (io.ReadCloser, error) { return f.f.Open() }
1189
1190 type dataFile struct {
1191 name string
1192 data []byte
1193 }
1194
1195 func (f dataFile) Path() string { return f.name }
1196 func (f dataFile) Lstat() (fs.FileInfo, error) { return dataFileInfo{f}, nil }
1197 func (f dataFile) Open() (io.ReadCloser, error) {
1198 return io.NopCloser(bytes.NewReader(f.data)), nil
1199 }
1200
1201 type dataFileInfo struct {
1202 f dataFile
1203 }
1204
1205 func (fi dataFileInfo) Name() string { return path.Base(fi.f.name) }
1206 func (fi dataFileInfo) Size() int64 { return int64(len(fi.f.data)) }
1207 func (fi dataFileInfo) Mode() fs.FileMode { return 0644 }
1208 func (fi dataFileInfo) ModTime() time.Time { return time.Time{} }
1209 func (fi dataFileInfo) IsDir() bool { return false }
1210 func (fi dataFileInfo) Sys() any { return nil }
1211
1212 func (fi dataFileInfo) String() string {
1213 return fs.FormatFileInfo(fi)
1214 }
1215
1216
1217
1218 func hasPathPrefix(s, prefix string) bool {
1219 switch {
1220 default:
1221 return false
1222 case len(s) == len(prefix):
1223 return s == prefix
1224 case len(s) > len(prefix):
1225 if prefix != "" && prefix[len(prefix)-1] == '/' {
1226 return strings.HasPrefix(s, prefix)
1227 }
1228 return s[len(prefix)] == '/' && s[:len(prefix)] == prefix
1229 }
1230 }
1231
View as plain text