rage-iodine 6.0.0 → 6.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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +1 -0
- data/.gitignore +1 -0
- data/AGENTS.md +48 -0
- data/CHANGELOG.md +16 -0
- data/CLAUDE.md +1 -0
- data/ext/iodine/fio.c +39 -32
- data/ext/iodine/http.c +11 -9
- data/ext/iodine/http.h +3 -0
- data/ext/iodine/iodine_helpers.c +62 -171
- data/lib/iodine/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 67d84c1a37795b81e5bfcdd207e94124b35015905a22908ff4ce99c4e5474ecd
|
|
4
|
+
data.tar.gz: cc5007e6625fdc36c1c39286e724bfc6cbb9f3711f8b6b3f7e39a84994ddfd63
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5b3af183fcc0234eb71ae9df561f01ded132b2d2b216cee114309b15f09b9b9814879c92c7c016bbe412cf4587d0cbda449303b9db390d7ed65ce851f64c18cd
|
|
7
|
+
data.tar.gz: b9341b9d06ce950d505d4bd1c7ecfe9671e93e08cc2d45062c1fdaeca6cbdcd25dfe328d6abe21421bf38548137708b4bb243a91936ce2d735a8f1974376e021
|
data/.gitignore
CHANGED
data/AGENTS.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Intro
|
|
2
|
+
|
|
3
|
+
High-performance C web server used as the runtime for [Rage](https://github.com/rage-rb/rage), a fiber-based Ruby framework.
|
|
4
|
+
|
|
5
|
+
# Main Components
|
|
6
|
+
|
|
7
|
+
- ext/iodine/fio.[ch] - reactor + base library (spinlocks, atomics, linked lists, strings)
|
|
8
|
+
- ext/iodine/fiobj_str.[ch], fio_ary.[ch], fio_data.[ch] - fio data structures
|
|
9
|
+
- ext/iodine/iodine_caller.[ch] - calling Ruby from C
|
|
10
|
+
- ext/iodine/iodine_http.[ch] - HTTP server, `rack.upgrade` handling
|
|
11
|
+
- ext/iodine/iodine_defer.[ch], iodine_pubsub.[ch] - Iodine Ruby API
|
|
12
|
+
- ext/iodine/scheduler.[ch] - Rage fiber scheduler hooks
|
|
13
|
+
- ext/iodine/iodine_worker_pool.[ch] - Ruby 4.0+ thread pool for `blocking_operation_wait`
|
|
14
|
+
|
|
15
|
+
## `__http_defer__`
|
|
16
|
+
|
|
17
|
+
`[:__http_defer__, fiber]` is a private contract between `Rage::FiberWrapper` and `iodine_http.c`:
|
|
18
|
+
|
|
19
|
+
1. Rage schedules each request in a fiber. If application code yields for non-blocking I/O, the Rack call returns `[:__http_defer__, fiber]`
|
|
20
|
+
2. Iodine pauses the HTTP request, and subscribes to the process-local pub/sub channel
|
|
21
|
+
3. When the fiber finishes, Rage publishes to that channel to resume the request
|
|
22
|
+
|
|
23
|
+
# rack.upgrade (WebSocket/SSE)
|
|
24
|
+
|
|
25
|
+
Iodine supports WebSocket/SSE. The upgrade flow:
|
|
26
|
+
|
|
27
|
+
1. Rage sets `env['rack.upgrade?']` to `:websocket`/`:sse` and `env['rack.upgrade'] = CallbackHandler`
|
|
28
|
+
2. Iodine invokes handler callbacks (`on_open`, `on_message`, `on_close`)
|
|
29
|
+
|
|
30
|
+
See ext/iodine/iodine_http.c and ext/iodine/iodine_connection.c for implementation.
|
|
31
|
+
|
|
32
|
+
# Threading
|
|
33
|
+
|
|
34
|
+
Rage runs Iodine single-threaded; parallelization uses forked processes. New code can exploit this for simplicity, but should remain thread-safe to respect existing Iodine patterns.
|
|
35
|
+
|
|
36
|
+
# Rules
|
|
37
|
+
|
|
38
|
+
## 1. Think First
|
|
39
|
+
State assumptions. Surface tradeoffs. If unclear, ask - don't guess.
|
|
40
|
+
|
|
41
|
+
## 2. Simplicity
|
|
42
|
+
Minimum code for the problem. No speculative features, no premature abstractions, no handling impossible cases. If 200 lines could be 50, rewrite.
|
|
43
|
+
|
|
44
|
+
## 3. Surgical Changes
|
|
45
|
+
Touch only what's needed. Match existing style. Remove only orphans YOUR changes created. Every changed line should trace to the request.
|
|
46
|
+
|
|
47
|
+
## 4. Verify
|
|
48
|
+
Define success criteria upfront. Transform vague asks into testable goals ("fix bug" → "write failing test, make it pass").
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@ Please notice that this change log contains changes for upcoming releases as wel
|
|
|
6
6
|
|
|
7
7
|
## Changes:
|
|
8
8
|
|
|
9
|
+
#### Change log v.5.9.0/v.6.2.0 (2026-10-15)
|
|
10
|
+
|
|
11
|
+
**Fix**: Prevent process hang on exit caused by a use-after-free in pub/sub teardown (`fio_cluster_at_exit` read a channel freed by `fio_unsubscribe` of its last subscription; it now performs a bounded best-effort cleanup instead). See #21.
|
|
12
|
+
|
|
13
|
+
#### Change log v.5.8.0 (2026-10-09)
|
|
14
|
+
|
|
15
|
+
**Update**: Encode parameters as UTF-8
|
|
16
|
+
|
|
17
|
+
#### Change log v.6.1.0 (2026-10-04)
|
|
18
|
+
|
|
19
|
+
**Update**: Refactor parameters parsing
|
|
20
|
+
|
|
21
|
+
#### Change log v.5.7.0 (2026-10-04)
|
|
22
|
+
|
|
23
|
+
**Update**: Refactor parameters parsing
|
|
24
|
+
|
|
9
25
|
#### Change log v.6.0.0 (2026-09-06)
|
|
10
26
|
|
|
11
27
|
**Update**: Update scheduler `read`/`write` hooks
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
AGENTS.md
|
data/ext/iodine/fio.c
CHANGED
|
@@ -7658,6 +7658,25 @@ static inline void fio_cluster_inform_root_about_channel(channel_s *ch,
|
|
|
7658
7658
|
/* *****************************************************************************
|
|
7659
7659
|
* Initialization
|
|
7660
7660
|
**************************************************************************** */
|
|
7661
|
+
/* best-effort teardown without fio_unsubscribe: it may free `ch` (and remove
|
|
7662
|
+
* it from the set) while this loop still uses it (#21) */
|
|
7663
|
+
FIO_FUNC void fio_pubsub_at_exit_clear(fio_ch_set_s *channels) {
|
|
7664
|
+
while (fio_ch_set_count(channels)) {
|
|
7665
|
+
channel_s *ch = fio_ch_set_last(channels);
|
|
7666
|
+
while (fio_ls_embd_any(&ch->subscriptions)) {
|
|
7667
|
+
subscription_s *sub =
|
|
7668
|
+
FIO_LS_EMBD_OBJ(subscription_s, node, ch->subscriptions.next);
|
|
7669
|
+
fio_ls_embd_remove(&sub->node);
|
|
7670
|
+
sub->unsubscribed = 1;
|
|
7671
|
+
sub->on_message = NULL;
|
|
7672
|
+
sub->on_unsubscribe = NULL;
|
|
7673
|
+
fio_subscription_free(sub);
|
|
7674
|
+
}
|
|
7675
|
+
fio_ch_set_pop(channels);
|
|
7676
|
+
}
|
|
7677
|
+
fio_ch_set_free(channels);
|
|
7678
|
+
}
|
|
7679
|
+
|
|
7661
7680
|
#ifndef __MINGW32__
|
|
7662
7681
|
static void fio_accept_after_fork(void *ignore) {
|
|
7663
7682
|
/* prevent `accept` backlog in parent */
|
|
@@ -7671,38 +7690,9 @@ static void fio_cluster_at_exit(void *ignore) {
|
|
|
7671
7690
|
/* unlock all */
|
|
7672
7691
|
fio_pubsub_on_fork();
|
|
7673
7692
|
/* clear subscriptions of all types */
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
subscription_s *sub =
|
|
7678
|
-
FIO_LS_EMBD_OBJ(subscription_s, node, ch->subscriptions.next);
|
|
7679
|
-
fio_unsubscribe(sub);
|
|
7680
|
-
}
|
|
7681
|
-
fio_ch_set_pop(&fio_postoffice.patterns.channels);
|
|
7682
|
-
}
|
|
7683
|
-
|
|
7684
|
-
while (fio_ch_set_count(&fio_postoffice.pubsub.channels)) {
|
|
7685
|
-
channel_s *ch = fio_ch_set_last(&fio_postoffice.pubsub.channels);
|
|
7686
|
-
while (fio_ls_embd_any(&ch->subscriptions)) {
|
|
7687
|
-
subscription_s *sub =
|
|
7688
|
-
FIO_LS_EMBD_OBJ(subscription_s, node, ch->subscriptions.next);
|
|
7689
|
-
fio_unsubscribe(sub);
|
|
7690
|
-
}
|
|
7691
|
-
fio_ch_set_pop(&fio_postoffice.pubsub.channels);
|
|
7692
|
-
}
|
|
7693
|
-
|
|
7694
|
-
while (fio_ch_set_count(&fio_postoffice.filters.channels)) {
|
|
7695
|
-
channel_s *ch = fio_ch_set_last(&fio_postoffice.filters.channels);
|
|
7696
|
-
while (fio_ls_embd_any(&ch->subscriptions)) {
|
|
7697
|
-
subscription_s *sub =
|
|
7698
|
-
FIO_LS_EMBD_OBJ(subscription_s, node, ch->subscriptions.next);
|
|
7699
|
-
fio_unsubscribe(sub);
|
|
7700
|
-
}
|
|
7701
|
-
fio_ch_set_pop(&fio_postoffice.filters.channels);
|
|
7702
|
-
}
|
|
7703
|
-
fio_ch_set_free(&fio_postoffice.filters.channels);
|
|
7704
|
-
fio_ch_set_free(&fio_postoffice.patterns.channels);
|
|
7705
|
-
fio_ch_set_free(&fio_postoffice.pubsub.channels);
|
|
7693
|
+
fio_pubsub_at_exit_clear(&fio_postoffice.patterns.channels);
|
|
7694
|
+
fio_pubsub_at_exit_clear(&fio_postoffice.pubsub.channels);
|
|
7695
|
+
fio_pubsub_at_exit_clear(&fio_postoffice.filters.channels);
|
|
7706
7696
|
|
|
7707
7697
|
/* clear engines */
|
|
7708
7698
|
FIO_PUBSUB_DEFAULT = FIO_PUBSUB_CLUSTER;
|
|
@@ -12106,6 +12096,23 @@ FIO_FUNC void fio_pubsub_test(void) {
|
|
|
12106
12096
|
++expect;
|
|
12107
12097
|
fio_defer_perform();
|
|
12108
12098
|
FIO_ASSERT(counter == expect, "unsubscribe wasn't called for named channel!");
|
|
12099
|
+
|
|
12100
|
+
/* Regression #21: at-exit cleanup must terminate and empty the set, even
|
|
12101
|
+
* with several subscriptions per channel or an already-marked one. */
|
|
12102
|
+
fio_subscribe(.channel = {0, 9, "hang-test"}, .udata1 = &counter,
|
|
12103
|
+
.on_message = fio_pubsub_test_on_message,
|
|
12104
|
+
.on_unsubscribe = fio_pubsub_test_on_unsubscribe);
|
|
12105
|
+
s = fio_subscribe(.channel = {0, 9, "hang-test"}, .udata1 = &counter,
|
|
12106
|
+
.on_message = fio_pubsub_test_on_message,
|
|
12107
|
+
.on_unsubscribe = fio_pubsub_test_on_unsubscribe);
|
|
12108
|
+
FIO_ASSERT(s, "fio_subscribe FAILED for hang-test channel.");
|
|
12109
|
+
fio_trylock(&s->unsubscribed);
|
|
12110
|
+
fio_pubsub_at_exit_clear(&fio_postoffice.pubsub.channels);
|
|
12111
|
+
FIO_ASSERT(!fio_ch_set_count(&fio_postoffice.pubsub.channels),
|
|
12112
|
+
"at-exit cleanup left channels behind (#21)");
|
|
12113
|
+
fio_defer_perform();
|
|
12114
|
+
FIO_ASSERT(counter == expect, "at-exit cleanup shouldn't call callbacks!");
|
|
12115
|
+
|
|
12109
12116
|
fio_data->is_worker = 0;
|
|
12110
12117
|
fio_data->active = 0;
|
|
12111
12118
|
fio_data->workers = 0;
|
data/ext/iodine/http.c
CHANGED
|
@@ -68,6 +68,7 @@ static VALUE cUploadedFile;
|
|
|
68
68
|
static ID create_id;
|
|
69
69
|
static ID path_id;
|
|
70
70
|
static rb_encoding *IodineBinaryEncoding;
|
|
71
|
+
static rb_encoding *IodineUTF8Encoding;
|
|
71
72
|
|
|
72
73
|
/* *****************************************************************************
|
|
73
74
|
Small Helpers
|
|
@@ -1842,8 +1843,8 @@ typedef struct {
|
|
|
1842
1843
|
|
|
1843
1844
|
#define http_mime_parser2fio(parser) ((http_fio_mime_s *)(parser))
|
|
1844
1845
|
|
|
1845
|
-
/** Parse a parameter key and add it to the `params` hash.
|
|
1846
|
-
|
|
1846
|
+
/** Parse a decoded parameter key and add it to the `params` hash. */
|
|
1847
|
+
void http_add_to_params(VALUE params, char *key, size_t key_len, VALUE value) {
|
|
1847
1848
|
char *pos = NULL;
|
|
1848
1849
|
if (key_len > 1) {
|
|
1849
1850
|
pos = memchr(key + 1, '[', key_len - 1);
|
|
@@ -1925,7 +1926,7 @@ static void add_to_params(VALUE params, char *key, size_t key_len, VALUE value)
|
|
|
1925
1926
|
if (RB_TYPE_P(nested_val, T_HASH)) {
|
|
1926
1927
|
char *n_pos_start = k_pos + 1, *n_pos_end = k_pos + 1;
|
|
1927
1928
|
|
|
1928
|
-
while (nested_val != Qnil && *n_pos_start == '[' && *(n_pos_start + 1) != ']'
|
|
1929
|
+
while (nested_val != Qnil && n_pos_end < end && *n_pos_start == '[' && *(n_pos_start + 1) != ']') {
|
|
1929
1930
|
n_pos_end++;
|
|
1930
1931
|
|
|
1931
1932
|
if (*n_pos_end == ']') {
|
|
@@ -2010,8 +2011,8 @@ static void http_mime_parser_on_data(http_mime_parser_s *parser, void *name,
|
|
|
2010
2011
|
size_t value_len) {
|
|
2011
2012
|
// for regular values - just add them to params
|
|
2012
2013
|
if (!filename_len) {
|
|
2013
|
-
VALUE r_value = rb_enc_str_new(value, value_len,
|
|
2014
|
-
|
|
2014
|
+
VALUE r_value = rb_enc_str_new(value, value_len, IodineUTF8Encoding);
|
|
2015
|
+
http_add_to_params(http_mime_parser2fio(parser)->params, name, name_len, r_value);
|
|
2015
2016
|
return;
|
|
2016
2017
|
}
|
|
2017
2018
|
|
|
@@ -2024,7 +2025,7 @@ static void http_mime_parser_on_data(http_mime_parser_s *parser, void *name,
|
|
|
2024
2025
|
close(file);
|
|
2025
2026
|
|
|
2026
2027
|
VALUE r_value = build_file_value(r_tempfile, filename, filename_len, mimetype, mimetype_len);
|
|
2027
|
-
|
|
2028
|
+
http_add_to_params(http_mime_parser2fio(parser)->params, name, name_len, r_value);
|
|
2028
2029
|
}
|
|
2029
2030
|
|
|
2030
2031
|
/** Called when the data didn't fit in the buffer. Data will be streamed. */
|
|
@@ -2073,7 +2074,7 @@ static void http_mime_parser_on_partial_end(http_mime_parser_s *parser) {
|
|
|
2073
2074
|
|
|
2074
2075
|
// for a file - add to params and close the fd
|
|
2075
2076
|
if (fio_parser->partial_fd) {
|
|
2076
|
-
|
|
2077
|
+
http_add_to_params(fio_parser->params, RSTRING_PTR(fio_parser->partial_name), RSTRING_LEN(fio_parser->partial_name), fio_parser->partial_tempfile);
|
|
2077
2078
|
close(fio_parser->partial_fd);
|
|
2078
2079
|
|
|
2079
2080
|
fio_parser->partial_name = Qnil;
|
|
@@ -2085,8 +2086,8 @@ static void http_mime_parser_on_partial_end(http_mime_parser_s *parser) {
|
|
|
2085
2086
|
|
|
2086
2087
|
FIOBJ slice = fiobj_data_slice(fio_parser->h->body, fio_parser->partial_offset, fio_parser->partial_length);
|
|
2087
2088
|
fio_str_info_s s_slice = fiobj_obj2cstr(slice);
|
|
2088
|
-
VALUE r_value = rb_enc_str_new(s_slice.data, s_slice.len,
|
|
2089
|
-
|
|
2089
|
+
VALUE r_value = rb_enc_str_new(s_slice.data, s_slice.len, IodineUTF8Encoding);
|
|
2090
|
+
http_add_to_params(fio_parser->params, RSTRING_PTR(fio_parser->partial_name), RSTRING_LEN(fio_parser->partial_name), r_value);
|
|
2090
2091
|
|
|
2091
2092
|
fio_parser->partial_name = Qnil;
|
|
2092
2093
|
fio_parser->partial_length = 0;
|
|
@@ -2701,6 +2702,7 @@ void http_init(void) {
|
|
|
2701
2702
|
path_id = rb_intern("path");
|
|
2702
2703
|
|
|
2703
2704
|
IodineBinaryEncoding = rb_enc_find("binary");
|
|
2705
|
+
IodineUTF8Encoding = rb_enc_find("UTF-8");
|
|
2704
2706
|
}
|
|
2705
2707
|
|
|
2706
2708
|
/* *****************************************************************************
|
data/ext/iodine/http.h
CHANGED
|
@@ -781,6 +781,9 @@ HTTP GET and POST parsing helpers
|
|
|
781
781
|
*/
|
|
782
782
|
VALUE http_parse_multipart(http_s *h, char *content_type, size_t content_type_len);
|
|
783
783
|
|
|
784
|
+
/** Adds a decoded parameter key and Ruby value to a nested parameters hash. */
|
|
785
|
+
void http_add_to_params(VALUE params, char *key, size_t key_len, VALUE value);
|
|
786
|
+
|
|
784
787
|
/**
|
|
785
788
|
* Parses the query part of an HTTP request/response. Uses `http_add2hash`.
|
|
786
789
|
*
|
data/ext/iodine/iodine_helpers.c
CHANGED
|
@@ -210,7 +210,7 @@ static VALUE iodine_rfc2109(VALUE self, VALUE rtm) {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
/**
|
|
213
|
-
Convert query string into a Ruby object
|
|
213
|
+
Convert query string into a Ruby object without recursion.
|
|
214
214
|
|
|
215
215
|
|
|
216
216
|
Iodine::Rack::Utils.parse_nested_query("a=1&b[]=2&c[d]=3") # => { "a" => "1", "b" => ["2"], "c" => { "d" => "3" } }
|
|
@@ -240,182 +240,68 @@ Convert query string into a Ruby object in (almost always) one pass with no recu
|
|
|
240
240
|
Rack: 64257.9 i/s - 16.94x slower
|
|
241
241
|
|
|
242
242
|
*/
|
|
243
|
+
static VALUE build_query_component(char *str, size_t len,
|
|
244
|
+
uint8_t should_decode) {
|
|
245
|
+
VALUE result = rb_enc_str_new(str, len, IodineUTF8Encoding);
|
|
246
|
+
if (should_decode) {
|
|
247
|
+
url_decode_inplace(Qnil, result);
|
|
248
|
+
}
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
|
|
243
252
|
static VALUE parse_nested_query_internal(char *str, size_t len) {
|
|
244
|
-
uint8_t should_decode = 0;
|
|
245
253
|
VALUE params = rb_hash_new();
|
|
246
|
-
|
|
247
|
-
char *
|
|
248
|
-
const char *end = str + len;
|
|
254
|
+
char *pos = str;
|
|
255
|
+
char *end = str + len;
|
|
249
256
|
|
|
250
257
|
while (pos < end) {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
VALUE k = ID2SYM(rb_intern2(pos, k_pos - pos)), v = rb_str_new(k_pos + 1, v_pos - k_pos - 1);
|
|
263
|
-
if (should_decode) {
|
|
264
|
-
rb_hash_aset(params, k, url_decode_inplace(Qnil, v));
|
|
265
|
-
should_decode = 0;
|
|
266
|
-
} else {
|
|
267
|
-
rb_hash_aset(params, k, v);
|
|
268
|
-
}
|
|
269
|
-
pos = k_pos = v_pos + 1;
|
|
270
|
-
|
|
271
|
-
} else if (*k_pos == '[') { // things are about to get rough now
|
|
272
|
-
VALUE arr = Qnil, hsh = Qnil, hsh_key = Qnil;
|
|
273
|
-
VALUE k = ID2SYM(rb_intern2(pos, k_pos - pos)), v;
|
|
274
|
-
pos = k_pos;
|
|
275
|
-
uint8_t depth = 0;
|
|
276
|
-
|
|
277
|
-
while (*pos != '=' && pos < end) {
|
|
278
|
-
if (depth++ == PARAMS_MAX_DEPTH) {
|
|
279
|
-
rb_raise(rb_eRuntimeError, "Params too deep");
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (*pos == '[' && *(pos + 1) == ']') { // array
|
|
283
|
-
if (arr != Qnil) {
|
|
284
|
-
VALUE tmp = rb_ary_new();
|
|
285
|
-
rb_ary_push(arr, tmp);
|
|
286
|
-
arr = tmp;
|
|
287
|
-
} else if (hsh != Qnil) {
|
|
288
|
-
arr = rb_hash_aref(hsh, hsh_key);
|
|
289
|
-
if (arr == Qnil) {
|
|
290
|
-
arr = rb_ary_new();
|
|
291
|
-
rb_hash_aset(hsh, hsh_key, arr);
|
|
292
|
-
} else {
|
|
293
|
-
Check_Type(arr, T_ARRAY);
|
|
294
|
-
}
|
|
295
|
-
hsh = hsh_key = Qnil;
|
|
296
|
-
} else {
|
|
297
|
-
VALUE tmp = rb_hash_aref(params, k);
|
|
298
|
-
if (tmp != Qnil) {
|
|
299
|
-
Check_Type(tmp, T_ARRAY);
|
|
300
|
-
arr = tmp;
|
|
301
|
-
} else {
|
|
302
|
-
arr = rb_ary_new();
|
|
303
|
-
rb_hash_aset(params, k, arr);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
pos += 2;
|
|
308
|
-
|
|
309
|
-
} else if (*pos == '[' && *(pos + 1) != ']') { // hash
|
|
310
|
-
if (pos + 2 < end) {
|
|
311
|
-
k_pos = memchr(pos + 2, ']', end - pos - 2);
|
|
312
|
-
} else {
|
|
313
|
-
k_pos = NULL;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (!k_pos) {
|
|
317
|
-
rb_raise(rb_eRuntimeError, "Bad params");
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
VALUE prev_hsh_key = hsh_key;
|
|
321
|
-
hsh_key = ID2SYM(rb_intern2(pos + 1, k_pos - pos - 1));
|
|
322
|
-
|
|
323
|
-
if (hsh != Qnil) {
|
|
324
|
-
VALUE existing = rb_hash_aref(hsh, prev_hsh_key);
|
|
325
|
-
if (existing != Qnil) {
|
|
326
|
-
Check_Type(existing, T_HASH);
|
|
327
|
-
hsh = existing;
|
|
328
|
-
} else {
|
|
329
|
-
VALUE tmp = rb_hash_new();
|
|
330
|
-
rb_hash_aset(hsh, prev_hsh_key, tmp);
|
|
331
|
-
hsh = tmp;
|
|
332
|
-
}
|
|
333
|
-
} else if (arr != Qnil) {
|
|
334
|
-
VALUE nested_val = Qnil;
|
|
335
|
-
if (RARRAY_LEN(arr) != 0) {
|
|
336
|
-
VALUE tmp = rb_ary_entry(arr, -1);
|
|
337
|
-
Check_Type(tmp, T_HASH);
|
|
338
|
-
nested_val = rb_hash_aref(tmp, hsh_key);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
// handle Rails' smart key grouping;
|
|
342
|
-
// "users[][data][id]=11&users[][data][name]=ross&users[][data][id]=22&users[][data][name]=chandler"
|
|
343
|
-
// should result in:
|
|
344
|
-
// { users: [{ data: { id: "11", name: "ross" } }, { data: { id: "22", name: "chandler" } }] }
|
|
345
|
-
if (RB_TYPE_P(nested_val, T_HASH)) {
|
|
346
|
-
char *n_pos_start = k_pos + 1, *n_pos_end = k_pos + 1;
|
|
347
|
-
|
|
348
|
-
while (nested_val != Qnil && *n_pos_start == '[' && *(n_pos_start + 1) != ']' && *n_pos_end != '&' && *n_pos_end != '=' && n_pos_end < end) {
|
|
349
|
-
n_pos_end++;
|
|
350
|
-
|
|
351
|
-
if (*n_pos_end == ']') {
|
|
352
|
-
VALUE nested_key = ID2SYM(rb_intern2(n_pos_start + 1, n_pos_end - n_pos_start - 1));
|
|
353
|
-
Check_Type(nested_val, T_HASH);
|
|
354
|
-
nested_val = rb_hash_aref(nested_val, nested_key);
|
|
355
|
-
n_pos_start = ++n_pos_end;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// use a new hash if there's none under this key or the existing one already has such key
|
|
361
|
-
if (RARRAY_LEN(arr) == 0 || nested_val != Qnil) {
|
|
362
|
-
hsh = rb_hash_new();
|
|
363
|
-
rb_ary_push(arr, hsh);
|
|
364
|
-
} else {
|
|
365
|
-
hsh = rb_ary_entry(arr, -1);
|
|
366
|
-
if (hsh != Qnil) {
|
|
367
|
-
Check_Type(hsh, T_HASH);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
arr = Qnil;
|
|
372
|
-
} else {
|
|
373
|
-
VALUE tmp = rb_hash_aref(params, k);
|
|
374
|
-
if (tmp != Qnil) {
|
|
375
|
-
Check_Type(tmp, T_HASH);
|
|
376
|
-
hsh = tmp;
|
|
377
|
-
} else {
|
|
378
|
-
hsh = rb_hash_new();
|
|
379
|
-
rb_hash_aset(params, k, hsh);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
pos = k_pos + 1;
|
|
384
|
-
|
|
258
|
+
char *pair_end = pos;
|
|
259
|
+
char *separator = NULL;
|
|
260
|
+
uint8_t key_should_decode = 0;
|
|
261
|
+
uint8_t value_should_decode = 0;
|
|
262
|
+
|
|
263
|
+
while (pair_end < end && *pair_end != '&') {
|
|
264
|
+
if (!separator && *pair_end == '=') {
|
|
265
|
+
separator = pair_end;
|
|
266
|
+
} else if (*pair_end == '%' || *pair_end == '+') {
|
|
267
|
+
if (separator) {
|
|
268
|
+
value_should_decode = 1;
|
|
385
269
|
} else {
|
|
386
|
-
|
|
270
|
+
key_should_decode = 1;
|
|
387
271
|
}
|
|
388
272
|
}
|
|
273
|
+
pair_end++;
|
|
274
|
+
}
|
|
389
275
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
for (v_pos = pos + 1; *v_pos != '&' && v_pos < end; v_pos++) {
|
|
395
|
-
if (*v_pos == '%' || *v_pos == '+') {
|
|
396
|
-
should_decode = 1;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
276
|
+
char *key_end = separator ? separator : pair_end;
|
|
277
|
+
VALUE key = Qnil;
|
|
278
|
+
char *key_data = pos;
|
|
279
|
+
size_t key_len = key_end - pos;
|
|
399
280
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
281
|
+
if (key_should_decode) {
|
|
282
|
+
key = build_query_component(pos, key_len, 1);
|
|
283
|
+
key_data = RSTRING_PTR(key);
|
|
284
|
+
key_len = RSTRING_LEN(key);
|
|
285
|
+
}
|
|
405
286
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
287
|
+
VALUE value;
|
|
288
|
+
if (separator) {
|
|
289
|
+
value = build_query_component(separator + 1,
|
|
290
|
+
pair_end - separator - 1,
|
|
291
|
+
value_should_decode);
|
|
292
|
+
} else if (key_len > 1 && memchr(key_data + 1, '[', key_len - 1)) {
|
|
293
|
+
value = rb_enc_str_new(NULL, 0, IodineUTF8Encoding);
|
|
294
|
+
} else {
|
|
295
|
+
value = Qnil;
|
|
296
|
+
}
|
|
411
297
|
|
|
412
|
-
|
|
298
|
+
http_add_to_params(params, key_data, key_len, value);
|
|
299
|
+
RB_GC_GUARD(key);
|
|
413
300
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
rb_hash_aset(params, k, Qnil);
|
|
417
|
-
pos = k_pos + 1;
|
|
301
|
+
if (pair_end == end) {
|
|
302
|
+
break;
|
|
418
303
|
}
|
|
304
|
+
pos = pair_end + 1;
|
|
419
305
|
}
|
|
420
306
|
|
|
421
307
|
return params;
|
|
@@ -425,21 +311,26 @@ static VALUE parse_nested_query_internal(char *str, size_t len) {
|
|
|
425
311
|
Convert query string into a Ruby object.
|
|
426
312
|
*/
|
|
427
313
|
static VALUE parse_nested_query(VALUE self, VALUE r_str) {
|
|
428
|
-
return parse_nested_query_internal(RSTRING_PTR(r_str), RSTRING_LEN(r_str));
|
|
429
314
|
(void)self;
|
|
315
|
+
Check_Type(r_str, T_STRING);
|
|
316
|
+
VALUE result =
|
|
317
|
+
parse_nested_query_internal(RSTRING_PTR(r_str), RSTRING_LEN(r_str));
|
|
318
|
+
RB_GC_GUARD(r_str);
|
|
319
|
+
return result;
|
|
430
320
|
}
|
|
431
321
|
|
|
432
322
|
/**
|
|
433
323
|
Convert urlencoded body into a Ruby object.
|
|
434
324
|
*/
|
|
435
325
|
static VALUE parse_urlencoded_nested_query(VALUE self, VALUE r_str) {
|
|
436
|
-
ssize_t len = http_decode_url(RSTRING_PTR(r_str), RSTRING_PTR(r_str), RSTRING_LEN(r_str));
|
|
437
|
-
if (len < 0) {
|
|
438
|
-
rb_raise(rb_eRuntimeError, "Invalid encoding");
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
return parse_nested_query_internal(RSTRING_PTR(r_str), len);
|
|
442
326
|
(void)self;
|
|
327
|
+
Check_Type(r_str, T_STRING);
|
|
328
|
+
char *str = RSTRING_PTR(r_str);
|
|
329
|
+
size_t len = RSTRING_LEN(r_str);
|
|
330
|
+
|
|
331
|
+
VALUE result = parse_nested_query_internal(str, len);
|
|
332
|
+
RB_GC_GUARD(r_str);
|
|
333
|
+
return result;
|
|
443
334
|
}
|
|
444
335
|
|
|
445
336
|
/**
|
data/lib/iodine/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rage-iodine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 6.
|
|
4
|
+
version: 6.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Boaz Segev
|
|
@@ -125,7 +125,9 @@ files:
|
|
|
125
125
|
- ".gitignore"
|
|
126
126
|
- ".rspec"
|
|
127
127
|
- ".yardopts"
|
|
128
|
+
- AGENTS.md
|
|
128
129
|
- CHANGELOG.md
|
|
130
|
+
- CLAUDE.md
|
|
129
131
|
- Gemfile
|
|
130
132
|
- LICENSE.txt
|
|
131
133
|
- LIMITS.md
|