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) retractedVersions(ctx context.Context) (func(string) bool, error) {
1013 vs, err := r.Versions(ctx, "")
1014 if err != nil {
1015 return nil, err
1016 }
1017 versions := vs.List
1018
1019 for i, v := range versions {
1020 if strings.HasSuffix(v, "+incompatible") {
1021
1022
1023
1024
1025 versions = versions[:i]
1026 break
1027 }
1028 }
1029 if len(versions) == 0 {
1030 return func(string) bool { return false }, nil
1031 }
1032
1033 var highest string
1034 for i := len(versions) - 1; i >= 0; i-- {
1035 v := versions[i]
1036 if semver.Prerelease(v) == "" {
1037 highest = v
1038 break
1039 }
1040 }
1041 if highest == "" {
1042 highest = versions[len(versions)-1]
1043 }
1044
1045 data, err := r.GoMod(ctx, highest)
1046 if err != nil {
1047 return nil, err
1048 }
1049 f, err := modfile.ParseLax("go.mod", data, nil)
1050 if err != nil {
1051 return nil, err
1052 }
1053 retractions := make([]modfile.VersionInterval, 0, len(f.Retract))
1054 for _, r := range f.Retract {
1055 retractions = append(retractions, r.VersionInterval)
1056 }
1057
1058 return func(v string) bool {
1059 for _, r := range retractions {
1060 if semver.Compare(r.Low, v) <= 0 && semver.Compare(v, r.High) <= 0 {
1061 return true
1062 }
1063 }
1064 return false
1065 }, nil
1066 }
1067
1068 func (r *codeRepo) Zip(ctx context.Context, dst io.Writer, version string) error {
1069 if version != module.CanonicalVersion(version) {
1070 return fmt.Errorf("version %s is not canonical", version)
1071 }
1072
1073 if module.IsPseudoVersion(version) {
1074
1075
1076
1077
1078 _, err := r.Stat(ctx, version)
1079 if err != nil {
1080 return err
1081 }
1082 }
1083
1084 rev, subdir, _, err := r.findDir(ctx, version)
1085 if err != nil {
1086 return err
1087 }
1088
1089 if gomod, err := r.code.ReadFile(ctx, rev, filepath.Join(subdir, "go.mod"), codehost.MaxGoMod); err == nil {
1090 goVers := gover.GoModLookup(gomod, "go")
1091 if gover.Compare(goVers, gover.Local()) > 0 {
1092 return &gover.TooNewError{What: r.ModulePath() + "@" + version, GoVersion: goVers}
1093 }
1094 } else if !errors.Is(err, fs.ErrNotExist) {
1095 return err
1096 }
1097
1098 dl, err := r.code.ReadZip(ctx, rev, subdir, codehost.MaxZipFile)
1099 if err != nil {
1100 return err
1101 }
1102 defer dl.Close()
1103 subdir = strings.Trim(subdir, "/")
1104
1105
1106 f, err := os.CreateTemp("", "go-codehost-")
1107 if err != nil {
1108 dl.Close()
1109 return err
1110 }
1111 defer os.Remove(f.Name())
1112 defer f.Close()
1113 maxSize := int64(codehost.MaxZipFile)
1114 lr := &io.LimitedReader{R: dl, N: maxSize + 1}
1115 if _, err := io.Copy(f, lr); err != nil {
1116 dl.Close()
1117 return err
1118 }
1119 dl.Close()
1120 if lr.N <= 0 {
1121 return fmt.Errorf("downloaded zip file too large")
1122 }
1123 size := (maxSize + 1) - lr.N
1124 if _, err := f.Seek(0, 0); err != nil {
1125 return err
1126 }
1127
1128
1129 zr, err := zip.NewReader(f, size)
1130 if err != nil {
1131 return err
1132 }
1133
1134 var files []modzip.File
1135 if subdir != "" {
1136 subdir += "/"
1137 }
1138 haveLICENSE := false
1139 topPrefix := ""
1140 for _, zf := range zr.File {
1141 if topPrefix == "" {
1142 i := strings.Index(zf.Name, "/")
1143 if i < 0 {
1144 return fmt.Errorf("missing top-level directory prefix")
1145 }
1146 topPrefix = zf.Name[:i+1]
1147 }
1148 var name string
1149 var found bool
1150 if name, found = strings.CutPrefix(zf.Name, topPrefix); !found {
1151 return fmt.Errorf("zip file contains more than one top-level directory")
1152 }
1153
1154 if name, found = strings.CutPrefix(name, subdir); !found {
1155 continue
1156 }
1157
1158 if name == "" || strings.HasSuffix(name, "/") {
1159 continue
1160 }
1161 files = append(files, zipFile{name: name, f: zf})
1162 if name == "LICENSE" {
1163 haveLICENSE = true
1164 }
1165 }
1166
1167 if !haveLICENSE && subdir != "" {
1168 data, err := r.code.ReadFile(ctx, rev, "LICENSE", codehost.MaxLICENSE)
1169 if err == nil {
1170 files = append(files, dataFile{name: "LICENSE", data: data})
1171 }
1172 }
1173
1174 return modzip.Create(dst, module.Version{Path: r.modPath, Version: version}, files)
1175 }
1176
1177 type zipFile struct {
1178 name string
1179 f *zip.File
1180 }
1181
1182 func (f zipFile) Path() string { return f.name }
1183 func (f zipFile) Lstat() (fs.FileInfo, error) { return f.f.FileInfo(), nil }
1184 func (f zipFile) Open() (io.ReadCloser, error) { return f.f.Open() }
1185
1186 type dataFile struct {
1187 name string
1188 data []byte
1189 }
1190
1191 func (f dataFile) Path() string { return f.name }
1192 func (f dataFile) Lstat() (fs.FileInfo, error) { return dataFileInfo{f}, nil }
1193 func (f dataFile) Open() (io.ReadCloser, error) {
1194 return io.NopCloser(bytes.NewReader(f.data)), nil
1195 }
1196
1197 type dataFileInfo struct {
1198 f dataFile
1199 }
1200
1201 func (fi dataFileInfo) Name() string { return path.Base(fi.f.name) }
1202 func (fi dataFileInfo) Size() int64 { return int64(len(fi.f.data)) }
1203 func (fi dataFileInfo) Mode() fs.FileMode { return 0644 }
1204 func (fi dataFileInfo) ModTime() time.Time { return time.Time{} }
1205 func (fi dataFileInfo) IsDir() bool { return false }
1206 func (fi dataFileInfo) Sys() any { return nil }
1207
1208 func (fi dataFileInfo) String() string {
1209 return fs.FormatFileInfo(fi)
1210 }
1211
1212
1213
1214 func hasPathPrefix(s, prefix string) bool {
1215 switch {
1216 default:
1217 return false
1218 case len(s) == len(prefix):
1219 return s == prefix
1220 case len(s) > len(prefix):
1221 if prefix != "" && prefix[len(prefix)-1] == '/' {
1222 return strings.HasPrefix(s, prefix)
1223 }
1224 return s[len(prefix)] == '/' && s[:len(prefix)] == prefix
1225 }
1226 }
1227
View as plain text