gemvault 0.1.4 → 0.2.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.containerignore +10 -0
  3. data/.rubocop.yml +8 -1
  4. data/CHANGELOG.md +56 -0
  5. data/CLAUDE.md +6 -5
  6. data/Dockerfile.test +14 -0
  7. data/README.md +18 -6
  8. data/Rakefile +19 -0
  9. data/docs/tarvault-findings.md +186 -0
  10. data/docs/tarvault.md +42 -0
  11. data/issues.rec +540 -17
  12. data/lib/bundler/plugin/vault_source.rb +61 -73
  13. data/lib/bundler/plugin/vaulted_gem.rb +66 -0
  14. data/lib/gemvault/archive_entry.rb +6 -0
  15. data/lib/gemvault/cli/command.rb +9 -5
  16. data/lib/gemvault/cli/commands/extract.rb +11 -11
  17. data/lib/gemvault/cli/commands/new.rb +3 -2
  18. data/lib/gemvault/cli/commands/remove.rb +15 -5
  19. data/lib/gemvault/cli/commands/upgrade.rb +72 -0
  20. data/lib/gemvault/dbvault.rb +101 -0
  21. data/lib/gemvault/deprecation.rb +48 -0
  22. data/lib/gemvault/gem_entry.rb +20 -24
  23. data/lib/gemvault/gem_extraction.rb +41 -0
  24. data/lib/gemvault/gem_reference/any_version.rb +6 -2
  25. data/lib/gemvault/gem_reference/parser.rb +44 -0
  26. data/lib/gemvault/gem_reference/specific_version.rb +9 -25
  27. data/lib/gemvault/gem_reference.rb +13 -45
  28. data/lib/gemvault/manifest.rb +78 -0
  29. data/lib/gemvault/tarball.rb +53 -0
  30. data/lib/gemvault/tarvault.rb +139 -0
  31. data/lib/gemvault/vault.rb +55 -157
  32. data/lib/gemvault/vault_path.rb +33 -0
  33. data/lib/gemvault/vault_session.rb +16 -0
  34. data/lib/gemvault/vault_upgrade.rb +94 -0
  35. data/lib/gemvault/version.rb +1 -1
  36. data/lib/gemvault.rb +2 -0
  37. data/lib/rubygems/resolver/vault_set.rb +5 -7
  38. data/lib/rubygems/source/vault.rb +28 -32
  39. data/lib/rubygems_plugin.rb +17 -6
  40. metadata +21 -33
@@ -1,193 +1,91 @@
1
- require "sqlite3"
2
- require "rubygems/package"
3
- require "fileutils"
4
- require "tempfile"
5
- require_relative "gem_entry"
6
- require_relative "gem_reference"
1
+ require "forwardable"
2
+ require "pathname"
3
+ require_relative "vault_session"
7
4
 
8
5
  module Gemvault
9
- # SQLite-backed archive of .gem blobs; supports add/remove/list/extract.
6
+ # The public vault interface. Delegates storage to a backend chosen by file
7
+ # format: a Dbvault (SQLite) for existing SQLite files, a Tarvault (tarball)
8
+ # otherwise. New vaults are Tarvaults. Only the selected backend is loaded,
9
+ # so the tar path never requires sqlite3.
10
10
  class Vault
11
+ extend VaultSession
12
+ extend Forwardable
13
+
11
14
  class Error < StandardError; end
12
15
  class NotFoundError < Error; end
13
16
  class DuplicateGemError < Error; end
14
17
  class InvalidGemError < Error; end
18
+ class UnsupportedVersionError < Error; end
19
+ class ReadOnlyError < Error; end
15
20
 
16
- SCHEMA_VERSION = "1".freeze
17
21
  SQLITE_MAGIC = "SQLite format 3#{0.chr}".freeze
22
+ TAR_MAGIC = "ustar".freeze
23
+ TAR_MAGIC_OFFSET = 257
18
24
 
19
- attr_reader :path
25
+ CURRENT_FORMAT = 2
26
+ MIN_READABLE_FORMAT = 1
20
27
 
21
- def self.open(path, **opts, &block)
22
- raise ArgumentError, "#{name}.open requires a block" unless block
28
+ def_delegators :@backend,
29
+ :add, :remove, :gem_data, :gem_entries, :specs,
30
+ :spec_from_blob, :with_gem_file, :size, :close, :closed?,
31
+ :path, :format_version
23
32
 
24
- vault = new(path, **opts)
25
- begin
26
- yield vault
27
- ensure
28
- vault.close
29
- end
30
- end
33
+ def self.assert_readable!(version:, path:)
34
+ return if version.between?(MIN_READABLE_FORMAT, CURRENT_FORMAT)
31
35
 
32
- def initialize(path, create: false)
33
- @path = File.expand_path(path)
34
- create ? create_vault! : open_vault!
36
+ raise UnsupportedVersionError,
37
+ "Vault #{path} is format #{version}; this gemvault reads up to #{CURRENT_FORMAT}. Upgrade gemvault."
35
38
  end
36
39
 
37
- def add(gem_path)
38
- gem_path = File.expand_path(gem_path)
39
- raise NotFoundError, "Gem file not found: #{gem_path}" unless File.file?(gem_path)
40
+ def self.backend_for(path, create:)
41
+ return build_tarvault(path, create: true) if create
42
+ raise NotFoundError, "Vault not found: #{path}" unless path.exist?
40
43
 
41
- spec = load_gem_spec(gem_path)
42
- raise_if_duplicate(spec)
43
- insert_gem(gem_path, spec)
44
- end
45
-
46
- def remove(reference)
47
- case reference
48
- in GemReference::AnyVersion[name:]
49
- @db.execute("DELETE FROM gems WHERE name = ?", [name])
50
- in GemReference::SpecificVersion[name:, version:]
51
- @db.execute(
52
- "DELETE FROM gems WHERE name = ? AND version = ?",
53
- [name, version.to_s],
54
- )
44
+ case container_kind(path)
45
+ when :sqlite then build_dbvault(path)
46
+ when :tar then build_tarvault(path, create: false)
47
+ else
48
+ raise Error, "Unrecognized vault format: #{path} (not a Dbvault or Tarvault; it may require a newer gemvault)"
55
49
  end
56
- @db.changes
57
50
  end
58
51
 
59
- def gem_data(name, version, platform: "ruby")
60
- row = @db.execute(
61
- "SELECT data FROM gems WHERE name = ? AND version = ? AND platform = ?",
62
- [name, version, platform],
63
- ).first
52
+ def self.container_kind(path)
53
+ return :sqlite if sqlite?(path)
54
+ return :tar if tar?(path)
64
55
 
65
- raise NotFoundError, "Gem not found: #{name}-#{version} (#{platform})" unless row
66
-
67
- row["data"]
56
+ :unknown
68
57
  end
69
58
 
70
- def specs
71
- gem_entries.map { |entry| spec_from_blob(entry.name, entry.version, entry.platform) }
59
+ def self.sqlite?(path)
60
+ path.exist? && path.binread(SQLITE_MAGIC.bytesize) == SQLITE_MAGIC
72
61
  end
73
62
 
74
- def gem_entries
75
- @db.execute(
76
- "SELECT name, version, platform, created_at FROM gems ORDER BY name, version",
77
- ).map { |row| GemEntry.new(**row.transform_keys(&:to_sym)) }
78
- end
63
+ def self.tar?(path)
64
+ return false unless path.exist?
79
65
 
80
- def size
81
- @db.execute("SELECT COUNT(*) AS count FROM gems").first["count"]
66
+ header = path.binread(TAR_MAGIC_OFFSET + TAR_MAGIC.bytesize)
67
+ header.to_s[TAR_MAGIC_OFFSET, TAR_MAGIC.bytesize] == TAR_MAGIC
82
68
  end
83
69
 
84
- def close
85
- @db.close if @db && !@db.closed?
86
- end
87
-
88
- def with_gem_file(name, version, platform: "ruby")
89
- data = gem_data(name, version, platform: platform)
90
- tmpfile = write_tempfile(data)
70
+ def self.build_dbvault(path)
91
71
  begin
92
- yield tmpfile.path
93
- ensure
94
- tmpfile.close unless tmpfile.closed?
95
- tmpfile.unlink
96
- end
97
- end
98
-
99
- def spec_from_blob(name, version, platform = "ruby")
100
- with_gem_file(name, version, platform: platform) do |path|
101
- Gem::Package.new(path).spec
72
+ require_relative "dbvault"
73
+ Dbvault.new(path)
74
+ rescue LoadError => e
75
+ raise Error,
76
+ "#{path} is a legacy SQLite vault; it needs the sqlite3 gem (#{e.message}). " \
77
+ "Install sqlite3, or upgrade the vault with a gemvault that includes it."
102
78
  end
103
79
  end
104
80
 
105
- private
106
-
107
- def create_vault!
108
- raise Error, "Vault already exists: #{@path}" if File.exist?(@path)
109
-
110
- @db = new_database
111
- create_schema
112
- end
113
-
114
- def open_vault!
115
- raise NotFoundError, "Vault not found: #{@path}" unless File.exist?(@path)
116
-
117
- validate_sqlite!
118
- @db = new_database
119
- end
120
-
121
- def new_database
122
- db = SQLite3::Database.new(@path)
123
- db.results_as_hash = true
124
- db
125
- end
126
-
127
- def load_gem_spec(gem_path)
128
- Gem::Package.new(gem_path).spec
129
- rescue StandardError => e
130
- raise InvalidGemError, "Invalid gem file #{gem_path}: #{e.message}"
131
- end
132
-
133
- def raise_if_duplicate(spec)
134
- existing = @db.execute(
135
- "SELECT 1 FROM gems WHERE name = ? AND version = ? AND platform = ?",
136
- [spec.name, spec.version.to_s, spec.platform.to_s],
137
- )
138
- return if existing.empty?
139
-
140
- raise DuplicateGemError,
141
- "Gem already in vault: #{spec.name}-#{spec.version} (#{spec.platform})"
81
+ def self.build_tarvault(path, create:)
82
+ require_relative "tarvault"
83
+ Tarvault.new(path, create:)
142
84
  end
143
85
 
144
- def insert_gem(gem_path, spec)
145
- data = File.binread(gem_path)
146
- @db.execute(
147
- "INSERT INTO gems (name, version, platform, data) VALUES (?, ?, ?, ?)",
148
- [spec.name, spec.version.to_s, spec.platform.to_s, SQLite3::Blob.new(data)],
149
- )
150
- end
151
-
152
- def write_tempfile(data)
153
- tmpfile = Tempfile.new(["vault_gem", ".gem"])
154
- tmpfile.binmode
155
- tmpfile.write(data)
156
- tmpfile.close
157
- tmpfile
158
- end
159
-
160
- def create_schema
161
- @db.execute_batch(<<~SQL)
162
- CREATE TABLE metadata (
163
- key TEXT PRIMARY KEY,
164
- value TEXT NOT NULL
165
- );
166
-
167
- CREATE TABLE gems (
168
- name TEXT NOT NULL,
169
- version TEXT NOT NULL,
170
- platform TEXT NOT NULL DEFAULT 'ruby',
171
- data BLOB NOT NULL,
172
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
173
- PRIMARY KEY (name, version, platform)
174
- );
175
- SQL
176
-
177
- @db.execute(
178
- "INSERT INTO metadata (key, value) VALUES (?, ?)",
179
- ["vault_version", SCHEMA_VERSION],
180
- )
181
- @db.execute(
182
- "INSERT INTO metadata (key, value) VALUES (?, ?)",
183
- ["created_at", Time.now.utc.strftime("%Y-%m-%d %H:%M:%S")],
184
- )
185
- end
186
-
187
- def validate_sqlite!
188
- return if File.binread(@path, SQLITE_MAGIC.bytesize) == SQLITE_MAGIC
189
-
190
- raise Error, "Not a valid vault file (not SQLite): #{@path}"
86
+ def initialize(path, create: false)
87
+ absolute_path = Pathname(path).expand_path
88
+ @backend = self.class.backend_for(absolute_path, create:)
191
89
  end
192
90
  end
193
91
  end
@@ -0,0 +1,33 @@
1
+ require "uri"
2
+ require "pathname"
3
+
4
+ module Gemvault
5
+ ##
6
+ # Translates vault locators into filesystem paths.
7
+ #
8
+ # A locator is whatever a user hands the CLI or a package manager as "the
9
+ # vault": a plain filesystem path, a <tt>file://</tt> URI, or a
10
+ # <tt>vault://</tt> URI. Anything else passes through unchanged.
11
+ module VaultPath
12
+ SCHEMES = ["file", "vault"].freeze
13
+
14
+ module_function
15
+
16
+ # :call-seq:
17
+ # resolve(locator) -> Pathname
18
+ #
19
+ # Resolves +locator+ to a Pathname. Two-slash relative forms such as
20
+ # <tt>file://vault.gemv</tt> parse their first segment as a URI host, so
21
+ # host and path are rejoined.
22
+ def resolve(locator)
23
+ begin
24
+ uri = URI.parse(locator.to_s)
25
+ return Pathname(locator.to_s) unless SCHEMES.include?(uri.scheme)
26
+
27
+ Pathname([uri.host, uri.path].compact.join)
28
+ rescue URI::InvalidURIError
29
+ Pathname(locator.to_s)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,16 @@
1
+ module Gemvault
2
+ # Class-method mixin giving vault types a block-scoped `open` that news up
3
+ # an instance, yields it, and guarantees `close`.
4
+ module VaultSession
5
+ def open(path, **opts)
6
+ raise ArgumentError, "#{name}.open requires a block" unless block_given?
7
+
8
+ vault = new(path, **opts)
9
+ begin
10
+ yield vault
11
+ ensure
12
+ vault.close
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,94 @@
1
+ require "pathname"
2
+ require "fileutils"
3
+ require_relative "vault"
4
+ require_relative "deprecation"
5
+
6
+ module Gemvault
7
+ # Migrates a vault to the current storage format by reading it through its
8
+ # existing backend and rewriting it through the current-format writer, then
9
+ # atomically swapping the file into place. Preserves each gem's created_at.
10
+ class VaultUpgrade
11
+ include FileUtils
12
+
13
+ # The from/to/gem-count summary of an upgrade; also detects an
14
+ # already-current (no-op) vault.
15
+ class Plan < Data.define(:from_version, :to_version, :gem_count)
16
+ def no_op? = from_version == to_version
17
+
18
+ def to_s = "format #{from_version} -> #{to_version}"
19
+
20
+ def deconstruct_keys(_keys)
21
+ { from_version:, to_version:, gem_count:, no_op: no_op? }
22
+ end
23
+ end
24
+
25
+ # Copies gems from a source vault into a target vault, preserving each
26
+ # gem's stored timestamp. Holds the two endpoints so the per-gem call
27
+ # takes only the entry.
28
+ class GemCopy < Data.define(:source, :target)
29
+ def call(entry)
30
+ source.with_gem_file(entry) do |gem_path|
31
+ target.add(gem_path, created_at: entry.created_at)
32
+ end
33
+ end
34
+ end
35
+
36
+ def initialize(path, backup: true)
37
+ @path = Pathname(path).expand_path
38
+ @backup = backup
39
+ end
40
+
41
+ def plan
42
+ Deprecation.silence do
43
+ Vault.open(@path) do |vault|
44
+ Plan.new(from_version: vault.format_version, to_version: Vault::CURRENT_FORMAT, gem_count: vault.size)
45
+ end
46
+ end
47
+ end
48
+
49
+ def call
50
+ summary = plan
51
+ return summary if summary.no_op?
52
+
53
+ backup! if @backup
54
+ rebuild(summary.gem_count)
55
+ summary
56
+ end
57
+
58
+ private
59
+
60
+ def backup!
61
+ backup_path = Pathname("#{@path}.bak")
62
+ raise Vault::Error, "Backup already exists: #{backup_path} (remove it or pass --no-backup)" if backup_path.exist?
63
+
64
+ cp(@path, backup_path)
65
+ end
66
+
67
+ def rebuild(expected_count)
68
+ tmp = Pathname("#{@path}.upgrading")
69
+ rm_f(tmp)
70
+ copy_current_format(tmp)
71
+ verify_count!(tmp:, expected_count:)
72
+ tmp.rename(@path)
73
+ end
74
+
75
+ def copy_current_format(tmp)
76
+ Deprecation.silence do
77
+ Vault.open(@path) do |old|
78
+ target = Vault.new(tmp, create: true)
79
+ copy = GemCopy.new(source: old, target:)
80
+ old.gem_entries.each { |entry| copy.call(entry) }
81
+ target.close
82
+ end
83
+ end
84
+ end
85
+
86
+ def verify_count!(tmp:, expected_count:)
87
+ actual = Vault.open(tmp, &:size)
88
+ return if actual == expected_count
89
+
90
+ rm_f(tmp)
91
+ raise Vault::Error, "Upgrade verification failed: expected #{expected_count} gems, got #{actual}"
92
+ end
93
+ end
94
+ end
@@ -1,3 +1,3 @@
1
1
  module Gemvault
2
- VERSION = "0.1.4".freeze
2
+ VERSION = "0.2.0".freeze
3
3
  end
data/lib/gemvault.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  require_relative "gemvault/version"
2
+ require_relative "gemvault/deprecation"
2
3
  require_relative "gemvault/vault"
4
+ require_relative "gemvault/vault_path"
3
5
 
4
6
  ##
5
7
  # Top-level namespace for the gemvault gem.
@@ -11,7 +11,7 @@ class Gem::Resolver::VaultSet < Gem::Resolver::Set
11
11
  end
12
12
 
13
13
  def find_all(req)
14
- @specs.filter_map { |tuple| index_spec_for(req, tuple) }
14
+ @specs.filter_map { |candidate| index_spec_for(candidate) if req.match?(candidate) }
15
15
  end
16
16
 
17
17
  def prefetch(reqs); end
@@ -22,19 +22,17 @@ class Gem::Resolver::VaultSet < Gem::Resolver::Set
22
22
 
23
23
  pp.breakable
24
24
 
25
- pp.seplist @specs do |tuple|
26
- pp.text tuple.full_name
25
+ pp.seplist @specs do |candidate|
26
+ pp.text candidate.full_name
27
27
  end
28
28
  end
29
29
  end
30
30
 
31
31
  private
32
32
 
33
- def index_spec_for(req, tuple)
34
- return unless req.match?(tuple)
35
-
33
+ def index_spec_for(candidate)
36
34
  Gem::Resolver::IndexSpecification.new(
37
- self, tuple.name, tuple.version, @source, tuple.platform
35
+ self, candidate.name, candidate.version, @source, candidate.platform
38
36
  )
39
37
  end
40
38
  end
@@ -1,8 +1,11 @@
1
1
  require "gemvault/vault"
2
- require "uri"
2
+ require "gemvault/vault_path"
3
+ require "gemvault/gem_entry"
4
+ require "pathname"
3
5
 
4
6
  ##
5
- # A source backed by a .gemv vault file (SQLite archive of .gem blobs).
7
+ # A source backed by a .gemv vault file: a tar archive of .gem files indexed
8
+ # by a manifest. Legacy SQLite vaults are read transparently.
6
9
  #
7
10
  # Used by the gemvault RubyGems plugin to support:
8
11
  #
@@ -10,12 +13,11 @@ require "uri"
10
13
  class Gem::Source::Vault < Gem::Source
11
14
  include Gem::UserInteraction
12
15
 
13
- VAULT_URI_SCHEMES = %w[file vault].freeze
14
-
15
16
  attr_reader :path
16
17
 
17
18
  def initialize(path)
18
- @path = File.expand_path(filesystem_path(path))
19
+ resolved = Gemvault::VaultPath.resolve(path)
20
+ @path = Pathname(resolved).expand_path.to_s
19
21
  super(@path)
20
22
  @uri = @path
21
23
  @specs = nil
@@ -24,7 +26,7 @@ class Gem::Source::Vault < Gem::Source
24
26
  def load_specs(type)
25
27
  verbose "Loading #{type} specs from vault at #{@path}"
26
28
  ensure_specs_loaded
27
- select_tuples(type)
29
+ select_candidates(type)
28
30
  end
29
31
 
30
32
  def fetch_spec(name_tuple)
@@ -38,17 +40,15 @@ class Gem::Source::Vault < Gem::Source
38
40
 
39
41
  def download(spec, dir = Dir.pwd)
40
42
  verbose "Extracting #{spec.file_name} from vault at #{@path}"
41
- cache_dir = File.join(dir, "cache")
42
- FileUtils.mkdir_p(cache_dir)
43
-
44
- dest = File.join(cache_dir, spec.file_name)
43
+ dest = Pathname(dir).join("cache").tap(&:mkpath).join(spec.file_name)
45
44
 
46
45
  Gemvault::Vault.open(@path) do |vault|
47
- data = vault.gem_data(spec.name, spec.version.to_s, platform: spec.platform.to_s)
48
- File.binwrite(dest, data)
46
+ entry = entry_for(spec)
47
+ bytes = vault.gem_data(entry)
48
+ dest.binwrite(bytes)
49
49
  end
50
50
 
51
- dest
51
+ dest.to_s
52
52
  end
53
53
 
54
54
  def dependency_resolver_set(prerelease = nil)
@@ -91,35 +91,32 @@ class Gem::Source::Vault < Gem::Source
91
91
 
92
92
  private
93
93
 
94
- def filesystem_path(path)
95
- uri = URI.parse(path.to_s)
96
- VAULT_URI_SCHEMES.include?(uri.scheme) ? uri.path : path.to_s
97
- rescue URI::InvalidURIError
98
- path.to_s
94
+ def entry_for(spec)
95
+ Gemvault::GemEntry.from_spec(spec)
99
96
  end
100
97
 
101
- def select_tuples(type)
98
+ def select_candidates(type)
102
99
  case type
103
- when :released then released_tuples
104
- when :prerelease then prerelease_tuples
105
- when :latest then latest_tuples
100
+ when :released then released_candidates
101
+ when :prerelease then prerelease_candidates
102
+ when :latest then latest_candidates
106
103
  else @specs.keys
107
104
  end
108
105
  end
109
106
 
110
- def released_tuples
111
- @specs.keys.reject { |tuple| tuple.version.prerelease? }
107
+ def released_candidates
108
+ @specs.keys.reject { |candidate| candidate.version.prerelease? }
112
109
  end
113
110
 
114
- def prerelease_tuples
115
- @specs.keys.select { |tuple| tuple.version.prerelease? }
111
+ def prerelease_candidates
112
+ @specs.keys.select { |candidate| candidate.version.prerelease? }
116
113
  end
117
114
 
118
- def latest_tuples
115
+ def latest_candidates
119
116
  @specs.keys
120
- .group_by { |tuple| [tuple.name, tuple.platform] }
117
+ .group_by { |candidate| [candidate.name, candidate.platform] }
121
118
  .values
122
- .map { |tuples| tuples.max_by(&:version) }
119
+ .map { |versions| versions.max_by(&:version) }
123
120
  end
124
121
 
125
122
  def ensure_specs_loaded
@@ -128,9 +125,8 @@ class Gem::Source::Vault < Gem::Source
128
125
  @specs = {}
129
126
  Gemvault::Vault.open(@path) do |vault|
130
127
  vault.gem_entries.each do |entry|
131
- spec = vault.spec_from_blob(entry.name, entry.version, entry.platform)
132
- tuple = spec.name_tuple
133
- @specs[tuple] = spec
128
+ spec = vault.spec_from_blob(entry)
129
+ @specs[spec.name_tuple] = spec
134
130
  end
135
131
  end
136
132
  end
@@ -1,3 +1,10 @@
1
+ # RubyGems can evaluate this plugin twice under Bundler: once through the
2
+ # install-generated stub (via require) and again through Gem.load_env_plugins
3
+ # (via load, which ignores $LOADED_FEATURES) once the gem's lib/ is on
4
+ # $LOAD_PATH. Skip the redundant second evaluation so the constants and
5
+ # monkey-patches below are installed exactly once.
6
+ return if defined?(Gemvault::PLUGIN_LOADED)
7
+
1
8
  require_relative "rubygems/source/vault"
2
9
  require "rubygems/source_list"
3
10
 
@@ -14,6 +21,8 @@ require "rubygems/source_list"
14
21
  # 3. SourceList#<< -- route .gemv strings to Gem::Source::Vault
15
22
 
16
23
  module Gemvault
24
+ PLUGIN_LOADED = true
25
+
17
26
  # Lets .gemv paths bypass RubyGems' URI scheme validation.
18
27
  module AcceptVaultURI
19
28
  VALID_URI_SCHEMES = ["http", "https", "file", "s3"].freeze
@@ -22,8 +31,7 @@ module Gemvault
22
31
  Gem::OptionParser.accept Gem::URI::HTTP do |value|
23
32
  next value if value.to_s.end_with?(".gemv")
24
33
 
25
- uri = parse_uri(value)
26
- validate_scheme!(uri, value)
34
+ validate_uri!(value)
27
35
  value
28
36
  end
29
37
  end
@@ -31,12 +39,15 @@ module Gemvault
31
39
  private
32
40
 
33
41
  def parse_uri(value)
34
- Gem::URI.parse value
35
- rescue Gem::URI::InvalidURIError
36
- raise Gem::OptionParser::InvalidArgument, value
42
+ begin
43
+ Gem::URI.parse value
44
+ rescue Gem::URI::InvalidURIError
45
+ raise Gem::OptionParser::InvalidArgument, value
46
+ end
37
47
  end
38
48
 
39
- def validate_scheme!(uri, value)
49
+ def validate_uri!(value)
50
+ uri = parse_uri(value)
40
51
  return if VALID_URI_SCHEMES.include?(uri.scheme)
41
52
 
42
53
  schemes = VALID_URI_SCHEMES.map { |scheme| "#{scheme}://" }