docker_distribution 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fc6f4c883560213fd738e4f724ec846985b89a54daeae499d302320ad17e91c1
4
- data.tar.gz: 6a1cb6d5549de71c1882db7752df770f79acbe733a9416a2e956e4b74cb98998
3
+ metadata.gz: df29a563df5c3905dee99b74d6b1dfef79175c73a27ad016e553391cfa026ed7
4
+ data.tar.gz: c4bcaf72d7525f20042b5cc91a22e6f8ac18692cd31f7744dfca6d0012929201
5
5
  SHA512:
6
- metadata.gz: 8bfe97167c18b19eca3abe3245bf864ce1f1ae745e4df99f63e5bb0d1b45db6f14ee6ec3519c8566a343c1f3f9e3737867b146842464db093eaa915f20779ccb
7
- data.tar.gz: 43706fbd4f9afead0036f4b3871f41e817b79824b7295a489e61bda5c0e0038d5bbe94c8ad633b536b41b88a8ab55876912515b45b7402444d716d98b7951fc8
6
+ metadata.gz: 6ab3c059d0149abaf42364f194b37923e33316fae3e5395991f8ecf9a462ac69245387cea1dcbafa58ad043e05202cca32cea3e580e67f1476a1b67b6fae6d3c
7
+ data.tar.gz: dce9c44d2c73b1b5982d1909de563227c21f8e2d499916f3d29cfb28d6c8a0796c189107131601ae7d0a6217d5bb4a0294266d9b3cce160ac917cd1e2b77d6b0
data/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # DockerDistribution
2
+
3
+ Implementation of OCI Distribution Specification on Ruby. Implementation copied from Golang version which used in Docker Cli and Kubectl Cli.
4
+
5
+ [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec)
6
+
7
+ [Golang implementation](https://github.com/docker/distribution/tree/main/reference)
8
+
9
+
10
+ ## Installation
11
+
12
+ ```
13
+ gem install docker_distribution
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ### Reference
19
+ * Add description from implementation comments
20
+ ```
21
+ DockerDistribution::Reference.parse(image_name)
22
+ DockerDistribution::Reference.parse_named(str)
23
+ DockerDistribution::Reference.with_name(name)
24
+ DockerDistribution::Reference.with_tag(named, tag)
25
+ DockerDistribution::Reference.with_digest(named, digest)
26
+ DockerDistribution::Reference.split_domain(name)
27
+ DockerDistribution::Reference.split_hostname(named)
28
+ ```
29
+
30
+ ### Normalize
31
+ * Add description from implementation comments
32
+ ```
33
+ DockerDistribution::Normalize.parse_normalized_named(str)
34
+ DockerDistribution::Normalize.parse_docker_ref(ref)
35
+ DockerDistribution::Normalize.split_docker_domain(name)
36
+ DockerDistribution::Normalize.familiarize_name(repo)
37
+ DockerDistribution::Normalize.tag_name_only(ref)
38
+ DockerDistribution::Normalize.parse_any_reference(ref)
39
+ DockerDistribution::Normalize.parse_any_reference_with_set(ref, digest_set)
40
+ ```
41
+
42
+ ### Regexp
43
+ * Add description from implementation comments
44
+ ```
45
+ DockerDistribution::Regexp.alpha_numeric
46
+ DockerDistribution::Regexp.separator
47
+ DockerDistribution::Regexp.name_component
48
+ DockerDistribution::Regexp.domain_name_component
49
+ DockerDistribution::Regexp.ipv6address
50
+ DockerDistribution::Regexp.domain_name
51
+ DockerDistribution::Regexp.host
52
+ DockerDistribution::Regexp.domain
53
+ DockerDistribution::Regexp.domain_regexp
54
+ DockerDistribution::Regexp.tag
55
+ DockerDistribution::Regexp.tag_regexp
56
+ DockerDistribution::Regexp.anchored_tag
57
+ DockerDistribution::Regexp.anchored_tag_regexp
58
+ DockerDistribution::Regexp.digest_pat
59
+ DockerDistribution::Regexp.digest_regexp
60
+ DockerDistribution::Regexp.anchored_digest
61
+ DockerDistribution::Regexp.anchored_digest_regexp
62
+ DockerDistribution::Regexp.name_pat
63
+ DockerDistribution::Regexp.name_regexp
64
+ DockerDistribution::Regexp.anchored_name
65
+ DockerDistribution::Regexp.anchored_name_regexp
66
+ DockerDistribution::Regexp.reference_pat
67
+ DockerDistribution::Regexp.reference_regexp
68
+ DockerDistribution::Regexp.identifier
69
+ DockerDistribution::Regexp.identifier_regexp
70
+ DockerDistribution::Regexp.short_identifier
71
+ DockerDistribution::Regexp.short_identifier_regexp
72
+ DockerDistribution::Regexp.anchored_identifier
73
+ DockerDistribution::Regexp.anchored_identifier_regexp
74
+ DockerDistribution::Regexp.anchored_short_identifier
75
+ DockerDistribution::Regexp.anchored_short_identifier_regexp
76
+ ```
77
+
78
+ ### Digest
79
+ * Add description from implementation comments
80
+ ```
81
+ DockerDistribution::Digest.parse(digest_string)
82
+ ```
83
+
84
+ ## Examples
85
+ ...
86
+
87
+ More examples in tests
88
+
89
+ ## Development
90
+ ```
91
+ docker-compose build gem
92
+ docker-compose run --rm gem bash
93
+ bundle install
94
+ ...
95
+ make test
96
+ make lint
97
+ ```
98
+
99
+ ## Release
100
+ 1. [Read this](https://guides.rubygems.org/publishing/)
101
+ 2. Make sure you logged in into Rubygems
102
+ 3. Update version accoring [semver](https://semver.org/)
103
+ 4. `gem build docker_distribution.gemspec`
104
+ 5. `gem push docker_distribution-[version].gem`
105
+
106
+ ## Todo
107
+ - Finish readme description / examples / installation guide / development guide / examples / ruby version support
108
+ - Add correct workflow in github to run tests and linters in docker env
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ end
11
+
12
+ require "rubocop/rake_task"
13
+
14
+ RuboCop::RakeTask.new
15
+
16
+ task default: %i[test rubocop]
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerDistribution
4
+ class CanonicalReference
5
+ extend ::Forwardable
6
+ def_delegators :repository, :name, :domain, :path
7
+
8
+ attr_accessor :repository, :digest
9
+
10
+ def initialize(repository, digest)
11
+ @repository = repository
12
+ @digest = digest
13
+ end
14
+
15
+ def to_s
16
+ [repository.name, digest].join("@")
17
+ end
18
+
19
+ def to_h
20
+ {
21
+ repository: name,
22
+ domain: domain,
23
+ path: path,
24
+ digest: digest
25
+ }
26
+ end
27
+
28
+ def familiar
29
+ self.class.new(Normalize.familiarize_name(self), digest)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module DockerDistribution
6
+ class Digest
7
+ DEFAULT_ALGORITHM = "sha256"
8
+ attr_accessor :digest_string, :encoded, :algorithm_type
9
+
10
+ # Parse parses digest_string and returns the validated digest object. An error will
11
+ # be raised if the format is invalid.
12
+ def self.parse!(digest_string)
13
+ dgst = new(digest_string)
14
+ dgst.validate!
15
+ dgst
16
+ end
17
+
18
+ def initialize(digest_string)
19
+ @digest_string = digest_string
20
+ @algorithm_type, @encoded = digest_string.index(":") ? digest_string.split(":") : [DEFAULT_ALGORITHM, digest_string]
21
+ end
22
+
23
+ def digest
24
+ [algorithm_type, encoded].compact.join(":")
25
+ end
26
+
27
+ def to_s
28
+ digest
29
+ end
30
+
31
+ # rubocop:disable Metrics/AbcSize
32
+ def validate!
33
+ index = digest_string.index(":")
34
+ raise DigestInvalidFormat if index.nil? || index + 1 == digest_string.length
35
+
36
+ raise DigestInvalidLength if algorithm.block_length != encoded.length
37
+
38
+ return true if Regexp.anchored_encoded_regexp(algorithm_type.upcase).match?(encoded)
39
+
40
+ raise DigestInvalidFormat
41
+ end
42
+ # rubocop:enable Metrics/AbcSize
43
+
44
+ def algorithm
45
+ send(:Digest, algorithm_type.upcase).new
46
+ rescue NameError, LoadError
47
+ raise DigestUnsupported
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerDistribution
4
+ class DigestReference
5
+ attr_accessor :digest
6
+
7
+ def initialize(digest)
8
+ @digest = digest
9
+ end
10
+
11
+ def to_s
12
+ digest
13
+ end
14
+
15
+ def to_h
16
+ { digest: digest }
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+
5
+ module DockerDistribution
6
+ class DigestSet
7
+ attr_writer :entries
8
+
9
+ def initialize(digest_strings)
10
+ @entries = digest_strings.map { |digest_string| Digest.new(digest_string) }
11
+ end
12
+
13
+ def entries
14
+ @entries ||= []
15
+ end
16
+
17
+ def add(digest_string)
18
+ @entries << Digest.new(digest_string)
19
+ end
20
+
21
+ def lookup!(digest_string)
22
+ raise DigestNotFound if entries.length.zero?
23
+
24
+ search_func, encoded, alg = parse!(digest_string)
25
+
26
+ entry = search(search_func, encoded, alg)
27
+ entry.digest
28
+ end
29
+
30
+ private
31
+
32
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
33
+ def parse!(digest_string)
34
+ dgst = Digest.parse!(digest_string)
35
+ alg = dgst.algorithm_type
36
+ encoded = dgst.encoded
37
+ search_func = lambda do |i|
38
+ return entries[i].algorithm_type >= dgst.algorithm_type if entries[i].encoded == encoded
39
+
40
+ entries[i].encoded >= encoded
41
+ end
42
+ [search_func, encoded, alg]
43
+ rescue DigestInvalidFormat
44
+ alg = Digest::DEFAULT_ALGORITHM
45
+ encoded = digest_string
46
+ search_func = lambda do |i|
47
+ entries[i].encoded >= digest_string
48
+ end
49
+ [search_func, encoded, alg]
50
+ end
51
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
52
+
53
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
54
+ def search(search_func, encoded, alg)
55
+ idx = entries.each_with_index.filter_map { |_el, i| i if search_func.call(i) }.first || entries.length
56
+ raise DigestNotFound if idx == entries.length || !check_short_match(entries[idx].algorithm_type, entries[idx].encoded, alg.to_s,
57
+ encoded)
58
+ return entries[idx] if entries[idx].algorithm_type == alg && entries[idx].encoded == encoded
59
+
60
+ raise DigestAmbiguous if idx + 1 < entries.length && check_short_match(entries[idx + 1].algorithm_type, entries[idx + 1].encoded,
61
+ alg.to_s, encoded)
62
+
63
+ entries[idx]
64
+ end
65
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
66
+
67
+ # rubocop:disable Metrics/CyclomaticComplexity
68
+ def check_short_match(alg, encoded, short_alg, short_encoded)
69
+ if encoded.length == short_encoded.length
70
+ return false if encoded != short_encoded
71
+
72
+ return false if short_alg.length.positive? && alg != short_alg
73
+ end
74
+
75
+ return false unless encoded.start_with?(short_encoded)
76
+
77
+ return false if short_alg.length.positive? && alg != short_alg
78
+
79
+ true
80
+ end
81
+ # rubocop:enable Metrics/CyclomaticComplexity
82
+ end
83
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerDistribution
4
+ # NameEmpty is raised for empty, invalid repository names.
5
+ class NameEmpty < StandardError; end
6
+ # ReferenceInvalidFormat represents an error while trying to parse a string as a reference.
7
+ class ReferenceInvalidFormat < StandardError; end
8
+ # TagInvalidFormat represents an error while trying to parse a string as a tag.
9
+ class TagInvalidFormat < StandardError; end
10
+ # DigestError raised when something wrong with Digest
11
+ class DigestError < StandardError; end
12
+ # DigestInvalidLength raised when digest has invalid length.
13
+ class DigestInvalidLength < DigestError; end
14
+ # DigestInvalidFormat raised when digest format invalid.
15
+ class DigestInvalidFormat < DigestError; end
16
+ # DigestUnsupported raised when the digest algorithm is unsupported.
17
+ class DigestUnsupported < DigestError; end
18
+ # DigestNotFound raised when the digest set not found
19
+ class DigestNotFound < DigestError; end
20
+ # DigestAmbiguous raised when the digest set has two same digests
21
+ class DigestAmbiguous < DigestError; end
22
+ # NameContainsUppercase is raised for invalid repository names that contain uppercase characters.
23
+ class NameContainsUppercase < StandardError; end
24
+ # NameTooLong is raised when a repository name is longer than 255 symbols.
25
+ class NameTooLong < StandardError; end
26
+ # NameNotCanonical is raised when a name is not canonical.
27
+ class NameNotCanonical < StandardError; end
28
+ # # ParseNormalizedNamedError is raised when a parse normalized error failed
29
+ class ParseNormalizedNamedError < StandardError; end
30
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerDistribution
4
+ class Helpers
5
+ class << self
6
+ def familiar_name(named)
7
+ return named.familiar.name if named.respond_to?(:familiar)
8
+
9
+ named.name
10
+ end
11
+
12
+ def familiar_string(named)
13
+ return named.familiar.to_s if named.respond_to?(:familiar)
14
+
15
+ named.to_s
16
+ end
17
+
18
+ def familiar_match(pattern, ref)
19
+ string_match = File.fnmatch(pattern, familiar_string(ref), File::FNM_PATHNAME)
20
+ name_match = File.fnmatch(pattern, familiar_name(ref), File::FNM_PATHNAME)
21
+
22
+ string_match || name_match
23
+ end
24
+
25
+ def tagged?(ref)
26
+ ref.respond_to?(:tag)
27
+ end
28
+
29
+ def canonical?(ref)
30
+ ref.respond_to?(:digest)
31
+ end
32
+
33
+ def named?(ref)
34
+ ref.respond_to?(:name)
35
+ end
36
+
37
+ def familiar?(ref)
38
+ ref.respond_to?(:familiar)
39
+ end
40
+
41
+ def name_only?(ref)
42
+ named?(ref) && !tagged?(ref) && !canonical?(ref)
43
+ end
44
+
45
+ def normalized_named?(ref)
46
+ named?(ref) && familiar?(ref)
47
+ end
48
+
49
+ def from(str, index)
50
+ str[index..]
51
+ end
52
+
53
+ def to(str, index)
54
+ str[0...index]
55
+ end
56
+
57
+ def empty?(object)
58
+ object == "" || object.nil?
59
+ end
60
+ end
61
+ end
62
+ end
@@ -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, 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, 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, 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, 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 zero or more matches.
215
+ def repeated(*res)
216
+ group("#{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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DockerDistribution
4
+ VERSION = "0.1.2"
5
+ 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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: docker_distribution
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Artem Petrov
@@ -44,7 +44,22 @@ email:
44
44
  executables: []
45
45
  extensions: []
46
46
  extra_rdoc_files: []
47
- files: []
47
+ files:
48
+ - README.md
49
+ - Rakefile
50
+ - lib/docker_distribution.rb
51
+ - lib/docker_distribution/canonical_reference.rb
52
+ - lib/docker_distribution/digest.rb
53
+ - lib/docker_distribution/digest_reference.rb
54
+ - lib/docker_distribution/digest_set.rb
55
+ - lib/docker_distribution/errors.rb
56
+ - lib/docker_distribution/helpers.rb
57
+ - lib/docker_distribution/normalize.rb
58
+ - lib/docker_distribution/reference.rb
59
+ - lib/docker_distribution/regexp.rb
60
+ - lib/docker_distribution/repository.rb
61
+ - lib/docker_distribution/tagged_reference.rb
62
+ - lib/docker_distribution/version.rb
48
63
  homepage: https://github.com/artempartos/docker_distribution
49
64
  licenses:
50
65
  - MIT