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 if tags.Origin.RepoSum != "" {
388 origin.RepoSum = tags.Origin.RepoSum
389 }
390 }
391 }
392 }()
393
394
395
396
397
398
399
400
401 incompatibleOk := map[string]bool{}
402 canUseIncompatible := func(v string) bool {
403 if r.codeDir != "" || r.pathMajor != "" {
404
405
406
407
408 return false
409 }
410
411 ok, seen := incompatibleOk[""]
412 if !seen {
413 _, errGoMod := r.code.ReadFile(ctx, info.Name, "go.mod", codehost.MaxGoMod)
414 ok = (errGoMod != nil)
415 incompatibleOk[""] = ok
416 }
417 if !ok {
418
419 return false
420 }
421
422
423
424
425
426
427 if v != "" && !strings.HasSuffix(statVers, "+incompatible") {
428 major := semver.Major(v)
429 ok, seen = incompatibleOk[major]
430 if !seen {
431 _, errGoModSub := r.code.ReadFile(ctx, info.Name, path.Join(major, "go.mod"), codehost.MaxGoMod)
432 ok = (errGoModSub != nil)
433 incompatibleOk[major] = ok
434 }
435 if !ok {
436 return false
437 }
438 }
439
440 return true
441 }
442
443
444
445
446
447
448 checkCanonical := func(v string) (*RevInfo, error) {
449
450
451
452
453
454
455
456
457
458
459 _, _, _, err := r.findDir(ctx, v)
460 if err != nil {
461
462
463 return nil, &module.ModuleError{
464 Path: r.modPath,
465 Err: &module.InvalidVersionError{
466 Version: v,
467 Err: notExistError{err: err},
468 },
469 }
470 }
471
472 invalidf := func(format string, args ...any) error {
473 return &module.ModuleError{
474 Path: r.modPath,
475 Err: &module.InvalidVersionError{
476 Version: v,
477 Err: fmt.Errorf(format, args...),
478 },
479 }
480 }
481
482
483
484
485
486 if v == strings.TrimSuffix(statVers, "+incompatible") {
487 v = statVers
488 }
489 base := strings.TrimSuffix(v, "+incompatible")
490 var errIncompatible error
491 if !module.MatchPathMajor(base, r.pathMajor) {
492 if canUseIncompatible(base) {
493 v = base + "+incompatible"
494 } else {
495 if r.pathMajor != "" {
496 errIncompatible = invalidf("module path includes a major version suffix, so major version must match")
497 } else {
498 errIncompatible = invalidf("module contains a go.mod file, so module path must match major version (%q)", path.Join(r.pathPrefix, semver.Major(v)))
499 }
500 }
501 } else if strings.HasSuffix(v, "+incompatible") {
502 errIncompatible = invalidf("+incompatible suffix not allowed: major version %s is compatible", semver.Major(v))
503 }
504
505 if statVers != "" && statVers == module.CanonicalVersion(statVers) {
506
507
508
509 if statBase := strings.TrimSuffix(statVers, "+incompatible"); statBase != base {
510 return nil, &module.ModuleError{
511 Path: r.modPath,
512 Err: &module.InvalidVersionError{
513 Version: statVers,
514 Err: fmt.Errorf("resolves to version %v (%s is not a tag)", v, statBase),
515 },
516 }
517 }
518 }
519
520 if errIncompatible != nil {
521 return nil, errIncompatible
522 }
523
524 return &RevInfo{
525 Name: info.Name,
526 Short: info.Short,
527 Time: info.Time,
528 Version: v,
529 }, nil
530 }
531
532
533
534 if module.IsPseudoVersion(statVers) {
535
536
537
538
539
540
541 revInfo, err = checkCanonical(statVers)
542 if err != nil {
543 return revInfo, err
544 }
545 if err := r.validatePseudoVersion(ctx, info, statVers); err != nil {
546 return nil, err
547 }
548 return revInfo, nil
549 }
550
551
552
553
554
555
556
557 tagPrefix := ""
558 if r.codeDir != "" {
559 tagPrefix = r.codeDir + "/"
560 }
561
562 isRetracted, err := r.retractedVersions(ctx)
563 if err != nil {
564 isRetracted = func(string) bool { return false }
565 }
566
567
568
569
570 tagToVersion := func(tag string) (v string, tagIsCanonical bool) {
571 if !strings.HasPrefix(tag, tagPrefix) {
572 return "", false
573 }
574 trimmed := tag[len(tagPrefix):]
575
576 if module.IsPseudoVersion(tag) {
577 return "", false
578 }
579
580 v = semver.Canonical(trimmed)
581 if v == "" || !strings.HasPrefix(trimmed, v) {
582 return "", false
583 }
584 if v == trimmed {
585 tagIsCanonical = true
586 }
587 return v, tagIsCanonical
588 }
589
590
591 if v, tagIsCanonical := tagToVersion(info.Version); tagIsCanonical {
592 if info, err := checkCanonical(v); err == nil {
593 return info, err
594 }
595 }
596
597
598
599 var (
600 highestCanonical string
601 pseudoBase string
602 )
603 for _, pathTag := range info.Tags {
604 v, tagIsCanonical := tagToVersion(pathTag)
605 if statVers != "" && semver.Compare(v, statVers) == 0 {
606
607 if tagIsCanonical {
608
609
610
611
612 return checkCanonical(v)
613 } else {
614
615
616
617
618
619
620
621
622
623
624 pseudoBase = v
625 }
626 }
627
628
629 if tagIsCanonical && semver.Compare(highestCanonical, v) < 0 && !isRetracted(v) {
630 if module.MatchPathMajor(v, r.pathMajor) || canUseIncompatible(v) {
631 highestCanonical = v
632 }
633 }
634 }
635
636
637
638 if highestCanonical != "" {
639 return checkCanonical(highestCanonical)
640 }
641
642
643
644
645
646 tagAllowed := func(tag string) bool {
647 v, _ := tagToVersion(tag)
648 if v == "" {
649 return false
650 }
651 if !module.MatchPathMajor(v, r.pathMajor) && !canUseIncompatible(v) {
652 return false
653 }
654 return !isRetracted(v)
655 }
656 if pseudoBase == "" {
657 tag, err := r.code.RecentTag(ctx, info.Name, tagPrefix, tagAllowed)
658 if err != nil && !errors.Is(err, errors.ErrUnsupported) {
659 return nil, err
660 }
661 if tag != "" {
662 pseudoBase, _ = tagToVersion(tag)
663 }
664 }
665
666 return checkCanonical(module.PseudoVersion(r.pseudoMajor, pseudoBase, info.Time, info.Short))
667 }
668
669
670
671
672
673
674
675
676
677
678 func (r *codeRepo) validatePseudoVersion(ctx context.Context, info *codehost.RevInfo, version string) (err error) {
679 defer func() {
680 if err != nil {
681 if _, ok := err.(*module.ModuleError); !ok {
682 if _, ok := err.(*module.InvalidVersionError); !ok {
683 err = &module.InvalidVersionError{Version: version, Pseudo: true, Err: err}
684 }
685 err = &module.ModuleError{Path: r.modPath, Err: err}
686 }
687 }
688 }()
689
690 rev, err := module.PseudoVersionRev(version)
691 if err != nil {
692 return err
693 }
694 if rev != info.Short {
695 switch {
696 case strings.HasPrefix(rev, info.Short):
697 return fmt.Errorf("revision is longer than canonical (expected %s)", info.Short)
698 case strings.HasPrefix(info.Short, rev):
699 return fmt.Errorf("revision is shorter than canonical (expected %s)", info.Short)
700 default:
701 return fmt.Errorf("does not match short name of revision (expected %s)", info.Short)
702 }
703 }
704
705 t, err := module.PseudoVersionTime(version)
706 if err != nil {
707 return err
708 }
709 if !t.Equal(info.Time.Truncate(time.Second)) {
710 return fmt.Errorf("does not match version-control timestamp (expected %s)", info.Time.UTC().Format(module.PseudoVersionTimestampFormat))
711 }
712
713 tagPrefix := ""
714 if r.codeDir != "" {
715 tagPrefix = r.codeDir + "/"
716 }
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734 base, err := module.PseudoVersionBase(strings.TrimSuffix(version, "+incompatible"))
735 if err != nil {
736 return err
737 }
738 if base == "" {
739 if r.pseudoMajor == "" && semver.Major(version) == "v1" {
740 return fmt.Errorf("major version without preceding tag must be v0, not v1")
741 }
742 return nil
743 } else {
744 for _, tag := range info.Tags {
745 versionOnly := strings.TrimPrefix(tag, tagPrefix)
746 if versionOnly == base {
747
748
749
750
751
752
753
754
755
756
757
758
759
760 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)
761 }
762 }
763 }
764
765 tags, err := r.code.Tags(ctx, tagPrefix+base)
766 if err != nil {
767 return err
768 }
769
770 var lastTag string
771 ancestorFound := false
772 for _, tag := range tags.List {
773 versionOnly := strings.TrimPrefix(tag.Name, tagPrefix)
774 if semver.Compare(versionOnly, base) == 0 {
775 lastTag = tag.Name
776 ancestorFound, err = r.code.DescendsFrom(ctx, info.Name, tag.Name)
777 if ancestorFound {
778 break
779 }
780 }
781 }
782
783 if lastTag == "" {
784 return fmt.Errorf("preceding tag (%s) not found", base)
785 }
786
787 if !ancestorFound {
788 if err != nil {
789 return err
790 }
791 rev, err := module.PseudoVersionRev(version)
792 if err != nil {
793 return fmt.Errorf("not a descendent of preceding tag (%s)", lastTag)
794 }
795 return fmt.Errorf("revision %s is not a descendent of preceding tag (%s)", rev, lastTag)
796 }
797 return nil
798 }
799
800 func (r *codeRepo) revToRev(rev string) string {
801 if semver.IsValid(rev) {
802 if module.IsPseudoVersion(rev) {
803 r, _ := module.PseudoVersionRev(rev)
804 return r
805 }
806 if semver.Build(rev) == "+incompatible" {
807 rev = rev[:len(rev)-len("+incompatible")]
808 }
809 if r.codeDir == "" {
810 return rev
811 }
812 return r.codeDir + "/" + rev
813 }
814 return rev
815 }
816
817 func (r *codeRepo) versionToRev(version string) (rev string, err error) {
818 if !semver.IsValid(version) {
819 return "", &module.ModuleError{
820 Path: r.modPath,
821 Err: &module.InvalidVersionError{
822 Version: version,
823 Err: errors.New("syntax error"),
824 },
825 }
826 }
827 return r.revToRev(version), nil
828 }
829
830
831
832
833
834 func (r *codeRepo) findDir(ctx context.Context, version string) (rev, dir string, gomod []byte, err error) {
835 rev, err = r.versionToRev(version)
836 if err != nil {
837 return "", "", nil, err
838 }
839
840
841
842 file1 := path.Join(r.codeDir, "go.mod")
843 gomod1, err1 := r.code.ReadFile(ctx, rev, file1, codehost.MaxGoMod)
844 if err1 != nil && !os.IsNotExist(err1) {
845 return "", "", nil, fmt.Errorf("reading %s/%s at revision %s: %v", r.codeRoot, file1, rev, err1)
846 }
847 mpath1 := modfile.ModulePath(gomod1)
848 found1 := err1 == nil && (isMajor(mpath1, r.pathMajor) || r.canReplaceMismatchedVersionDueToBug(mpath1))
849
850 var file2 string
851 if r.pathMajor != "" && r.codeRoot != r.modPath && !strings.HasPrefix(r.pathMajor, ".") {
852
853
854
855
856
857
858
859 dir2 := path.Join(r.codeDir, r.pathMajor[1:])
860 file2 = path.Join(dir2, "go.mod")
861 gomod2, err2 := r.code.ReadFile(ctx, rev, file2, codehost.MaxGoMod)
862 if err2 != nil && !os.IsNotExist(err2) {
863 return "", "", nil, fmt.Errorf("reading %s/%s at revision %s: %v", r.codeRoot, file2, rev, err2)
864 }
865 mpath2 := modfile.ModulePath(gomod2)
866 found2 := err2 == nil && isMajor(mpath2, r.pathMajor)
867
868 if found1 && found2 {
869 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)
870 }
871 if found2 {
872 return rev, dir2, gomod2, nil
873 }
874 if err2 == nil {
875 if mpath2 == "" {
876 return "", "", nil, fmt.Errorf("%s/%s is missing module path at revision %s", r.codeRoot, file2, rev)
877 }
878 return "", "", nil, fmt.Errorf("%s/%s has non-...%s module path %q at revision %s", r.codeRoot, file2, r.pathMajor, mpath2, rev)
879 }
880 }
881
882
883 if found1 {
884
885 return rev, r.codeDir, gomod1, nil
886 }
887 if err1 == nil {
888
889 suffix := ""
890 if file2 != "" {
891 suffix = fmt.Sprintf(" (and ...%s/go.mod does not exist)", r.pathMajor)
892 }
893 if mpath1 == "" {
894 return "", "", nil, fmt.Errorf("%s is missing module path%s at revision %s", file1, suffix, rev)
895 }
896 if r.pathMajor != "" {
897 return "", "", nil, fmt.Errorf("%s has non-...%s module path %q%s at revision %s", file1, r.pathMajor, mpath1, suffix, rev)
898 }
899 if _, _, ok := module.SplitPathVersion(mpath1); !ok {
900 return "", "", nil, fmt.Errorf("%s has malformed module path %q%s at revision %s", file1, mpath1, suffix, rev)
901 }
902 return "", "", nil, fmt.Errorf("%s has post-%s module path %q%s at revision %s", file1, semver.Major(version), mpath1, suffix, rev)
903 }
904
905 if r.codeDir == "" && (r.pathMajor == "" || strings.HasPrefix(r.pathMajor, ".")) {
906
907 return rev, "", nil, nil
908 }
909
910
911
912 if file2 != "" {
913 return "", "", nil, fmt.Errorf("missing %s/go.mod and ...%s/go.mod at revision %s", r.pathPrefix, r.pathMajor, rev)
914 }
915 return "", "", nil, fmt.Errorf("missing %s/go.mod at revision %s", r.pathPrefix, rev)
916 }
917
918
919
920
921 func isMajor(mpath, pathMajor string) bool {
922 if mpath == "" {
923
924 return false
925 }
926 _, mpathMajor, ok := module.SplitPathVersion(mpath)
927 if !ok {
928
929 return false
930 }
931 if pathMajor == "" {
932
933
934
935 switch module.PathMajorPrefix(mpathMajor) {
936 case "", "v0", "v1":
937 return true
938 default:
939 return false
940 }
941 }
942 if mpathMajor == "" {
943
944
945
946
947
948 return false
949 }
950
951
952
953
954 return pathMajor[1:] == mpathMajor[1:]
955 }
956
957
958
959
960 func (r *codeRepo) canReplaceMismatchedVersionDueToBug(mpath string) bool {
961
962
963 unversioned := r.pathMajor == ""
964 replacingGopkgIn := strings.HasPrefix(mpath, "gopkg.in/")
965 return unversioned && replacingGopkgIn
966 }
967
968 func (r *codeRepo) GoMod(ctx context.Context, version string) (data []byte, err error) {
969 if version != module.CanonicalVersion(version) {
970 return nil, fmt.Errorf("version %s is not canonical", version)
971 }
972
973 if module.IsPseudoVersion(version) {
974
975
976
977
978 _, err := r.Stat(ctx, version)
979 if err != nil {
980 return nil, err
981 }
982 }
983
984 rev, dir, gomod, err := r.findDir(ctx, version)
985 if err != nil {
986 return nil, err
987 }
988 if gomod != nil {
989 return gomod, nil
990 }
991 data, err = r.code.ReadFile(ctx, rev, path.Join(dir, "go.mod"), codehost.MaxGoMod)
992 if err != nil {
993 if os.IsNotExist(err) {
994 return LegacyGoMod(r.modPath), nil
995 }
996 return nil, err
997 }
998 return data, nil
999 }
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011 func LegacyGoMod(modPath string) []byte {
1012 return fmt.Appendf(nil, "module %s\n", modfile.AutoQuote(modPath))
1013 }
1014
1015 func (r *codeRepo) retractedVersions(ctx context.Context) (func(string) bool, error) {
1016 vs, err := r.Versions(ctx, "")
1017 if err != nil {
1018 return nil, err
1019 }
1020 versions := vs.List
1021
1022 for i, v := range versions {
1023 if strings.HasSuffix(v, "+incompatible") {
1024
1025
1026
1027
1028 versions = versions[:i]
1029 break
1030 }
1031 }
1032 if len(versions) == 0 {
1033 return func(string) bool { return false }, nil
1034 }
1035
1036 var highest string
1037 for i := len(versions) - 1; i >= 0; i-- {
1038 v := versions[i]
1039 if semver.Prerelease(v) == "" {
1040 highest = v
1041 break
1042 }
1043 }
1044 if highest == "" {
1045 highest = versions[len(versions)-1]
1046 }
1047
1048 data, err := r.GoMod(ctx, highest)
1049 if err != nil {
1050 return nil, err
1051 }
1052 f, err := modfile.ParseLax("go.mod", data, nil)
1053 if err != nil {
1054 return nil, err
1055 }
1056 retractions := make([]modfile.VersionInterval, 0, len(f.Retract))
1057 for _, r := range f.Retract {
1058 retractions = append(retractions, r.VersionInterval)
1059 }
1060
1061 return func(v string) bool {
1062 for _, r := range retractions {
1063 if semver.Compare(r.Low, v) <= 0 && semver.Compare(v, r.High) <= 0 {
1064 return true
1065 }
1066 }
1067 return false
1068 }, nil
1069 }
1070
1071 func (r *codeRepo) Zip(ctx context.Context, dst io.Writer, version string) error {
1072 if version != module.CanonicalVersion(version) {
1073 return fmt.Errorf("version %s is not canonical", version)
1074 }
1075
1076 if module.IsPseudoVersion(version) {
1077
1078
1079
1080
1081 _, err := r.Stat(ctx, version)
1082 if err != nil {
1083 return err
1084 }
1085 }
1086
1087 rev, subdir, _, err := r.findDir(ctx, version)
1088 if err != nil {
1089 return err
1090 }
1091
1092 if gomod, err := r.code.ReadFile(ctx, rev, filepath.Join(subdir, "go.mod"), codehost.MaxGoMod); err == nil {
1093 goVers := gover.GoModLookup(gomod, "go")
1094 if gover.Compare(goVers, gover.Local()) > 0 {
1095 return &gover.TooNewError{What: r.ModulePath() + "@" + version, GoVersion: goVers}
1096 }
1097 } else if !errors.Is(err, fs.ErrNotExist) {
1098 return err
1099 }
1100
1101 dl, err := r.code.ReadZip(ctx, rev, subdir, codehost.MaxZipFile)
1102 if err != nil {
1103 return err
1104 }
1105 defer dl.Close()
1106 subdir = strings.Trim(subdir, "/")
1107
1108
1109 f, err := os.CreateTemp("", "go-codehost-")
1110 if err != nil {
1111 dl.Close()
1112 return err
1113 }
1114 defer os.Remove(f.Name())
1115 defer f.Close()
1116 maxSize := int64(codehost.MaxZipFile)
1117 lr := &io.LimitedReader{R: dl, N: maxSize + 1}
1118 if _, err := io.Copy(f, lr); err != nil {
1119 dl.Close()
1120 return err
1121 }
1122 dl.Close()
1123 if lr.N <= 0 {
1124 return fmt.Errorf("downloaded zip file too large")
1125 }
1126 size := (maxSize + 1) - lr.N
1127 if _, err := f.Seek(0, 0); err != nil {
1128 return err
1129 }
1130
1131
1132 zr, err := zip.NewReader(f, size)
1133 if err != nil {
1134 return err
1135 }
1136
1137 var files []modzip.File
1138 if subdir != "" {
1139 subdir += "/"
1140 }
1141 haveLICENSE := false
1142 topPrefix := ""
1143 for _, zf := range zr.File {
1144 if topPrefix == "" {
1145 i := strings.Index(zf.Name, "/")
1146 if i < 0 {
1147 return fmt.Errorf("missing top-level directory prefix")
1148 }
1149 topPrefix = zf.Name[:i+1]
1150 }
1151 var name string
1152 var found bool
1153 if name, found = strings.CutPrefix(zf.Name, topPrefix); !found {
1154 return fmt.Errorf("zip file contains more than one top-level directory")
1155 }
1156
1157 if name, found = strings.CutPrefix(name, subdir); !found {
1158 continue
1159 }
1160
1161 if name == "" || strings.HasSuffix(name, "/") {
1162 continue
1163 }
1164 files = append(files, zipFile{name: name, f: zf})
1165 if name == "LICENSE" {
1166 haveLICENSE = true
1167 }
1168 }
1169
1170 if !haveLICENSE && subdir != "" {
1171 data, err := r.code.ReadFile(ctx, rev, "LICENSE", codehost.MaxLICENSE)
1172 if err == nil {
1173 files = append(files, dataFile{name: "LICENSE", data: data})
1174 }
1175 }
1176
1177 return modzip.Create(dst, module.Version{Path: r.modPath, Version: version}, files)
1178 }
1179
1180 type zipFile struct {
1181 name string
1182 f *zip.File
1183 }
1184
1185 func (f zipFile) Path() string { return f.name }
1186 func (f zipFile) Lstat() (fs.FileInfo, error) { return f.f.FileInfo(), nil }
1187 func (f zipFile) Open() (io.ReadCloser, error) { return f.f.Open() }
1188
1189 type dataFile struct {
1190 name string
1191 data []byte
1192 }
1193
1194 func (f dataFile) Path() string { return f.name }
1195 func (f dataFile) Lstat() (fs.FileInfo, error) { return dataFileInfo{f}, nil }
1196 func (f dataFile) Open() (io.ReadCloser, error) {
1197 return io.NopCloser(bytes.NewReader(f.data)), nil
1198 }
1199
1200 type dataFileInfo struct {
1201 f dataFile
1202 }
1203
1204 func (fi dataFileInfo) Name() string { return path.Base(fi.f.name) }
1205 func (fi dataFileInfo) Size() int64 { return int64(len(fi.f.data)) }
1206 func (fi dataFileInfo) Mode() fs.FileMode { return 0644 }
1207 func (fi dataFileInfo) ModTime() time.Time { return time.Time{} }
1208 func (fi dataFileInfo) IsDir() bool { return false }
1209 func (fi dataFileInfo) Sys() any { return nil }
1210
1211 func (fi dataFileInfo) String() string {
1212 return fs.FormatFileInfo(fi)
1213 }
1214
1215
1216
1217 func hasPathPrefix(s, prefix string) bool {
1218 switch {
1219 default:
1220 return false
1221 case len(s) == len(prefix):
1222 return s == prefix
1223 case len(s) > len(prefix):
1224 if prefix != "" && prefix[len(prefix)-1] == '/' {
1225 return strings.HasPrefix(s, prefix)
1226 }
1227 return s[len(prefix)] == '/' && s[:len(prefix)] == prefix
1228 }
1229 }
1230
View as plain text