curb 1.3.4 → 1.3.6

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.
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'tempfile'
5
+
6
+ module Curl
7
+ class DownloadTargetExistsError < Errno::EEXIST; end
8
+ DOWNLOAD_OPTION_KEYS = [:download_dir, :overwrite].freeze
9
+
10
+ class << self
11
+ def download_options_hash?(value)
12
+ value.is_a?(Hash) && value.keys.all? { |key| DOWNLOAD_OPTION_KEYS.include?(key) }
13
+ end
14
+
15
+ def normalize_download_arguments(filename, options)
16
+ if download_options_hash?(filename) && options.empty?
17
+ options = filename
18
+ filename = nil
19
+ end
20
+
21
+ [filename, options || {}]
22
+ end
23
+
24
+ def parse_download_options(options)
25
+ raise ArgumentError, "download options must be a Hash" unless options.is_a?(Hash)
26
+
27
+ options = options.dup
28
+ download_dir = options.delete(:download_dir)
29
+ overwrite = options.key?(:overwrite) ? !!options.delete(:overwrite) : false
30
+ raise ArgumentError, "unsupported download option(s): #{options.keys.join(', ')}" unless options.empty?
31
+
32
+ [download_dir, overwrite]
33
+ end
34
+
35
+ def resolve_download_output(url, filename = nil, options = {})
36
+ filename, options = normalize_download_arguments(filename, options)
37
+ download_dir, overwrite = parse_download_options(options)
38
+
39
+ if filename.is_a?(IO)
40
+ filename.binmode if filename.respond_to?(:binmode)
41
+ return [nil, filename, false, overwrite]
42
+ end
43
+
44
+ path = if download_dir
45
+ safe_download_path(url, download_dir, filename: filename)
46
+ elsif filename.nil?
47
+ safe_download_path(url)
48
+ else
49
+ filename.to_s
50
+ end
51
+
52
+ [path, nil, true, overwrite]
53
+ end
54
+
55
+ def prepare_download_output(url, filename = nil, options = {})
56
+ path, io, safe_output, overwrite = resolve_download_output(url, filename, options)
57
+ return [path, io, safe_output] unless safe_output
58
+
59
+ [path, open_safe_download_output(path, overwrite: overwrite), true]
60
+ end
61
+
62
+ def download_filename_from_url(url)
63
+ path = begin
64
+ URI.parse(url.to_s).path
65
+ rescue URI::InvalidURIError
66
+ url.to_s.split(/\?/, 2).first
67
+ end
68
+
69
+ basename = File.basename(path.to_s)
70
+ validate_download_filename!(basename)
71
+ end
72
+
73
+ def safe_download_path(url, destination_dir = Dir.pwd, filename: nil)
74
+ dir = File.expand_path(destination_dir.to_s)
75
+ raise ArgumentError, "download destination directory does not exist: #{destination_dir}" unless File.directory?(dir)
76
+
77
+ File.join(dir, filename.nil? ? download_filename_from_url(url) : validate_download_filename!(filename))
78
+ end
79
+
80
+ def validate_download_filename!(filename)
81
+ filename = filename.to_s
82
+ invalid = filename.empty? ||
83
+ filename == '.' ||
84
+ filename == '..' ||
85
+ filename == File::SEPARATOR ||
86
+ filename.start_with?('.') ||
87
+ filename.include?("\0") ||
88
+ filename.include?(File::SEPARATOR) ||
89
+ filename.include?('\\') ||
90
+ filename.match?(/\A[A-Za-z]:/) ||
91
+ (File::ALT_SEPARATOR && filename.include?(File::ALT_SEPARATOR))
92
+
93
+ raise ArgumentError, "unsafe download filename derived from URL: #{filename.inspect}" if invalid
94
+
95
+ filename
96
+ end
97
+
98
+ def open_safe_download_output(path, overwrite: false)
99
+ SafeDownloadOutput.new(path, overwrite: overwrite)
100
+ end
101
+ end
102
+
103
+ class SafeDownloadOutput
104
+ def initialize(path, overwrite: false)
105
+ @path = File.expand_path(path.to_s)
106
+ @overwrite = overwrite
107
+ raise DownloadTargetExistsError, @path if !@overwrite && File.exist?(@path)
108
+
109
+ @tmp = Tempfile.new(['.curb-download-', '.tmp'], File.dirname(@path))
110
+ @tmp.binmode
111
+ @closed = false
112
+ end
113
+
114
+ attr_reader :path
115
+
116
+ def write(data)
117
+ @tmp.write(data)
118
+ end
119
+
120
+ def <<(data)
121
+ write(data)
122
+ end
123
+
124
+ def close(success = false)
125
+ return if @closed
126
+
127
+ @tmp.flush unless @tmp.closed?
128
+ @tmp.close unless @tmp.closed?
129
+ install_tmp_file if success
130
+ ensure
131
+ @tmp.close! if @tmp
132
+ @closed = true
133
+ end
134
+
135
+ private
136
+
137
+ def install_tmp_file
138
+ if @overwrite
139
+ File.rename(@tmp.path, @path)
140
+ else
141
+ File.link(@tmp.path, @path)
142
+ end
143
+ rescue Errno::EEXIST
144
+ raise DownloadTargetExistsError, @path
145
+ rescue NotImplementedError
146
+ install_tmp_file_by_copy
147
+ end
148
+
149
+ def install_tmp_file_by_copy
150
+ flags = File::WRONLY | File::CREAT | File::EXCL
151
+ flags |= File::BINARY if File.const_defined?(:BINARY)
152
+
153
+ File.open(@path, flags) do |dest|
154
+ File.open(@tmp.path, 'rb') { |src| IO.copy_stream(src, dest) }
155
+ end
156
+ rescue Errno::EEXIST
157
+ raise DownloadTargetExistsError, @path
158
+ end
159
+ end
160
+ end
data/lib/curl/easy.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require 'curl/download'
2
3
  module Curl
3
4
  class Easy
4
5
  class << self
@@ -83,10 +84,12 @@ module Curl
83
84
 
84
85
  alias_method :_curb_native_close, :close
85
86
  alias_method :_curb_native_multi_set, :multi=
87
+ alias_method :_curb_native_reset, :reset
86
88
 
87
89
  def close
88
90
  previous_multi = self.multi
89
91
  result = _curb_native_close
92
+ __curb_clear_safety_override!
90
93
  previous_multi.__send__(:__unregister_idle_easy_reference, self) if previous_multi
91
94
  result
92
95
  end
@@ -94,12 +97,22 @@ module Curl
94
97
  def multi=(multi)
95
98
  previous_multi = self.multi
96
99
  return multi if previous_multi.equal?(multi)
97
-
100
+
101
+ if previous_multi && previous_multi.requests[self.object_id]
102
+ previous_multi.remove(self)
103
+ end
104
+
98
105
  result = _curb_native_multi_set(multi)
99
106
  previous_multi.__send__(:__unregister_idle_easy_reference, self) if previous_multi
100
107
  multi.__send__(:__register_idle_easy_reference, self) if multi
101
108
  result
102
109
  end
110
+
111
+ def reset
112
+ result = _curb_native_reset
113
+ __curb_clear_safety_override!
114
+ result
115
+ end
103
116
 
104
117
  alias post http_post
105
118
  alias put http_put
@@ -154,6 +167,84 @@ module Curl
154
167
  Curl.const_get("CURLOPT_#{opt.to_s.upcase}")
155
168
  end
156
169
 
170
+ def allowed_protocols=(protocols)
171
+ set_protocol_allowlist('CURLOPT_PROTOCOLS_STR', 'CURLOPT_PROTOCOLS', protocols)
172
+ end
173
+
174
+ def allowed_redirect_protocols=(protocols)
175
+ set_protocol_allowlist('CURLOPT_REDIR_PROTOCOLS_STR', 'CURLOPT_REDIR_PROTOCOLS', protocols)
176
+ end
177
+
178
+ def safe_http!
179
+ __curb_set_safety_override!(
180
+ :protocols => [:http, :https],
181
+ :redirect_protocols => [:http, :https]
182
+ )
183
+ end
184
+
185
+ private
186
+
187
+ def __curb_set_safety_override!(options)
188
+ @__curb_safety_override = (defined?(@__curb_safety_override) && @__curb_safety_override) ? @__curb_safety_override.dup : {}
189
+ @__curb_safety_override[:protocols] = Array(options[:protocols]).map { |protocol| protocol.to_s.downcase.to_sym } if options.key?(:protocols)
190
+ @__curb_safety_override[:redirect_protocols] = Array(options[:redirect_protocols]).map { |protocol| protocol.to_s.downcase.to_sym } if options.key?(:redirect_protocols)
191
+ @__curb_safety_override[:max_body_bytes] = options[:max_body_bytes] if options.key?(:max_body_bytes) && options[:max_body_bytes]
192
+ @__curb_safety_override_generation = __curb_safety_override_generation + 1
193
+ Curl.__send__(:apply_safety!, self) if Curl.respond_to?(:apply_safety!, true)
194
+ self
195
+ end
196
+
197
+ def __curb_clear_safety_override!
198
+ had_override = defined?(@__curb_safety_override) && @__curb_safety_override
199
+ return unless had_override
200
+ return if frozen?
201
+
202
+ @__curb_safety_override = nil
203
+ @__curb_safety_override_generation = __curb_safety_override_generation + 1
204
+ end
205
+
206
+ def __curb_safety_override
207
+ defined?(@__curb_safety_override) ? @__curb_safety_override : nil
208
+ end
209
+
210
+ def __curb_safety_override_generation
211
+ defined?(@__curb_safety_override_generation) ? @__curb_safety_override_generation : 0
212
+ end
213
+
214
+ def set_protocol_allowlist(string_option, bitmask_option, protocols)
215
+ protocol_names = Array(protocols).map { |protocol| protocol.to_s.downcase }
216
+ raise ArgumentError, "at least one protocol is required" if protocol_names.empty?
217
+
218
+ valid_names = %w[
219
+ dict file ftp ftps gopher gophers http https imap imaps ldap ldaps
220
+ mqtt pop3 pop3s rtmp rtmpe rtmps rtmpt rtmpte rtmpts rtsp scp sftp
221
+ smb smbs smtp smtps telnet tftp ws wss
222
+ ]
223
+
224
+ if Curl.const_defined?(string_option)
225
+ protocol_names.each do |name|
226
+ raise ArgumentError, "unsupported protocol: #{name.inspect}" unless valid_names.include?(name)
227
+ end
228
+
229
+ setopt(Curl.const_get(string_option), protocol_names.join(','))
230
+ elsif Curl.const_defined?(bitmask_option)
231
+ protocol_pairs = protocol_names.map do |name|
232
+ const_name = "CURLPROTO_#{name.upcase}"
233
+ raise ArgumentError, "unsupported protocol: #{name.inspect}" unless Curl.const_defined?(const_name)
234
+
235
+ [name, Curl.const_get(const_name)]
236
+ end
237
+
238
+ setopt(Curl.const_get(bitmask_option), protocol_pairs.inject(0) { |mask, pair| mask | pair.last })
239
+ else
240
+ raise NotImplementedError, "protocol allowlists are not supported by this libcurl"
241
+ end
242
+
243
+ protocols
244
+ end
245
+
246
+ public
247
+
157
248
  #
158
249
  # call-seq:
159
250
  # easy.perform => true
@@ -163,6 +254,7 @@ module Curl
163
254
  # the configured HTTP Verb.
164
255
  #
165
256
  def perform
257
+ Curl.__send__(:apply_safety!, self) if Curl.respond_to?(:apply_safety!, true)
166
258
  self.class.flush_deferred_multi_closes
167
259
 
168
260
  if Curl.scheduler_active? && self.multi.nil?
@@ -210,6 +302,10 @@ module Curl
210
302
  raise callback_error
211
303
  end
212
304
 
305
+ if respond_to?(:unsafe_destination_error) && (unsafe_destination_error = self.unsafe_destination_error)
306
+ raise Curl::Err::UnsafeDestinationError, unsafe_destination_error
307
+ end
308
+
213
309
  if self.last_result != 0 && self.on_failure.nil?
214
310
  err_class, err_summary = Curl::Easy.error(self.last_result)
215
311
  err_detail = self.last_error
@@ -641,11 +737,15 @@ module Curl
641
737
  end
642
738
 
643
739
  # call-seq:
644
- # Curl::Easy.download(url, filename = url.split(/\?/).first.split(/\//).last) { |curl| ... }
740
+ # Curl::Easy.download(url, filename = nil, options = {}) { |curl| ... }
645
741
  #
646
742
  # Stream the specified url (via perform) and save the data directly to the
647
- # supplied filename (defaults to the last component of the URL path, which will
648
- # usually be the filename most simple urls).
743
+ # supplied filename. The destination is written through a temporary file and
744
+ # existing files are not overwritten unless <tt>:overwrite => true</tt> is
745
+ # passed. When filename is omitted, the destination is safely derived from the
746
+ # last URL path component in the current directory. Pass
747
+ # <tt>:download_dir</tt> to treat the filename as a basename inside a trusted
748
+ # directory and reject absolute, parent-directory, dotfile, and nested names.
649
749
  #
650
750
  # If a block is supplied, it will be passed the curl instance prior to the
651
751
  # perform call.
@@ -658,16 +758,11 @@ module Curl
658
758
  # returns a size that differs from the data chunk size - in this case, the
659
759
  # offending chunk will *not* be written to the file, the file will be closed,
660
760
  # and a Curl::Err::AbortedByCallbackError will be raised.
661
- def download(url, filename = url.split(/\?/).first.split(/\//).last, &blk)
761
+ def download(url, filename = nil, download_options = {}, &blk)
662
762
  curl = Curl::Easy.new(url, &blk)
763
+ _download_path, output, safe_output = Curl.prepare_download_output(url, filename, download_options)
663
764
 
664
- output = if filename.is_a? IO
665
- filename.binmode if filename.respond_to?(:binmode)
666
- filename
667
- else
668
- File.open(filename, 'wb')
669
- end
670
-
765
+ performed = false
671
766
  begin
672
767
  old_on_body = curl.on_body do |data|
673
768
  result = old_on_body ? old_on_body.call(data) : data.length
@@ -675,8 +770,13 @@ module Curl
675
770
  result
676
771
  end
677
772
  curl.perform
773
+ performed = true
678
774
  ensure
679
- output.close rescue IOError
775
+ if safe_output
776
+ output.close(performed)
777
+ else
778
+ output.close rescue IOError
779
+ end
680
780
  end
681
781
 
682
782
  return curl
data/lib/curl/multi.rb CHANGED
@@ -1,9 +1,19 @@
1
1
  # frozen_string_literal: true
2
+ require 'curl/download'
2
3
  module Curl
3
4
  class Multi
4
5
  class DownloadError < RuntimeError
5
6
  attr_accessor :errors
6
7
  end
8
+
9
+ IDLE_EASY_REFERENCES_USE_WEAK_MAP = begin
10
+ probe = ObjectSpace::WeakMap.new
11
+ probe[Object.new.freeze] = true
12
+ true
13
+ rescue ArgumentError, FrozenError, NameError
14
+ false
15
+ end
16
+
7
17
  class << self
8
18
  # call-seq:
9
19
  # Curl::Multi.get(['url1','url2','url3','url4','url5'], :follow_location => true) do|easy|
@@ -102,6 +112,7 @@ module Curl
102
112
  url = c.delete(:url)
103
113
  method = c.delete(:method)
104
114
  headers = c.delete(:headers)
115
+ internal_info = c.delete(:__curb_internal_info)
105
116
 
106
117
  easy = Curl::Easy.new if easy.nil?
107
118
 
@@ -140,7 +151,13 @@ module Curl
140
151
 
141
152
  easy.on_complete {|curl|
142
153
  free_handles << curl
143
- blk.call(curl,curl.response_code,method) if blk
154
+ if blk
155
+ if internal_info
156
+ blk.call(curl,curl.response_code,method,internal_info)
157
+ else
158
+ blk.call(curl,curl.response_code,method)
159
+ end
160
+ end
144
161
  }
145
162
  m.add(easy)
146
163
  end
@@ -182,73 +199,122 @@ module Curl
182
199
  #
183
200
  # Curl::Multi.download(['http://example.com/p/a/t/h/file1.txt','http://example.com/p/a/t/h/file2.txt']){|c|}
184
201
  #
185
- # will create 2 new files file1.txt and file2.txt
202
+ # will create 2 new files file1.txt and file2.txt, unless either file
203
+ # already exists. Auto-derived filenames are safely derived from the last
204
+ # URL path component. Pass <tt>:download_dir</tt> as the fifth argument to
205
+ # treat download_paths as basenames inside a trusted directory and reject
206
+ # absolute, parent-directory, dotfile, and nested names.
186
207
  #
187
208
  # 2 files will be opened, and remain open until the call completes
188
209
  #
189
210
  # when using the :post or :put method, urls should be a hash, including the individual post fields per post
190
211
  #
191
- def download(urls,easy_options={},multi_options={},download_paths=nil,&blk)
212
+ def download(urls,easy_options={},multi_options={},download_paths=nil,download_options={},&blk)
192
213
  errors = []
193
214
  procs = []
194
215
  files = []
195
216
  urls_with_config = []
196
- url_to_download_paths = {}
217
+ seen_download_paths = {}
218
+ download_infos = []
219
+
220
+ if Curl.download_options_hash?(download_paths) && download_options.empty?
221
+ download_options = download_paths
222
+ download_paths = nil
223
+ end
197
224
 
198
225
  urls.each_with_index do|urlcfg,i|
199
226
  if urlcfg.is_a?(Hash)
200
- url = url[:url]
227
+ url = urlcfg[:url]
201
228
  else
202
229
  url = urlcfg
203
230
  end
204
231
 
205
- if download_paths and download_paths[i]
206
- download_path = download_paths[i]
207
- else
208
- download_path = File.basename(url)
232
+ download_path_arg = download_paths && download_paths[i]
233
+ download_path, file, safe_output, overwrite = Curl.resolve_download_output(url, download_path_arg, download_options)
234
+
235
+ if safe_output
236
+ expanded_path = File.expand_path(download_path)
237
+ raise ArgumentError, "duplicate download destination: #{download_path}" if seen_download_paths[expanded_path]
238
+
239
+ seen_download_paths[expanded_path] = true
209
240
  end
210
241
 
211
- file = lambda do|dp|
212
- file = File.open(dp,"wb")
213
- procs << (lambda {|data| file.write data; data.size })
214
- files << file
215
- file
216
- end.call(download_path)
242
+ download_infos << {
243
+ :url => url,
244
+ :urlcfg => urlcfg,
245
+ :path => download_path,
246
+ :file => file,
247
+ :safe_output => safe_output,
248
+ :overwrite => overwrite
249
+ }
250
+ end
217
251
 
218
- if urlcfg.is_a?(Hash)
219
- urls_with_config << urlcfg.merge({:on_body => procs.last}.merge(easy_options))
252
+ download_infos.each do |info|
253
+ info[:file] ||= Curl.open_safe_download_output(info[:path], :overwrite => info[:overwrite])
254
+ file = info[:file]
255
+ procs << (lambda {|data| file.write data; data.size })
256
+ files << file
257
+
258
+ if info[:urlcfg].is_a?(Hash)
259
+ urls_with_config << info[:urlcfg].merge({:on_body => procs.last, :__curb_internal_info => info}.merge(easy_options))
220
260
  else
221
- urls_with_config << {:url => url, :on_body => procs.last, :method => :get}.merge(easy_options)
261
+ urls_with_config << {:url => info[:url], :on_body => procs.last, :method => :get, :__curb_internal_info => info}.merge(easy_options)
222
262
  end
223
- url_to_download_paths[url] = {:path => download_path, :file => file} # store for later
224
263
  end
225
264
 
226
- if blk
227
- # when injecting the block, ensure file is closed before yielding
228
- Curl::Multi.http(urls_with_config, multi_options) do |c,code,method|
229
- info = url_to_download_paths[c.url]
265
+ finalize_download = lambda do |curl, info|
266
+ file = info[:file]
267
+ files.reject!{|f| f == file }
268
+
269
+ if curl.last_result != 0
230
270
  begin
231
- file = info[:file]
232
- files.reject!{|f| f == file }
233
- file.close
271
+ if info[:safe_output]
272
+ file.close(false)
273
+ else
274
+ file.close
275
+ end
234
276
  rescue => e
235
277
  errors << e
236
278
  end
279
+ err_class, err_summary = Curl::Easy.error(curl.last_result)
280
+ err_detail = curl.last_error
281
+ errors << err_class.new([err_summary, err_detail].compact.join(": "))
282
+ false
283
+ else
284
+ begin
285
+ if info[:safe_output]
286
+ file.close(true)
287
+ else
288
+ file.close
289
+ end
290
+ true
291
+ rescue => e
292
+ errors << e
293
+ false
294
+ end
295
+ end
296
+ end
297
+
298
+ Curl::Multi.http(urls_with_config, multi_options) do |c,code,method,info|
299
+ if finalize_download.call(c, info) && blk
237
300
  blk.call(c,info[:path])
238
301
  end
239
- else
240
- Curl::Multi.http(urls_with_config, multi_options)
241
302
  end
242
303
 
243
304
  ensure
305
+ pending_exception = $!
244
306
  files.each {|f|
245
307
  begin
246
- f.close
308
+ if f.is_a?(Curl::SafeDownloadOutput)
309
+ f.close(false)
310
+ else
311
+ f.close
312
+ end
247
313
  rescue => e
248
314
  errors << e
249
315
  end
250
316
  }
251
- if errors.any?
317
+ if errors.any? && !pending_exception
252
318
  de = Curl::Multi::DownloadError.new
253
319
  de.errors = errors
254
320
  raise de
@@ -270,19 +336,46 @@ module Curl
270
336
  @requests ||= {}
271
337
  end
272
338
 
339
+ def __curb_native_safety_signatures
340
+ @__curb_native_safety_signatures ||= {}
341
+ end
342
+
343
+ def __curb_safety_signature_for(easy)
344
+ safety_signature = if Curl.respond_to?(:safety_signature_for, true)
345
+ Curl.__send__(:safety_signature_for, easy)
346
+ end
347
+
348
+ [
349
+ safety_signature,
350
+ easy.respond_to?(:network_policy) ? easy.network_policy : nil,
351
+ easy.respond_to?(:allowed_hosts) ? easy.allowed_hosts : nil,
352
+ easy.respond_to?(:allowed_cidrs) ? easy.allowed_cidrs : nil
353
+ ]
354
+ end
355
+
356
+ def __record_native_safety_signature(easy)
357
+ __curb_native_safety_signatures[easy.object_id] = __curb_safety_signature_for(easy)
358
+ end
359
+
273
360
  def __idle_easy_references
274
- @__curb_idle_easy_references ||= ObjectSpace::WeakMap.new
361
+ @__curb_idle_easy_references ||= __new_idle_easy_references
275
362
  end
276
363
 
277
364
  def __register_idle_easy_reference(easy)
278
- __idle_easy_references[easy] = true
365
+ if IDLE_EASY_REFERENCES_USE_WEAK_MAP
366
+ __idle_easy_references[easy] = true
367
+ else
368
+ __idle_easy_references[easy.object_id] = true
369
+ end
279
370
  self
280
371
  end
281
372
 
282
373
  def __unregister_idle_easy_reference(easy)
283
374
  return self unless instance_variable_defined?(:@__curb_idle_easy_references)
284
375
 
285
- if @__curb_idle_easy_references.respond_to?(:delete)
376
+ if !IDLE_EASY_REFERENCES_USE_WEAK_MAP
377
+ @__curb_idle_easy_references.delete(easy.object_id)
378
+ elsif @__curb_idle_easy_references.respond_to?(:delete)
286
379
  @__curb_idle_easy_references.delete(easy)
287
380
  else
288
381
  retained_references = ObjectSpace::WeakMap.new
@@ -299,30 +392,70 @@ module Curl
299
392
 
300
393
  def __clear_idle_easy_references
301
394
  return unless instance_variable_defined?(:@__curb_idle_easy_references)
302
-
303
- @__curb_idle_easy_references.keys.each do |easy|
304
- easy.multi = nil if easy.multi.equal?(self)
395
+
396
+ if IDLE_EASY_REFERENCES_USE_WEAK_MAP
397
+ @__curb_idle_easy_references.keys.each do |easy|
398
+ easy.multi = nil if easy.multi.equal?(self)
399
+ end
400
+ else
401
+ @__curb_idle_easy_references.keys.each do |easy_object_id|
402
+ begin
403
+ easy = ObjectSpace._id2ref(easy_object_id)
404
+ rescue RangeError
405
+ next
406
+ end
407
+
408
+ next unless easy.is_a?(Curl::Easy)
409
+
410
+ easy.multi = nil if easy.multi.equal?(self)
411
+ end
305
412
  end
306
- @__curb_idle_easy_references = ObjectSpace::WeakMap.new
413
+ @__curb_idle_easy_references = __new_idle_easy_references
307
414
  end
308
415
 
309
- private :__idle_easy_references, :__register_idle_easy_reference,
310
- :__unregister_idle_easy_reference, :__clear_idle_easy_references
416
+ def __new_idle_easy_references
417
+ IDLE_EASY_REFERENCES_USE_WEAK_MAP ? ObjectSpace::WeakMap.new : {}
418
+ end
419
+
420
+ private :__idle_easy_references, :__curb_native_safety_signatures,
421
+ :__curb_safety_signature_for, :__record_native_safety_signature,
422
+ :__register_idle_easy_reference,
423
+ :__unregister_idle_easy_reference, :__clear_idle_easy_references,
424
+ :__new_idle_easy_references
425
+
426
+ alias_method :_curb_native_perform, :perform
427
+
428
+ def perform(*args, &block)
429
+ requests.each_value do |easy|
430
+ Curl.__send__(:apply_safety!, easy) if Curl.respond_to?(:apply_safety!, true)
431
+ signature = __curb_safety_signature_for(easy)
432
+ if __curb_native_safety_signatures[easy.object_id] != signature &&
433
+ easy.respond_to?(:__curb_native_setup!, true)
434
+ easy.__send__(:__curb_native_setup!)
435
+ __curb_native_safety_signatures[easy.object_id] = signature
436
+ end
437
+ end
438
+
439
+ _curb_native_perform(*args, &block)
440
+ end
311
441
 
312
442
  def add(easy)
313
443
  return self if requests[easy.object_id]
314
444
  # Once a deferred callback exception is pending, Multi#perform is
315
445
  # draining existing transfers only and must not start replacement work.
316
446
  return self if instance_variable_defined?(:@__curb_deferred_exception)
447
+ Curl.__send__(:apply_safety!, easy) if Curl.respond_to?(:apply_safety!, true)
317
448
  _add(easy)
318
449
  __unregister_idle_easy_reference(easy)
319
450
  requests[easy.object_id] = easy
451
+ __record_native_safety_signature(easy)
320
452
  self
321
453
  end
322
454
 
323
455
  def remove(easy)
324
456
  return self if !requests[easy.object_id]
325
457
  requests.delete(easy.object_id)
458
+ __curb_native_safety_signatures.delete(easy.object_id)
326
459
  _remove(easy)
327
460
  self
328
461
  end