module.go 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840
  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. // Package module defines the module.Version type along with support code.
  5. //
  6. // The [module.Version] type is a simple Path, Version pair:
  7. //
  8. // type Version struct {
  9. // Path string
  10. // Version string
  11. // }
  12. //
  13. // There are no restrictions imposed directly by use of this structure,
  14. // but additional checking functions, most notably [Check], verify that
  15. // a particular path, version pair is valid.
  16. //
  17. // # Escaped Paths
  18. //
  19. // Module paths appear as substrings of file system paths
  20. // (in the download cache) and of web server URLs in the proxy protocol.
  21. // In general we cannot rely on file systems to be case-sensitive,
  22. // nor can we rely on web servers, since they read from file systems.
  23. // That is, we cannot rely on the file system to keep rsc.io/QUOTE
  24. // and rsc.io/quote separate. Windows and macOS don't.
  25. // Instead, we must never require two different casings of a file path.
  26. // Because we want the download cache to match the proxy protocol,
  27. // and because we want the proxy protocol to be possible to serve
  28. // from a tree of static files (which might be stored on a case-insensitive
  29. // file system), the proxy protocol must never require two different casings
  30. // of a URL path either.
  31. //
  32. // One possibility would be to make the escaped form be the lowercase
  33. // hexadecimal encoding of the actual path bytes. This would avoid ever
  34. // needing different casings of a file path, but it would be fairly illegible
  35. // to most programmers when those paths appeared in the file system
  36. // (including in file paths in compiler errors and stack traces)
  37. // in web server logs, and so on. Instead, we want a safe escaped form that
  38. // leaves most paths unaltered.
  39. //
  40. // The safe escaped form is to replace every uppercase letter
  41. // with an exclamation mark followed by the letter's lowercase equivalent.
  42. //
  43. // For example,
  44. //
  45. // github.com/Azure/azure-sdk-for-go -> github.com/!azure/azure-sdk-for-go.
  46. // github.com/GoogleCloudPlatform/cloudsql-proxy -> github.com/!google!cloud!platform/cloudsql-proxy
  47. // github.com/Sirupsen/logrus -> github.com/!sirupsen/logrus.
  48. //
  49. // Import paths that avoid upper-case letters are left unchanged.
  50. // Note that because import paths are ASCII-only and avoid various
  51. // problematic punctuation (like : < and >), the escaped form is also ASCII-only
  52. // and avoids the same problematic punctuation.
  53. //
  54. // Import paths have never allowed exclamation marks, so there is no
  55. // need to define how to escape a literal !.
  56. //
  57. // # Unicode Restrictions
  58. //
  59. // Today, paths are disallowed from using Unicode.
  60. //
  61. // Although paths are currently disallowed from using Unicode,
  62. // we would like at some point to allow Unicode letters as well, to assume that
  63. // file systems and URLs are Unicode-safe (storing UTF-8), and apply
  64. // the !-for-uppercase convention for escaping them in the file system.
  65. // But there are at least two subtle considerations.
  66. //
  67. // First, note that not all case-fold equivalent distinct runes
  68. // form an upper/lower pair.
  69. // For example, U+004B ('K'), U+006B ('k'), and U+212A ('K' for Kelvin)
  70. // are three distinct runes that case-fold to each other.
  71. // When we do add Unicode letters, we must not assume that upper/lower
  72. // are the only case-equivalent pairs.
  73. // Perhaps the Kelvin symbol would be disallowed entirely, for example.
  74. // Or perhaps it would escape as "!!k", or perhaps as "(212A)".
  75. //
  76. // Second, it would be nice to allow Unicode marks as well as letters,
  77. // but marks include combining marks, and then we must deal not
  78. // only with case folding but also normalization: both U+00E9 ('é')
  79. // and U+0065 U+0301 ('e' followed by combining acute accent)
  80. // look the same on the page and are treated by some file systems
  81. // as the same path. If we do allow Unicode marks in paths, there
  82. // must be some kind of normalization to allow only one canonical
  83. // encoding of any character used in an import path.
  84. package module
  85. // IMPORTANT NOTE
  86. //
  87. // This file essentially defines the set of valid import paths for the go command.
  88. // There are many subtle considerations, including Unicode ambiguity,
  89. // security, network, and file system representations.
  90. //
  91. // This file also defines the set of valid module path and version combinations,
  92. // another topic with many subtle considerations.
  93. //
  94. // Changes to the semantics in this file require approval from rsc.
  95. import (
  96. "cmp"
  97. "errors"
  98. "fmt"
  99. "path"
  100. "slices"
  101. "strings"
  102. "unicode"
  103. "unicode/utf8"
  104. "golang.org/x/mod/semver"
  105. )
  106. // A Version (for clients, a module.Version) is defined by a module path and version pair.
  107. // These are stored in their plain (unescaped) form.
  108. type Version struct {
  109. // Path is a module path, like "golang.org/x/text" or "rsc.io/quote/v2".
  110. Path string
  111. // Version is usually a semantic version in canonical form.
  112. // There are three exceptions to this general rule.
  113. // First, the top-level target of a build has no specific version
  114. // and uses Version = "".
  115. // Second, during MVS calculations the version "none" is used
  116. // to represent the decision to take no version of a given module.
  117. // Third, filesystem paths found in "replace" directives are
  118. // represented by a path with an empty version.
  119. Version string `json:",omitempty"`
  120. }
  121. // String returns a representation of the Version suitable for logging
  122. // (Path@Version, or just Path if Version is empty).
  123. func (m Version) String() string {
  124. if m.Version == "" {
  125. return m.Path
  126. }
  127. return m.Path + "@" + m.Version
  128. }
  129. // A ModuleError indicates an error specific to a module.
  130. type ModuleError struct {
  131. Path string
  132. Version string
  133. Err error
  134. }
  135. // VersionError returns a [ModuleError] derived from a [Version] and error,
  136. // or err itself if it is already such an error.
  137. func VersionError(v Version, err error) error {
  138. var mErr *ModuleError
  139. if errors.As(err, &mErr) && mErr.Path == v.Path && mErr.Version == v.Version {
  140. return err
  141. }
  142. return &ModuleError{
  143. Path: v.Path,
  144. Version: v.Version,
  145. Err: err,
  146. }
  147. }
  148. func (e *ModuleError) Error() string {
  149. if v, ok := e.Err.(*InvalidVersionError); ok {
  150. return fmt.Sprintf("%s@%s: invalid %s: %v", e.Path, v.Version, v.noun(), v.Err)
  151. }
  152. if e.Version != "" {
  153. return fmt.Sprintf("%s@%s: %v", e.Path, e.Version, e.Err)
  154. }
  155. return fmt.Sprintf("module %s: %v", e.Path, e.Err)
  156. }
  157. func (e *ModuleError) Unwrap() error { return e.Err }
  158. // An InvalidVersionError indicates an error specific to a version, with the
  159. // module path unknown or specified externally.
  160. //
  161. // A [ModuleError] may wrap an InvalidVersionError, but an InvalidVersionError
  162. // must not wrap a ModuleError.
  163. type InvalidVersionError struct {
  164. Version string
  165. Pseudo bool
  166. Err error
  167. }
  168. // noun returns either "version" or "pseudo-version", depending on whether
  169. // e.Version is a pseudo-version.
  170. func (e *InvalidVersionError) noun() string {
  171. if e.Pseudo {
  172. return "pseudo-version"
  173. }
  174. return "version"
  175. }
  176. func (e *InvalidVersionError) Error() string {
  177. return fmt.Sprintf("%s %q invalid: %s", e.noun(), e.Version, e.Err)
  178. }
  179. func (e *InvalidVersionError) Unwrap() error { return e.Err }
  180. // An InvalidPathError indicates a module, import, or file path doesn't
  181. // satisfy all naming constraints. See [CheckPath], [CheckImportPath],
  182. // and [CheckFilePath] for specific restrictions.
  183. type InvalidPathError struct {
  184. Kind string // "module", "import", or "file"
  185. Path string
  186. Err error
  187. }
  188. func (e *InvalidPathError) Error() string {
  189. return fmt.Sprintf("malformed %s path %q: %v", e.Kind, e.Path, e.Err)
  190. }
  191. func (e *InvalidPathError) Unwrap() error { return e.Err }
  192. // Check checks that a given module path, version pair is valid.
  193. // In addition to the path being a valid module path
  194. // and the version being a valid semantic version,
  195. // the two must correspond.
  196. // For example, the path "yaml/v2" only corresponds to
  197. // semantic versions beginning with "v2.".
  198. func Check(path, version string) error {
  199. if err := CheckPath(path); err != nil {
  200. return err
  201. }
  202. if !semver.IsValid(version) {
  203. return &ModuleError{
  204. Path: path,
  205. Err: &InvalidVersionError{Version: version, Err: errors.New("not a semantic version")},
  206. }
  207. }
  208. _, pathMajor, _ := SplitPathVersion(path)
  209. if err := CheckPathMajor(version, pathMajor); err != nil {
  210. return &ModuleError{Path: path, Err: err}
  211. }
  212. return nil
  213. }
  214. // firstPathOK reports whether r can appear in the first element of a module path.
  215. // The first element of the path must be an LDH domain name, at least for now.
  216. // To avoid case ambiguity, the domain name must be entirely lower case.
  217. func firstPathOK(r rune) bool {
  218. return r == '-' || r == '.' ||
  219. '0' <= r && r <= '9' ||
  220. 'a' <= r && r <= 'z'
  221. }
  222. // modPathOK reports whether r can appear in a module path element.
  223. // Paths can be ASCII letters, ASCII digits, and limited ASCII punctuation: - . _ and ~.
  224. //
  225. // This matches what "go get" has historically recognized in import paths,
  226. // and avoids confusing sequences like '%20' or '+' that would change meaning
  227. // if used in a URL.
  228. //
  229. // TODO(rsc): We would like to allow Unicode letters, but that requires additional
  230. // care in the safe encoding (see "escaped paths" above).
  231. func modPathOK(r rune) bool {
  232. if r < utf8.RuneSelf {
  233. return r == '-' || r == '.' || r == '_' || r == '~' ||
  234. '0' <= r && r <= '9' ||
  235. 'A' <= r && r <= 'Z' ||
  236. 'a' <= r && r <= 'z'
  237. }
  238. return false
  239. }
  240. // importPathOK reports whether r can appear in a package import path element.
  241. //
  242. // Import paths are intermediate between module paths and file paths: we allow
  243. // disallow characters that would be confusing or ambiguous as arguments to
  244. // 'go get' (such as '@' and ' ' ), but allow certain characters that are
  245. // otherwise-unambiguous on the command line and historically used for some
  246. // binary names (such as '++' as a suffix for compiler binaries and wrappers).
  247. func importPathOK(r rune) bool {
  248. return modPathOK(r) || r == '+'
  249. }
  250. // fileNameOK reports whether r can appear in a file name.
  251. // For now we allow all Unicode letters but otherwise limit to pathOK plus a few more punctuation characters.
  252. // If we expand the set of allowed characters here, we have to
  253. // work harder at detecting potential case-folding and normalization collisions.
  254. // See note about "escaped paths" above.
  255. func fileNameOK(r rune) bool {
  256. if r < utf8.RuneSelf {
  257. // Entire set of ASCII punctuation, from which we remove characters:
  258. // ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~
  259. // We disallow some shell special characters: " ' * < > ? ` |
  260. // (Note that some of those are disallowed by the Windows file system as well.)
  261. // We also disallow path separators / : and \ (fileNameOK is only called on path element characters).
  262. // We allow spaces (U+0020) in file names.
  263. const allowed = "!#$%&()+,-.=@[]^_{}~ "
  264. if '0' <= r && r <= '9' || 'A' <= r && r <= 'Z' || 'a' <= r && r <= 'z' {
  265. return true
  266. }
  267. return strings.ContainsRune(allowed, r)
  268. }
  269. // It may be OK to add more ASCII punctuation here, but only carefully.
  270. // For example Windows disallows < > \, and macOS disallows :, so we must not allow those.
  271. return unicode.IsLetter(r)
  272. }
  273. // CheckPath checks that a module path is valid.
  274. // A valid module path is a valid import path, as checked by [CheckImportPath],
  275. // with three additional constraints.
  276. // First, the leading path element (up to the first slash, if any),
  277. // by convention a domain name, must contain only lower-case ASCII letters,
  278. // ASCII digits, dots (U+002E), and dashes (U+002D);
  279. // it must contain at least one dot and cannot start with a dash.
  280. // Second, for a final path element of the form /vN, where N looks numeric
  281. // (ASCII digits and dots) must not begin with a leading zero, must not be /v1,
  282. // and must not contain any dots. For paths beginning with "gopkg.in/",
  283. // this second requirement is replaced by a requirement that the path
  284. // follow the gopkg.in server's conventions.
  285. // Third, no path element may begin with a dot.
  286. func CheckPath(path string) (err error) {
  287. defer func() {
  288. if err != nil {
  289. err = &InvalidPathError{Kind: "module", Path: path, Err: err}
  290. }
  291. }()
  292. if err := checkPath(path, modulePath); err != nil {
  293. return err
  294. }
  295. i := strings.Index(path, "/")
  296. if i < 0 {
  297. i = len(path)
  298. }
  299. if i == 0 {
  300. return fmt.Errorf("leading slash")
  301. }
  302. if !strings.Contains(path[:i], ".") {
  303. return fmt.Errorf("missing dot in first path element")
  304. }
  305. if path[0] == '-' {
  306. return fmt.Errorf("leading dash in first path element")
  307. }
  308. for _, r := range path[:i] {
  309. if !firstPathOK(r) {
  310. return fmt.Errorf("invalid char %q in first path element", r)
  311. }
  312. }
  313. if _, _, ok := SplitPathVersion(path); !ok {
  314. return fmt.Errorf("invalid version")
  315. }
  316. return nil
  317. }
  318. // CheckImportPath checks that an import path is valid.
  319. //
  320. // A valid import path consists of one or more valid path elements
  321. // separated by slashes (U+002F). (It must not begin with nor end in a slash.)
  322. //
  323. // A valid path element is a non-empty string made up of
  324. // ASCII letters, ASCII digits, and limited ASCII punctuation: - . _ and ~.
  325. // It must not end with a dot (U+002E), nor contain two dots in a row.
  326. //
  327. // The element prefix up to the first dot must not be a reserved file name
  328. // on Windows, regardless of case (CON, com1, NuL, and so on). The element
  329. // must not have a suffix of a tilde followed by one or more ASCII digits
  330. // (to exclude paths elements that look like Windows short-names).
  331. //
  332. // CheckImportPath may be less restrictive in the future, but see the
  333. // top-level package documentation for additional information about
  334. // subtleties of Unicode.
  335. func CheckImportPath(path string) error {
  336. if err := checkPath(path, importPath); err != nil {
  337. return &InvalidPathError{Kind: "import", Path: path, Err: err}
  338. }
  339. return nil
  340. }
  341. // pathKind indicates what kind of path we're checking. Module paths,
  342. // import paths, and file paths have different restrictions.
  343. type pathKind int
  344. const (
  345. modulePath pathKind = iota
  346. importPath
  347. filePath
  348. )
  349. // checkPath checks that a general path is valid. kind indicates what
  350. // specific constraints should be applied.
  351. //
  352. // checkPath returns an error describing why the path is not valid.
  353. // Because these checks apply to module, import, and file paths,
  354. // and because other checks may be applied, the caller is expected to wrap
  355. // this error with [InvalidPathError].
  356. func checkPath(path string, kind pathKind) error {
  357. if !utf8.ValidString(path) {
  358. return fmt.Errorf("invalid UTF-8")
  359. }
  360. if path == "" {
  361. return fmt.Errorf("empty string")
  362. }
  363. if path[0] == '-' && kind != filePath {
  364. return fmt.Errorf("leading dash")
  365. }
  366. if strings.Contains(path, "//") {
  367. return fmt.Errorf("double slash")
  368. }
  369. if path[len(path)-1] == '/' {
  370. return fmt.Errorf("trailing slash")
  371. }
  372. elemStart := 0
  373. for i, r := range path {
  374. if r == '/' {
  375. if err := checkElem(path[elemStart:i], kind); err != nil {
  376. return err
  377. }
  378. elemStart = i + 1
  379. }
  380. }
  381. if err := checkElem(path[elemStart:], kind); err != nil {
  382. return err
  383. }
  384. return nil
  385. }
  386. // checkElem checks whether an individual path element is valid.
  387. func checkElem(elem string, kind pathKind) error {
  388. if elem == "" {
  389. return fmt.Errorf("empty path element")
  390. }
  391. if strings.Count(elem, ".") == len(elem) {
  392. return fmt.Errorf("invalid path element %q", elem)
  393. }
  394. if elem[0] == '.' && kind == modulePath {
  395. return fmt.Errorf("leading dot in path element")
  396. }
  397. if elem[len(elem)-1] == '.' {
  398. return fmt.Errorf("trailing dot in path element")
  399. }
  400. for _, r := range elem {
  401. ok := false
  402. switch kind {
  403. case modulePath:
  404. ok = modPathOK(r)
  405. case importPath:
  406. ok = importPathOK(r)
  407. case filePath:
  408. ok = fileNameOK(r)
  409. default:
  410. panic(fmt.Sprintf("internal error: invalid kind %v", kind))
  411. }
  412. if !ok {
  413. return fmt.Errorf("invalid char %q", r)
  414. }
  415. }
  416. // Windows disallows a bunch of path elements, sadly.
  417. // See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
  418. short := elem
  419. if i := strings.Index(short, "."); i >= 0 {
  420. short = short[:i]
  421. }
  422. for _, bad := range badWindowsNames {
  423. if strings.EqualFold(bad, short) {
  424. return fmt.Errorf("%q disallowed as path element component on Windows", short)
  425. }
  426. }
  427. if kind == filePath {
  428. // don't check for Windows short-names in file names. They're
  429. // only an issue for import paths.
  430. return nil
  431. }
  432. // Reject path components that look like Windows short-names.
  433. // Those usually end in a tilde followed by one or more ASCII digits.
  434. if tilde := strings.LastIndexByte(short, '~'); tilde >= 0 && tilde < len(short)-1 {
  435. suffix := short[tilde+1:]
  436. suffixIsDigits := true
  437. for _, r := range suffix {
  438. if r < '0' || r > '9' {
  439. suffixIsDigits = false
  440. break
  441. }
  442. }
  443. if suffixIsDigits {
  444. return fmt.Errorf("trailing tilde and digits in path element")
  445. }
  446. }
  447. return nil
  448. }
  449. // CheckFilePath checks that a slash-separated file path is valid.
  450. // The definition of a valid file path is the same as the definition
  451. // of a valid import path except that the set of allowed characters is larger:
  452. // all Unicode letters, ASCII digits, the ASCII space character (U+0020),
  453. // and the ASCII punctuation characters
  454. // “!#$%&()+,-.=@[]^_{}~”.
  455. // (The excluded punctuation characters, " * < > ? ` ' | / \ and :,
  456. // have special meanings in certain shells or operating systems.)
  457. //
  458. // CheckFilePath may be less restrictive in the future, but see the
  459. // top-level package documentation for additional information about
  460. // subtleties of Unicode.
  461. func CheckFilePath(path string) error {
  462. if err := checkPath(path, filePath); err != nil {
  463. return &InvalidPathError{Kind: "file", Path: path, Err: err}
  464. }
  465. return nil
  466. }
  467. // badWindowsNames are the reserved file path elements on Windows.
  468. // See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
  469. var badWindowsNames = []string{
  470. "CON",
  471. "PRN",
  472. "AUX",
  473. "NUL",
  474. "COM1",
  475. "COM2",
  476. "COM3",
  477. "COM4",
  478. "COM5",
  479. "COM6",
  480. "COM7",
  481. "COM8",
  482. "COM9",
  483. "LPT1",
  484. "LPT2",
  485. "LPT3",
  486. "LPT4",
  487. "LPT5",
  488. "LPT6",
  489. "LPT7",
  490. "LPT8",
  491. "LPT9",
  492. }
  493. // SplitPathVersion returns prefix and major version such that prefix+pathMajor == path
  494. // and version is either empty or "/vN" for N >= 2.
  495. // As a special case, gopkg.in paths are recognized directly;
  496. // they require ".vN" instead of "/vN", and for all N, not just N >= 2.
  497. // SplitPathVersion returns with ok = false when presented with
  498. // a path whose last path element does not satisfy the constraints
  499. // applied by [CheckPath], such as "example.com/pkg/v1" or "example.com/pkg/v1.2".
  500. func SplitPathVersion(path string) (prefix, pathMajor string, ok bool) {
  501. if strings.HasPrefix(path, "gopkg.in/") {
  502. return splitGopkgIn(path)
  503. }
  504. i := len(path)
  505. dot := false
  506. for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9' || path[i-1] == '.') {
  507. if path[i-1] == '.' {
  508. dot = true
  509. }
  510. i--
  511. }
  512. if i <= 1 || i == len(path) || path[i-1] != 'v' || path[i-2] != '/' {
  513. return path, "", true
  514. }
  515. prefix, pathMajor = path[:i-2], path[i-2:]
  516. if dot || len(pathMajor) <= 2 || pathMajor[2] == '0' || pathMajor == "/v1" {
  517. return path, "", false
  518. }
  519. return prefix, pathMajor, true
  520. }
  521. // splitGopkgIn is like SplitPathVersion but only for gopkg.in paths.
  522. func splitGopkgIn(path string) (prefix, pathMajor string, ok bool) {
  523. if !strings.HasPrefix(path, "gopkg.in/") {
  524. return path, "", false
  525. }
  526. i := len(path)
  527. if strings.HasSuffix(path, "-unstable") {
  528. i -= len("-unstable")
  529. }
  530. for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9') {
  531. i--
  532. }
  533. if i <= 1 || path[i-1] != 'v' || path[i-2] != '.' {
  534. // All gopkg.in paths must end in vN for some N.
  535. return path, "", false
  536. }
  537. prefix, pathMajor = path[:i-2], path[i-2:]
  538. if len(pathMajor) <= 2 || pathMajor[2] == '0' && pathMajor != ".v0" {
  539. return path, "", false
  540. }
  541. return prefix, pathMajor, true
  542. }
  543. // MatchPathMajor reports whether the semantic version v
  544. // matches the path major version pathMajor.
  545. //
  546. // MatchPathMajor returns true if and only if [CheckPathMajor] returns nil.
  547. func MatchPathMajor(v, pathMajor string) bool {
  548. return CheckPathMajor(v, pathMajor) == nil
  549. }
  550. // CheckPathMajor returns a non-nil error if the semantic version v
  551. // does not match the path major version pathMajor.
  552. func CheckPathMajor(v, pathMajor string) error {
  553. // TODO(jayconrod): return errors or panic for invalid inputs. This function
  554. // (and others) was covered by integration tests for cmd/go, and surrounding
  555. // code protected against invalid inputs like non-canonical versions.
  556. if strings.HasPrefix(pathMajor, ".v") && strings.HasSuffix(pathMajor, "-unstable") {
  557. pathMajor = strings.TrimSuffix(pathMajor, "-unstable")
  558. }
  559. if strings.HasPrefix(v, "v0.0.0-") && pathMajor == ".v1" {
  560. // Allow old bug in pseudo-versions that generated v0.0.0- pseudoversion for gopkg .v1.
  561. // For example, gopkg.in/yaml.v2@v2.2.1's go.mod requires gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405.
  562. return nil
  563. }
  564. m := semver.Major(v)
  565. if pathMajor == "" {
  566. if m == "v0" || m == "v1" || semver.Build(v) == "+incompatible" {
  567. return nil
  568. }
  569. pathMajor = "v0 or v1"
  570. } else if pathMajor[0] == '/' || pathMajor[0] == '.' {
  571. if m == pathMajor[1:] {
  572. return nil
  573. }
  574. pathMajor = pathMajor[1:]
  575. }
  576. return &InvalidVersionError{
  577. Version: v,
  578. Err: fmt.Errorf("should be %s, not %s", pathMajor, semver.Major(v)),
  579. }
  580. }
  581. // PathMajorPrefix returns the major-version tag prefix implied by pathMajor.
  582. // An empty PathMajorPrefix allows either v0 or v1.
  583. //
  584. // Note that [MatchPathMajor] may accept some versions that do not actually begin
  585. // with this prefix: namely, it accepts a 'v0.0.0-' prefix for a '.v1'
  586. // pathMajor, even though that pathMajor implies 'v1' tagging.
  587. func PathMajorPrefix(pathMajor string) string {
  588. if pathMajor == "" {
  589. return ""
  590. }
  591. if pathMajor[0] != '/' && pathMajor[0] != '.' {
  592. panic("pathMajor suffix " + pathMajor + " passed to PathMajorPrefix lacks separator")
  593. }
  594. if strings.HasPrefix(pathMajor, ".v") && strings.HasSuffix(pathMajor, "-unstable") {
  595. pathMajor = strings.TrimSuffix(pathMajor, "-unstable")
  596. }
  597. m := pathMajor[1:]
  598. if m != semver.Major(m) {
  599. panic("pathMajor suffix " + pathMajor + "passed to PathMajorPrefix is not a valid major version")
  600. }
  601. return m
  602. }
  603. // CanonicalVersion returns the canonical form of the version string v.
  604. // It is the same as [semver.Canonical] except that it preserves the special build suffix "+incompatible".
  605. func CanonicalVersion(v string) string {
  606. cv := semver.Canonical(v)
  607. if semver.Build(v) == "+incompatible" {
  608. cv += "+incompatible"
  609. }
  610. return cv
  611. }
  612. // Sort sorts the list by Path, breaking ties by comparing [Version] fields.
  613. // The Version fields are interpreted as semantic versions (using [semver.Compare])
  614. // optionally followed by a tie-breaking suffix introduced by a slash character,
  615. // like in "v0.0.1/go.mod".
  616. func Sort(list []Version) {
  617. slices.SortFunc(list, func(i, j Version) int {
  618. if i.Path != j.Path {
  619. return strings.Compare(i.Path, j.Path)
  620. }
  621. // To help go.sum formatting, allow version/file.
  622. // Compare semver prefix by semver rules,
  623. // file by string order.
  624. vi := i.Version
  625. vj := j.Version
  626. var fi, fj string
  627. if k := strings.Index(vi, "/"); k >= 0 {
  628. vi, fi = vi[:k], vi[k:]
  629. }
  630. if k := strings.Index(vj, "/"); k >= 0 {
  631. vj, fj = vj[:k], vj[k:]
  632. }
  633. if vi != vj {
  634. return semver.Compare(vi, vj)
  635. }
  636. return cmp.Compare(fi, fj)
  637. })
  638. }
  639. // EscapePath returns the escaped form of the given module path.
  640. // It fails if the module path is invalid.
  641. func EscapePath(path string) (escaped string, err error) {
  642. if err := CheckPath(path); err != nil {
  643. return "", err
  644. }
  645. return escapeString(path)
  646. }
  647. // EscapeVersion returns the escaped form of the given module version.
  648. // Versions are allowed to be in non-semver form but must be valid file names
  649. // and not contain exclamation marks.
  650. func EscapeVersion(v string) (escaped string, err error) {
  651. if err := checkElem(v, filePath); err != nil || strings.Contains(v, "!") {
  652. return "", &InvalidVersionError{
  653. Version: v,
  654. Err: fmt.Errorf("disallowed version string"),
  655. }
  656. }
  657. return escapeString(v)
  658. }
  659. func escapeString(s string) (escaped string, err error) {
  660. haveUpper := false
  661. for _, r := range s {
  662. if r == '!' || r >= utf8.RuneSelf {
  663. // This should be disallowed by CheckPath, but diagnose anyway.
  664. // The correctness of the escaping loop below depends on it.
  665. return "", fmt.Errorf("internal error: inconsistency in EscapePath")
  666. }
  667. if 'A' <= r && r <= 'Z' {
  668. haveUpper = true
  669. }
  670. }
  671. if !haveUpper {
  672. return s, nil
  673. }
  674. var buf []byte
  675. for _, r := range s {
  676. if 'A' <= r && r <= 'Z' {
  677. buf = append(buf, '!', byte(r+'a'-'A'))
  678. } else {
  679. buf = append(buf, byte(r))
  680. }
  681. }
  682. return string(buf), nil
  683. }
  684. // UnescapePath returns the module path for the given escaped path.
  685. // It fails if the escaped path is invalid or describes an invalid path.
  686. func UnescapePath(escaped string) (path string, err error) {
  687. path, ok := unescapeString(escaped)
  688. if !ok {
  689. return "", fmt.Errorf("invalid escaped module path %q", escaped)
  690. }
  691. if err := CheckPath(path); err != nil {
  692. return "", fmt.Errorf("invalid escaped module path %q: %v", escaped, err)
  693. }
  694. return path, nil
  695. }
  696. // UnescapeVersion returns the version string for the given escaped version.
  697. // It fails if the escaped form is invalid or describes an invalid version.
  698. // Versions are allowed to be in non-semver form but must be valid file names
  699. // and not contain exclamation marks.
  700. func UnescapeVersion(escaped string) (v string, err error) {
  701. v, ok := unescapeString(escaped)
  702. if !ok {
  703. return "", fmt.Errorf("invalid escaped version %q", escaped)
  704. }
  705. if err := checkElem(v, filePath); err != nil {
  706. return "", fmt.Errorf("invalid escaped version %q: %v", v, err)
  707. }
  708. return v, nil
  709. }
  710. func unescapeString(escaped string) (string, bool) {
  711. var buf []byte
  712. bang := false
  713. for _, r := range escaped {
  714. if r >= utf8.RuneSelf {
  715. return "", false
  716. }
  717. if bang {
  718. bang = false
  719. if r < 'a' || 'z' < r {
  720. return "", false
  721. }
  722. buf = append(buf, byte(r+'A'-'a'))
  723. continue
  724. }
  725. if r == '!' {
  726. bang = true
  727. continue
  728. }
  729. if 'A' <= r && r <= 'Z' {
  730. return "", false
  731. }
  732. buf = append(buf, byte(r))
  733. }
  734. if bang {
  735. return "", false
  736. }
  737. return string(buf), true
  738. }
  739. // MatchPrefixPatterns reports whether any path prefix of target matches one of
  740. // the glob patterns (as defined by [path.Match]) in the comma-separated globs
  741. // list. This implements the algorithm used when matching a module path to the
  742. // GOPRIVATE environment variable, as described by 'go help module-private'.
  743. //
  744. // It ignores any empty or malformed patterns in the list.
  745. // Trailing slashes on patterns are ignored.
  746. func MatchPrefixPatterns(globs, target string) bool {
  747. for globs != "" {
  748. // Extract next non-empty glob in comma-separated list.
  749. var glob string
  750. if i := strings.Index(globs, ","); i >= 0 {
  751. glob, globs = globs[:i], globs[i+1:]
  752. } else {
  753. glob, globs = globs, ""
  754. }
  755. glob = strings.TrimSuffix(glob, "/")
  756. if glob == "" {
  757. continue
  758. }
  759. // A glob with N+1 path elements (N slashes) needs to be matched
  760. // against the first N+1 path elements of target,
  761. // which end just before the N+1'th slash.
  762. n := strings.Count(glob, "/")
  763. prefix := target
  764. // Walk target, counting slashes, truncating at the N+1'th slash.
  765. for i := 0; i < len(target); i++ {
  766. if target[i] == '/' {
  767. if n == 0 {
  768. prefix = target[:i]
  769. break
  770. }
  771. n--
  772. }
  773. }
  774. if n > 0 {
  775. // Not enough prefix elements.
  776. continue
  777. }
  778. matched, _ := path.Match(glob, prefix)
  779. if matched {
  780. return true
  781. }
  782. }
  783. return false
  784. }