1# frozen_string_literal: true
2
3module Gitlab
4  module Golang
5    PseudoVersion = Struct.new(:semver, :timestamp, :commit_id)
6
7    extend self
8
9    def local_module_prefix
10      @gitlab_prefix ||= "#{Settings.build_gitlab_go_url}/"
11    end
12
13    def semver_tag?(tag)
14      return false if tag.dereferenced_target.nil?
15
16      Packages::SemVer.match?(tag.name, prefixed: true)
17    end
18
19    def pseudo_version?(version)
20      return false unless version
21
22      if version.is_a? String
23        version = parse_semver version
24        return false unless version
25      end
26
27      pre = version.prerelease
28
29      # Valid pseudo-versions are:
30      #   vX.0.0-yyyymmddhhmmss-sha1337beef0, when no earlier tagged commit exists for X
31      #   vX.Y.Z-pre.0.yyyymmddhhmmss-sha1337beef0, when most recent prior tag is vX.Y.Z-pre
32      #   vX.Y.(Z+1)-0.yyyymmddhhmmss-sha1337beef0, when most recent prior tag is vX.Y.Z
33
34      if version.minor != 0 || version.patch != 0
35        m = /\A(.*\.)?0\./.freeze.match pre
36        return false unless m
37
38        pre = pre[m[0].length..]
39      end
40
41      # This pattern is intentionally more forgiving than the patterns
42      # above. Correctness is verified by #validate_pseudo_version.
43      /\A\d{14}-\h+\z/.freeze.match? pre
44    end
45
46    def parse_pseudo_version(semver)
47      # Per Go's implementation of pseudo-versions, a tag should be
48      # considered a pseudo-version if it matches one of the patterns
49      # listed in #pseudo_version?, regardless of the content of the
50      # timestamp or the length of the SHA fragment. However, an error
51      # should be returned if the timestamp is not correct or if the SHA
52      # fragment is not exactly 12 characters long. See also Go's
53      # implementation of:
54      #
55      # - [*codeRepo.validatePseudoVersion](https://github.com/golang/go/blob/daf70d6c1688a1ba1699c933b3c3f04d6f2f73d9/src/cmd/go/internal/modfetch/coderepo.go#L530)
56      # - [Pseudo-version parsing](https://github.com/golang/go/blob/master/src/cmd/go/internal/modfetch/pseudo.go)
57      # - [Pseudo-version request processing](https://github.com/golang/go/blob/master/src/cmd/go/internal/modfetch/coderepo.go)
58
59      # Go ignores anything before '.' or after the second '-', so we will do the same
60      timestamp, commit_id = semver.prerelease.split('-').last 2
61      timestamp = timestamp.split('.').last
62
63      PseudoVersion.new(semver, timestamp, commit_id)
64    end
65
66    def validate_pseudo_version(project, version, commit = nil)
67      commit ||= project.repository.commit_by(oid: version.commit_id)
68
69      # Error messages are based on the responses of proxy.golang.org
70
71      # Verify that the SHA fragment references a commit
72      raise ArgumentError, 'invalid pseudo-version: unknown commit' unless commit
73
74      # Require the SHA fragment to be 12 characters long
75      raise ArgumentError, 'invalid pseudo-version: revision is shorter than canonical' unless version.commit_id.length == 12
76
77      # Require the timestamp to match that of the commit
78      raise ArgumentError, 'invalid pseudo-version: does not match version-control timestamp' unless commit.committed_date.strftime('%Y%m%d%H%M%S') == version.timestamp
79
80      commit
81    end
82
83    def parse_semver(str)
84      Packages::SemVer.parse(str, prefixed: true)
85    end
86
87    def go_path(project, path = nil)
88      if path.blank?
89        "#{local_module_prefix}/#{project.full_path}"
90      else
91        "#{local_module_prefix}/#{project.full_path}/#{path}"
92      end
93    end
94
95    def pkg_go_dev_url(name, version = nil)
96      if version
97        "https://pkg.go.dev/#{name}@#{version}"
98      else
99        "https://pkg.go.dev/#{name}"
100      end
101    end
102
103    def package_url(name, version = nil)
104      return unless UrlSanitizer.valid?("https://#{name}")
105
106      return pkg_go_dev_url(name, version) unless name.starts_with?(local_module_prefix)
107
108      # This will not work if `name` refers to a subdirectory of a project. This
109      # could be expanded with logic similar to Gitlab::Middleware::Go to locate
110      # the project, check for permissions, and return a smarter result.
111      "#{Gitlab.config.gitlab.protocol}://#{name}/"
112    end
113  end
114end
115