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