curb 0.8.5 → 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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +579 -0
  3. data/Rakefile +85 -27
  4. data/doc.rb +48 -8
  5. data/ext/banned.h +32 -0
  6. data/ext/curb.c +563 -233
  7. data/ext/curb.h +19 -10
  8. data/ext/curb_easy.c +3229 -368
  9. data/ext/curb_easy.h +42 -0
  10. data/ext/curb_errors.c +117 -18
  11. data/ext/curb_errors.h +9 -5
  12. data/ext/curb_macros.h +33 -21
  13. data/ext/curb_multi.c +1904 -272
  14. data/ext/curb_multi.h +11 -3
  15. data/ext/curb_postfield.c +149 -77
  16. data/ext/curb_postfield.h +1 -0
  17. data/ext/curb_upload.c +38 -11
  18. data/ext/curb_upload.h +2 -0
  19. data/ext/extconf.rb +355 -35
  20. data/lib/curb.rb +1 -0
  21. data/lib/curl/download.rb +160 -0
  22. data/lib/curl/easy.rb +422 -88
  23. data/lib/curl/multi.rb +295 -56
  24. data/lib/curl.rb +676 -11
  25. data/tests/bug_crash_on_debug.rb +14 -28
  26. data/tests/bug_crash_on_progress.rb +32 -16
  27. data/tests/bug_curb_easy_blocks_ruby_threads.rb +10 -15
  28. data/tests/bug_curb_easy_post_with_string_no_content_length_header.rb +6 -30
  29. data/tests/bug_follow_redirect_288.rb +83 -0
  30. data/tests/bug_instance_post_differs_from_class_post.rb +3 -5
  31. data/tests/bug_issue102.rb +17 -0
  32. data/tests/bug_issue_noproxy.rb +56 -0
  33. data/tests/bug_issue_post_redirect.rb +93 -0
  34. data/tests/bug_issue_spnego.rb +41 -0
  35. data/tests/bug_multi_segfault.rb +1 -0
  36. data/tests/bug_poison.rb +29 -0
  37. data/tests/bug_raise_on_callback.rb +30 -0
  38. data/tests/helper.rb +400 -44
  39. data/tests/leak_trace.rb +237 -0
  40. data/tests/mem_check.rb +3 -0
  41. data/tests/tc_curl.rb +31 -1
  42. data/tests/tc_curl_download.rb +98 -7
  43. data/tests/tc_curl_easy.rb +985 -66
  44. data/tests/tc_curl_easy_cookielist.rb +277 -0
  45. data/tests/tc_curl_easy_request_target.rb +41 -0
  46. data/tests/tc_curl_easy_resolve.rb +48 -0
  47. data/tests/tc_curl_maxfilesize.rb +212 -0
  48. data/tests/tc_curl_multi.rb +1111 -51
  49. data/tests/tc_curl_native_coverage.rb +145 -0
  50. data/tests/tc_curl_network_policy.rb +1475 -0
  51. data/tests/tc_curl_postfield.rb +207 -30
  52. data/tests/tc_curl_protocols.rb +388 -0
  53. data/tests/tc_fiber_scheduler.rb +584 -0
  54. data/tests/tc_ftp_options.rb +39 -0
  55. data/tests/tc_gc_compact.rb +223 -0
  56. data/tests/tc_test_server_methods.rb +110 -0
  57. data/tests/timeout.rb +30 -6
  58. metadata +66 -31
  59. data/README +0 -194
data/ext/extconf.rb CHANGED
@@ -1,4 +1,10 @@
1
1
  require 'mkmf'
2
+ require 'tmpdir'
3
+ begin
4
+ require 'etc'
5
+ rescue LoadError
6
+ # Etc may not be available on all Ruby builds (very rare). Fallback later.
7
+ end
2
8
 
3
9
  dir_config('curl')
4
10
 
@@ -9,7 +15,7 @@ if find_executable('curl-config')
9
15
  else
10
16
  $LIBS << " #{`curl-config --libs`.strip}"
11
17
  end
12
- ca_bundle_path=`curl-config --ca`.strip
18
+ ca_bundle_path=`curl-config --ca`.strip.gsub(/^"([^"]+)"$/,'\1')
13
19
  if !ca_bundle_path.nil? and ca_bundle_path != ''
14
20
  $defs.push( %{-D HAVE_CURL_CONFIG_CA} )
15
21
  $defs.push( %{-D CURL_CONFIG_CA='#{ca_bundle_path.inspect}'} )
@@ -18,6 +24,8 @@ elsif !have_library('curl') or !have_header('curl/curl.h')
18
24
  fail <<-EOM
19
25
  Can't find libcurl or curl/curl.h
20
26
 
27
+ Make sure development libs (ie libcurl4-openssl-dev) are installed on the system.
28
+
21
29
  Try passing --with-curl-dir or --with-curl-lib and --with-curl-include
22
30
  options to extconf.
23
31
  EOM
@@ -35,30 +43,202 @@ end
35
43
  # puts "Selected arch: #{archs.first}"
36
44
  #end
37
45
 
38
- def define(s, v=1)
39
- $defs.push( format("-D HAVE_%s=%d", s.to_s.upcase, v) )
46
+ def define(s, v = 1)
47
+ $defs.push(format("-D HAVE_%s=%d", s.to_s.upcase, v))
40
48
  end
41
49
 
42
- def have_constant(name)
43
- sname = name.is_a?(Symbol) ? name.to_s : name.upcase
44
- checking_for name do
45
- src = %{
46
- #include <curl/curl.h>
47
- int main() {
48
- int test = (int)#{sname};
49
- return 0;
50
- }
50
+ # Optional parallelization support for constant checks only.
51
+ # Enable automatically when job hints are present (JOBS, BUNDLE_JOBS, MAKEFLAGS -jN),
52
+ # or explicitly via EXTCONF_JOBS/EXTCONF_PARALLEL.
53
+
54
+ def parse_jobs_from_makeflags(flags)
55
+ return nil if flags.nil? || flags.empty?
56
+ tokens = flags.to_s.split(/\s+/)
57
+ jobs = nil
58
+ tokens.each_with_index do |tok, i|
59
+ case tok
60
+ when /\A-j(\d+)\z/
61
+ jobs = $1.to_i
62
+ when '-j'
63
+ nxt = tokens[i + 1]
64
+ jobs = nxt.to_i if nxt && nxt =~ /\A\d+\z/
65
+ when /\A--jobs(?:=(\d+)|\s+(\d+))\z/
66
+ jobs = ($1 || $2).to_i
67
+ when /\Aj(\d+)\z/ # sometimes make condenses flags
68
+ jobs = $1.to_i
69
+ end
70
+ break if jobs && jobs > 0
71
+ end
72
+ jobs
73
+ end
74
+
75
+ def detect_job_hints
76
+ # Priority: explicit extconf hint, then common envs used by bundler/rubygems
77
+ [
78
+ ENV['EXTCONF_JOBS'],
79
+ ENV['JOBS'],
80
+ ENV['BUNDLE_JOBS'],
81
+ parse_jobs_from_makeflags(ENV['MAKEFLAGS'])
82
+ ].each do |v|
83
+ n = v.to_i if v
84
+ return n if n && n > 0
85
+ end
86
+ nil
87
+ end
88
+
89
+ explicit_parallel = ENV.key?('EXTCONF_PARALLEL') && ENV['EXTCONF_PARALLEL'] == '1'
90
+ job_hints = detect_job_hints
91
+
92
+ DEFAULT_PARALLEL_JOBS = begin
93
+ n = Etc.respond_to?(:nprocessors) ? Etc.nprocessors.to_i : 1
94
+ n = 1 if n <= 0
95
+ # Use half the CPUs by default (rounded up), but at least 1
96
+ jobs = [(n.to_f / 2).ceil, 1].max
97
+ jobs
98
+ rescue
99
+ 1
100
+ end
101
+
102
+ PARALLEL_JOBS = begin
103
+ if job_hints && job_hints > 0
104
+ job_hints
105
+ else
106
+ # If no hints provided, default to half the CPUs.
107
+ DEFAULT_PARALLEL_JOBS
108
+ end
109
+ end
110
+
111
+ # Only enable if the platform supports fork. On Windows this stays sequential.
112
+ PARALLEL_CONSTANT_CHECKS = PARALLEL_JOBS > 1 && Process.respond_to?(:fork)
113
+
114
+ $queued_constants = []
115
+
116
+ def try_constant_compile(sname)
117
+ src = %{
118
+ #include <curl/curl.h>
119
+ int main() {
120
+ int test = (int)#{sname};
121
+ (void)test;
122
+ return 0;
51
123
  }
52
- if try_compile(src,"#{$CFLAGS} #{$LIBS}")
53
- define name
54
- true
55
- else
56
- #define name, 0
57
- false
124
+ }
125
+ try_compile(src, "#{$CFLAGS} #{$LIBS}")
126
+ end
127
+
128
+ def have_constant(name)
129
+ # Queue constants for optional parallel probing. Falls back to sequential.
130
+ if PARALLEL_CONSTANT_CHECKS
131
+ $queued_constants << name
132
+ true
133
+ else
134
+ sname = name.is_a?(Symbol) ? name.to_s : name.upcase
135
+ checking_for name do
136
+ if try_constant_compile(sname)
137
+ define name
138
+ true
139
+ else
140
+ false
141
+ end
58
142
  end
59
143
  end
60
144
  end
61
145
 
146
+ def flush_constant_checks
147
+ return if $queued_constants.empty?
148
+
149
+ constants = $queued_constants.uniq
150
+ $queued_constants.clear
151
+
152
+ # On platforms without fork (e.g., Windows), run sequentially.
153
+ unless PARALLEL_CONSTANT_CHECKS
154
+ constants.each { |name| have_constant(name) }
155
+ return
156
+ end
157
+
158
+ # Run constant probes in isolated child processes to avoid mkmf
159
+ # scratch file and logfile contention.
160
+ results = {}
161
+ pending = constants.dup
162
+ running = {}
163
+ max_jobs = PARALLEL_JOBS
164
+
165
+ spawn_probe = lambda do |const_name|
166
+ r, w = IO.pipe
167
+ pid = fork do
168
+ begin
169
+ r.close
170
+ Dir.mktmpdir('curb-mkmf-') do |dir|
171
+ Dir.chdir(dir) do
172
+ begin
173
+ # Avoid clobbering the main mkmf.log
174
+ Logging::logfile = File.open(File::NULL, 'w') rescue File.open(File.join(dir, 'mkmf.log'), 'w')
175
+ rescue
176
+ # best-effort
177
+ end
178
+ sname = const_name.is_a?(Symbol) ? const_name.to_s : const_name.upcase
179
+ ok = try_constant_compile(sname)
180
+ w.write([const_name, ok ? 1 : 0].join("\t"))
181
+ end
182
+ end
183
+ rescue
184
+ # Treat as failure if anything unexpected happens in child
185
+ begin
186
+ w.write([const_name, 0].join("\t"))
187
+ rescue
188
+ end
189
+ ensure
190
+ begin w.close rescue nil end
191
+ # Ensure the child exits without running at_exit handlers
192
+ exit! 0
193
+ end
194
+ end
195
+ w.close
196
+ running[pid] = r
197
+ end
198
+
199
+ # Start initial batch
200
+ while running.size < max_jobs && !pending.empty?
201
+ spawn_probe.call(pending.shift)
202
+ end
203
+
204
+ # Collect results and keep spawning until done
205
+ until running.empty?
206
+ pid = Process.wait
207
+ io = running.delete(pid)
208
+ if io
209
+ begin
210
+ msg = io.read.to_s
211
+ name_str, ok_str = msg.split("\t", 2)
212
+ if name_str
213
+ # Map back to the original object if symbol-like
214
+ original = constants.find { |n| n.to_s == name_str }
215
+ results[original || name_str] = ok_str.to_i == 1
216
+ end
217
+ ensure
218
+ begin io.close rescue nil end
219
+ end
220
+ end
221
+ # Fill next task slot
222
+ spawn_probe.call(pending.shift) unless pending.empty?
223
+ end
224
+
225
+ # Apply results to $defs and output summary via checking_for
226
+ results.each do |const_name, ok|
227
+ sname = const_name.is_a?(Symbol) ? const_name.to_s : const_name.upcase
228
+ checking_for const_name do
229
+ if ok
230
+ define const_name
231
+ true
232
+ else
233
+ false
234
+ end
235
+ end
236
+ end
237
+ end
238
+
239
+ have_constant "curlopt_tcp_keepalive"
240
+ have_constant "curlopt_tcp_keepidle"
241
+ have_constant "curlopt_tcp_keepintvl"
62
242
  have_constant "curlinfo_appconnect_time"
63
243
  have_constant "curlinfo_redirect_time"
64
244
  have_constant "curlinfo_response_code"
@@ -66,6 +246,7 @@ have_constant "curlinfo_filetime"
66
246
  have_constant "curlinfo_redirect_count"
67
247
  have_constant "curlinfo_os_errno"
68
248
  have_constant "curlinfo_num_connects"
249
+ have_constant "curlinfo_cookielist"
69
250
  have_constant "curlinfo_ftp_entry_path"
70
251
  have_constant "curl_version_ssl"
71
252
  have_constant "curl_version_libz"
@@ -78,9 +259,12 @@ have_constant "curl_version_largefile"
78
259
  have_constant "curl_version_idn"
79
260
  have_constant "curl_version_sspi"
80
261
  have_constant "curl_version_conv"
262
+ have_constant "curl_version_http2"
81
263
  have_constant "curlproxy_http"
82
264
  have_constant "curlproxy_socks4"
265
+ have_constant "curlproxy_socks4a"
83
266
  have_constant "curlproxy_socks5"
267
+ have_constant "curlproxy_socks5_hostname"
84
268
  have_constant "curlauth_basic"
85
269
  have_constant "curlauth_digest"
86
270
  have_constant "curlauth_gssnegotiate"
@@ -99,6 +283,12 @@ have_constant "curle_send_fail_rewind"
99
283
  have_constant "curle_ssl_engine_initfailed"
100
284
  have_constant "curle_login_denied"
101
285
 
286
+ # older than 7.10.0
287
+ have_constant "curlopt_nosignal"
288
+
289
+ # older than 7.16.0
290
+ have_constant "curlmopt_pipelining"
291
+
102
292
  # older than 7.16.3
103
293
  have_constant "curlmopt_maxconnects"
104
294
 
@@ -108,6 +298,40 @@ have_constant "curlopt_sockoptfunction"
108
298
  have_constant "curlopt_sockoptdata"
109
299
  have_constant "curlopt_opensocketfunction"
110
300
  have_constant "curlopt_opensocketdata"
301
+ have_constant "curlopt_prereqfunction"
302
+ have_constant "curlopt_prereqdata"
303
+
304
+ # Deprecated constants (still check for them for backward compat)
305
+ have_constant "curlopt_ioctlfunction"
306
+ have_constant "curlopt_ioctldata"
307
+ have_constant "curlopt_progressfunction"
308
+ have_constant "curlopt_progressdata"
309
+ have_constant "curlopt_conv_to_network_function"
310
+ have_constant "curlopt_conv_from_network_function"
311
+ have_constant "curlopt_conv_from_utf8_function"
312
+
313
+ # Replacements for deprecated constants
314
+ # CURLOPT_PROGRESSFUNCTION -> CURLOPT_XFERINFOFUNCTION (since 7.32.0)
315
+ have_constant "curlopt_xferinfofunction"
316
+ have_constant "curlopt_xferinfodata"
317
+
318
+ # CURLOPT_PROTOCOLS -> CURLOPT_PROTOCOLS_STR (since 7.85.0)
319
+ have_constant "curlopt_protocols_str"
320
+ have_constant "curlopt_redir_protocols_str"
321
+
322
+ # CURLOPT_SOCKS5_GSSAPI_SERVICE -> CURLOPT_PROXY_SERVICE_NAME (since 7.49.0)
323
+ have_constant "curlopt_proxy_service_name"
324
+
325
+ # CURLOPT_HTTPPOST -> CURLOPT_MIMEPOST (since 7.56.0)
326
+ have_constant "curlopt_mimepost"
327
+
328
+ # CURLINFO_* -> CURLINFO_*_T (since 7.55.0)
329
+ have_constant "curlinfo_size_upload_t"
330
+ have_constant "curlinfo_size_download_t"
331
+ have_constant "curlinfo_speed_upload_t"
332
+ have_constant "curlinfo_speed_download_t"
333
+ have_constant "curlinfo_content_length_download_t"
334
+ have_constant "curlinfo_content_length_upload_t"
111
335
 
112
336
  # additional consts
113
337
  have_constant "curle_conv_failed"
@@ -143,6 +367,8 @@ have_func("curl_multi_timeout")
143
367
  have_func("curl_multi_fdset")
144
368
  have_func("curl_multi_perform")
145
369
 
370
+ have_constant "curlopt_haproxyprotocol"
371
+
146
372
  # constants
147
373
  have_constant "curlopt_interleavefunction"
148
374
  have_constant "curlopt_interleavedata"
@@ -167,6 +393,7 @@ have_constant "curlopt_socks5_gssapi_nec"
167
393
  have_constant "curlopt_interface"
168
394
  have_constant "curlopt_localport"
169
395
  have_constant "curlopt_dns_cache_timeout"
396
+ have_constant "curlopt_dns_servers"
170
397
  have_constant "curlopt_dns_use_global_cache"
171
398
  have_constant "curlopt_buffersize"
172
399
  have_constant "curlopt_port"
@@ -207,6 +434,7 @@ have_constant "curlopt_httppost"
207
434
  have_constant "curlopt_referer"
208
435
  have_constant "curlopt_useragent"
209
436
  have_constant "curlopt_httpheader"
437
+ have_constant "curlopt_proxyheader"
210
438
  have_constant "curlopt_http200aliases"
211
439
  have_constant "curlopt_cookie"
212
440
  have_constant "curlopt_cookiefile"
@@ -306,7 +534,9 @@ have_constant "curlusessl_none"
306
534
  have_constant "curlusessl_try"
307
535
  have_constant "curlusessl_control"
308
536
  have_constant "curlusessl_all"
537
+ have_constant "curlopt_connect_to"
309
538
  have_constant "curlopt_resolve"
539
+ have_constant "curlopt_request_target"
310
540
  have_constant "curlopt_sslcert"
311
541
  have_constant "curlopt_sslcerttype"
312
542
  have_constant "curlopt_sslkey"
@@ -319,7 +549,23 @@ have_constant "curl_sslversion_default"
319
549
  have_constant :CURL_SSLVERSION_TLSv1
320
550
  have_constant :CURL_SSLVERSION_SSLv2
321
551
  have_constant :CURL_SSLVERSION_SSLv3
552
+
553
+ # Added in 7.30.0
554
+ have_constant "curlmopt_max_host_connections"
555
+
556
+ # Added in 7.34.0
557
+ have_constant :CURL_SSLVERSION_TLSv1_0
558
+ have_constant :CURL_SSLVERSION_TLSv1_1
559
+ have_constant :CURL_SSLVERSION_TLSv1_2
560
+
561
+ # Added in 7.52.0
562
+ have_constant :CURL_SSLVERSION_TLSv1_3
563
+
322
564
  have_constant "curlopt_ssl_verifypeer"
565
+ have_constant "curlopt_doh_url"
566
+ have_constant "curlopt_doh_ssl_verifypeer"
567
+ have_constant "curlopt_doh_ssl_verifyhost"
568
+ have_constant "curlopt_doh_ssl_verifystatus"
323
569
  have_constant "curlopt_cainfo"
324
570
  have_constant "curlopt_issuercert"
325
571
  have_constant "curlopt_capath"
@@ -352,15 +598,77 @@ have_constant "curle_not_built_in"
352
598
 
353
599
  have_constant "curle_obsolete" # removed in 7.24 ?
354
600
 
601
+ have_constant "curle_ftp_pret_failed"
602
+ have_constant "curle_rtsp_cseq_error"
603
+ have_constant "curle_rtsp_session_error"
604
+ have_constant "curle_ftp_bad_file_list"
605
+ have_constant "curle_chunk_failed"
606
+ have_constant "curle_no_connection_available"
607
+ have_constant "curle_ssl_pinnedpubkeynotmatch"
608
+ have_constant "curle_ssl_invalidcertstatus"
609
+ have_constant "curle_http2_stream"
610
+
355
611
  # gssapi/spnego delegation related constants
356
612
  have_constant "curlopt_gssapi_delegation"
357
613
  have_constant "curlgssapi_delegation_policy_flag"
358
614
  have_constant "curlgssapi_delegation_flag"
359
615
 
616
+ have_constant "CURLM_ADDED_ALREADY"
617
+
618
+ # added in 7.40.0
619
+ have_constant "curlopt_unix_socket_path"
620
+
621
+ # added in 7.42.0
622
+ have_constant "curlopt_path_as_is"
623
+
624
+ # added in 7.43.0
625
+ have_constant "curlopt_pipewait"
626
+
627
+ have_constant "curlopt_proxy_ssl_verifyhost"
628
+
629
+ # protocol constants
630
+ have_constant "curlproto_all"
631
+ have_constant "curlproto_dict"
632
+ have_constant "curlproto_file"
633
+ have_constant "curlproto_ftp"
634
+ have_constant "curlproto_ftps"
635
+ have_constant "curlproto_gopher"
636
+ have_constant "curlproto_http"
637
+ have_constant "curlproto_https"
638
+ have_constant "curlproto_imap"
639
+ have_constant "curlproto_imaps"
640
+ have_constant "curlproto_ldap"
641
+ have_constant "curlproto_ldaps"
642
+ have_constant "curlproto_pop3"
643
+ have_constant "curlproto_pop3s"
644
+ have_constant "curlproto_rtmp"
645
+ have_constant "curlproto_rtmpe"
646
+ have_constant "curlproto_rtmps"
647
+ have_constant "curlproto_rtmpt"
648
+ have_constant "curlproto_rtmpte"
649
+ have_constant "curlproto_rtmpts"
650
+ have_constant "curlproto_rtsp"
651
+ have_constant "curlproto_scp"
652
+ have_constant "curlproto_sftp"
653
+ have_constant "curlproto_smb"
654
+ have_constant "curlproto_smbs"
655
+ have_constant "curlproto_smtp"
656
+ have_constant "curlproto_smtps"
657
+ have_constant "curlproto_telnet"
658
+ have_constant "curlproto_tftp"
659
+
360
660
  if try_compile('int main() { return 0; }','-Wall')
361
661
  $CFLAGS << ' -Wall'
362
662
  end
363
663
 
664
+ # Clang-specific warning suppressions (not recognized by GCC)
665
+ # These are used to suppress warnings in Ruby header macros
666
+ %w[-Wno-self-assign -Wno-parentheses-equality -Wno-constant-logical-operand].each do |flag|
667
+ if try_compile('int main() { return 0; }', flag)
668
+ $CFLAGS << " #{flag}"
669
+ end
670
+ end
671
+
364
672
  # do some checking to detect ruby 1.8 hash.c vs ruby 1.9 hash.c
365
673
  def test_for(name, const, src)
366
674
  checking_for name do
@@ -372,23 +680,6 @@ def test_for(name, const, src)
372
680
  end
373
681
  end
374
682
  end
375
- test_for("Ruby 1.9 Hash", "RUBY19_HASH", %{
376
- #include <ruby.h>
377
- int main() {
378
- VALUE hash = rb_hash_new();
379
- if( RHASH(hash)->ntbl->num_entries ) {
380
- return 0;
381
- }
382
- return 1;
383
- }
384
- })
385
- test_for("Ruby 1.9 st.h", "RUBY19_ST_H", %{
386
- #include <ruby.h>
387
- #include <ruby/st.h>
388
- int main() {
389
- return 0;
390
- }
391
- })
392
683
 
393
684
  test_for("curl_easy_escape", "CURL_EASY_ESCAPE", %{
394
685
  #include <curl/curl.h>
@@ -400,6 +691,35 @@ test_for("curl_easy_escape", "CURL_EASY_ESCAPE", %{
400
691
  })
401
692
 
402
693
  have_func('rb_thread_blocking_region')
694
+ have_header('ruby/thread.h') && have_func('rb_thread_call_without_gvl', 'ruby/thread.h')
695
+ have_header('ruby/io.h')
696
+ # Ruby 4.x exports rb_thread_fd_select without declaring it in ruby/io.h.
697
+ have_func('rb_thread_fd_select')
698
+ have_func('rb_wait_for_single_fd', 'ruby/io.h')
699
+ have_header('ruby/fiber/scheduler.h')
700
+ have_func('rb_fiber_scheduler_current', 'ruby/fiber/scheduler.h')
701
+ have_func('rb_fiber_scheduler_io_wait', 'ruby/fiber/scheduler.h')
702
+ have_func('rb_fiber_scheduler_io_select', 'ruby/fiber/scheduler.h')
703
+ have_func('rb_io_stdio_file')
704
+ have_func('curl_multi_wait')
705
+ have_func('curl_multi_socket_action')
706
+ have_func('curl_multi_assign')
707
+ have_func('curl_multi_wakeup')
708
+ have_func('curl_multi_poll')
709
+ have_func('curl_multi_socket')
710
+ have_func('curl_multi_timer_callback')
711
+ have_constant 'curlmopt_socketfunction'
712
+ have_constant 'curlmopt_timerfunction'
713
+ have_func('curl_easy_duphandle')
714
+
715
+ # Optional: enable verbose socket-action debug logging.
716
+ # Set CURB_SOCKET_DEBUG=1 in the environment before running extconf to enable.
717
+ if ENV['CURB_SOCKET_DEBUG'] == '1'
718
+ $defs << '-DCURB_SOCKET_DEBUG=1'
719
+ end
720
+
721
+ # Run any queued constant checks (in parallel if enabled) before header generation.
722
+ flush_constant_checks
403
723
 
404
724
  create_header('curb_config.h')
405
725
  create_makefile('curb_core')
data/lib/curb.rb CHANGED
@@ -1 +1,2 @@
1
+ # frozen_string_literal: true
1
2
  require 'curl'
@@ -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