github.com/octohelm/cuemod@v0.9.4/internal/cmd/go/internals/modfetch/coderepo.go (about) 1 // Copyright 2018 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 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 "github.com/octohelm/cuemod/internal/cmd/go/internals/gover" 23 "github.com/octohelm/cuemod/internal/cmd/go/internals/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 // A codeRepo implements modfetch.Repo using an underlying codehost.Repo. 32 type codeRepo struct { 33 modPath string 34 35 // code is the repository containing this module. 36 code codehost.Repo 37 // codeRoot is the import path at the root of code. 38 codeRoot string 39 // codeDir is the directory (relative to root) at which we expect to find the module. 40 // If pathMajor is non-empty and codeRoot is not the full modPath, 41 // then we look in both codeDir and codeDir/pathMajor[1:]. 42 codeDir string 43 44 // pathMajor is the suffix of modPath that indicates its major version, 45 // or the empty string if modPath is at major version 0 or 1. 46 // 47 // pathMajor is typically of the form "/vN", but possibly ".vN", or 48 // ".vN-unstable" for modules resolved using gopkg.in. 49 pathMajor string 50 // pathPrefix is the prefix of modPath that excludes pathMajor. 51 // It is used only for logging. 52 pathPrefix string 53 54 // pseudoMajor is the major version prefix to require when generating 55 // pseudo-versions for this module, derived from the module path. pseudoMajor 56 // is empty if the module path does not include a version suffix (that is, 57 // accepts either v0 or v1). 58 pseudoMajor string 59 } 60 61 // newCodeRepo returns a Repo that reads the source code for the module with the 62 // given path, from the repo stored in code, with the root of the repo 63 // containing the path given by codeRoot. 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 // Compute codeDir = bar, the subdirectory within the repo 78 // corresponding to the module root. 79 // 80 // At this point we might have: 81 // path = github.com/rsc/foo/bar/v2 82 // codeRoot = github.com/rsc/foo 83 // pathPrefix = github.com/rsc/foo/bar 84 // pathMajor = /v2 85 // pseudoMajor = v2 86 // 87 // which gives 88 // codeDir = bar 89 // 90 // We know that pathPrefix is a prefix of path, and codeRoot is a prefix of 91 // path, but codeRoot may or may not be a prefix of pathPrefix, because 92 // codeRoot may be the entire path (in which case codeDir should be empty). 93 // That occurs in two situations. 94 // 95 // One is when a go-import meta tag resolves the complete module path, 96 // including the pathMajor suffix: 97 // path = nanomsg.org/go/mangos/v2 98 // codeRoot = nanomsg.org/go/mangos/v2 99 // pathPrefix = nanomsg.org/go/mangos 100 // pathMajor = /v2 101 // pseudoMajor = v2 102 // 103 // The other is similar: for gopkg.in only, the major version is encoded 104 // with a dot rather than a slash, and thus can't be in a subdirectory. 105 // path = gopkg.in/yaml.v2 106 // codeRoot = gopkg.in/yaml.v2 107 // pathPrefix = gopkg.in/yaml 108 // pathMajor = .v2 109 // pseudoMajor = v2 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 // Special case: gopkg.in/macaroon-bakery.v2-unstable 142 // does not use the v2 tags (those are for macaroon-bakery.v2). 143 // It has no possible tags at all. 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 // Note: ./codehost/codehost.go's isOriginTag knows about these conditions too. 173 // If these are relaxed, isOriginTag will need to be relaxed as well. 174 if v == "" || v != semver.Canonical(v) { 175 // Ignore non-canonical tags: Stat rewrites those to canonical 176 // pseudo-versions. Note that we compare against semver.Canonical here 177 // instead of module.CanonicalVersion: revToRev strips "+incompatible" 178 // suffixes before looking up tags, so a tag like "v2.0.0+incompatible" 179 // would not resolve at all. (The Go version string "v2.0.0+incompatible" 180 // refers to the "v2.0.0" version tag, which we handle below.) 181 continue 182 } 183 if module.IsPseudoVersion(v) { 184 // Ignore tags that look like pseudo-versions: Stat rewrites those 185 // unambiguously to the underlying commit, and tagToVersion drops them. 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 // appendIncompatibleVersions appends "+incompatible" versions to list if 205 // appropriate, returning the final list. 206 // 207 // The incompatible list contains candidate versions without the '+incompatible' 208 // prefix. 209 // 210 // Both list and incompatible must be sorted in semantic order. 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 // No +incompatible versions are possible, so no need to check them. 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 // The latest compatible version has a go.mod file, so assume that all 242 // subsequent versions do as well, and do not include any +incompatible 243 // versions. Even if we are wrong, the author clearly intends module 244 // consumers to be on the v0/v1 line instead of a higher +incompatible 245 // version. (See https://golang.org/issue/34189.) 246 // 247 // We know of at least two examples where this behavior is desired 248 // (github.com/russross/blackfriday@v2.0.0 and 249 // github.com/libp2p/go-libp2p@v6.0.23), and (as of 2019-10-29) have no 250 // concrete examples for which it is undesired. 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 // The latest release of this major version has a go.mod file, so it is 279 // not allowed as +incompatible. It would be confusing to include some 280 // minor versions of this major version as +incompatible but require 281 // semantic import versioning for others, so drop all +incompatible 282 // versions for this major version. 283 // 284 // If we're wrong about a minor version in the middle, users will still be 285 // able to 'go get' specific tags for that version explicitly — they just 286 // won't appear in 'go list' or as the results for queries with inequality 287 // bounds. 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 // Note: info may be non-nil to supply Origin for caching error. 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 // convert converts a version as reported by the code host to a version as 334 // interpreted by the module system. 335 // 336 // If statVers is a valid module version, it is used for the Version field. 337 // Otherwise, the Version is derived from the passed-in info and recent tags. 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 // Add tags that are relevant to pseudo-version calculation to origin. 356 prefix := r.codeDir 357 if prefix != "" { 358 prefix += "/" 359 } 360 if r.pathMajor != "" { // "/v2" or "/.v2" 361 prefix += r.pathMajor[1:] + "." // += "v2." 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 // If this is a plain tag (no dir/ prefix) 377 // and the module path is unversioned, 378 // and if the underlying file tree has no go.mod, 379 // then allow using the tag with a +incompatible suffix. 380 // 381 // (If the version is +incompatible, then the go.mod file must not exist: 382 // +incompatible is not an ongoing opt-out from semantic import versioning.) 383 incompatibleOk := map[string]bool{} 384 canUseIncompatible := func(v string) bool { 385 if r.codeDir != "" || r.pathMajor != "" { 386 // A non-empty codeDir indicates a module within a subdirectory, 387 // which necessarily has a go.mod file indicating the module boundary. 388 // A non-empty pathMajor indicates a module path with a major-version 389 // suffix, which must match. 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 // A go.mod file exists at the repo root. 401 return false 402 } 403 404 // Per https://go.dev/issue/51324, previous versions of the 'go' command 405 // didn't always check for go.mod files in subdirectories, so if the user 406 // requests a +incompatible version explicitly, we should continue to allow 407 // it. Otherwise, if vN/go.mod exists, expect that release tags for that 408 // major version are intended for the vN module. 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 // checkCanonical verifies that the canonical version v is compatible with the 426 // module path represented by r, adding a "+incompatible" suffix if needed. 427 // 428 // If statVers is also canonical, checkCanonical also verifies that v is 429 // either statVers or statVers with the added "+incompatible" suffix. 430 checkCanonical := func(v string) (*RevInfo, error) { 431 // If r.codeDir is non-empty, then the go.mod file must exist: the module 432 // author — not the module consumer, — gets to decide how to carve up the repo 433 // into modules. 434 // 435 // Conversely, if the go.mod file exists, the module author — not the module 436 // consumer — gets to determine the module's path 437 // 438 // r.findDir verifies both of these conditions. Execute it now so that 439 // r.Stat will correctly return a notExistError if the go.mod location or 440 // declared module path doesn't match. 441 _, _, _, err := r.findDir(ctx, v) 442 if err != nil { 443 // TODO: It would be nice to return an error like "not a module". 444 // Right now we return "missing go.mod", which is a little confusing. 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 // Add the +incompatible suffix if needed or requested explicitly, and 465 // verify that its presence or absence is appropriate for this version 466 // (which depends on whether it has an explicit go.mod file). 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 // Since the caller-requested version is canonical, it would be very 489 // confusing to resolve it to anything but itself, possibly with a 490 // "+incompatible" suffix. Error out explicitly. 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 // Determine version. 515 516 if module.IsPseudoVersion(statVers) { 517 // Validate the go.mod location and major version before 518 // we check for an ancestor tagged with the pseude-version base. 519 // 520 // We can rule out an invalid subdirectory or major version with only 521 // shallow commit information, but checking the pseudo-version base may 522 // require downloading a (potentially more expensive) full history. 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 // statVers is not a pseudo-version, so we need to either resolve it to a 534 // canonical version or verify that it is already a canonical tag 535 // (not a branch). 536 537 // Derive or verify a version from a code repo tag. 538 // Tag must have a prefix matching codeDir. 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 // tagToVersion returns the version obtained by trimming tagPrefix from tag. 550 // If the tag is invalid, retracted, or a pseudo-version, tagToVersion returns 551 // an empty version. 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 // Tags that look like pseudo-versions would be confusing. Ignore them. 558 if module.IsPseudoVersion(tag) { 559 return "", false 560 } 561 562 v = semver.Canonical(trimmed) // Not module.Canonical: we don't want to pick up an explicit "+incompatible" suffix from the tag. 563 if v == "" || !strings.HasPrefix(trimmed, v) { 564 return "", false // Invalid or incomplete version (just vX or vX.Y). 565 } 566 if v == trimmed { 567 tagIsCanonical = true 568 } 569 return v, tagIsCanonical 570 } 571 572 // If the VCS gave us a valid version, use that. 573 if v, tagIsCanonical := tagToVersion(info.Version); tagIsCanonical { 574 if info, err := checkCanonical(v); err == nil { 575 return info, err 576 } 577 } 578 579 // Look through the tags on the revision for either a usable canonical version 580 // or an appropriate base for a pseudo-version. 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 // The tag is equivalent to the version requested by the user. 589 if tagIsCanonical { 590 // This tag is the canonical form of the requested version, 591 // not some other form with extra build metadata. 592 // Use this tag so that the resolved version will match exactly. 593 // (If it isn't actually allowed, we'll error out in checkCanonical.) 594 return checkCanonical(v) 595 } else { 596 // The user explicitly requested something equivalent to this tag. We 597 // can't use the version from the tag directly: since the tag is not 598 // canonical, it could be ambiguous. For example, tags v0.0.1+a and 599 // v0.0.1+b might both exist and refer to different revisions. 600 // 601 // The tag is otherwise valid for the module, so we can at least use it as 602 // the base of an unambiguous pseudo-version. 603 // 604 // If multiple tags match, tagToVersion will canonicalize them to the same 605 // base version. 606 pseudoBase = v 607 } 608 } 609 // Save the highest non-retracted canonical tag for the revision. 610 // If we don't find a better match, we'll use it as the canonical version. 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 // If we found a valid canonical tag for the revision, return it. 619 // Even if we found a good pseudo-version base, a canonical version is better. 620 if highestCanonical != "" { 621 return checkCanonical(highestCanonical) 622 } 623 624 // Find the highest tagged version in the revision's history, subject to 625 // major version and +incompatible constraints. Use that version as the 626 // pseudo-version base so that the pseudo-version sorts higher. Ignore 627 // retracted versions. 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 // validatePseudoVersion checks that version has a major version compatible with 652 // r.modPath and encodes a base version and commit metadata that agrees with 653 // info. 654 // 655 // Note that verifying a nontrivial base version in particular may be somewhat 656 // expensive: in order to do so, r.code.DescendsFrom will need to fetch at least 657 // enough of the commit history to find a path between version and its base. 658 // Fortunately, many pseudo-versions — such as those for untagged repositories — 659 // have trivial bases! 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 // A pseudo-version should have a precedence just above its parent revisions, 701 // and no higher. Otherwise, it would be possible for library authors to "pin" 702 // dependency versions (and bypass the usual minimum version selection) by 703 // naming an extremely high pseudo-version rather than an accurate one. 704 // 705 // Moreover, if we allow a pseudo-version to use any arbitrary pre-release 706 // tag, we end up with infinitely many possible names for each commit. Each 707 // name consumes resources in the module cache and proxies, so we want to 708 // restrict them to a finite set under control of the module author. 709 // 710 // We address both of these issues by requiring the tag upon which the 711 // pseudo-version is based to refer to some ancestor of the revision. We 712 // prefer the highest such tag when constructing a new pseudo-version, but do 713 // not enforce that property when resolving existing pseudo-versions: we don't 714 // know when the parent tags were added, and the highest-tagged parent may not 715 // have existed when the pseudo-version was first resolved. 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 // The base version is canonical, so if the version from the tag is 730 // literally equal (not just equivalent), then the tag is canonical too. 731 // 732 // We allow pseudo-versions to be derived from non-canonical tags on the 733 // same commit, so that tags like "v1.1.0+some-metadata" resolve as 734 // close as possible to the canonical version ("v1.1.0") while still 735 // enforcing a total ordering ("v1.1.1-0.[…]" with a unique suffix). 736 // 737 // However, canonical tags already have a total ordering, so there is no 738 // reason not to use the canonical tag directly, and we know that the 739 // canonical tag must already exist because the pseudo-version is 740 // derived from it. In that case, referring to the revision by a 741 // pseudo-version derived from its own canonical tag is just confusing. 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 // Prefer to log some real tag rather than a canonically-equivalent base. 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 // findDir locates the directory within the repo containing the module. 813 // 814 // If r.pathMajor is non-empty, this can be either r.codeDir or — if a go.mod 815 // file exists — r.codeDir/r.pathMajor[1:]. 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 // Load info about go.mod but delay consideration 823 // (except I/O error) until we rule out v2/go.mod. 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 // Suppose pathMajor is "/v2". 835 // Either go.mod should claim v2 and v2/go.mod should not exist, 836 // or v2/go.mod should exist and claim v2. Not both. 837 // Note that we don't check the full path, just the major suffix, 838 // because of replacement modules. This might be a fork of 839 // the real module, found at a different path, usable only in 840 // a replace directive. 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 // Not v2/go.mod, so it's either go.mod or nothing. Which is it? 865 if found1 { 866 // Explicit go.mod with matching major version ok. 867 return rev, r.codeDir, gomod1, nil 868 } 869 if err1 == nil { 870 // Explicit go.mod with non-matching major version disallowed. 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 != "" { // ".v1", ".v2" for gopkg.in 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 // Implicit go.mod at root of repo OK for v0/v1 and for gopkg.in. 889 return rev, "", nil, nil 890 } 891 892 // Implicit go.mod below root of repo or at v2+ disallowed. 893 // Be clear about possibility of using either location for v2+. 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 // isMajor reports whether the versions allowed for mpath are compatible with 901 // the major version(s) implied by pathMajor, or false if mpath has an invalid 902 // version suffix. 903 func isMajor(mpath, pathMajor string) bool { 904 if mpath == "" { 905 // If we don't have a path, we don't know what version(s) it is compatible with. 906 return false 907 } 908 _, mpathMajor, ok := module.SplitPathVersion(mpath) 909 if !ok { 910 // An invalid module path is not compatible with any version. 911 return false 912 } 913 if pathMajor == "" { 914 // All of the valid versions for a gopkg.in module that requires major 915 // version v0 or v1 are compatible with the "v0 or v1" implied by an empty 916 // pathMajor. 917 switch module.PathMajorPrefix(mpathMajor) { 918 case "", "v0", "v1": 919 return true 920 default: 921 return false 922 } 923 } 924 if mpathMajor == "" { 925 // Even if pathMajor is ".v0" or ".v1", we can't be sure that a module 926 // without a suffix is tagged appropriately. Besides, we don't expect clones 927 // of non-gopkg.in modules to have gopkg.in paths, so a non-empty, 928 // non-gopkg.in mpath is probably the wrong module for any such pathMajor 929 // anyway. 930 return false 931 } 932 // If both pathMajor and mpathMajor are non-empty, then we only care that they 933 // have the same major-version validation rules. A clone fetched via a /v2 934 // path might replace a module with path gopkg.in/foo.v2-unstable, and that's 935 // ok. 936 return pathMajor[1:] == mpathMajor[1:] 937 } 938 939 // canReplaceMismatchedVersionDueToBug reports whether versions of r 940 // could replace versions of mpath with otherwise-mismatched major versions 941 // due to a historical bug in the Go command (golang.org/issue/34254). 942 func (r *codeRepo) canReplaceMismatchedVersionDueToBug(mpath string) bool { 943 // The bug caused us to erroneously accept unversioned paths as replacements 944 // for versioned gopkg.in paths. 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 // findDir ignores the metadata encoded in a pseudo-version, 957 // only using the revision at the end. 958 // Invoke Stat to verify the metadata explicitly so we don't return 959 // a bogus file for an invalid version. 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 // LegacyGoMod generates a fake go.mod file for a module that doesn't have one. 984 // The go.mod file contains a module directive and nothing else: no go version, 985 // no requirements. 986 // 987 // We used to try to build a go.mod reflecting pre-existing 988 // package management metadata files, but the conversion 989 // was inherently imperfect (because those files don't have 990 // exactly the same semantics as go.mod) and, when done 991 // for dependencies in the middle of a build, impossible to 992 // correct. So we stopped. 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 // We're looking for the latest release tag that may list retractions in a 1011 // go.mod file. +incompatible versions necessarily do not, and they start 1012 // at major version 2 — which is higher than any version that could 1013 // validly contain a go.mod file. 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 // findDir ignores the metadata encoded in a pseudo-version, 1064 // only using the revision at the end. 1065 // Invoke Stat to verify the metadata explicitly so we don't return 1066 // a bogus file for an invalid version. 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 // Spool to local file. 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 // Translate from zip file we have to zip file we want. 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 // hasPathPrefix reports whether the path s begins with the 1202 // elements in prefix. 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 }