curb 1.3.2 → 1.3.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3f62e867480076ceef877a4adcb5c3b4a344769207af06ded4535a3ce1e87375
4
- data.tar.gz: 4caa926321d05804edbd4b0470951a617192ffd2504ac741524306edec1fe193
3
+ metadata.gz: 363701a514e690ec0d3daab68555267a6640caa7242583941b674b2712eac9e4
4
+ data.tar.gz: 9cc165c1079d65dbff540ccdf10b4e456da09d21b81da52ecc9038b414e2c906
5
5
  SHA512:
6
- metadata.gz: 917f5e735b7187b5089896a83442462ed7f7b9311dbf616e58ab2cae3b2dff38e28f51f401e183214779d8f5159da6fa6dc7d842c0b5e21993cd428f61868d1f
7
- data.tar.gz: 986d63577872fc80a563a4f2ac29a169888eb549d7ba462ecdef89636f3087be48ad7895e13954824e5af069614601ae30730a0af88cd4b3ad3391997b37c3e3
6
+ metadata.gz: 04cd7fece28ee5166ccf853d73686ff083b8ffac74c225f09f7795a39db27753a523736cfe604ad9880816f750774ba7f03f2ac0b2771276e8a3cefb75c60cf1
7
+ data.tar.gz: 7cf816b75ca9cafdd36bd9a7138735f19d609b9d2287066aff29f541f9581c5c10dcd70ac915a962e001d745ee5e8b7f37b2328819ac455f680f835225b64a75
data/ext/curb.h CHANGED
@@ -28,11 +28,11 @@
28
28
  #include "curb_macros.h"
29
29
 
30
30
  // These should be managed from the Rake 'release' task.
31
- #define CURB_VERSION "1.3.2"
32
- #define CURB_VER_NUM 1032
31
+ #define CURB_VERSION "1.3.3"
32
+ #define CURB_VER_NUM 1033
33
33
  #define CURB_VER_MAJ 1
34
34
  #define CURB_VER_MIN 3
35
- #define CURB_VER_MIC 2
35
+ #define CURB_VER_MIC 3
36
36
  #define CURB_VER_PATCH 0
37
37
 
38
38
 
data/ext/curb_easy.c CHANGED
@@ -829,6 +829,10 @@ static VALUE ruby_curl_easy_clone(VALUE self) {
829
829
 
830
830
  /* now deep copy */
831
831
  newrbce->curl = curl_easy_duphandle(rbce->curl);
832
+ if (!newrbce->curl) {
833
+ free(newrbce);
834
+ rb_raise(rb_eNoMemError, "Failed to duplicate Curl::Easy handle");
835
+ }
832
836
  newrbce->curl_headers = (rbce->curl_headers) ? duplicate_curl_slist(rbce->curl_headers) : NULL;
833
837
  newrbce->curl_proxy_headers = (rbce->curl_proxy_headers) ? duplicate_curl_slist(rbce->curl_proxy_headers) : NULL;
834
838
  newrbce->curl_ftp_commands = (rbce->curl_ftp_commands) ? duplicate_curl_slist(rbce->curl_ftp_commands) : NULL;
@@ -1367,35 +1371,15 @@ static VALUE ruby_curl_easy_put_data_set(VALUE self, VALUE data) {
1367
1371
  ruby_curl_easy *rbce;
1368
1372
  CURL *curl;
1369
1373
  VALUE upload;
1374
+ VALUE upload_stream = data;
1370
1375
  VALUE headers;
1376
+ VALUE infile_size = Qnil;
1371
1377
 
1372
1378
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
1373
1379
 
1374
- upload = ruby_curl_upload_new(cCurlUpload);
1375
- ruby_curl_upload_stream_set(upload,data);
1376
-
1377
- curl = rbce->curl;
1378
- rb_easy_set("upload", upload); /* keep the upload object alive as long as
1379
- the easy handle is active or until the upload
1380
- is complete or terminated... */
1381
-
1382
- curl_easy_setopt(curl, CURLOPT_NOBODY, 0);
1383
- curl_easy_setopt(curl, CURLOPT_POST, 0);
1384
- curl_easy_setopt(curl, CURLOPT_POSTFIELDS, NULL);
1385
- curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0);
1386
- curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);
1387
- curl_easy_setopt(curl, CURLOPT_READFUNCTION, (curl_read_callback)read_data_handler);
1388
- #ifdef HAVE_CURLOPT_SEEKFUNCTION
1389
- curl_easy_setopt(curl, CURLOPT_SEEKFUNCTION, (curl_seek_callback)seek_data_handler);
1390
- #endif
1391
- curl_easy_setopt(curl, CURLOPT_READDATA, rbce);
1392
- #ifdef HAVE_CURLOPT_SEEKDATA
1393
- curl_easy_setopt(curl, CURLOPT_SEEKDATA, rbce);
1394
- #endif
1395
-
1396
1380
  /*
1397
- * we need to set specific headers for the PUT to work... so
1398
- * convert the internal headers structure to a HASH if one is set
1381
+ * Validate and prepare Ruby-visible state before mutating the CURL handle.
1382
+ * Several branches below can raise (header type, stat, size, to_s).
1399
1383
  */
1400
1384
  if (!rb_easy_nil("headers")) {
1401
1385
  if (rb_easy_type_check("headers", T_ARRAY) || rb_easy_type_check("headers", T_STRING)) {
@@ -1403,43 +1387,80 @@ static VALUE ruby_curl_easy_put_data_set(VALUE self, VALUE data) {
1403
1387
  }
1404
1388
  }
1405
1389
 
1406
- // exit fast if the payload is empty
1407
- if (NIL_P(data)) { return data; }
1390
+ if (!NIL_P(data) && !rb_respond_to(data, rb_intern("read"))) {
1391
+ if (rb_respond_to(data, rb_intern("to_s"))) {
1392
+ upload_stream = rb_obj_as_string(data);
1393
+ } else {
1394
+ rb_raise(rb_eRuntimeError, "PUT data must respond to read or to_s");
1395
+ }
1396
+ }
1408
1397
 
1409
1398
  headers = rb_easy_get("headers");
1410
1399
  if( headers == Qnil ) {
1411
1400
  headers = rb_hash_new();
1412
1401
  }
1413
1402
 
1414
- if (rb_respond_to(data, rb_intern("read"))) {
1415
- VALUE stat = rb_funcall(data, rb_intern("stat"), 0);
1416
- if( stat && rb_hash_aref(headers, rb_str_new2("Content-Length")) == Qnil) {
1403
+ if (!NIL_P(data) && rb_respond_to(data, rb_intern("read"))) {
1404
+ VALUE stat = Qnil;
1405
+ if (rb_respond_to(data, rb_intern("stat"))) {
1406
+ stat = rb_funcall(data, rb_intern("stat"), 0);
1407
+ }
1408
+ if(!NIL_P(stat) && stat != Qfalse && rb_hash_aref(headers, rb_str_new2("Content-Length")) == Qnil) {
1417
1409
  VALUE size;
1418
1410
  if( rb_hash_aref(headers, rb_str_new2("Expect")) == Qnil ) {
1419
1411
  rb_hash_aset(headers, rb_str_new2("Expect"), rb_str_new2(""));
1420
1412
  }
1421
1413
  size = rb_funcall(stat, rb_intern("size"), 0);
1422
- curl_easy_setopt(curl, CURLOPT_INFILESIZE, NUM2LONG(size));
1414
+ infile_size = size;
1423
1415
  }
1424
1416
  else if( rb_hash_aref(headers, rb_str_new2("Content-Length")) == Qnil && rb_hash_aref(headers, rb_str_new2("Transfer-Encoding")) == Qnil ) {
1425
1417
  rb_hash_aset(headers, rb_str_new2("Transfer-Encoding"), rb_str_new2("chunked"));
1426
1418
  }
1427
1419
  else if( rb_hash_aref(headers, rb_str_new2("Content-Length")) ) {
1428
1420
  VALUE size = rb_funcall(rb_hash_aref(headers, rb_str_new2("Content-Length")), rb_intern("to_i"), 0);
1429
- curl_easy_setopt(curl, CURLOPT_INFILESIZE, NUM2LONG(size));
1421
+ infile_size = size;
1430
1422
  }
1431
1423
  }
1432
- else if (rb_respond_to(data, rb_intern("to_s"))) {
1433
- curl_easy_setopt(curl, CURLOPT_INFILESIZE, RSTRING_LEN(data));
1424
+ else if (!NIL_P(data) && rb_respond_to(data, rb_intern("to_s"))) {
1425
+ infile_size = LONG2NUM(RSTRING_LEN(upload_stream));
1434
1426
  if( rb_hash_aref(headers, rb_str_new2("Expect")) == Qnil ) {
1435
1427
  rb_hash_aset(headers, rb_str_new2("Expect"), rb_str_new2(""));
1436
1428
  }
1437
1429
  }
1430
+ else if (NIL_P(data)) {
1431
+ /* Preserve legacy nil handling: configure an upload with no payload. */
1432
+ }
1438
1433
  else {
1439
1434
  rb_raise(rb_eRuntimeError, "PUT data must respond to read or to_s");
1440
1435
  }
1441
1436
  rb_easy_set("headers",headers);
1442
1437
 
1438
+ upload = ruby_curl_upload_new(cCurlUpload);
1439
+ ruby_curl_upload_stream_set(upload, upload_stream);
1440
+
1441
+ curl = rbce->curl;
1442
+ rb_easy_set("upload", upload); /* keep the upload object alive as long as
1443
+ the easy handle is active or until the upload
1444
+ is complete or terminated... */
1445
+
1446
+ curl_easy_setopt(curl, CURLOPT_NOBODY, 0);
1447
+ curl_easy_setopt(curl, CURLOPT_POST, 0);
1448
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDS, NULL);
1449
+ curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0);
1450
+ curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);
1451
+ curl_easy_setopt(curl, CURLOPT_READFUNCTION, (curl_read_callback)read_data_handler);
1452
+ #ifdef HAVE_CURLOPT_SEEKFUNCTION
1453
+ curl_easy_setopt(curl, CURLOPT_SEEKFUNCTION, (curl_seek_callback)seek_data_handler);
1454
+ #endif
1455
+ curl_easy_setopt(curl, CURLOPT_READDATA, rbce);
1456
+ #ifdef HAVE_CURLOPT_SEEKDATA
1457
+ curl_easy_setopt(curl, CURLOPT_SEEKDATA, rbce);
1458
+ #endif
1459
+
1460
+ if (!NIL_P(infile_size)) {
1461
+ curl_easy_setopt(curl, CURLOPT_INFILESIZE, NUM2LONG(infile_size));
1462
+ }
1463
+
1443
1464
  // if we made it this far, all should be well.
1444
1465
  return data;
1445
1466
  }
@@ -2754,7 +2775,7 @@ static VALUE cb_each_ftp_command(VALUE ftp_command, VALUE wrap, int _c, const VA
2754
2775
  TypedData_Get_Struct(wrap, struct curl_slist *, &curl_slist_ptr_type, list);
2755
2776
 
2756
2777
  ftp_command_string = rb_obj_as_string(ftp_command);
2757
- struct curl_slist *new_list = curl_slist_append(*list, StringValuePtr(ftp_command));
2778
+ struct curl_slist *new_list = curl_slist_append(*list, StringValuePtr(ftp_command_string));
2758
2779
  if (!new_list) {
2759
2780
  rb_raise(rb_eNoMemError, "Failed to append to FTP command list");
2760
2781
  }
@@ -2772,7 +2793,7 @@ static VALUE cb_each_resolve(VALUE resolve, VALUE wrap, int _c, const VALUE *_pt
2772
2793
  TypedData_Get_Struct(wrap, struct curl_slist *, &curl_slist_ptr_type, list);
2773
2794
 
2774
2795
  resolve_string = rb_obj_as_string(resolve);
2775
- struct curl_slist *new_list = curl_slist_append(*list, StringValuePtr(resolve));
2796
+ struct curl_slist *new_list = curl_slist_append(*list, StringValuePtr(resolve_string));
2776
2797
  if (!new_list) {
2777
2798
  rb_raise(rb_eNoMemError, "Failed to append to resolve list");
2778
2799
  }
@@ -3153,6 +3174,13 @@ VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
3153
3174
  if (rb_easy_type_check("resolve", T_ARRAY)) {
3154
3175
  VALUE wrap = TypedData_Wrap_Struct(rb_cObject, &curl_slist_ptr_type, rslv);
3155
3176
  rb_block_call(rb_easy_get("resolve"), rb_intern("each"), 0, NULL, cb_each_resolve, wrap);
3177
+ } else {
3178
+ VALUE resolve_str = rb_obj_as_string(rb_easy_get("resolve"));
3179
+ struct curl_slist *new_list = curl_slist_append(*rslv, StringValuePtr(resolve_str));
3180
+ if (!new_list) {
3181
+ rb_raise(rb_eNoMemError, "Failed to append to resolve list");
3182
+ }
3183
+ *rslv = new_list;
3156
3184
  }
3157
3185
 
3158
3186
  if (*rslv) {
@@ -3209,6 +3237,44 @@ VALUE ruby_curl_easy_cleanup( VALUE self, ruby_curl_easy *rbce ) {
3209
3237
  return Qnil;
3210
3238
  }
3211
3239
 
3240
+ struct easy_perform_request_restore_args {
3241
+ VALUE self;
3242
+ CURL *curl;
3243
+ ruby_curl_easy *rbce;
3244
+ int clear_customrequest;
3245
+ int clear_nobody;
3246
+ int clear_postfields;
3247
+ };
3248
+
3249
+ static VALUE perform_with_request_restore_body(VALUE argp) {
3250
+ struct easy_perform_request_restore_args *args = (struct easy_perform_request_restore_args *)argp;
3251
+ return rb_funcall(args->self, rb_intern("perform"), 0);
3252
+ }
3253
+
3254
+ static VALUE perform_with_request_restore_ensure(VALUE argp) {
3255
+ struct easy_perform_request_restore_args *args = (struct easy_perform_request_restore_args *)argp;
3256
+
3257
+ if (args->curl) {
3258
+ if (args->clear_nobody) {
3259
+ curl_easy_setopt(args->curl, CURLOPT_NOBODY, 0L);
3260
+ }
3261
+ if (args->clear_customrequest) {
3262
+ curl_easy_setopt(args->curl, CURLOPT_CUSTOMREQUEST, NULL);
3263
+ }
3264
+ if (args->clear_postfields) {
3265
+ curl_easy_setopt(args->curl, CURLOPT_POST, 0L);
3266
+ curl_easy_setopt(args->curl, CURLOPT_POSTFIELDS, NULL);
3267
+ curl_easy_setopt(args->curl, CURLOPT_POSTFIELDSIZE, 0L);
3268
+ curl_easy_setopt(args->curl, CURLOPT_HTTPGET, 1L);
3269
+ if (args->rbce && !NIL_P(args->rbce->opts)) {
3270
+ rb_hash_delete(args->rbce->opts, rb_easy_hkey("postdata_buffer"));
3271
+ }
3272
+ }
3273
+ }
3274
+
3275
+ return Qnil;
3276
+ }
3277
+
3212
3278
  /*
3213
3279
  * Common implementation of easy.http(verb) and easy.http_delete
3214
3280
  */
@@ -3241,13 +3307,9 @@ static VALUE ruby_curl_easy_perform_verb_str(VALUE self, const char *verb) {
3241
3307
  curl_easy_setopt(curl, CURLOPT_POST, 0L);
3242
3308
  }
3243
3309
 
3244
- retval = rb_funcall(self, rb_intern("perform"), 0);
3245
-
3246
- /* Restore state after request. */
3247
- if (is_head) {
3248
- curl_easy_setopt(curl, CURLOPT_NOBODY, 0L);
3249
- }
3250
- curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, NULL);
3310
+ struct easy_perform_request_restore_args restore_args = { self, curl, rbce, 1, is_head, 0 };
3311
+ retval = rb_ensure(perform_with_request_restore_body, (VALUE)&restore_args,
3312
+ perform_with_request_restore_ensure, (VALUE)&restore_args);
3251
3313
 
3252
3314
  return retval;
3253
3315
  }
@@ -3405,7 +3467,9 @@ static VALUE ruby_curl_easy_perform_post(int argc, VALUE *argv, VALUE self) {
3405
3467
  ruby_curl_easy_post_body_set(self, post_body);
3406
3468
  }
3407
3469
 
3408
- return rb_funcall(self, rb_intern("perform"), 0);
3470
+ struct easy_perform_request_restore_args restore_args = { self, curl, rbce, 1, 0, 0 };
3471
+ return rb_ensure(perform_with_request_restore_body, (VALUE)&restore_args,
3472
+ perform_with_request_restore_ensure, (VALUE)&restore_args);
3409
3473
  }
3410
3474
  }
3411
3475
  }
@@ -3459,7 +3523,9 @@ static VALUE ruby_curl_easy_perform_patch(int argc, VALUE *argv, VALUE self) {
3459
3523
  if (rb_easy_nil("postdata_buffer")) {
3460
3524
  ruby_curl_easy_post_body_set(self, patch_body);
3461
3525
  }
3462
- return rb_funcall(self, rb_intern("perform"), 0);
3526
+ struct easy_perform_request_restore_args restore_args = { self, curl, rbce, 1, 0, 1 };
3527
+ return rb_ensure(perform_with_request_restore_body, (VALUE)&restore_args,
3528
+ perform_with_request_restore_ensure, (VALUE)&restore_args);
3463
3529
  }
3464
3530
  }
3465
3531
  }
@@ -3510,7 +3576,9 @@ static VALUE ruby_curl_easy_perform_put(int argc, VALUE *argv, VALUE self) {
3510
3576
  ruby_curl_easy_put_data_set(self, post_body);
3511
3577
  }
3512
3578
  }
3513
- return rb_funcall(self, rb_intern("perform"), 0);
3579
+ struct easy_perform_request_restore_args restore_args = { self, curl, rbce, 1, 0, 0 };
3580
+ return rb_ensure(perform_with_request_restore_body, (VALUE)&restore_args,
3581
+ perform_with_request_restore_ensure, (VALUE)&restore_args);
3514
3582
  }
3515
3583
 
3516
3584
 
@@ -4577,6 +4645,7 @@ static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
4577
4645
  }
4578
4646
  /* Save the list pointer in the ruby_curl_easy structure for cleanup later */
4579
4647
  rbce->curl_resolve = list;
4648
+ rb_hash_aset(rbce->opts, rb_easy_hkey("resolve"), val);
4580
4649
  curl_easy_setopt(rbce->curl, CURLOPT_RESOLVE, list);
4581
4650
  } break;
4582
4651
  #endif
data/ext/curb_multi.c CHANGED
@@ -28,6 +28,7 @@
28
28
 
29
29
  #include <errno.h>
30
30
  #include <fcntl.h>
31
+ #include <stdint.h>
31
32
  #include <stdarg.h>
32
33
 
33
34
  /*
@@ -52,9 +53,7 @@
52
53
  #include <fcntl.h>
53
54
  #endif
54
55
 
55
- #if 0 /* disabled curl_multi_wait in favor of scheduler-aware fdsets */
56
- #include <stdint.h> /* for intptr_t */
57
-
56
+ #if defined(HAVE_CURL_MULTI_WAIT) && !defined(HAVE_RB_THREAD_FD_SELECT)
58
57
  struct wait_args {
59
58
  CURLM *handle;
60
59
  long timeout_ms;
@@ -95,6 +94,34 @@ static void rb_curl_multi_remove_request_reference(VALUE self, VALUE easy);
95
94
  static VALUE ruby_curl_multi_mark_closed(VALUE self);
96
95
  static VALUE ruby_curl_multi_alloc(VALUE klass);
97
96
  static VALUE ruby_curl_multi_initialize(VALUE self);
97
+ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self);
98
+ #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
99
+ static VALUE ruby_curl_multi_socket_perform_impl(int argc, VALUE *argv, VALUE self);
100
+ #endif
101
+
102
+ static ruby_curl_multi *ruby_curl_multi_pointer_if_compatible(VALUE multi_val) {
103
+ if (NIL_P(multi_val) || !RB_TYPE_P(multi_val, T_DATA)) {
104
+ return NULL;
105
+ }
106
+
107
+ #if defined(RTYPEDDATA_P) && defined(RTYPEDDATA_TYPE) && defined(RTYPEDDATA_DATA)
108
+ if (!RTYPEDDATA_P(multi_val)) {
109
+ return NULL;
110
+ }
111
+
112
+ if (RTYPEDDATA_TYPE(multi_val) != &ruby_curl_multi_data_type) {
113
+ return NULL;
114
+ }
115
+
116
+ return (ruby_curl_multi *)RTYPEDDATA_DATA(multi_val);
117
+ #else
118
+ if (!rb_typeddata_is_kind_of(multi_val, &ruby_curl_multi_data_type)) {
119
+ return NULL;
120
+ }
121
+
122
+ return DATA_PTR(multi_val);
123
+ #endif
124
+ }
98
125
 
99
126
  static VALUE callback_exception(VALUE did_raise, VALUE exception) {
100
127
  // TODO: we could have an option to enable exception reporting
@@ -342,6 +369,9 @@ static void ruby_curl_multi_init(ruby_curl_multi *rbcm) {
342
369
  rbcm->active = 0;
343
370
  rbcm->running = 0;
344
371
  rbcm->closed = 0;
372
+ rbcm->perform_active = 0;
373
+ rbcm->callback_active = 0;
374
+ rbcm->allow_close_during_perform = 0;
345
375
 
346
376
  if (rbcm->attached) {
347
377
  st_free_table(rbcm->attached);
@@ -537,11 +567,21 @@ VALUE ruby_curl_multi_add(VALUE self, VALUE easy) {
537
567
  CURLMcode mcode;
538
568
  ruby_curl_easy *rbce;
539
569
  ruby_curl_multi *rbcm;
570
+ ruby_curl_multi *existing_rbcm;
540
571
 
541
572
  TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
542
573
  TypedData_Get_Struct(easy, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
543
574
  ruby_curl_multi_ensure_handle(rbcm);
544
575
 
576
+ if (rb_curl_multi_has_easy(rbcm, rbce)) {
577
+ return self;
578
+ }
579
+
580
+ existing_rbcm = ruby_curl_multi_pointer_if_compatible(rbce->multi);
581
+ if (existing_rbcm && existing_rbcm != rbcm && rb_curl_multi_has_easy(existing_rbcm, rbce)) {
582
+ rb_raise(rb_eRuntimeError, "Cannot add an active Curl::Easy handle to another Curl::Multi");
583
+ }
584
+
545
585
  /* setup the easy handle */
546
586
  ruby_curl_easy_setup( rbce );
547
587
 
@@ -797,6 +837,8 @@ static VALUE rb_curl_multi_run_completion_callbacks(VALUE argp) {
797
837
  VALUE did_raise = rb_hash_new();
798
838
  long redirect_count;
799
839
 
840
+ args->rbcm->callback_active = 1;
841
+
800
842
  easy_callback_error = take_easy_callback_error_if_any(args->easy);
801
843
  if (!NIL_P(easy_callback_error)) {
802
844
  stash_multi_exception_if_unset(args->self, easy_callback_error, args->easy);
@@ -877,6 +919,10 @@ static VALUE rb_curl_multi_finish_completion_callbacks(VALUE argp) {
877
919
  args->rbce->callback_active = 0;
878
920
  }
879
921
 
922
+ if (args->rbcm) {
923
+ args->rbcm->callback_active = 0;
924
+ }
925
+
880
926
  if (args->rbce->multi == args->self && !rb_curl_multi_has_easy(args->rbcm, args->rbce)) {
881
927
  args->rbce->multi = Qnil;
882
928
  }
@@ -1072,6 +1118,41 @@ static VALUE fiber_io_wait_protected(VALUE argp) {
1072
1118
  }
1073
1119
  #endif
1074
1120
 
1121
+ #if defined(RB_INTEGER_TYPE_P)
1122
+ #define CURB_INTEGER_P(value) RB_INTEGER_TYPE_P(value)
1123
+ #else
1124
+ #define CURB_INTEGER_P(value) (FIXNUM_P(value) || RB_TYPE_P((value), T_BIGNUM))
1125
+ #endif
1126
+
1127
+ static int multi_socket_wait_events_for_curl_poll(int what) {
1128
+ if (what == CURL_POLL_IN) return RB_WAITFD_IN;
1129
+ if (what == CURL_POLL_OUT) return RB_WAITFD_OUT;
1130
+ if (what == CURL_POLL_INOUT) return RB_WAITFD_IN | RB_WAITFD_OUT;
1131
+ return RB_WAITFD_IN | RB_WAITFD_OUT;
1132
+ }
1133
+
1134
+ static int multi_socket_cselect_flags_for_curl_poll(int what) {
1135
+ int flags = 0;
1136
+
1137
+ if (what == CURL_POLL_IN || what == CURL_POLL_INOUT) flags |= CURL_CSELECT_IN;
1138
+ if (what == CURL_POLL_OUT || what == CURL_POLL_INOUT) flags |= CURL_CSELECT_OUT;
1139
+ if (flags == 0) flags = CURL_CSELECT_IN | CURL_CSELECT_OUT;
1140
+
1141
+ return flags;
1142
+ }
1143
+
1144
+ static int multi_socket_cselect_flags_for_wait_events(int events) {
1145
+ int flags = 0;
1146
+
1147
+ if (events & RB_WAITFD_IN) flags |= CURL_CSELECT_IN;
1148
+ if (events & RB_WAITFD_OUT) flags |= CURL_CSELECT_OUT;
1149
+ #ifdef RB_WAITFD_PRI
1150
+ if (events & RB_WAITFD_PRI) flags |= CURL_CSELECT_ERR;
1151
+ #endif
1152
+
1153
+ return flags;
1154
+ }
1155
+
1075
1156
  static void multi_socket_forget_fd(multi_socket_ctx *ctx, int fd) {
1076
1157
  st_data_t key = (st_data_t)fd;
1077
1158
  st_data_t rec;
@@ -1236,6 +1317,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1236
1317
 
1237
1318
  int did_timeout = 0;
1238
1319
  int any_ready = 0;
1320
+ int ready_flags = 0;
1239
1321
 
1240
1322
  int handled_wait = 0;
1241
1323
  if (count_tracked > 1) {
@@ -1270,10 +1352,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1270
1352
  if (scheduler != Qnil) {
1271
1353
  int events = 0;
1272
1354
  if (wait_fd >= 0) {
1273
- if (wait_what == CURL_POLL_IN) events = RB_WAITFD_IN;
1274
- else if (wait_what == CURL_POLL_OUT) events = RB_WAITFD_OUT;
1275
- else if (wait_what == CURL_POLL_INOUT) events = RB_WAITFD_IN|RB_WAITFD_OUT;
1276
- else events = RB_WAITFD_IN|RB_WAITFD_OUT;
1355
+ events = multi_socket_wait_events_for_curl_poll(wait_what);
1277
1356
  }
1278
1357
  double timeout_s = (double)tv.tv_sec + ((double)tv.tv_usec / 1e6);
1279
1358
  VALUE timeout = rb_float_new(timeout_s);
@@ -1305,8 +1384,21 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1305
1384
  did_timeout = 1;
1306
1385
  any_ready = 0;
1307
1386
  } else {
1308
- any_ready = (ready != Qfalse);
1387
+ any_ready = (ready != Qfalse && !NIL_P(ready));
1309
1388
  did_timeout = !any_ready;
1389
+ if (any_ready) {
1390
+ if (ready == Qtrue) {
1391
+ ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
1392
+ } else if (CURB_INTEGER_P(ready)) {
1393
+ ready_flags = multi_socket_cselect_flags_for_wait_events(NUM2INT(ready));
1394
+ if (ready_flags == 0) {
1395
+ any_ready = 0;
1396
+ did_timeout = 1;
1397
+ }
1398
+ } else {
1399
+ ready_flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
1400
+ }
1401
+ }
1310
1402
  }
1311
1403
  }
1312
1404
  }
@@ -1316,10 +1408,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1316
1408
  #endif
1317
1409
  #if defined(HAVE_RB_WAIT_FOR_SINGLE_FD)
1318
1410
  if (!handled_wait && wait_fd >= 0) {
1319
- int ev = 0;
1320
- if (wait_what == CURL_POLL_IN) ev = RB_WAITFD_IN;
1321
- else if (wait_what == CURL_POLL_OUT) ev = RB_WAITFD_OUT;
1322
- else if (wait_what == CURL_POLL_INOUT) ev = RB_WAITFD_IN|RB_WAITFD_OUT;
1411
+ int ev = multi_socket_wait_events_for_curl_poll(wait_what);
1323
1412
  int rc = rb_wait_for_single_fd(wait_fd, ev, &tv);
1324
1413
  curb_debugf("[curb.socket] rb_wait_for_single_fd rc=%d fd=%d ev=%d", rc, wait_fd, ev);
1325
1414
  if (rc < 0) {
@@ -1328,6 +1417,7 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1328
1417
  }
1329
1418
  any_ready = (rc != 0);
1330
1419
  did_timeout = (rc == 0);
1420
+ if (any_ready) ready_flags = multi_socket_cselect_flags_for_wait_events(rc);
1331
1421
  handled_wait = 1;
1332
1422
  }
1333
1423
  #endif
@@ -1351,6 +1441,11 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1351
1441
  }
1352
1442
  any_ready = (rc > 0);
1353
1443
  did_timeout = (rc == 0);
1444
+ if (any_ready && wait_fd >= 0) {
1445
+ if (rb_fd_isset(wait_fd, &rfds)) ready_flags |= CURL_CSELECT_IN;
1446
+ if (rb_fd_isset(wait_fd, &wfds)) ready_flags |= CURL_CSELECT_OUT;
1447
+ if (rb_fd_isset(wait_fd, &efds)) ready_flags |= CURL_CSELECT_ERR;
1448
+ }
1354
1449
  rb_fd_term(&rfds); rb_fd_term(&wfds); rb_fd_term(&efds);
1355
1450
  }
1356
1451
  } else { /* count_tracked == 0 */
@@ -1368,10 +1463,8 @@ static void rb_curl_multi_socket_drive(VALUE self, ruby_curl_multi *rbcm, multi_
1368
1463
  if (mrc != CURLM_OK) raise_curl_multi_error_exception(mrc);
1369
1464
  } else if (any_ready) {
1370
1465
  if (count_tracked == 1 && wait_fd >= 0) {
1371
- int flags = 0;
1372
- if (wait_what == CURL_POLL_IN || wait_what == CURL_POLL_INOUT) flags |= CURL_CSELECT_IN;
1373
- if (wait_what == CURL_POLL_OUT || wait_what == CURL_POLL_INOUT) flags |= CURL_CSELECT_OUT;
1374
- flags |= CURL_CSELECT_ERR;
1466
+ int flags = ready_flags;
1467
+ if (flags == 0) flags = multi_socket_cselect_flags_for_curl_poll(wait_what);
1375
1468
  #if CURB_SOCKET_DEBUG
1376
1469
  {
1377
1470
  char b[32];
@@ -1415,7 +1508,7 @@ static VALUE ruby_curl_multi_socket_drive_ensure(VALUE argp) {
1415
1508
  return Qnil;
1416
1509
  }
1417
1510
 
1418
- VALUE ruby_curl_multi_socket_perform(int argc, VALUE *argv, VALUE self) {
1511
+ static VALUE ruby_curl_multi_socket_perform_impl(int argc, VALUE *argv, VALUE self) {
1419
1512
  ruby_curl_multi *rbcm;
1420
1513
  VALUE block = Qnil;
1421
1514
  rb_scan_args(argc, argv, "0&", &block);
@@ -1445,7 +1538,11 @@ VALUE ruby_curl_multi_socket_perform(int argc, VALUE *argv, VALUE self) {
1445
1538
  /* finalize */
1446
1539
  rb_curl_multi_read_info(self, rbcm->handle);
1447
1540
  rb_curl_multi_yield_if_given(self, block);
1448
- if (cCurlMutiAutoClose == 1) rb_funcall(self, rb_intern("_autoclose"), 0);
1541
+ if (cCurlMutiAutoClose == 1) {
1542
+ rbcm->allow_close_during_perform = 1;
1543
+ rb_funcall(self, rb_intern("_autoclose"), 0);
1544
+ rbcm->allow_close_during_perform = 0;
1545
+ }
1449
1546
 
1450
1547
  return Qtrue;
1451
1548
  }
@@ -1493,6 +1590,14 @@ static VALUE curb_select(void *args) {
1493
1590
  int rc = select(set->maxfd, set->fdread, set->fdwrite, set->fdexcep, set->tv);
1494
1591
  return INT2FIX(rc);
1495
1592
  }
1593
+
1594
+ #ifdef HAVE_RB_THREAD_CALL_WITHOUT_GVL
1595
+ static void *curb_select_without_gvl(void *args) {
1596
+ struct _select_set* set = args;
1597
+ int rc = select(set->maxfd, set->fdread, set->fdwrite, set->fdexcep, set->tv);
1598
+ return (void *)(intptr_t)rc;
1599
+ }
1600
+ #endif
1496
1601
  #endif
1497
1602
 
1498
1603
  /*
@@ -1510,7 +1615,7 @@ static VALUE curb_select(void *args) {
1510
1615
  *
1511
1616
  * Run multi handles, looping selecting when data can be transfered
1512
1617
  */
1513
- VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1618
+ static VALUE ruby_curl_multi_perform_impl(int argc, VALUE *argv, VALUE self) {
1514
1619
  CURLMcode mcode;
1515
1620
  ruby_curl_multi *rbcm;
1516
1621
  int maxfd, rc = -1;
@@ -1691,7 +1796,7 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1691
1796
  rb_fd_term(&efds);
1692
1797
  }
1693
1798
  #elif defined(HAVE_RB_THREAD_CALL_WITHOUT_GVL)
1694
- rc = (int)(VALUE) rb_thread_call_without_gvl((void *(*)(void *))curb_select, &fdset_args, RUBY_UBF_IO, 0);
1799
+ rc = (int)(intptr_t) rb_thread_call_without_gvl(curb_select_without_gvl, &fdset_args, RUBY_UBF_IO, 0);
1695
1800
  #elif HAVE_RB_THREAD_BLOCKING_REGION
1696
1801
  rc = rb_thread_blocking_region(curb_select, &fdset_args, RUBY_UBF_IO, 0);
1697
1802
  #else
@@ -1725,11 +1830,66 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1725
1830
  rb_curl_multi_read_info( self, rbcm->handle );
1726
1831
  rb_curl_multi_yield_if_given(self, block);
1727
1832
  if (cCurlMutiAutoClose == 1) {
1833
+ rbcm->allow_close_during_perform = 1;
1728
1834
  rb_funcall(self, rb_intern("_autoclose"), 0);
1835
+ rbcm->allow_close_during_perform = 0;
1729
1836
  }
1730
1837
  return Qtrue;
1731
1838
  }
1732
1839
 
1840
+ struct multi_perform_call_args {
1841
+ int argc;
1842
+ VALUE *argv;
1843
+ VALUE self;
1844
+ VALUE result;
1845
+ VALUE (*func)(int, VALUE *, VALUE);
1846
+ };
1847
+
1848
+ static VALUE ruby_curl_multi_perform_guard_body(VALUE argp) {
1849
+ struct multi_perform_call_args *args = (struct multi_perform_call_args *)argp;
1850
+ args->result = args->func(args->argc, args->argv, args->self);
1851
+ return args->result;
1852
+ }
1853
+
1854
+ static VALUE ruby_curl_multi_perform_guard_ensure(VALUE self) {
1855
+ ruby_curl_multi *rbcm;
1856
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
1857
+ rbcm->perform_active = 0;
1858
+ rbcm->callback_active = 0;
1859
+ rbcm->allow_close_during_perform = 0;
1860
+ return Qnil;
1861
+ }
1862
+
1863
+ static VALUE ruby_curl_multi_with_perform_guard(int argc, VALUE *argv, VALUE self, VALUE (*func)(int, VALUE *, VALUE)) {
1864
+ ruby_curl_multi *rbcm;
1865
+ struct multi_perform_call_args args;
1866
+
1867
+ TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
1868
+ if (rbcm->perform_active) {
1869
+ rb_raise(rb_eRuntimeError, "Cannot recursively perform an active Curl::Multi handle");
1870
+ }
1871
+
1872
+ rbcm->perform_active = 1;
1873
+ args.argc = argc;
1874
+ args.argv = argv;
1875
+ args.self = self;
1876
+ args.result = Qnil;
1877
+ args.func = func;
1878
+
1879
+ return rb_ensure(ruby_curl_multi_perform_guard_body, (VALUE)&args,
1880
+ ruby_curl_multi_perform_guard_ensure, self);
1881
+ }
1882
+
1883
+ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1884
+ return ruby_curl_multi_with_perform_guard(argc, argv, self, ruby_curl_multi_perform_impl);
1885
+ }
1886
+
1887
+ #if defined(HAVE_CURL_MULTI_SOCKET_ACTION) && defined(HAVE_CURLMOPT_SOCKETFUNCTION) && defined(HAVE_CURLMOPT_TIMERFUNCTION) && defined(HAVE_RB_THREAD_FD_SELECT) && !defined(_WIN32)
1888
+ VALUE ruby_curl_multi_socket_perform(int argc, VALUE *argv, VALUE self) {
1889
+ return ruby_curl_multi_with_perform_guard(argc, argv, self, ruby_curl_multi_socket_perform_impl);
1890
+ }
1891
+ #endif
1892
+
1733
1893
  /*
1734
1894
  * call-seq:
1735
1895
  *
@@ -1740,6 +1900,11 @@ VALUE ruby_curl_multi_perform(int argc, VALUE *argv, VALUE self) {
1740
1900
  VALUE ruby_curl_multi_close(VALUE self) {
1741
1901
  ruby_curl_multi *rbcm;
1742
1902
  TypedData_Get_Struct(self, ruby_curl_multi, &ruby_curl_multi_data_type, rbcm);
1903
+
1904
+ if ((rbcm->perform_active || rbcm->callback_active) && !rbcm->allow_close_during_perform) {
1905
+ rb_raise(rb_eRuntimeError, "Cannot close an active Curl::Multi handle during perform");
1906
+ }
1907
+
1743
1908
  rb_curl_multi_detach_all(rbcm);
1744
1909
 
1745
1910
  if (rbcm->handle) {
data/ext/curb_multi.h CHANGED
@@ -16,6 +16,9 @@ typedef struct {
16
16
  int active;
17
17
  int running;
18
18
  char closed;
19
+ char perform_active;
20
+ char callback_active;
21
+ char allow_close_during_perform;
19
22
  CURLM *handle;
20
23
  struct st_table *attached;
21
24
  } ruby_curl_multi;
@@ -4,7 +4,7 @@ class BugTestInstancePostDiffersFromClassPost < Test::Unit::TestCase
4
4
  include BugTestServerSetupTeardown
5
5
 
6
6
  def setup
7
- @port = 9999
7
+ @port = unused_local_port
8
8
  @response_proc = lambda do|res|
9
9
  sleep 0.5
10
10
  res.body = "hi"
@@ -19,7 +19,7 @@ class BugTestInstancePostDiffersFromClassPost < Test::Unit::TestCase
19
19
 
20
20
  5.times do |i|
21
21
  t = Thread.new do
22
- c = Curl::Easy.perform('http://127.0.0.1:9999/test')
22
+ c = Curl::Easy.perform("http://127.0.0.1:#{@port}/test")
23
23
  c.header_str
24
24
  end
25
25
  threads << t
@@ -35,7 +35,7 @@ class BugTestInstancePostDiffersFromClassPost < Test::Unit::TestCase
35
35
  timer = Time.now
36
36
  single_responses = []
37
37
  5.times do |i|
38
- c = Curl::Easy.perform('http://127.0.0.1:9999/test')
38
+ c = Curl::Easy.perform("http://127.0.0.1:#{@port}/test")
39
39
  single_responses << c.header_str
40
40
  end
41
41
 
@@ -4,7 +4,7 @@ class BugFollowRedirect288 < Test::Unit::TestCase
4
4
  include BugTestServerSetupTeardown
5
5
 
6
6
  def setup
7
- @port = 9999
7
+ @port = unused_local_port
8
8
  super
9
9
  @server.mount_proc("/redirect_to_test") do|req,res|
10
10
  res.set_redirect(WEBrick::HTTPStatus::TemporaryRedirect, "/test")
@@ -13,7 +13,7 @@ class BugFollowRedirect288 < Test::Unit::TestCase
13
13
 
14
14
  def test_follow_redirect_with_no_redirect
15
15
 
16
- c = Curl::Easy.new('http://127.0.0.1:9999/test')
16
+ c = Curl::Easy.new("http://127.0.0.1:#{@port}/test")
17
17
  did_call_redirect = false
18
18
  c.on_redirect do|x|
19
19
  did_call_redirect = true
@@ -22,7 +22,7 @@ class BugFollowRedirect288 < Test::Unit::TestCase
22
22
 
23
23
  assert !did_call_redirect, "should reach this point redirect should not have been called"
24
24
 
25
- c = Curl::Easy.new('http://127.0.0.1:9999/test')
25
+ c = Curl::Easy.new("http://127.0.0.1:#{@port}/test")
26
26
  did_call_redirect = false
27
27
  c.on_redirect do|x|
28
28
  did_call_redirect = true
@@ -33,7 +33,7 @@ class BugFollowRedirect288 < Test::Unit::TestCase
33
33
  assert_equal 0, c.redirect_count
34
34
  assert !did_call_redirect, "should reach this point redirect should not have been called"
35
35
 
36
- c = Curl::Easy.new('http://127.0.0.1:9999/redirect_to_test')
36
+ c = Curl::Easy.new("http://127.0.0.1:#{@port}/redirect_to_test")
37
37
  did_call_redirect = false
38
38
  c.on_redirect do|x|
39
39
  did_call_redirect = true
@@ -43,7 +43,7 @@ class BugFollowRedirect288 < Test::Unit::TestCase
43
43
 
44
44
  assert did_call_redirect, "we should have called on_redirect"
45
45
 
46
- c = Curl::Easy.new('http://127.0.0.1:9999/redirect_to_test')
46
+ c = Curl::Easy.new("http://127.0.0.1:#{@port}/redirect_to_test")
47
47
  did_call_redirect = false
48
48
  c.follow_location = true
49
49
  # NOTE: while this API is not supported by libcurl e.g. there is no redirect function callback in libcurl we could
@@ -57,7 +57,7 @@ class BugFollowRedirect288 < Test::Unit::TestCase
57
57
 
58
58
  assert did_call_redirect, "we should have called on_redirect"
59
59
 
60
- c.url = 'http://127.0.0.1:9999/test'
60
+ c.url = "http://127.0.0.1:#{@port}/test"
61
61
  c.perform
62
62
  assert_equal 0, c.redirect_count
63
63
  assert_equal 200, c.response_code
@@ -65,7 +65,7 @@ class BugFollowRedirect288 < Test::Unit::TestCase
65
65
  puts "checking for raise support"
66
66
  did_raise = false
67
67
  begin
68
- c = Curl::Easy.new('http://127.0.0.1:9999/redirect_to_test')
68
+ c = Curl::Easy.new("http://127.0.0.1:#{@port}/redirect_to_test")
69
69
  did_call_redirect = false
70
70
  c.on_redirect do|x|
71
71
  raise "raise"
@@ -4,16 +4,17 @@ class BugRaiseOnCallback < Test::Unit::TestCase
4
4
  include BugTestServerSetupTeardown
5
5
 
6
6
  def setup
7
- @port = 9999
7
+ @port = unused_local_port
8
8
  super
9
9
  end
10
10
 
11
11
  def test_on_complte
12
- c = Curl::Easy.new('http://127.0.0.1:9999/test')
12
+ url = "http://127.0.0.1:#{@port}/test"
13
+ c = Curl::Easy.new(url)
13
14
  did_raise = false
14
15
  begin
15
16
  c.on_complete do|x|
16
- assert_equal 'http://127.0.0.1:9999/test', x.url
17
+ assert_equal url, x.url
17
18
  raise "error complete" # this will get swallowed
18
19
  end
19
20
  c.perform
data/tests/helper.rb CHANGED
@@ -206,6 +206,13 @@ class TestServlet < WEBrick::HTTPServlet::AbstractServlet
206
206
  end
207
207
 
208
208
  module BugTestServerSetupTeardown
209
+ def unused_local_port
210
+ socket = TCPServer.new('127.0.0.1', 0)
211
+ socket.addr[1]
212
+ ensure
213
+ socket.close if socket
214
+ end
215
+
209
216
  def setup
210
217
  @port ||= 9992
211
218
  @server = WEBrick::HTTPServer.new(:Port => @port, :Logger => WEBRICK_TEST_LOG, :AccessLog => [])
@@ -123,6 +123,50 @@ class TestCurbCurlEasy < Test::Unit::TestCase
123
123
  assert_equal "", easy.body_str.to_s
124
124
  end
125
125
 
126
+ def test_head_request_restores_easy_state_after_callback_exception
127
+ easy = Curl::Easy.new(TestServlet.url)
128
+ easy.on_complete { raise "head complete blew up" }
129
+
130
+ error = assert_raise(Curl::Err::AbortedByCallbackError) { easy.http("HEAD") }
131
+ assert_equal "head complete blew up", error.message
132
+
133
+ easy.on_complete { |_curl| }
134
+ easy.perform
135
+
136
+ assert_equal "GET", easy.body_str
137
+ end
138
+
139
+ def test_patch_request_does_not_leak_custom_method_to_next_perform
140
+ easy = Curl::Easy.new(TestServlet.url)
141
+
142
+ easy.http_patch("a=b")
143
+ assert_equal "PATCH\na=b", easy.body_str
144
+
145
+ easy.perform
146
+ assert_equal "GET", easy.body_str
147
+ end
148
+
149
+ def test_put_request_does_not_leak_custom_method_to_next_perform
150
+ easy = Curl::Easy.new(TestServlet.url)
151
+
152
+ easy.http_put("payload")
153
+ assert_equal "PUT\npayload", easy.body_str
154
+
155
+ easy.perform
156
+ assert_equal "GET", easy.body_str
157
+ end
158
+
159
+ def test_failed_put_data_setup_does_not_leave_easy_in_upload_mode
160
+ easy = Curl::Easy.new(TestServlet.url)
161
+ easy.headers = ["X-Test: true"]
162
+
163
+ assert_raise(RuntimeError) { easy.put_data = "payload" }
164
+
165
+ easy.headers = {}
166
+ easy.perform
167
+ assert_equal "GET", easy.body_str
168
+ end
169
+
126
170
  def test_perform_releases_failed_implicit_multi_without_follow_up_easy_perform
127
171
  Curl::Easy.flush_deferred_multi_closes
128
172
  assert_equal 0, Curl::Easy.deferred_multi_closes.length
@@ -1376,6 +1420,15 @@ class TestCurbCurlEasy < Test::Unit::TestCase
1376
1420
  assert_match(/message$/, curl.body_str)
1377
1421
  end
1378
1422
 
1423
+ def test_put_data_from_to_s_object
1424
+ curl = Curl::Easy.new(TestServlet.url)
1425
+ curl.put_data = :message
1426
+
1427
+ curl.perform
1428
+
1429
+ assert_equal "PUT\nmessage", curl.body_str
1430
+ end
1431
+
1379
1432
  def test_put_data_read_exception_sets_result_and_releases_callback_state
1380
1433
  error_class = Class.new(StandardError)
1381
1434
  reader_class = Class.new do
@@ -1507,6 +1560,10 @@ class TestCurbCurlEasy < Test::Unit::TestCase
1507
1560
  # header value and encoding; skip the NTLM-specific assertion.
1508
1561
  return
1509
1562
  end
1563
+ # curl 8.20.0 disables NTLM by default and plans to remove it in
1564
+ # September 2026; libcurl silently downgrades to Basic when NTLM is
1565
+ # unavailable, so skip the NTLM-specific assertion in that case.
1566
+ return unless Curl.ntlm?
1510
1567
  curl = Curl::Easy.new(TestServlet.url)
1511
1568
  curl.username = "foo"
1512
1569
  curl.password = "bar"
@@ -1,7 +1,10 @@
1
1
  require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
2
2
 
3
3
  class TestCurbCurlEasyResolve < Test::Unit::TestCase
4
+ include TestServerMethods
5
+
4
6
  def setup
7
+ server_setup
5
8
  @easy = Curl::Easy.new
6
9
  end
7
10
 
@@ -13,4 +16,33 @@ class TestCurbCurlEasyResolve < Test::Unit::TestCase
13
16
  def test_empty_resolve
14
17
  assert_equal @easy.resolve, nil
15
18
  end
19
+
20
+ def test_setopt_resolve_persists_across_performs
21
+ host = "curb-setopt-resolve.invalid"
22
+ mapping = "#{host}:#{TestServlet.port}:127.0.0.1"
23
+
24
+ @easy.url = "http://#{host}:#{TestServlet.port}#{TestServlet.path}"
25
+ @easy.dns_cache_timeout = 0
26
+ @easy.set(:resolve, [mapping])
27
+
28
+ @easy.perform
29
+ assert_match(/GET/, @easy.body_str)
30
+
31
+ @easy.perform
32
+ assert_match(/GET/, @easy.body_str)
33
+ end
34
+
35
+ def test_resolve_entries_can_be_objects_that_convert_to_string
36
+ host = "curb-resolve-object.invalid"
37
+ mapping = "#{host}:#{TestServlet.port}:127.0.0.1"
38
+ entry = Object.new
39
+ entry.define_singleton_method(:to_s) { mapping }
40
+
41
+ @easy.url = "http://#{host}:#{TestServlet.port}#{TestServlet.path}"
42
+ @easy.dns_cache_timeout = 0
43
+ @easy.resolve = [entry]
44
+
45
+ @easy.perform
46
+ assert_match(/GET/, @easy.body_str)
47
+ end
16
48
  end
@@ -133,6 +133,62 @@ class TestCurbCurlMulti < Test::Unit::TestCase
133
133
  Curl::Multi.autoclose = false
134
134
  end
135
135
 
136
+ def test_close_inside_completion_callback_is_rejected_without_invalidating_multi
137
+ multi = Curl::Multi.new
138
+ easy = Curl::Easy.new(TestServlet.url)
139
+
140
+ easy.on_complete { multi.close }
141
+ multi.add(easy)
142
+
143
+ error = assert_raise(Curl::Err::AbortedByCallbackError) { multi.perform }
144
+ assert_match(/Cannot close an active Curl::Multi handle/, error.message)
145
+ assert multi.idle?
146
+ assert_equal({}, multi.requests)
147
+
148
+ easy.on_complete { |_curl| }
149
+ multi.add(easy)
150
+ multi.perform
151
+
152
+ assert_equal "GET", easy.body_str
153
+ ensure
154
+ multi.close if defined?(multi) && multi
155
+ end
156
+
157
+ def test_close_inside_perform_block_is_rejected_without_invalidating_multi
158
+ multi = Curl::Multi.new
159
+ easy = Curl::Easy.new(TestServlet.url)
160
+ multi.add(easy)
161
+
162
+ error = assert_raise(RuntimeError) do
163
+ multi.perform { multi.close }
164
+ end
165
+ assert_match(/Cannot close an active Curl::Multi handle/, error.message)
166
+
167
+ assert_nothing_raised { multi.close }
168
+ multi = nil
169
+ ensure
170
+ multi.close if defined?(multi) && multi
171
+ end
172
+
173
+ def test_active_easy_cannot_be_added_to_another_multi
174
+ first_multi = Curl::Multi.new
175
+ second_multi = Curl::Multi.new
176
+ easy = Curl::Easy.new(TestServlet.url)
177
+
178
+ first_multi.add(easy)
179
+
180
+ error = assert_raise(RuntimeError) { second_multi.add(easy) }
181
+ assert_match(/active Curl::Easy/, error.message)
182
+ assert_same first_multi, easy.multi
183
+ assert_equal easy, first_multi.requests[easy.object_id]
184
+
185
+ assert_nothing_raised { first_multi.close }
186
+ first_multi = nil
187
+ ensure
188
+ first_multi.close if defined?(first_multi) && first_multi
189
+ second_multi.close if defined?(second_multi) && second_multi
190
+ end
191
+
136
192
  def test_close_makes_multi_unusable
137
193
  multi = Curl::Multi.new
138
194
  multi.close
@@ -22,5 +22,18 @@ class TestCurbFtpOptions < Test::Unit::TestCase
22
22
  assert_kind_of(Array, c.ftp_commands)
23
23
  assert_equal ["PWD", "CWD /"], c.ftp_commands
24
24
  end
25
- end
26
25
 
26
+ def test_ftp_command_entries_can_be_objects_that_convert_to_string
27
+ command = Object.new
28
+ command.define_singleton_method(:to_s) { "PWD" }
29
+
30
+ c = Curl::Easy.new($TEST_URL)
31
+ c.ftp_commands = [command]
32
+ m = Curl::Multi.new
33
+
34
+ assert_nothing_raised { m.add(c) }
35
+ ensure
36
+ m.remove(c) if defined?(m) && m && m.requests[c.object_id]
37
+ m.close if defined?(m) && m
38
+ end
39
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: curb
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.2
4
+ version: 1.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ross Bamford
8
8
  - Todd A. Fisher
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-23 00:00:00.000000000 Z
11
+ date: 2026-05-11 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Curb (probably CUrl-RuBy or something) provides Ruby-language bindings
14
14
  for the libcurl(3), a fully-featured client-side URL transfer library. cURL and