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