docker_distribution 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +27 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Dockerfile +12 -0
- data/Gemfile +19 -0
- data/Gemfile.lock +60 -0
- data/LICENSE.txt +21 -0
- data/Makefile +9 -0
- data/README.md +108 -0
- data/Rakefile +16 -0
- data/docker-compose.yml +26 -0
- data/lib/docker_distribution/canonical_reference.rb +32 -0
- data/lib/docker_distribution/digest.rb +50 -0
- data/lib/docker_distribution/digest_reference.rb +19 -0
- data/lib/docker_distribution/digest_set.rb +83 -0
- data/lib/docker_distribution/errors.rb +30 -0
- data/lib/docker_distribution/helpers.rb +62 -0
- data/lib/docker_distribution/normalize.rb +125 -0
- data/lib/docker_distribution/reference.rb +178 -0
- data/lib/docker_distribution/regexp.rb +226 -0
- data/lib/docker_distribution/repository.rb +34 -0
- data/lib/docker_distribution/tagged_reference.rb +32 -0
- data/lib/docker_distribution/version.rb +5 -0
- data/lib/docker_distribution.rb +47 -0
- data/sig/docker_distribution.rbs +4 -0
- metadata +85 -0
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DockerDistribution
|
4
|
+
class Normalize
|
5
|
+
LEGACY_DEFAULT_DOMAIN = "index.docker.io"
|
6
|
+
DEFAULT_DOMAIN = "docker.io"
|
7
|
+
OFFICIAL_REPO_NAME = "library"
|
8
|
+
DEFAULT_TAG = "latest"
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# ParseNormalizedNamed parses a string into a named reference
|
12
|
+
# transforming a familiar name from Docker UI to a fully
|
13
|
+
# qualified reference. If the value may be an identifier
|
14
|
+
# use ParseAnyReference.
|
15
|
+
def parse_normalized_named(str)
|
16
|
+
raise ParseNormalizedNamedError if Regexp.anchored_identifier_regexp.match?(str)
|
17
|
+
|
18
|
+
domain, remainder = split_docker_domain(str)
|
19
|
+
|
20
|
+
tag_sep = remainder.index(":") || -1
|
21
|
+
remote_name = tag_sep > -1 ? Helpers.to(remainder, tag_sep) : remainder
|
22
|
+
raise ParseNormalizedNamedError if remote_name != remote_name.downcase
|
23
|
+
|
24
|
+
ref = DockerDistribution::Reference.parse("#{domain}/#{remainder}")
|
25
|
+
raise ParseNormalizedNamedError if Helpers.empty?(ref)
|
26
|
+
|
27
|
+
ref
|
28
|
+
end
|
29
|
+
|
30
|
+
# ParseDockerRef normalizes the image reference following the docker convention. This is added
|
31
|
+
# mainly for backward compatibility.
|
32
|
+
# The reference returned can only be either tagged or digested. For reference contains both tag
|
33
|
+
# and digest, the function returns digested reference, e.g. docker.io/library/busybox:latest@
|
34
|
+
# sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa will be returned as
|
35
|
+
# docker.io/library/busybox@sha256:7cc4b5aefd1d0cadf8d97d4350462ba51c694ebca145b08d7d41b41acc8db5aa.
|
36
|
+
def parse_docker_ref(ref)
|
37
|
+
named = parse_normalized_named(ref)
|
38
|
+
if Helpers.tagged?(named) && Helpers.canonical?(named)
|
39
|
+
new_named = Reference.with_name(named.name)
|
40
|
+
new_canonical = Reference.with_digest(new_named, named.digest)
|
41
|
+
return new_canonical
|
42
|
+
end
|
43
|
+
|
44
|
+
tag_name_only(named)
|
45
|
+
end
|
46
|
+
|
47
|
+
# splitDockerDomain splits a repository name to domain and remote name string.
|
48
|
+
# If no valid domain is found, the default domain is used. Repository name
|
49
|
+
# needs to be already validated before.
|
50
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
51
|
+
def split_docker_domain(name)
|
52
|
+
i = name.index("/") || -1
|
53
|
+
|
54
|
+
start_part = Helpers.to(name, i)
|
55
|
+
end_part = Helpers.from(name, i + 1)
|
56
|
+
|
57
|
+
if (i == -1 || [".", ":"].none? do |sep|
|
58
|
+
start_part.include?(sep)
|
59
|
+
end) && start_part != "localhost" && start_part.downcase == start_part
|
60
|
+
domain = DEFAULT_DOMAIN
|
61
|
+
remainder = name
|
62
|
+
else
|
63
|
+
domain = start_part
|
64
|
+
remainder = end_part
|
65
|
+
end
|
66
|
+
|
67
|
+
domain = DEFAULT_DOMAIN if domain == LEGACY_DEFAULT_DOMAIN
|
68
|
+
remainder = [OFFICIAL_REPO_NAME, remainder].join("/") if domain == DEFAULT_DOMAIN && !remainder.include?("/")
|
69
|
+
|
70
|
+
[domain, remainder]
|
71
|
+
end
|
72
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
73
|
+
|
74
|
+
# familiarizeName returns a shortened version of the name familiar
|
75
|
+
# to to the Docker UI. Familiar names have the default domain
|
76
|
+
# "docker.io" and "library/" repository prefix removed.
|
77
|
+
# For example, "docker.io/library/redis" will have the familiar
|
78
|
+
# name "redis" and "docker.io/dmcgowan/myapp" will be "dmcgowan/myapp".
|
79
|
+
# Returns a familiarized named only reference.
|
80
|
+
def familiarize_name(repo)
|
81
|
+
familiar_repo = Repository.new(repo.domain, repo.path)
|
82
|
+
|
83
|
+
if familiar_repo.domain == DEFAULT_DOMAIN
|
84
|
+
familiar_repo.domain = nil
|
85
|
+
parts = familiar_repo.path.split("/")
|
86
|
+
familiar_repo.path = parts[1] if parts.length == 2 && parts[0] == OFFICIAL_REPO_NAME
|
87
|
+
end
|
88
|
+
familiar_repo
|
89
|
+
end
|
90
|
+
|
91
|
+
# TagNameOnly adds the default tag "latest" to a reference if it only has
|
92
|
+
# a repo name.
|
93
|
+
def tag_name_only(ref)
|
94
|
+
return Reference.with_tag(ref, DEFAULT_TAG) if Helpers.name_only?(ref)
|
95
|
+
|
96
|
+
ref
|
97
|
+
end
|
98
|
+
|
99
|
+
# ParseAnyReference parses a reference string as a possible identifier,
|
100
|
+
# full digest, or familiar name.
|
101
|
+
def parse_any_reference(ref)
|
102
|
+
return DigestReference.new("sha256:#{ref}") if Regexp.anchored_identifier_regexp.match?(ref)
|
103
|
+
|
104
|
+
digest = Digest.parse!(ref)
|
105
|
+
DigestReference.new(digest.digest)
|
106
|
+
rescue DigestError
|
107
|
+
parse_normalized_named(ref)
|
108
|
+
end
|
109
|
+
|
110
|
+
# ParseAnyReferenceWithSet parses a reference string as a possible short
|
111
|
+
# identifier to be matched in a digest set, a full digest, or familiar name.
|
112
|
+
def parse_any_reference_with_set(ref, digest_set)
|
113
|
+
if Regexp.anchored_short_identifier_regexp.match?(ref)
|
114
|
+
dgst = digest_set.lookup!(ref)
|
115
|
+
return DigestReference.new(dgst) if dgst
|
116
|
+
else
|
117
|
+
dgst = Digest.parse!(ref)
|
118
|
+
DigestReference.new(dgst.digest)
|
119
|
+
end
|
120
|
+
rescue DigestError
|
121
|
+
parse_normalized_named(ref)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity, Naming/MethodParameterName
|
4
|
+
module DockerDistribution
|
5
|
+
class Reference
|
6
|
+
MAX_NAME_TOTAL_LENGTH = 255
|
7
|
+
extend ::Forwardable
|
8
|
+
def_delegators :repository, :name, :domain, :path
|
9
|
+
|
10
|
+
attr_accessor :repository, :tag, :digest
|
11
|
+
|
12
|
+
def initialize(repository = nil, tag = nil, digest = nil)
|
13
|
+
@repository = repository
|
14
|
+
@tag = tag
|
15
|
+
@digest = digest
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
[name, ":", tag, "@", digest].join("")
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_h
|
23
|
+
{
|
24
|
+
repository: name,
|
25
|
+
domain: domain,
|
26
|
+
path: path,
|
27
|
+
tag: tag,
|
28
|
+
digest: digest
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def familiar
|
33
|
+
self.class.new(Normalize.familiarize_name(self), tag, digest)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Parse parses s and returns a syntactically valid Reference.
|
37
|
+
# If an error was encountered it is returned, along with a nil Reference.
|
38
|
+
# NOTE: Parse will not handle short digests.
|
39
|
+
def self.parse(s)
|
40
|
+
matches = Regexp.reference_regexp.match(s)
|
41
|
+
if matches.nil?
|
42
|
+
raise NameEmpty if Helpers.empty?(s)
|
43
|
+
raise NameContainsUppercase unless Regexp.reference_regexp.match(s.downcase).nil?
|
44
|
+
|
45
|
+
raise ReferenceInvalidFormat
|
46
|
+
end
|
47
|
+
|
48
|
+
match_results = matches.to_a
|
49
|
+
raise NameTooLong if match_results[1] && match_results[1].length > MAX_NAME_TOTAL_LENGTH
|
50
|
+
|
51
|
+
repo = Repository.new
|
52
|
+
name_match = Regexp.anchored_name_regexp.match(match_results[1])
|
53
|
+
name_match_results = name_match&.to_a
|
54
|
+
|
55
|
+
if name_match_results && name_match_results.length == 3
|
56
|
+
repo.domain = name_match_results[1]
|
57
|
+
repo.path = name_match_results[2]
|
58
|
+
else
|
59
|
+
repo.domain = nil
|
60
|
+
repo.path = name_match_results[1]
|
61
|
+
end
|
62
|
+
|
63
|
+
ref = Reference.new(repo, match_results[2])
|
64
|
+
|
65
|
+
unless Helpers.empty?(match_results[3])
|
66
|
+
digest = Digest.parse!(match_results[3])
|
67
|
+
ref.digest = digest.digest
|
68
|
+
end
|
69
|
+
|
70
|
+
r = get_best_reference_type(ref)
|
71
|
+
raise NameEmpty if r.nil?
|
72
|
+
|
73
|
+
r
|
74
|
+
end
|
75
|
+
|
76
|
+
# ParseNamed parses s and returns a syntactically valid reference implementing
|
77
|
+
# the Named interface. The reference must have a name and be in the canonical
|
78
|
+
# form, otherwise an error is returned.
|
79
|
+
# If an error was encountered it is returned, along with a nil Reference.
|
80
|
+
# NOTE: ParseNamed will not handle short digests.
|
81
|
+
def self.parse_named(str)
|
82
|
+
named = Normalize.parse_normalized_named(str)
|
83
|
+
raise NameNotCanonical if named.to_s != str
|
84
|
+
|
85
|
+
named
|
86
|
+
end
|
87
|
+
|
88
|
+
# WithName returns a named object representing the given string. If the input
|
89
|
+
# is invalid ErrReferenceInvalidFormat will be returned.
|
90
|
+
def self.with_name(name)
|
91
|
+
raise NameEmpty if Helpers.empty?(name)
|
92
|
+
raise NameTooLong if name.length > MAX_NAME_TOTAL_LENGTH
|
93
|
+
|
94
|
+
match = Regexp.anchored_name_regexp.match(name)
|
95
|
+
raise ReferenceInvalidFormat if match.nil? || match.to_a.length != 3
|
96
|
+
|
97
|
+
match_result = match.to_a
|
98
|
+
Repository.new(match_result[1], match_result[2])
|
99
|
+
end
|
100
|
+
|
101
|
+
# WithTag combines the name from "name" and the tag from "tag" to form a
|
102
|
+
# reference incorporating both the name and the tag.
|
103
|
+
def self.with_tag(named, tag)
|
104
|
+
match = Regexp.anchored_tag_regexp.match(tag)
|
105
|
+
raise TagInvalidFormat if match.nil?
|
106
|
+
|
107
|
+
repo = Repository.new
|
108
|
+
if named.is_a?(Repository)
|
109
|
+
repo.domain = named.domain
|
110
|
+
repo.path = named.path
|
111
|
+
else
|
112
|
+
repo.path = named.name
|
113
|
+
end
|
114
|
+
|
115
|
+
return Reference.new(repo, tag, named.digest) if named.is_a?(CanonicalReference)
|
116
|
+
|
117
|
+
TaggedReference.new(repo, tag)
|
118
|
+
end
|
119
|
+
|
120
|
+
# WithDigest combines the name from "name" and the digest from "digest" to form
|
121
|
+
# a reference incorporating both the name and the digest.
|
122
|
+
def self.with_digest(named, digest)
|
123
|
+
match = Regexp.anchored_digest_regexp.match(digest)
|
124
|
+
raise DigestInvalidFormat if match.nil?
|
125
|
+
|
126
|
+
repo = Repository.new
|
127
|
+
if named.is_a?(Repository)
|
128
|
+
repo.domain = named.domain
|
129
|
+
repo.path = named.path
|
130
|
+
else
|
131
|
+
repo.path = named.name
|
132
|
+
end
|
133
|
+
|
134
|
+
return Reference.new(repo, named.tag, digest) if named.is_a?(TaggedReference)
|
135
|
+
|
136
|
+
CanonicalReference.new(repo, digest)
|
137
|
+
end
|
138
|
+
|
139
|
+
# TrimNamed removes any tag or digest from the named reference.
|
140
|
+
def self.trim_named(ref)
|
141
|
+
domain, path = split_hostname(ref)
|
142
|
+
new Repository(domain, path)
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.get_best_reference_type(ref)
|
146
|
+
if Helpers.empty?(ref.name)
|
147
|
+
return DigestReference.new(ref.digest) unless Helpers.empty?(ref.digest)
|
148
|
+
|
149
|
+
return nil
|
150
|
+
end
|
151
|
+
|
152
|
+
if Helpers.empty?(ref.tag)
|
153
|
+
return CanonicalReference.new(ref.repository, ref.digest) unless Helpers.empty?(ref.digest)
|
154
|
+
|
155
|
+
return ref.repository
|
156
|
+
end
|
157
|
+
|
158
|
+
return TaggedReference.new(ref.repository, ref.tag) if Helpers.empty?(ref.digest)
|
159
|
+
|
160
|
+
ref
|
161
|
+
end
|
162
|
+
|
163
|
+
def self.split_domain(name)
|
164
|
+
match = Regexp.anchored_name_regexp.match(name)
|
165
|
+
return [nil, name] if match && match.to_a.length != 3
|
166
|
+
|
167
|
+
match_results = match.to_a
|
168
|
+
[match_results[1], match_results[2]]
|
169
|
+
end
|
170
|
+
|
171
|
+
def self.split_hostname(named)
|
172
|
+
return [named.domain, named.path] if named.is_a?(Repository)
|
173
|
+
|
174
|
+
split_domain(name)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
# rubocop:enable all
|
@@ -0,0 +1,226 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Metrics/ClassLength
|
4
|
+
module DockerDistribution
|
5
|
+
# Representation of regxexps we will use
|
6
|
+
class Regexp
|
7
|
+
class << self
|
8
|
+
# alphaNumeric defines the alpha numeric atom, typically a
|
9
|
+
# component of names. This only allows lower case characters and digits.
|
10
|
+
def alpha_numeric
|
11
|
+
"[a-z0-9]+"
|
12
|
+
end
|
13
|
+
|
14
|
+
# separator defines the separators allowed to be embedded in name
|
15
|
+
# components. This allow one period, one or two underscore and multiple
|
16
|
+
# dashes. Repeated dashes and underscores are intentionally treated
|
17
|
+
# differently. In order to support valid hostnames as name components,
|
18
|
+
# supporting repeated dash was added. Additionally double underscore is
|
19
|
+
# now allowed as a separator to loosen the restriction for previously
|
20
|
+
# supported names.
|
21
|
+
def separator
|
22
|
+
"(?:[._]|__|[-]*)"
|
23
|
+
end
|
24
|
+
|
25
|
+
# nameComponent restricts registry path component names to start
|
26
|
+
# with at least one letter or number, with following parts able to be
|
27
|
+
# separated by one period, one or two underscore and multiple dashes.
|
28
|
+
def name_component
|
29
|
+
expression(alpha_numeric, optional(repeated(separator, alpha_numeric)))
|
30
|
+
end
|
31
|
+
|
32
|
+
# domainNameComponent restricts the registry domain component of a
|
33
|
+
# repository name to start with a component as defined by DomainRegexp.
|
34
|
+
def domain_name_component
|
35
|
+
"(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])"
|
36
|
+
end
|
37
|
+
|
38
|
+
# ipv6address are enclosed between square brackets and may be represented
|
39
|
+
# in many ways, see rfc5952. Only IPv6 in compressed or uncompressed format
|
40
|
+
# are allowed, IPv6 zone identifiers (rfc6874) or Special addresses such as
|
41
|
+
# IPv4-Mapped are deliberately excluded.
|
42
|
+
def ipv6address
|
43
|
+
expression(literal("["), "(?:[a-fA-F0-9:]+)", literal("]"))
|
44
|
+
end
|
45
|
+
|
46
|
+
# domainName defines the structure of potential domain components
|
47
|
+
# that may be part of image names. This is purposely a subset of what is
|
48
|
+
# allowed by DNS to ensure backwards compatibility with Docker image
|
49
|
+
# names. This includes IPv4 addresses on decimal format.
|
50
|
+
def domain_name
|
51
|
+
expression(domain_name_component, optional(repeated(literal("."), domain_name_component)))
|
52
|
+
end
|
53
|
+
|
54
|
+
# host defines the structure of potential domains based on the URI
|
55
|
+
# Host subcomponent on rfc3986. It may be a subset of DNS domain name,
|
56
|
+
# or an IPv4 address in decimal format, or an IPv6 address between square
|
57
|
+
# brackets (excluding zone identifiers as defined by rfc6874 or special
|
58
|
+
# addresses such as IPv4-Mapped).
|
59
|
+
def host
|
60
|
+
"(?:#{domain_name}|#{ipv6address})"
|
61
|
+
end
|
62
|
+
|
63
|
+
# allowed by the URI Host subcomponent on rfc3986 to ensure backwards
|
64
|
+
# compatibility with Docker image names.
|
65
|
+
def domain
|
66
|
+
expression(host, optional(literal(":"), "[0-9]+"))
|
67
|
+
end
|
68
|
+
|
69
|
+
# DomainRegexp defines the structure of potential domain components that may be part of image names.
|
70
|
+
# This is purposely a subset of what is allowed by DNS to ensure backwards compatibility with Docker image names.
|
71
|
+
def domain_regexp
|
72
|
+
@domain_regexp ||= ::Regexp.new(domain)
|
73
|
+
end
|
74
|
+
|
75
|
+
def tag
|
76
|
+
"[\\w][\\w.-]{0,127}"
|
77
|
+
end
|
78
|
+
|
79
|
+
# TagRegexp matches valid tag names
|
80
|
+
def tag_regexp
|
81
|
+
::Regexp.new(tag)
|
82
|
+
end
|
83
|
+
|
84
|
+
def anchored_tag
|
85
|
+
anchored(tag)
|
86
|
+
end
|
87
|
+
|
88
|
+
# anchoredTagRegexp matches valid tag names, anchored at the start and end of the matched string.
|
89
|
+
def anchored_tag_regexp
|
90
|
+
::Regexp.new(anchored_tag)
|
91
|
+
end
|
92
|
+
|
93
|
+
def digest_pat
|
94
|
+
"[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}"
|
95
|
+
end
|
96
|
+
|
97
|
+
# DigestRegexp matches valid digests.
|
98
|
+
def digest_regexp
|
99
|
+
::Regexp.new(digest_pat)
|
100
|
+
end
|
101
|
+
|
102
|
+
def anchored_digest
|
103
|
+
anchored(digest_pat)
|
104
|
+
end
|
105
|
+
|
106
|
+
# anchoredDigestRegexp matches valid digests, anchored at the start and end of the matched string.
|
107
|
+
def anchored_digest_regexp
|
108
|
+
::Regexp.new(anchored_digest)
|
109
|
+
end
|
110
|
+
|
111
|
+
def name_pat
|
112
|
+
expression(optional(domain, literal("/")), name_component, optional(repeated(literal("/"), name_component)))
|
113
|
+
end
|
114
|
+
|
115
|
+
# NameRegexp is the format for the name component of references. The
|
116
|
+
# regexp has capturing groups for the domain and name part omitting
|
117
|
+
# the separating forward slash from either.
|
118
|
+
def name_regexp
|
119
|
+
::Regexp.new(name_pat)
|
120
|
+
end
|
121
|
+
|
122
|
+
def anchored_name
|
123
|
+
anchored(optional(capture(domain), literal("/")), capture(name_component, optional(repeated(literal("/"), name_component))))
|
124
|
+
end
|
125
|
+
|
126
|
+
# anchoredNameRegexp is used to parse a name value, capturing the domain and trailing components.
|
127
|
+
def anchored_name_regexp
|
128
|
+
::Regexp.new(anchored_name)
|
129
|
+
end
|
130
|
+
|
131
|
+
def reference_pat
|
132
|
+
anchored(capture(name_pat), optional(literal(":"), capture(tag)), optional(literal("@"), capture(digest_pat)))
|
133
|
+
end
|
134
|
+
|
135
|
+
# ReferenceRegexp is the full supported format of a reference. The regexp
|
136
|
+
# is anchored and has capturing groups for name, tag, and digest components.
|
137
|
+
def reference_regexp
|
138
|
+
::Regexp.new(reference_pat)
|
139
|
+
end
|
140
|
+
|
141
|
+
def identifier
|
142
|
+
"([a-f0-9]{64})"
|
143
|
+
end
|
144
|
+
|
145
|
+
# IdentifierRegexp is the format for string identifier used as a content addressable identifier using sha256.
|
146
|
+
# These identifiers are like digests without the algorithm, since sha256 is used.
|
147
|
+
def identifier_regexp
|
148
|
+
::Regexp.new(identifier)
|
149
|
+
end
|
150
|
+
|
151
|
+
def short_identifier
|
152
|
+
"([a-f0-9]{6,64})"
|
153
|
+
end
|
154
|
+
|
155
|
+
# ShortIdentifierRegexp is the format used to represent a prefix of an identifier.
|
156
|
+
# A prefix may be used to match a sha256 identifier within a list of trusted identifiers.
|
157
|
+
def short_identifier_regexp
|
158
|
+
::Regexp.new(short_identifier)
|
159
|
+
end
|
160
|
+
|
161
|
+
def anchored_identifier
|
162
|
+
anchored(identifier)
|
163
|
+
end
|
164
|
+
|
165
|
+
# anchoredIdentifierRegexp is used to check or match an # identifier value, anchored at start and end of string.
|
166
|
+
def anchored_identifier_regexp
|
167
|
+
::Regexp.new(anchored_identifier)
|
168
|
+
end
|
169
|
+
|
170
|
+
def anchored_short_identifier
|
171
|
+
anchored(short_identifier)
|
172
|
+
end
|
173
|
+
|
174
|
+
# anchoredShortIdentifierRegexp is used to check if a value is a possible identifier prefix, anchored at start and end of string.
|
175
|
+
def anchored_short_identifier_regexp
|
176
|
+
::Regexp.new(anchored_short_identifier)
|
177
|
+
end
|
178
|
+
|
179
|
+
def anchored_encoded_regexp(type)
|
180
|
+
map = {
|
181
|
+
SHA256: ::Regexp.new("^[a-f0-9]{64}$"),
|
182
|
+
SHA384: ::Regexp.new("^[a-f0-9]{96}$"),
|
183
|
+
SHA512: ::Regexp.new("^[a-f0-9]{128}$")
|
184
|
+
}
|
185
|
+
map[type.to_sym]
|
186
|
+
end
|
187
|
+
|
188
|
+
# literal compiles s into a literal regular expression, escaping any regexp reserved characters.
|
189
|
+
def literal(str)
|
190
|
+
re = ::Regexp.new(::Regexp.quote(str))
|
191
|
+
re.source
|
192
|
+
end
|
193
|
+
|
194
|
+
# expression defines a full expression, where each regular expression must follow the previous.
|
195
|
+
def expression(*res)
|
196
|
+
res.join("")
|
197
|
+
end
|
198
|
+
|
199
|
+
# group wraps the regexp in a non-capturing group.
|
200
|
+
def group(*res)
|
201
|
+
"(?:#{expression(*res)})"
|
202
|
+
end
|
203
|
+
|
204
|
+
# optional wraps the expression in a non-capturing group and makes the production optional.
|
205
|
+
def optional(*res)
|
206
|
+
"#{group(expression(*res))}?"
|
207
|
+
end
|
208
|
+
|
209
|
+
# anchored anchors the regular expression by adding start and end delimiters.
|
210
|
+
def anchored(*res)
|
211
|
+
"^#{expression(*res)}$"
|
212
|
+
end
|
213
|
+
|
214
|
+
# repeated wraps the regexp in a non-capturing group to get one or more matches.
|
215
|
+
def repeated(*res)
|
216
|
+
"#{group(expression(*res))}+"
|
217
|
+
end
|
218
|
+
|
219
|
+
# capture wraps the expression in a capturing group.
|
220
|
+
def capture(*res)
|
221
|
+
"(#{expression(*res)})"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
# rubocop:enable Metrics/ClassLength
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DockerDistribution
|
4
|
+
class Repository
|
5
|
+
attr_accessor :domain, :path
|
6
|
+
|
7
|
+
def initialize(domain = nil, path = nil)
|
8
|
+
@domain = domain
|
9
|
+
@path = path
|
10
|
+
end
|
11
|
+
|
12
|
+
def name
|
13
|
+
return path if Helpers.empty?(domain)
|
14
|
+
|
15
|
+
[domain, path].join("/")
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
name
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_h
|
23
|
+
{
|
24
|
+
repository: name,
|
25
|
+
domain: domain,
|
26
|
+
path: path
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def familiar
|
31
|
+
Normalize.familiarize_name(self)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DockerDistribution
|
4
|
+
class TaggedReference
|
5
|
+
extend ::Forwardable
|
6
|
+
def_delegators :repository, :name, :domain, :path
|
7
|
+
|
8
|
+
attr_accessor :repository, :tag
|
9
|
+
|
10
|
+
def initialize(repository = nil, tag = nil)
|
11
|
+
@repository = repository
|
12
|
+
@tag = tag
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
[@repository.name, tag].join(":")
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_h
|
20
|
+
{
|
21
|
+
repository: name,
|
22
|
+
domain: domain,
|
23
|
+
path: path,
|
24
|
+
tag: tag
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def familiar
|
29
|
+
self.class.new(Normalize.familiarize_name(self), tag)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Grammar
|
4
|
+
#
|
5
|
+
# reference := name [ ":" tag ] [ "@" digest ]
|
6
|
+
# name := [domain '/'] path-component ['/' path-component]*
|
7
|
+
# domain := host [':' port-number]
|
8
|
+
# host := domain-name | IPv4address | \[ IPv6address \] ; rfc3986 appendix-A
|
9
|
+
# domain-name := domain-component ['.' domain-component]*
|
10
|
+
# domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
|
11
|
+
# port-number := /[0-9]+/
|
12
|
+
# path-component := alpha-numeric [separator alpha-numeric]*
|
13
|
+
# alpha-numeric := /[a-z0-9]+/
|
14
|
+
# separator := /[_.]|__|[-]*/
|
15
|
+
#
|
16
|
+
# tag := /[\w][\w.-]{0,127}/
|
17
|
+
#
|
18
|
+
# digest := digest-algorithm ":" digest-hex
|
19
|
+
# digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]*
|
20
|
+
# digest-algorithm-separator := /[+.-_]/
|
21
|
+
# digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/
|
22
|
+
# digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value
|
23
|
+
#
|
24
|
+
# identifier := /[a-f0-9]{64}/
|
25
|
+
# short-identifier := /[a-f0-9]{6,64}/
|
26
|
+
|
27
|
+
require "forwardable"
|
28
|
+
|
29
|
+
require_relative "docker_distribution/version"
|
30
|
+
require_relative "docker_distribution/errors"
|
31
|
+
require_relative "docker_distribution/helpers"
|
32
|
+
|
33
|
+
require_relative "docker_distribution/digest"
|
34
|
+
require_relative "docker_distribution/digest_set"
|
35
|
+
|
36
|
+
require_relative "docker_distribution/regexp"
|
37
|
+
require_relative "docker_distribution/repository"
|
38
|
+
require_relative "docker_distribution/reference"
|
39
|
+
require_relative "docker_distribution/tagged_reference"
|
40
|
+
require_relative "docker_distribution/canonical_reference"
|
41
|
+
require_relative "docker_distribution/digest_reference"
|
42
|
+
require_relative "docker_distribution/normalize"
|
43
|
+
|
44
|
+
# Package reference provides a general type to represent any way of referencing images within the registry.
|
45
|
+
# Its main purpose is to abstract tags and digests (content-addressable hash).
|
46
|
+
module DockerDistribution
|
47
|
+
end
|