docker_distribution 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|