very_fast_yaml 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/ext/rapid_yaml/block.h +207 -0
- data/ext/rapid_yaml/extconf.rb +15 -0
- data/ext/rapid_yaml/rapid_yaml.c +225 -0
- data/ext/rapid_yaml/scan.h +29 -0
- data/lib/rapid_yaml/version.rb +4 -0
- data/lib/rapid_yaml/yaml.rb +3 -0
- data/lib/rapid_yaml.rb +136 -0
- metadata +85 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: b1692e3ff72ddcb470bd2aacf948130320973863685317ada5eabccbcb4811d7
|
|
4
|
+
data.tar.gz: 2a763988384b4f273190a0f03b882b00fe649922c5011605fa74edfdf1074808
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2b643c0974004ca486564625088956cb69909c3d2ddbf144e169250df7fe20d00efde85eb73c1e5e8fd0a271100dbb62693e5a30a47b52d1929eb12e41c0d473
|
|
7
|
+
data.tar.gz: 659f5f80f09ae9ce5fca24304e4cb6ab048c43dae589a9fdb6a2cfeb981d0c9d253061f662f7aacc6ca8a299a73b2955c07c5813d76d4965d3608ae6d43a624e
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Peter Arato
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/* Conservative ASCII block-YAML recognizer. Any uncertainty restarts in libyaml.
|
|
2
|
+
* Not a separate YAML dialect: acceptance is deliberately narrower than YAML.
|
|
3
|
+
* Source slices refer to the native copy, never movable Ruby string storage. */
|
|
4
|
+
#include "scan.h"
|
|
5
|
+
|
|
6
|
+
typedef struct {
|
|
7
|
+
const unsigned char *s, *end;
|
|
8
|
+
size_t indent;
|
|
9
|
+
} block_line;
|
|
10
|
+
typedef struct {
|
|
11
|
+
load_ctx *load;
|
|
12
|
+
const unsigned char *next, *end;
|
|
13
|
+
block_line line;
|
|
14
|
+
int failed;
|
|
15
|
+
} block_ctx;
|
|
16
|
+
|
|
17
|
+
static int indicator(block_line l, unsigned char ch) {
|
|
18
|
+
return l.s < l.end && l.s[0] == ch && (l.s + 1 == l.end || l.s[1] == ' ');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static void next_line(block_ctx *b) {
|
|
22
|
+
while (b->next < b->end) {
|
|
23
|
+
if ((++b->load->operations & 4095) == 0) rb_thread_check_ints();
|
|
24
|
+
const unsigned char *start = b->next;
|
|
25
|
+
const unsigned char *end = memchr(start, '\n', (size_t)(b->end - start));
|
|
26
|
+
if (!end) end = b->end;
|
|
27
|
+
b->next = end < b->end ? end + 1 : end;
|
|
28
|
+
const unsigned char *s = start;
|
|
29
|
+
while (s < end && *s == ' ') s++;
|
|
30
|
+
while (end > s && end[-1] == ' ') end--;
|
|
31
|
+
if (s == end) continue;
|
|
32
|
+
b->line = (block_line){s, end, (size_t)(s - start)};
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
b->line = (block_line){b->end, b->end, 0};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static void after_indicator(block_ctx *b) {
|
|
39
|
+
const unsigned char *start = b->line.s;
|
|
40
|
+
b->line.s++;
|
|
41
|
+
while (b->line.s < b->line.end && *b->line.s == ' ') b->line.s++;
|
|
42
|
+
b->line.indent += (size_t)(b->line.s - start);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Locate a block map delimiter; quotes are accepted only without escaping. */
|
|
46
|
+
static const unsigned char *map_colon(block_line l) {
|
|
47
|
+
const unsigned char *p = l.s;
|
|
48
|
+
if (p < l.end && (*p == '\'' || *p == '"')) {
|
|
49
|
+
unsigned char quote = *p++;
|
|
50
|
+
while (p < l.end && *p != quote) { if (*p == '\\') return NULL; p++; }
|
|
51
|
+
if (p == l.end) return NULL;
|
|
52
|
+
p++;
|
|
53
|
+
while (p < l.end && *p == ' ') p++;
|
|
54
|
+
return p < l.end && *p == ':' && (p + 1 == l.end || p[1] == ' ') ? p : NULL;
|
|
55
|
+
}
|
|
56
|
+
for (; p < l.end; p++)
|
|
57
|
+
if (*p == ':' && (p + 1 == l.end || p[1] == ' ')) return p;
|
|
58
|
+
return NULL;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
#if defined(__GNUC__) || defined(__clang__)
|
|
62
|
+
#define RY_UNLIKELY(x) __builtin_expect(!!(x), 0)
|
|
63
|
+
#else
|
|
64
|
+
#define RY_UNLIKELY(x) (x)
|
|
65
|
+
#endif
|
|
66
|
+
static const unsigned char reserved_initial[128] = {
|
|
67
|
+
[','] = 1, ['!'] = 1, ['&'] = 1, ['*'] = 1, ['#'] = 1,
|
|
68
|
+
['|'] = 1, ['>'] = 1, ['@'] = 1, ['"'] = 1, [39] = 1,
|
|
69
|
+
['%'] = 1, [96] = 1, ['['] = 1, [']'] = 1, ['{'] = 1, ['}'] = 1
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
static VALUE block_inline(block_ctx *b, const unsigned char *s, const unsigned char *end) {
|
|
73
|
+
while (end > s && end[-1] == ' ') end--;
|
|
74
|
+
size_t n = (size_t)(end - s);
|
|
75
|
+
if (!n) return Qnil;
|
|
76
|
+
if (n == 2 && s[0] == '[' && s[1] == ']') return rb_ary_new();
|
|
77
|
+
if (n == 2 && s[0] == '{' && s[1] == '}') return rb_hash_new();
|
|
78
|
+
if (RY_UNLIKELY(s[0] == '\'' || s[0] == '"')) {
|
|
79
|
+
unsigned char quote = s[0];
|
|
80
|
+
if (n < 2 || end[-1] != quote) { b->failed = 1; return Qnil; }
|
|
81
|
+
for (const unsigned char *p = s + 1; p < end - 1; p++)
|
|
82
|
+
if (*p == quote || (quote == '"' && *p == '\\')) { b->failed = 1; return Qnil; }
|
|
83
|
+
return yaml_string(b->load, s + 1, n - 2);
|
|
84
|
+
}
|
|
85
|
+
/* Fail closed for properties, directives, comments, block/flow scalars,
|
|
86
|
+
* document boundaries, and reserved indicators. */
|
|
87
|
+
if (reserved_initial[s[0]] ||
|
|
88
|
+
((s[0] == '-' || s[0] == '?' || s[0] == ':') && (n == 1 || s[1] == ' ')) ||
|
|
89
|
+
(n >= 3 && (!memcmp(s, "---", 3) || !memcmp(s, "...", 3)))) {
|
|
90
|
+
b->failed = 1; return Qnil;
|
|
91
|
+
}
|
|
92
|
+
for (const unsigned char *p = s; p < end; p++) {
|
|
93
|
+
if ((*p == '#' && p > s && p[-1] == ' ') ||
|
|
94
|
+
(*p == ':' && (p + 1 == end || p[1] == ' '))) { b->failed = 1; return Qnil; }
|
|
95
|
+
}
|
|
96
|
+
return scalar(b->load, s, n, 1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
static int generic_mapping_tag(const unsigned char *s, size_t n) {
|
|
100
|
+
if (n < 2 || s[0] != '!' || s[1] == '!' || s[1] == '<') return 0;
|
|
101
|
+
/* Reserved Ruby and shorthand tags are always delegated. */
|
|
102
|
+
if ((n >= 6 && !memcmp(s, "!ruby/", 6)) || (n >= 5 && !memcmp(s, "!map:", 5)) || (n >= 4 && !memcmp(s, "!str", 4)) ||
|
|
103
|
+
(n == 4 && !memcmp(s, "!set", 4)) || (n == 5 && !memcmp(s, "!omap", 5))) return 0;
|
|
104
|
+
for (size_t i = 1; i < n; i++) {
|
|
105
|
+
unsigned char ch = s[i];
|
|
106
|
+
if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
|
|
107
|
+
(ch >= '0' && ch <= '9') || ch == '_' || ch == ':' || ch == '-' || ch == '/' || ch == '.')) return 0;
|
|
108
|
+
}
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
static VALUE block_node(block_ctx *b);
|
|
113
|
+
|
|
114
|
+
static VALUE block_value(block_ctx *b, const unsigned char *s, size_t parent_indent) {
|
|
115
|
+
while (s < b->line.end && *s == ' ') s++;
|
|
116
|
+
if (s < b->line.end) {
|
|
117
|
+
if (generic_mapping_tag(s, (size_t)(b->line.end - s))) {
|
|
118
|
+
next_line(b);
|
|
119
|
+
if (b->line.s == b->line.end || b->line.indent <= parent_indent) { b->failed = 1; return Qnil; }
|
|
120
|
+
VALUE result = block_node(b);
|
|
121
|
+
if (!RB_TYPE_P(result, T_HASH)) b->failed = 1;
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
VALUE value = block_inline(b, s, b->line.end);
|
|
125
|
+
next_line(b);
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
next_line(b);
|
|
129
|
+
if (b->line.s < b->line.end && (b->line.indent > parent_indent ||
|
|
130
|
+
(b->line.indent == parent_indent && indicator(b->line, '-')))) return block_node(b);
|
|
131
|
+
return Qnil;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
static VALUE block_node(block_ctx *b) {
|
|
135
|
+
load_ctx *c = b->load;
|
|
136
|
+
if (b->failed || c->depth >= 128) { b->failed = 1; return Qnil; }
|
|
137
|
+
block_line first = b->line;
|
|
138
|
+
if (first.s == first.end) return Qnil;
|
|
139
|
+
int sequence = indicator(first, '-');
|
|
140
|
+
if (!sequence && !indicator(first, '?') && !map_colon(first)) {
|
|
141
|
+
VALUE value = block_inline(b, first.s, first.end);
|
|
142
|
+
next_line(b);
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
VALUE container = sequence ? rb_ary_new() : rb_hash_new();
|
|
146
|
+
rb_ary_push(c->stack, container);
|
|
147
|
+
rb_ary_push(c->stack, Qnil);
|
|
148
|
+
c->wants_value[c->depth++] = 0;
|
|
149
|
+
while (!b->failed && b->line.s < b->line.end && b->line.indent == first.indent) {
|
|
150
|
+
if (b->line.indent == 0 && b->line.end - b->line.s == 3 && !memcmp(b->line.s, "...", 3)) break;
|
|
151
|
+
if (sequence) {
|
|
152
|
+
if (!indicator(b->line, '-')) break;
|
|
153
|
+
after_indicator(b);
|
|
154
|
+
VALUE value;
|
|
155
|
+
if (b->line.s < b->line.end) {
|
|
156
|
+
if (generic_mapping_tag(b->line.s, (size_t)(b->line.end - b->line.s)))
|
|
157
|
+
value = block_value(b, b->line.s, first.indent);
|
|
158
|
+
else
|
|
159
|
+
value = block_node(b);
|
|
160
|
+
} else {
|
|
161
|
+
next_line(b);
|
|
162
|
+
value = b->line.s < b->line.end && b->line.indent > first.indent ? block_node(b) : Qnil;
|
|
163
|
+
}
|
|
164
|
+
rb_ary_push(container, value);
|
|
165
|
+
} else {
|
|
166
|
+
VALUE key, value;
|
|
167
|
+
if (indicator(b->line, '?')) {
|
|
168
|
+
after_indicator(b);
|
|
169
|
+
if (b->line.s < b->line.end) key = block_node(b);
|
|
170
|
+
else {
|
|
171
|
+
next_line(b);
|
|
172
|
+
key = b->line.s < b->line.end && b->line.indent > first.indent ? block_node(b) : Qnil;
|
|
173
|
+
}
|
|
174
|
+
if (b->failed || b->line.indent != first.indent || !indicator(b->line, ':')) { b->failed = 1; break; }
|
|
175
|
+
if (!attach(c, key)) { b->failed = 1; break; }
|
|
176
|
+
value = block_value(b, b->line.s + 1, first.indent);
|
|
177
|
+
} else {
|
|
178
|
+
const unsigned char *colon = map_colon(b->line);
|
|
179
|
+
if (!colon || colon == b->line.s) { b->failed = 1; break; }
|
|
180
|
+
/* YAML limits implicit keys to 1024 Unicode characters. */
|
|
181
|
+
if ((size_t)(colon - b->line.s) > 1024) { b->failed = 1; break; }
|
|
182
|
+
key = block_inline(b, b->line.s, colon);
|
|
183
|
+
if (b->failed || !attach(c, key)) { b->failed = 1; break; }
|
|
184
|
+
value = block_value(b, colon + 1, first.indent);
|
|
185
|
+
}
|
|
186
|
+
if (b->failed || !attach(c, value)) { b->failed = 1; break; }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
c->depth--;
|
|
190
|
+
rb_ary_pop(c->stack);
|
|
191
|
+
rb_ary_pop(c->stack);
|
|
192
|
+
RB_GC_GUARD(container);
|
|
193
|
+
return container;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
static VALUE block_load(load_ctx *c, size_t length) {
|
|
197
|
+
const unsigned char *end = c->input + length;
|
|
198
|
+
if (!block_ascii(c->input, end)) return unsupported;
|
|
199
|
+
block_ctx b = {c, c->input, end, {NULL, NULL, 0}, 0};
|
|
200
|
+
next_line(&b);
|
|
201
|
+
if (b.line.indent == 0 && b.line.end - b.line.s == 3 && !memcmp(b.line.s, "---", 3)) next_line(&b);
|
|
202
|
+
if (b.line.s == b.line.end) return unsupported;
|
|
203
|
+
VALUE result = block_node(&b);
|
|
204
|
+
if (!b.failed && b.line.indent == 0 && b.line.end - b.line.s == 3 && !memcmp(b.line.s, "...", 3)) next_line(&b);
|
|
205
|
+
if (b.failed || b.line.s != b.line.end) return unsupported;
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "mkmf"
|
|
3
|
+
if with_config("yaml-dir") || with_config("yaml-include") || with_config("yaml-lib")
|
|
4
|
+
dir_config("yaml")
|
|
5
|
+
elsif !pkg_config("yaml-0.1")
|
|
6
|
+
prefix = ["/opt/homebrew/opt/libyaml", "/usr/local/opt/libyaml"].find { |p| File.file?("#{p}/include/yaml.h") }
|
|
7
|
+
dir_config("yaml", "#{prefix}/include", "#{prefix}/lib") if prefix
|
|
8
|
+
end
|
|
9
|
+
yaml_available = have_header("yaml.h") &&
|
|
10
|
+
(have_func("yaml_parser_initialize", "yaml.h") || have_library("yaml", "yaml_parser_initialize"))
|
|
11
|
+
abort "libyaml headers required (brew install libyaml / apt install libyaml-dev)" unless yaml_available
|
|
12
|
+
$CFLAGS << " -O3" unless $CFLAGS.match?(/(?:^|\s)-O(?:[0-3sgz]|fast)(?:\s|$)/)
|
|
13
|
+
$CFLAGS << " -std=c99 -Wall -Wextra -Wno-unused-parameter"
|
|
14
|
+
$defs << "-DRY_DISABLE_SIMD" unless enable_config("simd", true)
|
|
15
|
+
create_makefile("rapid_yaml/rapid_yaml")
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#include <ruby.h>
|
|
2
|
+
#include <ruby/encoding.h>
|
|
3
|
+
#include <yaml.h>
|
|
4
|
+
#include <string.h>
|
|
5
|
+
|
|
6
|
+
#define MAX_DEPTH 512
|
|
7
|
+
static VALUE unsupported;
|
|
8
|
+
static ID id_tokenize;
|
|
9
|
+
static rb_encoding *utf8;
|
|
10
|
+
|
|
11
|
+
typedef struct {
|
|
12
|
+
yaml_parser_t parser;
|
|
13
|
+
yaml_event_t event;
|
|
14
|
+
int initialized, live_event, depth, mode;
|
|
15
|
+
size_t length;
|
|
16
|
+
unsigned int operations;
|
|
17
|
+
unsigned char wants_value[MAX_DEPTH];
|
|
18
|
+
unsigned char *input;
|
|
19
|
+
VALUE stack, scanner, result, key_cache;
|
|
20
|
+
} load_ctx;
|
|
21
|
+
|
|
22
|
+
static int attach(load_ctx *c, VALUE value) {
|
|
23
|
+
if (!c->depth) { c->result = value; return 1; }
|
|
24
|
+
int d = c->depth - 1;
|
|
25
|
+
VALUE parent = rb_ary_entry(c->stack, d * 2);
|
|
26
|
+
if (RB_TYPE_P(parent, T_ARRAY)) {
|
|
27
|
+
rb_ary_push(parent, value);
|
|
28
|
+
} else if (!c->wants_value[d]) {
|
|
29
|
+
if (RB_TYPE_P(value, T_STRING) && RSTRING_LEN(value) == 2 && !memcmp(RSTRING_PTR(value), "<<", 2)) return 0;
|
|
30
|
+
rb_ary_store(c->stack, d * 2 + 1, value);
|
|
31
|
+
c->wants_value[d] = 1;
|
|
32
|
+
} else {
|
|
33
|
+
rb_hash_aset(parent, rb_ary_entry(c->stack, d * 2 + 1), value);
|
|
34
|
+
/* The next key overwrites this slot; no write barrier is needed to clear it. */
|
|
35
|
+
c->wants_value[d] = 0;
|
|
36
|
+
}
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static int ascii_equal_ci(const unsigned char *s, const char *word, size_t n) {
|
|
41
|
+
for (size_t i = 0; i < n; i++) if ((s[i] | 32) != (unsigned char)word[i]) return 0;
|
|
42
|
+
return 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static VALUE yaml_string(load_ctx *c, const unsigned char *s, size_t n) {
|
|
46
|
+
if (c->depth && !c->wants_value[c->depth - 1] && RB_TYPE_P(rb_ary_entry(c->stack, (c->depth - 1) * 2), T_HASH)) {
|
|
47
|
+
if (n && n <= 64) {
|
|
48
|
+
unsigned int slot = (s[0] * 3 + s[n - 1] * 17 + n * 7) & 127;
|
|
49
|
+
VALUE key = rb_ary_entry(c->key_cache, slot);
|
|
50
|
+
if (!NIL_P(key) && (size_t)RSTRING_LEN(key) == n && !memcmp(RSTRING_PTR(key), s, n)) return key;
|
|
51
|
+
key = rb_enc_interned_str((const char *)s, (long)n, utf8);
|
|
52
|
+
rb_ary_store(c->key_cache, slot, key);
|
|
53
|
+
return key;
|
|
54
|
+
}
|
|
55
|
+
return rb_enc_interned_str((const char *)s, (long)n, utf8);
|
|
56
|
+
}
|
|
57
|
+
return rb_utf8_str_new((const char *)s, (long)n);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Reject impossible numeric candidates before invoking Psych's regex scanner.
|
|
61
|
+
* This is a superset recognizer, not an alternative scalar schema: timestamps,
|
|
62
|
+
* radix numbers, symbols and all ambiguous candidates still use Psych. */
|
|
63
|
+
static int possibly_typed(const unsigned char *s, size_t n) {
|
|
64
|
+
size_t start = (n && (s[0] == '-' || s[0] == '+')) ? 1 : 0;
|
|
65
|
+
if (!n || s[0] == ':' || start == n) return 1;
|
|
66
|
+
if (n > start + 4 && s[start + 4] == '-') return 1;
|
|
67
|
+
if (n > start + 1 && s[start] == '0' && (s[start + 1] == 'x' || s[start + 1] == 'b')) return 1;
|
|
68
|
+
if (s[start] == '.' && n - start == 4) return 1;
|
|
69
|
+
unsigned int dots = 0, colons = 0, exponents = 0;
|
|
70
|
+
for (size_t i = start; i < n; i++) {
|
|
71
|
+
unsigned char ch = s[i];
|
|
72
|
+
if ((ch >= '0' && ch <= '9') || ch == ',' || ch == '_') continue;
|
|
73
|
+
if (ch == '.' && ++dots == 1) continue;
|
|
74
|
+
if (ch == ':' && ++colons <= 2) continue;
|
|
75
|
+
if ((ch == 'e' || ch == 'E') && ++exponents == 1) continue;
|
|
76
|
+
if ((ch == '-' || ch == '+') && i > start && (s[i-1] == 'e' || s[i-1] == 'E')) continue;
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Plain shortcuts require single-line ASCII; callers must establish this. */
|
|
83
|
+
static VALUE scalar(load_ctx *c, const unsigned char *s, size_t n, int plain) {
|
|
84
|
+
if (plain) {
|
|
85
|
+
if (!n || (n == 1 && s[0] == '~')) return Qnil;
|
|
86
|
+
unsigned char first = s[0] | 32;
|
|
87
|
+
if ((first >= 'a' && first <= 'z') || s[0] == '_') {
|
|
88
|
+
if ((n == 4 && ascii_equal_ci(s, "null", 4))) return Qnil;
|
|
89
|
+
if ((n == 4 && ascii_equal_ci(s, "true", 4)) || (n == 3 && ascii_equal_ci(s, "yes", 3)) || (n == 2 && ascii_equal_ci(s, "on", 2))) return Qtrue;
|
|
90
|
+
if ((n == 5 && ascii_equal_ci(s, "false", 5)) || (n == 2 && ascii_equal_ci(s, "no", 2)) || (n == 3 && ascii_equal_ci(s, "off", 3))) return Qfalse;
|
|
91
|
+
return yaml_string(c, s, n);
|
|
92
|
+
}
|
|
93
|
+
size_t i = (s[0] == '-' || s[0] == '+') ? 1 : 0;
|
|
94
|
+
if (i < n && n - i <= 18 && (s[i] != '0' || n - i == 1)) {
|
|
95
|
+
unsigned long long number = 0;
|
|
96
|
+
for (; i < n && s[i] >= '0' && s[i] <= '9'; i++) number = number * 10 + s[i] - '0';
|
|
97
|
+
if (i == n) return s[0] == '-' ? LL2NUM(-(long long)number) : ULL2NUM(number);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
VALUE string = yaml_string(c, s, n);
|
|
101
|
+
return plain && possibly_typed(s, n) ? rb_funcall(c->scanner, id_tokenize, 1, string) : string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
#include "block.h"
|
|
105
|
+
|
|
106
|
+
static VALUE event_scalar(load_ctx *c, yaml_event_t *e) {
|
|
107
|
+
const unsigned char *s = e->data.scalar.value;
|
|
108
|
+
size_t n = e->data.scalar.length;
|
|
109
|
+
int plain = e->data.scalar.style == YAML_PLAIN_SCALAR_STYLE;
|
|
110
|
+
if (plain) {
|
|
111
|
+
/* Psych uses Unicode case folding and line-anchored scalar regexes.
|
|
112
|
+
* Preserve their long-s/ligature and multiline behavior. The block
|
|
113
|
+
* tier already proves single-line ASCII; this tier must check. */
|
|
114
|
+
for (size_t i = 0; i < n; i++) {
|
|
115
|
+
if (s[i] >= 128 || s[i] == '\n' || s[i] == '\r')
|
|
116
|
+
return rb_funcall(c->scanner, id_tokenize, 1, yaml_string(c, s, n));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return scalar(c, s, n, plain);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
static VALUE load_body(VALUE arg) {
|
|
123
|
+
load_ctx *c = (load_ctx *)arg;
|
|
124
|
+
if (c->mode != 1) {
|
|
125
|
+
VALUE fast = block_load(c, c->length);
|
|
126
|
+
if (fast != unsupported || c->mode == 2) return fast;
|
|
127
|
+
rb_ary_clear(c->stack);
|
|
128
|
+
c->depth = 0;
|
|
129
|
+
}
|
|
130
|
+
if (!yaml_parser_initialize(&c->parser)) rb_memerror();
|
|
131
|
+
c->initialized = 1;
|
|
132
|
+
/* Psych treats UTF-8/ASCII/binary Strings as UTF-8, unlike binary IO. */
|
|
133
|
+
yaml_parser_set_encoding(&c->parser, YAML_UTF8_ENCODING);
|
|
134
|
+
yaml_parser_set_input_string(&c->parser, c->input, c->length);
|
|
135
|
+
for (;;) {
|
|
136
|
+
if ((++c->operations & 4095) == 0) rb_thread_check_ints();
|
|
137
|
+
if (!yaml_parser_parse(&c->parser, &c->event)) return unsupported;
|
|
138
|
+
c->live_event = 1;
|
|
139
|
+
yaml_event_t *e = &c->event;
|
|
140
|
+
VALUE value;
|
|
141
|
+
switch (e->type) {
|
|
142
|
+
case YAML_STREAM_START_EVENT: case YAML_DOCUMENT_START_EVENT: break;
|
|
143
|
+
case YAML_STREAM_END_EVENT: return unsupported; /* empty stream: use Psych's fallback */
|
|
144
|
+
case YAML_DOCUMENT_END_EVENT: return c->result;
|
|
145
|
+
case YAML_ALIAS_EVENT: return unsupported;
|
|
146
|
+
case YAML_SCALAR_EVENT:
|
|
147
|
+
if (e->data.scalar.anchor || e->data.scalar.tag) return unsupported;
|
|
148
|
+
value = event_scalar(c, e);
|
|
149
|
+
if (!attach(c, value)) return unsupported;
|
|
150
|
+
break;
|
|
151
|
+
case YAML_SEQUENCE_START_EVENT: case YAML_MAPPING_START_EVENT:
|
|
152
|
+
if (c->depth == MAX_DEPTH) return unsupported;
|
|
153
|
+
if (e->type == YAML_SEQUENCE_START_EVENT) {
|
|
154
|
+
if (e->data.sequence_start.anchor || e->data.sequence_start.tag) return unsupported;
|
|
155
|
+
value = rb_ary_new();
|
|
156
|
+
} else {
|
|
157
|
+
if (e->data.mapping_start.anchor) return unsupported;
|
|
158
|
+
const char *tag = (const char *)e->data.mapping_start.tag;
|
|
159
|
+
/* Psych treats unregistered, non-special mapping tags as hashes.
|
|
160
|
+
* !str is a special Ruby string shorthand, not a generic tag. */
|
|
161
|
+
if (tag && (strstr(tag, "ruby/") || !strncmp(tag, "!map:", 5) || !strncmp(tag, "!str", 4) ||
|
|
162
|
+
!strncmp(tag, "!set", 4) || !strncmp(tag, "!omap", 5) ||
|
|
163
|
+
!strncmp(tag, "tag:yaml.org,2002:", 18))) return unsupported;
|
|
164
|
+
value = rb_hash_new();
|
|
165
|
+
}
|
|
166
|
+
rb_ary_push(c->stack, value);
|
|
167
|
+
rb_ary_push(c->stack, Qnil);
|
|
168
|
+
c->wants_value[c->depth++] = 0;
|
|
169
|
+
break;
|
|
170
|
+
case YAML_SEQUENCE_END_EVENT: case YAML_MAPPING_END_EVENT:
|
|
171
|
+
c->depth--;
|
|
172
|
+
rb_ary_pop(c->stack);
|
|
173
|
+
value = rb_ary_pop(c->stack);
|
|
174
|
+
if (!attach(c, value)) return unsupported;
|
|
175
|
+
break;
|
|
176
|
+
default: return unsupported;
|
|
177
|
+
}
|
|
178
|
+
yaml_event_delete(&c->event);
|
|
179
|
+
c->live_event = 0;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
static VALUE load_cleanup(VALUE arg) {
|
|
184
|
+
load_ctx *c = (load_ctx *)arg;
|
|
185
|
+
if (c->live_event) yaml_event_delete(&c->event);
|
|
186
|
+
if (c->initialized) yaml_parser_delete(&c->parser);
|
|
187
|
+
xfree(c->input);
|
|
188
|
+
return Qnil;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
static VALUE native_load_impl(VALUE source, VALUE scanner, int mode) {
|
|
192
|
+
Check_Type(source, T_STRING);
|
|
193
|
+
load_ctx c = {0};
|
|
194
|
+
c.stack = rb_ary_new();
|
|
195
|
+
c.key_cache = rb_ary_new_capa(128);
|
|
196
|
+
c.scanner = scanner;
|
|
197
|
+
c.result = Qnil;
|
|
198
|
+
size_t length = (size_t)RSTRING_LEN(source);
|
|
199
|
+
c.length = length;
|
|
200
|
+
c.mode = mode;
|
|
201
|
+
c.input = ALLOC_N(unsigned char, length ? length : 1);
|
|
202
|
+
memcpy(c.input, RSTRING_PTR(source), length);
|
|
203
|
+
VALUE result = rb_ensure(load_body, (VALUE)&c, load_cleanup, (VALUE)&c);
|
|
204
|
+
RB_GC_GUARD(c.stack);
|
|
205
|
+
RB_GC_GUARD(c.key_cache);
|
|
206
|
+
RB_GC_GUARD(c.scanner);
|
|
207
|
+
RB_GC_GUARD(c.result);
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
static VALUE native_load(VALUE self, VALUE source, VALUE scanner) { return native_load_impl(source, scanner, 0); }
|
|
212
|
+
static VALUE native_events_load(VALUE self, VALUE source, VALUE scanner) { return native_load_impl(source, scanner, 1); }
|
|
213
|
+
static VALUE native_block_load(VALUE self, VALUE source, VALUE scanner) { return native_load_impl(source, scanner, 2); }
|
|
214
|
+
|
|
215
|
+
void Init_rapid_yaml(void) {
|
|
216
|
+
VALUE mod = rb_define_module("RapidYAML");
|
|
217
|
+
unsupported = rb_obj_freeze(rb_obj_alloc(rb_cObject));
|
|
218
|
+
rb_define_const(mod, "UNSUPPORTED", unsupported);
|
|
219
|
+
rb_global_variable(&unsupported);
|
|
220
|
+
id_tokenize = rb_intern("tokenize");
|
|
221
|
+
utf8 = rb_utf8_encoding();
|
|
222
|
+
rb_define_singleton_method(mod, "native_load", native_load, 2);
|
|
223
|
+
rb_define_singleton_method(mod, "native_events_load", native_events_load, 2);
|
|
224
|
+
rb_define_singleton_method(mod, "native_block_load", native_block_load, 2);
|
|
225
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* Bounded unaligned vector loads; no padding, aliasing, or endian assumptions.
|
|
2
|
+
* Advanced SIMD is baseline on AArch64, SSE2 on x86-64. Other CPUs use C. */
|
|
3
|
+
#if !defined(RY_DISABLE_SIMD) && defined(__aarch64__)
|
|
4
|
+
#include <arm_neon.h>
|
|
5
|
+
#elif !defined(RY_DISABLE_SIMD) && defined(__SSE2__)
|
|
6
|
+
#include <emmintrin.h>
|
|
7
|
+
#endif
|
|
8
|
+
|
|
9
|
+
static int block_ascii(const unsigned char *p, const unsigned char *end) {
|
|
10
|
+
#if !defined(RY_DISABLE_SIMD) && defined(__aarch64__)
|
|
11
|
+
while (end - p >= 16) {
|
|
12
|
+
uint8x16_t v = vld1q_u8(p);
|
|
13
|
+
uint8x16_t valid = vorrq_u8(vceqq_u8(v, vdupq_n_u8(10)),
|
|
14
|
+
vandq_u8(vcgeq_u8(v, vdupq_n_u8(32)), vcleq_u8(v, vdupq_n_u8(126))));
|
|
15
|
+
if (vminvq_u8(valid) != 255) return 0;
|
|
16
|
+
p += 16;
|
|
17
|
+
}
|
|
18
|
+
#elif !defined(RY_DISABLE_SIMD) && defined(__SSE2__)
|
|
19
|
+
while (end - p >= 16) {
|
|
20
|
+
__m128i v = _mm_loadu_si128((const __m128i *)p);
|
|
21
|
+
__m128i valid = _mm_or_si128(_mm_cmpeq_epi8(v, _mm_set1_epi8(10)),
|
|
22
|
+
_mm_andnot_si128(_mm_cmpgt_epi8(v, _mm_set1_epi8(126)), _mm_cmpgt_epi8(v, _mm_set1_epi8(31))));
|
|
23
|
+
if (_mm_movemask_epi8(valid) != 65535) return 0;
|
|
24
|
+
p += 16;
|
|
25
|
+
}
|
|
26
|
+
#endif
|
|
27
|
+
for (; p < end; p++) if ((*p < 32 && *p != 10) || *p > 126) return 0;
|
|
28
|
+
return 1;
|
|
29
|
+
}
|
data/lib/rapid_yaml.rb
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "yaml"
|
|
3
|
+
require "forwardable"
|
|
4
|
+
require_relative "rapid_yaml/version"
|
|
5
|
+
require "rapid_yaml/rapid_yaml"
|
|
6
|
+
|
|
7
|
+
# Requiring rapid_yaml does not patch Psych. See rapid_yaml/yaml for opt-in.
|
|
8
|
+
module RapidYAML
|
|
9
|
+
PSYCH_METHODS = Psych.singleton_methods(false).to_h { |name| [name, Psych.method(name)] }.freeze
|
|
10
|
+
FAST_ENCODINGS = [Encoding::UTF_8, Encoding::US_ASCII, Encoding::ASCII_8BIT].freeze
|
|
11
|
+
NATIVE_LOADER = ENV["RAPID_YAML_ENGINE"] == "events" ? :native_events_load : :native_load
|
|
12
|
+
ACCELERATED_METHODS = %i[load safe_load unsafe_load load_file safe_load_file unsafe_load_file].freeze
|
|
13
|
+
MAX_FILE_BYTES = 16 * 1024 * 1024
|
|
14
|
+
LOAD_KEYWORDS = %i[aliases filename fallback].freeze
|
|
15
|
+
UNSAFE_KEYWORDS = %i[filename fallback].freeze
|
|
16
|
+
private_constant :PSYCH_METHODS, :FAST_ENCODINGS, :NATIVE_LOADER, :ACCELERATED_METHODS, :MAX_FILE_BYTES, :LOAD_KEYWORDS, :UNSAFE_KEYWORDS
|
|
17
|
+
|
|
18
|
+
# Real method definitions, rather than captured Proc bodies, can run in Ractors.
|
|
19
|
+
extend SingleForwardable
|
|
20
|
+
def_single_delegators "::Psych", *(PSYCH_METHODS.keys - ACCELERATED_METHODS)
|
|
21
|
+
|
|
22
|
+
def self.secondary_ractor?
|
|
23
|
+
defined?(Ractor) && Ractor.current != Ractor.main
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.load(source, **options)
|
|
27
|
+
return Psych.load(source, **options) if secondary_ractor?
|
|
28
|
+
accelerated_load(:load, source, options)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.safe_load(source, **options)
|
|
32
|
+
return Psych.safe_load(source, **options) if secondary_ractor?
|
|
33
|
+
accelerated_load(:safe_load, source, options)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.unsafe_load(source, **options)
|
|
37
|
+
return Psych.unsafe_load(source, **options) if secondary_ractor?
|
|
38
|
+
accelerated_load(:unsafe_load, source, options)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.load_file(filename, **options)
|
|
42
|
+
return Psych.load_file(filename, **options) if secondary_ractor?
|
|
43
|
+
accelerated_file(:load, filename, options)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.safe_load_file(filename, **options)
|
|
47
|
+
return Psych.safe_load_file(filename, **options) if secondary_ractor?
|
|
48
|
+
accelerated_file(:safe_load, filename, options)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.unsafe_load_file(filename, **options)
|
|
52
|
+
return Psych.unsafe_load_file(filename, **options) if secondary_ractor?
|
|
53
|
+
accelerated_file(:unsafe_load, filename, options)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.accelerated_load(name, source, options)
|
|
57
|
+
original = PSYCH_METHODS.fetch(name)
|
|
58
|
+
fast_keywords = name == :unsafe_load ? UNSAFE_KEYWORDS : LOAD_KEYWORDS
|
|
59
|
+
tags = Psych.load_tags
|
|
60
|
+
unless source.instance_of?(String) && FAST_ENCODINGS.include?(source.encoding) &&
|
|
61
|
+
(options.keys - fast_keywords).empty? &&
|
|
62
|
+
(options[:filename].nil? || options[:filename].instance_of?(String)) &&
|
|
63
|
+
tags.empty? && tags.default.nil? && tags.default_proc.nil? && Psych.domain_types.empty?
|
|
64
|
+
return original.call(source, **options)
|
|
65
|
+
end
|
|
66
|
+
loader = name == :unsafe_load ? Psych::ClassLoader.new : Psych::ClassLoader::Restricted.new(name == :load ? ["Symbol"] : [], [])
|
|
67
|
+
begin
|
|
68
|
+
value = public_send(NATIVE_LOADER, source, Psych::ScalarScanner.new(loader))
|
|
69
|
+
rescue StandardError
|
|
70
|
+
# Psych parses before resolving scalars: retain its error precedence.
|
|
71
|
+
return original.call(source, **options)
|
|
72
|
+
end
|
|
73
|
+
value.equal?(UNSUPPORTED) ? original.call(source, **options) : value
|
|
74
|
+
end
|
|
75
|
+
private_class_method :accelerated_load
|
|
76
|
+
|
|
77
|
+
def self.accelerated_file(name, filename, options)
|
|
78
|
+
File.open(filename, "r:bom|utf-8") do |file|
|
|
79
|
+
stat = file.stat
|
|
80
|
+
source = if stat.file? && stat.size.positive? && stat.size <= MAX_FILE_BYTES && Encoding.default_internal.nil?
|
|
81
|
+
# Bound reads of changing files; preserve the BOM-selected encoding.
|
|
82
|
+
(file.read(stat.size) || "").force_encoding(file.external_encoding)
|
|
83
|
+
else
|
|
84
|
+
# Preserve incremental IO and transcoding for devices and large files.
|
|
85
|
+
file
|
|
86
|
+
end
|
|
87
|
+
public_send(name, source, filename: filename, **options)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
private_class_method :accelerated_file
|
|
91
|
+
|
|
92
|
+
def self.const_missing(name)
|
|
93
|
+
Psych.const_get(name)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Explicit, process-wide and idempotent; call during application boot.
|
|
97
|
+
def self.install!
|
|
98
|
+
Psych.singleton_class.prepend(YAMLAcceleration) unless Psych.singleton_class < YAMLAcceleration
|
|
99
|
+
self
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
module YAMLAcceleration
|
|
103
|
+
# Do not enter Proc-backed methods or read captured Method objects outside
|
|
104
|
+
# the main Ractor. `super` retains Psych's own Ractor-local configuration.
|
|
105
|
+
def load(...)
|
|
106
|
+
return super if RapidYAML.secondary_ractor?
|
|
107
|
+
RapidYAML.load(...)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def safe_load(...)
|
|
111
|
+
return super if RapidYAML.secondary_ractor?
|
|
112
|
+
RapidYAML.safe_load(...)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def unsafe_load(...)
|
|
116
|
+
return super if RapidYAML.secondary_ractor?
|
|
117
|
+
RapidYAML.unsafe_load(...)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def load_file(...)
|
|
121
|
+
return super if RapidYAML.secondary_ractor?
|
|
122
|
+
RapidYAML.load_file(...)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def safe_load_file(...)
|
|
126
|
+
return super if RapidYAML.secondary_ractor?
|
|
127
|
+
RapidYAML.safe_load_file(...)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def unsafe_load_file(...)
|
|
131
|
+
return super if RapidYAML.secondary_ractor?
|
|
132
|
+
RapidYAML.unsafe_load_file(...)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
private_constant :YAMLAcceleration
|
|
136
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: very_fast_yaml
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.3
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Peter Arato
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: psych
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '4.0'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '6'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '4.0'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '6'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: forwardable
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '1.3'
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '2'
|
|
42
|
+
type: :runtime
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '1.3'
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '2'
|
|
52
|
+
executables: []
|
|
53
|
+
extensions:
|
|
54
|
+
- ext/rapid_yaml/extconf.rb
|
|
55
|
+
extra_rdoc_files: []
|
|
56
|
+
files:
|
|
57
|
+
- LICENSE
|
|
58
|
+
- ext/rapid_yaml/block.h
|
|
59
|
+
- ext/rapid_yaml/extconf.rb
|
|
60
|
+
- ext/rapid_yaml/rapid_yaml.c
|
|
61
|
+
- ext/rapid_yaml/scan.h
|
|
62
|
+
- lib/rapid_yaml.rb
|
|
63
|
+
- lib/rapid_yaml/version.rb
|
|
64
|
+
- lib/rapid_yaml/yaml.rb
|
|
65
|
+
licenses:
|
|
66
|
+
- MIT
|
|
67
|
+
metadata: {}
|
|
68
|
+
rdoc_options: []
|
|
69
|
+
require_paths:
|
|
70
|
+
- lib
|
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '3.1'
|
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0'
|
|
81
|
+
requirements: []
|
|
82
|
+
rubygems_version: 3.6.9
|
|
83
|
+
specification_version: 4
|
|
84
|
+
summary: Native YAML loading with a Psych-compatible Ruby interface
|
|
85
|
+
test_files: []
|