schemurai 3.0.0 → 3.0.1

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: 8fde4b5a2e6ccf979c4948fd13476c8250acf053346de28c4d62b32ed0c92e4f
4
- data.tar.gz: 7e3a5fb2dfa17195b1a45cde045f7256a1c5f3687f19f8d2c7b821980ffe2ab1
3
+ metadata.gz: f82df02813a6067fc242975190077fb66c9d104866c0e373497ad639f1ba12e3
4
+ data.tar.gz: d8ba792bfd5a7fbbdf2295bfa98f26e5804c702ad249ebf6efd3e8ba683af76e
5
5
  SHA512:
6
- metadata.gz: 42c0e94ce30932d09c9f1617592338f2386076eca4d176f1e1a9461557ea723abf512f94d7c68fb52a392021e9b45cba4ab329ee3669754b6d0a996f0319a07f
7
- data.tar.gz: 68b8c4e585cd40e11d4d7fbae8d9345f52a4bfca437704b86999eab79f219cf52e276b623faf0271cd49c4044c596f544399be77ba8a8fd36946f20524708cc2
6
+ metadata.gz: ce2508c012b1bf512bd51c7d2d67f1feacb6935e78d73577e0769cb2dd4664b12eaa6fbf58b57f3027f6d46b30aa684fde9cfda15ced02b24eaf004488be6a18
7
+ data.tar.gz: 1282ae16eed2abaecb7f352c9030e5d6ef1417c55c4c24b4c5e4860214a3e01fa2cbeed81a9aea506e43b57e62373f53f2f669e4d3e46b2db02bc43064ab9140
@@ -82,7 +82,7 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
82
82
 
83
83
  [[package]]
84
84
  name = "schemurai-native"
85
- version = "3.0.0"
85
+ version = "3.0.1"
86
86
  dependencies = [
87
87
  "base64-simd",
88
88
  "base64-turbo",
@@ -90,8 +90,15 @@ dependencies = [
90
90
  "regex-automata",
91
91
  "regex-syntax",
92
92
  "ryu",
93
+ "simdutf8",
93
94
  ]
94
95
 
96
+ [[package]]
97
+ name = "simdutf8"
98
+ version = "0.1.5"
99
+ source = "registry+https://github.com/rust-lang/crates.io-index"
100
+ checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
101
+
95
102
  [[package]]
96
103
  name = "vsimd"
97
104
  version = "0.8.0"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "schemurai-native"
3
- version = "3.0.0"
3
+ version = "3.0.1"
4
4
  edition = "2024"
5
5
  rust-version = "1.93"
6
6
  publish = false
@@ -12,6 +12,7 @@ memchr = "2.8.3"
12
12
  regex-automata = "0.4.18"
13
13
  regex-syntax = "0.8.11"
14
14
  ryu = "1.0.23"
15
+ simdutf8 = { version = "0.1.5", features = ["aarch64_neon"] }
15
16
 
16
17
  [lib]
17
18
  crate-type = ["staticlib"]
@@ -167,6 +167,17 @@ fn decode_json_direct(bytes: &[u8], length: usize) -> Verdict {
167
167
  decode_json_turbo(bytes, length, &mut scratch)
168
168
  }
169
169
 
170
+ #[cfg(target_arch = "aarch64")]
171
+ fn decode_json_turbo(bytes: &[u8], length: usize, scratch: &mut [u8]) -> Verdict {
172
+ let capacity = base64_turbo::STANDARD.decoded_len_estimate(bytes.len());
173
+ if decode_groups(bytes, &mut scratch[..capacity], length) {
174
+ json(&scratch[..length])
175
+ } else {
176
+ Invalid
177
+ }
178
+ }
179
+
180
+ #[cfg(not(target_arch = "aarch64"))]
170
181
  fn decode_json_turbo(bytes: &[u8], length: usize, scratch: &mut [u8]) -> Verdict {
171
182
  // The slice API requires an initialized upper-bound buffer, including
172
183
  // up to two padding bytes beyond the exact decoded length. Only the
@@ -195,6 +206,13 @@ fn decode_json_stream(bytes: &[u8], length: usize) -> Verdict {
195
206
 
196
207
  // Decode borrowed bytes into the caller's fixed stack scratch. The STANDARD
197
208
  // variant also checks each chunk's alphabet, padding and unused trailing bits.
209
+ #[cfg(target_arch = "aarch64")]
210
+ fn decode_groups(bytes: &[u8], output: &mut [u8], expected: usize) -> bool {
211
+ use base64_simd::AsOut;
212
+ matches!(base64_simd::STANDARD.decode(bytes, output.as_out()), Ok(decoded) if decoded.len() == expected)
213
+ }
214
+
215
+ #[cfg(not(target_arch = "aarch64"))]
198
216
  fn decode_groups(bytes: &[u8], output: &mut [u8], expected: usize) -> bool {
199
217
  matches!(
200
218
  base64_turbo::STANDARD.decode_slice(bytes, output),
@@ -118,16 +118,100 @@ fn date_exact(bytes: &[u8]) -> bool {
118
118
  fn date_time(bytes: &[u8]) -> bool {
119
119
  bytes.len() >= 20 && date(bytes) && matches!(bytes[10], b'T' | b't') && time(&bytes[11..])
120
120
  }
121
+ // Keep the SIMD body out of the shared format dispatchers. Local x64 benchmarks
122
+ // and disassembly showed that inlining it shifted the unchanged JSON Pointer
123
+ // loop into a slower instruction layout; outlining retains the UUID SIMD gain.
124
+ #[inline(never)]
121
125
  fn uuid(bytes: &[u8]) -> bool {
122
- bytes.len() == 36
123
- && bytes.iter().enumerate().all(|(index, byte)| {
124
- if matches!(index, 8 | 13 | 18 | 23) {
125
- *byte == b'-'
126
- } else {
127
- byte.is_ascii_hexdigit()
128
- }
129
- })
126
+ if bytes.len() != 36 || [8, 13, 18, 23].iter().any(|&at| bytes[at] != b'-') {
127
+ return false;
128
+ }
129
+ #[cfg(any(
130
+ all(target_arch = "x86_64", target_feature = "sse2"),
131
+ all(
132
+ target_arch = "aarch64",
133
+ target_feature = "neon",
134
+ target_endian = "little"
135
+ )
136
+ ))]
137
+ {
138
+ // The first two vectors contain all four separators. Their exact
139
+ // positions were checked above; only those lanes may bypass hex checks.
140
+ hex_block(&bytes[..16], (1 << 8) | (1 << 13))
141
+ && hex_block(&bytes[16..32], (1 << 2) | (1 << 7))
142
+ && bytes[32..].iter().all(u8::is_ascii_hexdigit)
143
+ }
144
+ #[cfg(not(any(
145
+ all(target_arch = "x86_64", target_feature = "sse2"),
146
+ all(
147
+ target_arch = "aarch64",
148
+ target_feature = "neon",
149
+ target_endian = "little"
150
+ )
151
+ )))]
152
+ bytes
153
+ .iter()
154
+ .enumerate()
155
+ .all(|(index, byte)| matches!(index, 8 | 13 | 18 | 23) || byte.is_ascii_hexdigit())
130
156
  }
157
+
158
+ #[cfg(all(target_arch = "x86_64", target_feature = "sse2"))]
159
+ fn hex_block(bytes: &[u8], separators: u16) -> bool {
160
+ use core::arch::x86_64::{
161
+ _mm_cmpeq_epi8, _mm_loadu_si128, _mm_movemask_epi8, _mm_or_si128, _mm_set1_epi8,
162
+ _mm_setzero_si128, _mm_sub_epi8, _mm_subs_epu8,
163
+ };
164
+ // The caller supplies sixteen readable bytes. SSE2 is a target baseline;
165
+ // unsigned wrapping subtraction rejects bytes below the ASCII ranges too.
166
+ let valid = unsafe {
167
+ let word = _mm_loadu_si128(bytes.as_ptr().cast());
168
+ let digits = _mm_sub_epi8(word, _mm_set1_epi8(b'0' as i8));
169
+ let letters = _mm_sub_epi8(
170
+ _mm_or_si128(word, _mm_set1_epi8(32)),
171
+ _mm_set1_epi8(b'a' as i8),
172
+ );
173
+ let digits = _mm_cmpeq_epi8(_mm_subs_epu8(digits, _mm_set1_epi8(9)), _mm_setzero_si128());
174
+ let letters = _mm_cmpeq_epi8(
175
+ _mm_subs_epu8(letters, _mm_set1_epi8(5)),
176
+ _mm_setzero_si128(),
177
+ );
178
+ _mm_movemask_epi8(_mm_or_si128(digits, letters)) as u16
179
+ };
180
+ valid | separators == u16::MAX
181
+ }
182
+
183
+ #[cfg(all(
184
+ target_arch = "aarch64",
185
+ target_feature = "neon",
186
+ target_endian = "little"
187
+ ))]
188
+ fn hex_block(bytes: &[u8], separators: u16) -> bool {
189
+ use core::arch::aarch64::{
190
+ vcleq_u8, vdupq_n_u8, vget_lane_u64, vld1q_u8, vorrq_u8, vreinterpret_u64_u8,
191
+ vreinterpretq_u16_u8, vshrn_n_u16, vsubq_u8,
192
+ };
193
+ // Each load remains within the sixteen-byte block. Narrowing packs two
194
+ // four-bit flags per byte in little-endian lane order.
195
+ let valid = unsafe {
196
+ let word = vld1q_u8(bytes.as_ptr());
197
+ let digits = vcleq_u8(vsubq_u8(word, vdupq_n_u8(b'0')), vdupq_n_u8(9));
198
+ let letters = vcleq_u8(
199
+ vsubq_u8(vorrq_u8(word, vdupq_n_u8(32)), vdupq_n_u8(b'a')),
200
+ vdupq_n_u8(5),
201
+ );
202
+ let flags = vorrq_u8(digits, letters);
203
+ let packed = vshrn_n_u16::<4>(vreinterpretq_u16_u8(flags));
204
+ vget_lane_u64::<0>(vreinterpret_u64_u8(packed))
205
+ };
206
+ let mut ignored = 0_u64;
207
+ for lane in 0..16 {
208
+ if separators & (1 << lane) != 0 {
209
+ ignored |= 0xf << (lane * 4);
210
+ }
211
+ }
212
+ valid | ignored == u64::MAX
213
+ }
214
+
131
215
  fn relative_pointer(bytes: &[u8]) -> bool {
132
216
  let Some(end) = digits_end(bytes, 0) else {
133
217
  return false;
@@ -355,3 +439,29 @@ fn ipv6(bytes: &[u8]) -> bool {
355
439
  }
356
440
  if compressed { groups < 8 } else { groups == 8 }
357
441
  }
442
+
443
+ #[cfg(test)]
444
+ mod tests {
445
+ use super::*;
446
+
447
+ #[test]
448
+ fn uuid_checks_every_byte_and_separator_without_accepting_non_ascii() {
449
+ let original = *b"123e4567-e89b-12d3-a456-426614174000";
450
+ for at in 0..36 {
451
+ for byte in 0..=255 {
452
+ let mut input = original;
453
+ input[at] = byte;
454
+ let expected = if matches!(at, 8 | 13 | 18 | 23) {
455
+ byte == b'-'
456
+ } else {
457
+ byte.is_ascii_hexdigit()
458
+ };
459
+ assert_eq!(uuid(&input), expected, "position {at}, byte {byte}");
460
+ }
461
+ }
462
+ assert!(!uuid(&original[..35]));
463
+ let mut longer = original.to_vec();
464
+ longer.push(b'0');
465
+ assert!(!uuid(&longer));
466
+ }
467
+ }
@@ -0,0 +1,132 @@
1
+ //! Validate complete blocks of borrowed, header-verified immediate integers.
2
+ //! Stop before any uncertain block so the ordinary evaluator retains ordering.
3
+ use super::Value;
4
+
5
+ pub(super) fn valid_prefix(values: &[Value], min: i64, max: i64, fixnum_flag: Value) -> usize {
6
+ if fixnum_flag != 1 || values.len() < 16 {
7
+ return 0;
8
+ }
9
+ // Comparing tagged values preserves signed integer order. Clamp the
10
+ // interval before tagging, since some schema limits exceed Fixnum range.
11
+ let min = min.max(-(1_i64 << 62));
12
+ let max = max.min((1_i64 << 62) - 1);
13
+ if min > max {
14
+ return 0;
15
+ }
16
+ let min = (min << 1) | 1;
17
+ let max = (max << 1) | 1;
18
+ #[cfg(target_arch = "x86_64")]
19
+ if std::is_x86_feature_detected!("avx2") {
20
+ // Detection includes OS support. The kernel only loads full blocks.
21
+ return unsafe { avx2_prefix(values, min, max) };
22
+ }
23
+ #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
24
+ return neon_prefix(values, min, max);
25
+ #[allow(unreachable_code)]
26
+ {
27
+ let _ = (min, max);
28
+ 0
29
+ }
30
+ }
31
+
32
+ #[cfg(target_arch = "x86_64")]
33
+ #[target_feature(enable = "avx2")]
34
+ unsafe fn avx2_prefix(values: &[Value], min: i64, max: i64) -> usize {
35
+ use core::arch::x86_64::{
36
+ _mm256_and_si256, _mm256_andnot_si256, _mm256_cmpeq_epi64, _mm256_cmpgt_epi64,
37
+ _mm256_loadu_si256, _mm256_movemask_epi8, _mm256_or_si256, _mm256_set1_epi64x,
38
+ };
39
+ let lower = _mm256_set1_epi64x(min);
40
+ let upper = _mm256_set1_epi64x(max);
41
+ let tag = _mm256_set1_epi64x(1);
42
+ let mut consumed = 0;
43
+ for block in values.as_chunks::<4>().0 {
44
+ // Four VALUE words provide all 32 readable bytes. No Ruby object is
45
+ // dereferenced; the header-validated low bit identifies each Fixnum.
46
+ let valid = unsafe {
47
+ let word = _mm256_loadu_si256(block.as_ptr().cast());
48
+ let tagged = _mm256_cmpeq_epi64(_mm256_and_si256(word, tag), tag);
49
+ let outside = _mm256_or_si256(
50
+ _mm256_cmpgt_epi64(lower, word),
51
+ _mm256_cmpgt_epi64(word, upper),
52
+ );
53
+ _mm256_movemask_epi8(_mm256_andnot_si256(outside, tagged)) == -1
54
+ };
55
+ if !valid {
56
+ break;
57
+ }
58
+ consumed += 4;
59
+ }
60
+ consumed
61
+ }
62
+
63
+ #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
64
+ fn neon_prefix(values: &[Value], min: i64, max: i64) -> usize {
65
+ use core::arch::aarch64::{
66
+ vandq_u64, vceqq_u64, vcgeq_s64, vcleq_s64, vdupq_n_s64, vdupq_n_u64, vgetq_lane_u64,
67
+ vld1q_u64, vreinterpretq_s64_u64,
68
+ };
69
+ // This function is compiled only when the target guarantees NEON.
70
+ let (lower, upper, tag) = unsafe { (vdupq_n_s64(min), vdupq_n_s64(max), vdupq_n_u64(1)) };
71
+ let mut consumed = 0;
72
+ for block in values.as_chunks::<2>().0 {
73
+ // Baseline NEON; the unaligned load stays within two borrowed VALUEs.
74
+ let valid = unsafe {
75
+ let word = vld1q_u64(block.as_ptr().cast());
76
+ let tagged = vceqq_u64(vandq_u64(word, tag), tag);
77
+ let signed = vreinterpretq_s64_u64(word);
78
+ let bounds = vandq_u64(vcgeq_s64(signed, lower), vcleq_s64(signed, upper));
79
+ let valid = vandq_u64(tagged, bounds);
80
+ vgetq_lane_u64::<0>(valid) & vgetq_lane_u64::<1>(valid) == u64::MAX
81
+ };
82
+ if !valid {
83
+ break;
84
+ }
85
+ consumed += 2;
86
+ }
87
+ consumed
88
+ }
89
+
90
+ #[cfg(test)]
91
+ mod tests {
92
+ use super::*;
93
+
94
+ fn tag(value: i64) -> Value {
95
+ ((value << 1) | 1) as Value
96
+ }
97
+
98
+ #[test]
99
+ fn skips_only_valid_fixnums_before_the_first_uncertain_block() {
100
+ for len in 0..65 {
101
+ let original: Vec<_> = (0..len).map(|at| tag((at % 33) as i64 - 16)).collect();
102
+ for at in 0..len {
103
+ for value in [0, 2, tag(-17), tag(17), tag(-(1 << 62)), tag((1 << 62) - 1)] {
104
+ let mut values = original.clone();
105
+ values[at] = value;
106
+ let prefix = valid_prefix(&values, -16, 16, 1);
107
+ assert!(prefix <= at, "length {len}, position {at}, prefix {prefix}");
108
+ assert_eq!(&values[..prefix], &original[..prefix]);
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ #[test]
115
+ fn clamps_schema_limits_without_overflow_and_obeys_disabled_hints() {
116
+ let values = [tag(-(1 << 62)), tag((1 << 62) - 1)].repeat(32);
117
+ let prefix = valid_prefix(&values, i64::MIN, i64::MAX, 1);
118
+ #[cfg(target_arch = "x86_64")]
119
+ if std::is_x86_feature_detected!("avx2") {
120
+ assert_eq!(prefix, values.len());
121
+ }
122
+ #[cfg(all(target_arch = "aarch64", target_feature = "neon"))]
123
+ assert_eq!(prefix, values.len());
124
+ assert!(prefix <= values.len());
125
+ assert_eq!(valid_prefix(&values, i64::MIN, i64::MAX, 0), 0);
126
+ assert_eq!(valid_prefix(&values, 1 << 62, i64::MAX, 1), 0);
127
+ assert_eq!(valid_prefix(&values, i64::MIN, -(1 << 62) - 1, 1), 0);
128
+ assert_eq!(valid_prefix(&values, 1, 0, 1), 0);
129
+ assert_eq!(valid_prefix(&values, -(1 << 62) + 1, i64::MAX, 1), 0);
130
+ assert_eq!(valid_prefix(&values, i64::MIN, (1 << 62) - 2, 1), 0);
131
+ }
132
+ }
@@ -13,6 +13,7 @@ mod evaluation;
13
13
  mod format;
14
14
  mod graph;
15
15
  mod instance_graph;
16
+ mod integer_scan;
16
17
  mod memo;
17
18
  mod memo_scope;
18
19
  mod number;
@@ -1658,7 +1659,12 @@ impl Execution<'_> {
1658
1659
  }
1659
1660
  _ => None,
1660
1661
  };
1661
- for &value in values {
1662
+ let prefix = if self.budget.timed() {
1663
+ 0
1664
+ } else {
1665
+ integer_scan::valid_prefix(values, min, max, self.fixnum_flag)
1666
+ };
1667
+ for &value in &values[prefix..] {
1662
1668
  if !self.budget.checkpoint() {
1663
1669
  return Unsupported;
1664
1670
  }
@@ -109,10 +109,7 @@ impl PatternGroup {
109
109
  return None;
110
110
  }
111
111
  let bytes = unsafe { byte_view(data, len) };
112
- if encoding != ENCODING_SEVEN_BIT
113
- && ((encoding == ENCODING_ASCII && !bytes.is_ascii())
114
- || std::str::from_utf8(bytes).is_err())
115
- {
112
+ if !valid_encoding(bytes, encoding) {
116
113
  return None;
117
114
  }
118
115
  let input = Input::new(bytes).anchored(Anchored::Yes);
@@ -252,10 +249,7 @@ impl Pattern {
252
249
  if encoding == ENCODING_OTHER || matches!(self, Self::Unsupported) {
253
250
  return Unsupported;
254
251
  }
255
- if encoding != ENCODING_SEVEN_BIT
256
- && ((encoding == ENCODING_ASCII && !bytes.is_ascii())
257
- || std::str::from_utf8(bytes).is_err())
258
- {
252
+ if !valid_encoding(bytes, encoding) {
259
253
  return Unsupported;
260
254
  }
261
255
  let matched = match self {
@@ -272,6 +266,20 @@ impl Pattern {
272
266
  }
273
267
  }
274
268
 
269
+ // Keep short checks in std and skip scanning when Ruby already proved ASCII.
270
+ // The compatibility SIMD API retains early rejection of malformed UTF-8.
271
+ fn valid_encoding(bytes: &[u8], encoding: i32) -> bool {
272
+ if encoding == ENCODING_SEVEN_BIT {
273
+ true
274
+ } else if encoding == ENCODING_ASCII {
275
+ bytes.is_ascii()
276
+ } else if bytes.len() < 64 {
277
+ std::str::from_utf8(bytes).is_ok()
278
+ } else {
279
+ simdutf8::compat::from_utf8(bytes).is_ok()
280
+ }
281
+ }
282
+
275
283
  fn scan_items(
276
284
  required: i32,
277
285
  values: &[Value],
@@ -298,10 +306,7 @@ fn scan_items(
298
306
  return Unsupported;
299
307
  }
300
308
  let bytes = unsafe { std::slice::from_raw_parts(data, len) };
301
- if encoding != ENCODING_SEVEN_BIT
302
- && ((encoding == ENCODING_ASCII && !bytes.is_ascii())
303
- || std::str::from_utf8(bytes).is_err())
304
- {
309
+ if !valid_encoding(bytes, encoding) {
305
310
  return Unsupported;
306
311
  }
307
312
  let result = matches(bytes);
@@ -416,6 +421,27 @@ fn compatible(ast: &Ast) -> bool {
416
421
  mod tests {
417
422
  use super::*;
418
423
 
424
+ #[test]
425
+ fn encoding_validation_matches_std_across_simd_blocks_and_tails() {
426
+ let original = "日𐀀é".repeat(32).into_bytes();
427
+ for length in [63, 64, 65, 127, 128, 129, original.len()] {
428
+ for at in 0..length {
429
+ for byte in 0..=255 {
430
+ let mut input = original[..length].to_vec();
431
+ input[at] = byte;
432
+ assert_eq!(
433
+ valid_encoding(&input, ENCODING_UTF8),
434
+ std::str::from_utf8(&input).is_ok(),
435
+ "length {length}, position {at}, byte {byte}"
436
+ );
437
+ assert_eq!(valid_encoding(&input, ENCODING_ASCII), input.is_ascii());
438
+ }
439
+ }
440
+ }
441
+ assert!(valid_encoding(&original, ENCODING_UTF8));
442
+ assert!(valid_encoding(b"already classified", ENCODING_SEVEN_BIT));
443
+ }
444
+
419
445
  #[test]
420
446
  fn reports_memory_when_a_search_cache_is_borrowed() {
421
447
  let caches = Caches::default();
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Schemurai
4
4
  # The installed Schemurai version.
5
- VERSION = "3.0.0"
5
+ VERSION = "3.0.1"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: schemurai
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kuya KOHARA (oakcask)
@@ -51,6 +51,7 @@ files:
51
51
  - ext/schemurai_native/src/format.rs
52
52
  - ext/schemurai_native/src/graph.rs
53
53
  - ext/schemurai_native/src/instance_graph.rs
54
+ - ext/schemurai_native/src/integer_scan.rs
54
55
  - ext/schemurai_native/src/lib.rs
55
56
  - ext/schemurai_native/src/memo.rs
56
57
  - ext/schemurai_native/src/memo_scope.rs
@@ -94,7 +95,7 @@ licenses:
94
95
  metadata:
95
96
  homepage_uri: https://github.com/oakcask/schemurai
96
97
  source_code_uri: https://github.com/oakcask/schemurai/tree/main
97
- documentation_uri: https://rubydoc.info/gems/schemurai/3.0.0
98
+ documentation_uri: https://rubydoc.info/gems/schemurai/3.0.1
98
99
  rubygems_mfa_required: 'true'
99
100
  rdoc_options:
100
101
  - "--main"