mini_racer_rs 1.0.4 → 1.0.5
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/CHANGELOG.md +7 -0
- data/Cargo.lock +4 -4
- data/Cargo.toml +1 -1
- data/crates/mini_racer_engine/src/lib.rs +2 -0
- data/crates/mini_racer_engine/src/request.rs +58 -22
- data/crates/mini_racer_engine/src/sys.rs +37 -0
- data/crates/mini_racer_engine/src/text.rs +182 -0
- data/crates/mini_racer_wire/src/write.rs +8 -3
- data/ext/mini_racer_rs/src/encode.rs +77 -15
- data/ext/mini_racer_rs/src/rb_raw.rs +13 -0
- data/lib/mini_racer_rs/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c1f337ddf902559ced22f49783b4f541c2ead54c00d1a72ee7a354e1e0b831fc
|
|
4
|
+
data.tar.gz: 45fe66cb3b1fd6af17aa4da11fa689d387a586eaab1ffd3fe0349fddc90bbec8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 924bb67c0d847c12570f983e86594d3ea432f76f206f73d725c3908b272634b5252699652a46231655e714ece2822942c474832b9618681d3bea3c120f7138b4
|
|
7
|
+
data.tar.gz: 8ead9bef3dac6010fe255d66e009583a7d1e2e8dc2c07aa655dbf21a3f9cda6b0416a74d676a28ff18e916a60f6c644d54e8265d3abe65c0927f5dc6870bcf89
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.5](https://github.com/andymai/mini_racer_rs/compare/v1.0.4...v1.0.5) (2026-10-19)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Performance
|
|
7
|
+
|
|
8
|
+
* **encode:** send Ruby strings to V8 in the form V8 stores them ([#93](https://github.com/andymai/mini_racer_rs/issues/93)) ([62f9c85](https://github.com/andymai/mini_racer_rs/commit/62f9c851917cb0ce7d760b5fc545aa014df0a53b))
|
|
9
|
+
|
|
3
10
|
## [1.0.4](https://github.com/andymai/mini_racer_rs/compare/v1.0.3...v1.0.4) (2026-10-19)
|
|
4
11
|
|
|
5
12
|
|
data/Cargo.lock
CHANGED
|
@@ -458,7 +458,7 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
|
|
458
458
|
|
|
459
459
|
[[package]]
|
|
460
460
|
name = "mini_racer_engine"
|
|
461
|
-
version = "1.0.
|
|
461
|
+
version = "1.0.5"
|
|
462
462
|
dependencies = [
|
|
463
463
|
"deno_core_icudata",
|
|
464
464
|
"libc",
|
|
@@ -470,7 +470,7 @@ dependencies = [
|
|
|
470
470
|
|
|
471
471
|
[[package]]
|
|
472
472
|
name = "mini_racer_rs"
|
|
473
|
-
version = "1.0.
|
|
473
|
+
version = "1.0.5"
|
|
474
474
|
dependencies = [
|
|
475
475
|
"magnus",
|
|
476
476
|
"mini_racer_engine",
|
|
@@ -481,7 +481,7 @@ dependencies = [
|
|
|
481
481
|
|
|
482
482
|
[[package]]
|
|
483
483
|
name = "mini_racer_sync"
|
|
484
|
-
version = "1.0.
|
|
484
|
+
version = "1.0.5"
|
|
485
485
|
dependencies = [
|
|
486
486
|
"loom",
|
|
487
487
|
"thiserror",
|
|
@@ -489,7 +489,7 @@ dependencies = [
|
|
|
489
489
|
|
|
490
490
|
[[package]]
|
|
491
491
|
name = "mini_racer_wire"
|
|
492
|
-
version = "1.0.
|
|
492
|
+
version = "1.0.5"
|
|
493
493
|
dependencies = [
|
|
494
494
|
"proptest",
|
|
495
495
|
"thiserror",
|
data/Cargo.toml
CHANGED
|
@@ -14,8 +14,10 @@ mod spin;
|
|
|
14
14
|
reason = "the engine's only FFI fence: symbols the linker routes into V8"
|
|
15
15
|
)]
|
|
16
16
|
mod sys;
|
|
17
|
+
mod text;
|
|
17
18
|
|
|
18
19
|
pub use context::{ContextHandle, Host, Unparker, WaitHandle, WaitOutcome};
|
|
19
20
|
pub use control::{CallRequest, CallbackReply, ContextConfig, JsText};
|
|
20
21
|
pub use error::{EngineError, FlagError, InvalidSnapshot, JsError, JsErrorKind, TerminationCause};
|
|
21
22
|
pub use snapshot::{SnapshotBlob, WeakSnapshotBlob};
|
|
23
|
+
pub use text::{Decoded, Transcoder};
|
|
@@ -4,6 +4,8 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
|
|
4
4
|
use mini_racer_wire::{WRITE_VERSION, tag};
|
|
5
5
|
use v8::ValueDeserializerHelper;
|
|
6
6
|
|
|
7
|
+
use crate::sys;
|
|
8
|
+
|
|
7
9
|
// Deeper values are left to V8, which reads as deep as its stack allows.
|
|
8
10
|
const MAX_DEPTH: u32 = 64;
|
|
9
11
|
// Far below V8's own limit on an array's length, which it enforces itself.
|
|
@@ -48,16 +50,16 @@ pub const fn worth_building(objects: usize, properties: usize) -> bool {
|
|
|
48
50
|
/// which its deserializer makes, element by element as a sparse array, so a
|
|
49
51
|
/// function returning an argument costs far more than it need.
|
|
50
52
|
///
|
|
51
|
-
/// A two-byte string is read by V8's own
|
|
52
|
-
///
|
|
53
|
-
///
|
|
54
|
-
///
|
|
55
|
-
///
|
|
56
|
-
///
|
|
57
|
-
///
|
|
58
|
-
///
|
|
59
|
-
///
|
|
60
|
-
/// (`TermCell::refuses`).
|
|
53
|
+
/// A two-byte string whose units all fit Latin-1 is read by V8's own
|
|
54
|
+
/// deserializer from its bytes alone, which keeps it two-byte as V8's read of
|
|
55
|
+
/// the whole would. The whole request is `Unbuilt` for anything else this would
|
|
56
|
+
/// not build as V8 does or that V8 would refuse (a binary, a key that is not a
|
|
57
|
+
/// string or an int32, a key given twice, an array inside itself, an object of
|
|
58
|
+
/// more than `MAX_KEYS` keys, nesting past `MAX_DEPTH`), decided from the bytes
|
|
59
|
+
/// before any value is made. A termination requested as it reads leaves it
|
|
60
|
+
/// `Unbuilt` too: V8 acts on one before each value it reads, an array or object
|
|
61
|
+
/// beginning as a value does, so this looks before the request and before each
|
|
62
|
+
/// value. One requested before the dispatch refused it (`TermCell::refuses`).
|
|
61
63
|
pub fn read<'s, 'b>(
|
|
62
64
|
scope: &v8::PinScope<'s, '_>,
|
|
63
65
|
bytes: &'b [u8],
|
|
@@ -74,6 +76,7 @@ pub fn read<'s, 'b>(
|
|
|
74
76
|
objects: Vec::new(),
|
|
75
77
|
elements: Vec::new(),
|
|
76
78
|
shapes: Vec::new(),
|
|
79
|
+
units: Vec::new(),
|
|
77
80
|
};
|
|
78
81
|
match builder.call() {
|
|
79
82
|
Some(call) => Read::Built(call),
|
|
@@ -119,6 +122,17 @@ impl<'b> Cursor<'b> {
|
|
|
119
122
|
}
|
|
120
123
|
}
|
|
121
124
|
|
|
125
|
+
// A value's tag, past the padding V8 skips, which aligns a two-byte
|
|
126
|
+
// string's units.
|
|
127
|
+
fn tag(&mut self) -> Option<u8> {
|
|
128
|
+
loop {
|
|
129
|
+
match self.byte()? {
|
|
130
|
+
tag::PADDING => {}
|
|
131
|
+
tag => return Some(tag),
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
122
136
|
fn varint(&mut self) -> Option<u64> {
|
|
123
137
|
let mut value = 0u64;
|
|
124
138
|
for shift in (0..64).step_by(7) {
|
|
@@ -153,9 +167,9 @@ impl<'b> Cursor<'b> {
|
|
|
153
167
|
.filter(|text| text.len() <= v8::String::MAX_LENGTH)
|
|
154
168
|
}
|
|
155
169
|
|
|
156
|
-
fn two_byte(&mut self) -> Option
|
|
157
|
-
|
|
158
|
-
|
|
170
|
+
fn two_byte(&mut self) -> Option<&'b [u8]> {
|
|
171
|
+
self.text()
|
|
172
|
+
.filter(|text| text.len().is_multiple_of(2) && text.len() / 2 <= v8::String::MAX_LENGTH)
|
|
159
173
|
}
|
|
160
174
|
|
|
161
175
|
// The length of `[path, ...arguments]`, past V8's header.
|
|
@@ -312,7 +326,7 @@ impl Check<'_> {
|
|
|
312
326
|
}
|
|
313
327
|
|
|
314
328
|
fn value(&mut self, depth: u32) -> Option<()> {
|
|
315
|
-
match self.bytes.
|
|
329
|
+
match self.bytes.tag()? {
|
|
316
330
|
tag::INT32 => {
|
|
317
331
|
self.bytes.varint_u32()?;
|
|
318
332
|
}
|
|
@@ -323,7 +337,9 @@ impl Check<'_> {
|
|
|
323
337
|
tag::UTF8_STRING | tag::ONE_BYTE_STRING => {
|
|
324
338
|
self.bytes.string()?;
|
|
325
339
|
}
|
|
326
|
-
tag::TWO_BYTE_STRING =>
|
|
340
|
+
tag::TWO_BYTE_STRING => {
|
|
341
|
+
self.bytes.two_byte()?;
|
|
342
|
+
}
|
|
327
343
|
tag::BIGINT => {
|
|
328
344
|
self.bytes.bigint_bytes()?;
|
|
329
345
|
}
|
|
@@ -393,6 +409,9 @@ struct Builder<'s, 'b, 'p, 'i> {
|
|
|
393
409
|
// The names last made for each key position at each depth, so objects of
|
|
394
410
|
// one shape share them instead of looking each up again.
|
|
395
411
|
shapes: Vec<Vec<(Key<'b>, v8::Local<'s, v8::String>)>>,
|
|
412
|
+
// A two-byte string's units where the request's bytes hold them
|
|
413
|
+
// unaligned.
|
|
414
|
+
units: Vec<u16>,
|
|
396
415
|
}
|
|
397
416
|
|
|
398
417
|
// Reads again what `Check` accepted, so only a termination stops it, or V8's
|
|
@@ -419,7 +438,7 @@ impl<'s, 'b> Builder<'s, 'b, '_, '_> {
|
|
|
419
438
|
self.unterminated()?;
|
|
420
439
|
let scope = self.scope;
|
|
421
440
|
let at = self.bytes;
|
|
422
|
-
Some(match self.bytes.
|
|
441
|
+
Some(match self.bytes.tag()? {
|
|
423
442
|
tag::INT32 => v8::Integer::new(scope, zigzag(self.bytes.varint_u32()?)).into(),
|
|
424
443
|
tag::DOUBLE => v8::Number::new(scope, self.bytes.double()?).into(),
|
|
425
444
|
tag::NULL => v8::null(scope).into(),
|
|
@@ -459,17 +478,34 @@ impl<'s, 'b> Builder<'s, 'b, '_, '_> {
|
|
|
459
478
|
tag::BEGIN_DENSE_ARRAY => self.array(depth.checked_sub(1)?)?,
|
|
460
479
|
tag::BEGIN_OBJECT => self.object(depth.checked_sub(1)?)?,
|
|
461
480
|
tag::TWO_BYTE_STRING => {
|
|
462
|
-
self.bytes.two_byte()?;
|
|
463
|
-
self.two_byte(at)?
|
|
481
|
+
let text = self.bytes.two_byte()?;
|
|
482
|
+
self.two_byte(text, at)?
|
|
464
483
|
}
|
|
465
484
|
_ => return None,
|
|
466
485
|
})
|
|
467
486
|
}
|
|
468
487
|
|
|
469
|
-
//
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
|
|
488
|
+
// V8's API makes a one-byte string of units that all fit Latin-1, where
|
|
489
|
+
// its deserializer keeps them two-byte, so only a string with a wider
|
|
490
|
+
// unit is made through the API, from the request's own bytes where the
|
|
491
|
+
// padding before them aligned them.
|
|
492
|
+
fn two_byte(&mut self, text: &[u8], at: Cursor<'b>) -> Option<v8::Local<'s, v8::Value>> {
|
|
493
|
+
let pairs = text.as_chunks::<2>().0;
|
|
494
|
+
if !pairs.iter().any(|&[_, high]| high != 0) {
|
|
495
|
+
return self.deserialized(at);
|
|
496
|
+
}
|
|
497
|
+
let units = if let Some(units) = sys::units(text) {
|
|
498
|
+
units
|
|
499
|
+
} else {
|
|
500
|
+
self.units.clear();
|
|
501
|
+
self.units
|
|
502
|
+
.extend(pairs.iter().map(|&pair| u16::from_le_bytes(pair)));
|
|
503
|
+
&self.units
|
|
504
|
+
};
|
|
505
|
+
v8::String::new_from_two_byte(self.scope, units, v8::NewStringType::Normal).map(Into::into)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
fn deserialized(&mut self, at: Cursor<'b>) -> Option<v8::Local<'s, v8::Value>> {
|
|
473
509
|
let len = at.rest.len().checked_sub(self.bytes.rest.len())?;
|
|
474
510
|
let mut bytes = vec![tag::HEADER, u8::try_from(WRITE_VERSION).ok()?];
|
|
475
511
|
bytes.extend_from_slice(at.rest.get(..len)?);
|
|
@@ -121,6 +121,43 @@ unsafe extern "C" fn release_two_byte(data: *mut u16, length: usize) {
|
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/// The UTF-16 units `bytes` hold, in the host's order as V8's deserializer
|
|
125
|
+
/// reads them, without a copy; `None` when they do not start where a unit
|
|
126
|
+
/// can be read from or hold an odd byte.
|
|
127
|
+
#[must_use]
|
|
128
|
+
pub fn units(bytes: &[u8]) -> Option<&[u16]> {
|
|
129
|
+
// SAFETY: every bit pattern is a valid u16, and `align_to` hands out only
|
|
130
|
+
// the aligned middle of `bytes`, borrowed for as long as `bytes` is.
|
|
131
|
+
let (head, units, tail) = unsafe { bytes.align_to::<u16>() };
|
|
132
|
+
(head.is_empty() && tail.is_empty()).then_some(units)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// Writes the Latin-1 bytes of `text` at the start of `out` when `text` is
|
|
136
|
+
/// valid UTF-8 and every character in it is at most U+00FF, and returns how
|
|
137
|
+
/// many; `None` otherwise, or when `out` is shorter than `text`.
|
|
138
|
+
pub fn utf8_to_latin1(text: &[u8], out: &mut [u8]) -> Option<usize> {
|
|
139
|
+
if out.len() < text.len() {
|
|
140
|
+
return None;
|
|
141
|
+
}
|
|
142
|
+
// SAFETY: simdutf writes at most one byte per byte of `text`, which `out`
|
|
143
|
+
// has room for.
|
|
144
|
+
let converted = unsafe { v8::simdutf::convert_utf8_to_latin1_with_errors(text, out) };
|
|
145
|
+
converted.is_ok().then_some(converted.count)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// Writes the UTF-16 units of `text` at the start of `out` when `text` is
|
|
149
|
+
/// valid UTF-8, and returns how many; `None` otherwise, or when `out` is
|
|
150
|
+
/// shorter than `text`.
|
|
151
|
+
pub fn utf8_to_utf16(text: &[u8], out: &mut [u16]) -> Option<usize> {
|
|
152
|
+
if out.len() < text.len() {
|
|
153
|
+
return None;
|
|
154
|
+
}
|
|
155
|
+
// SAFETY: simdutf writes at most one unit per byte of `text`, which `out`
|
|
156
|
+
// has room for.
|
|
157
|
+
let converted = unsafe { v8::simdutf::convert_utf8_to_utf16le_with_errors(text, out) };
|
|
158
|
+
converted.is_ok().then_some(converted.count)
|
|
159
|
+
}
|
|
160
|
+
|
|
124
161
|
/// The address V8 names `isolate` by when it posts the isolate a task.
|
|
125
162
|
#[must_use]
|
|
126
163
|
pub fn isolate_key(isolate: &v8::Isolate) -> usize {
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
use crate::sys;
|
|
2
|
+
|
|
3
|
+
// V8 decodes shorter UTF-8 faster than a conversion starts: converting all of
|
|
4
|
+
// it made arrays of short BMP and emoji strings about a tenth slower to send.
|
|
5
|
+
const SHORT: usize = 16;
|
|
6
|
+
// UTF-8 longer than this goes to V8 as mini_racer sends it. Decoded here, it
|
|
7
|
+
// takes a buffer of two bytes per byte and a request up to twice its length, both
|
|
8
|
+
// under the GVL: a 100 MB String with one character past Latin-1 would peak
|
|
9
|
+
// near 500 MB where its UTF-8 takes 200 MB. Below this, that extra memory
|
|
10
|
+
// stays within 24 MiB, and the values measured, up to 1 MB, all gain.
|
|
11
|
+
const LONG: usize = 8 << 20;
|
|
12
|
+
const WIDE_LEAD: u8 = 0xC4;
|
|
13
|
+
|
|
14
|
+
/// The characters of valid UTF-8 as V8 stores them once it has decoded it:
|
|
15
|
+
/// one byte each when every one is at most U+00FF, UTF-16 units otherwise.
|
|
16
|
+
pub enum Decoded<'a> {
|
|
17
|
+
OneByte(&'a [u8]),
|
|
18
|
+
TwoByte(&'a [u16]),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Decodes UTF-8 ahead of V8, which copies one-byte and two-byte text where
|
|
22
|
+
/// it would decode UTF-8 a byte at a time, into buffers kept for the next
|
|
23
|
+
/// text.
|
|
24
|
+
#[derive(Default)]
|
|
25
|
+
pub struct Transcoder {
|
|
26
|
+
one_byte: Vec<u8>,
|
|
27
|
+
two_byte: Vec<u16>,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
impl Transcoder {
|
|
31
|
+
/// `text` as V8 would decode it; `None` when it is short enough for V8 to
|
|
32
|
+
/// decode as fast, past `LONG` bytes, or not valid UTF-8, whose
|
|
33
|
+
/// replacement characters are V8's to choose.
|
|
34
|
+
#[expect(
|
|
35
|
+
clippy::inline_always,
|
|
36
|
+
reason = "the encoder calls it for each non-ASCII value; outlined, it slowed short ones 12%"
|
|
37
|
+
)]
|
|
38
|
+
#[inline(always)]
|
|
39
|
+
pub fn decode(&mut self, text: &[u8]) -> Option<Decoded<'_>> {
|
|
40
|
+
if text.len() < SHORT || text.len() > LONG {
|
|
41
|
+
return None;
|
|
42
|
+
}
|
|
43
|
+
self.decode_long(text)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// One conversion each: in valid UTF-8 a byte from 0xC4 up starts a
|
|
47
|
+
// character past U+00FF, and text without one that does not convert to
|
|
48
|
+
// Latin-1 is not valid.
|
|
49
|
+
fn decode_long(&mut self, text: &[u8]) -> Option<Decoded<'_>> {
|
|
50
|
+
if text.iter().fold(0, |most, &byte| most.max(byte)) < WIDE_LEAD {
|
|
51
|
+
grow(&mut self.one_byte, text.len());
|
|
52
|
+
let count = sys::utf8_to_latin1(text, &mut self.one_byte)?;
|
|
53
|
+
return self.one_byte.get(..count).map(Decoded::OneByte);
|
|
54
|
+
}
|
|
55
|
+
grow(&mut self.two_byte, text.len());
|
|
56
|
+
let count = sys::utf8_to_utf16(text, &mut self.two_byte)?;
|
|
57
|
+
self.two_byte.get(..count).map(Decoded::TwoByte)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Each has room for as many units as the text has bytes, which simdutf needs,
|
|
62
|
+
// and is made afresh as it grows: the allocator hands out a long one's zeroed
|
|
63
|
+
// pages untouched, where zeroing it in place would commit room for three
|
|
64
|
+
// times the units a CJK text decodes to.
|
|
65
|
+
fn grow<T: Copy + Default>(buffer: &mut Vec<T>, len: usize) {
|
|
66
|
+
if buffer.len() < len {
|
|
67
|
+
*buffer = vec![T::default(); len];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#[cfg(test)]
|
|
72
|
+
mod tests {
|
|
73
|
+
use super::{Decoded, LONG, SHORT, Transcoder};
|
|
74
|
+
|
|
75
|
+
// What V8 makes of valid UTF-8, worked out from its characters.
|
|
76
|
+
fn expected(text: &str) -> (Option<Vec<u8>>, Vec<u16>) {
|
|
77
|
+
let latin1 = text.chars().map(|c| u8::try_from(c).ok()).collect();
|
|
78
|
+
(latin1, text.encode_utf16().collect())
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fn check(transcoder: &mut Transcoder, text: &str) {
|
|
82
|
+
let (latin1, utf16) = expected(text);
|
|
83
|
+
match (transcoder.decode(text.as_bytes()), latin1) {
|
|
84
|
+
(Some(Decoded::OneByte(bytes)), Some(latin1)) => assert_eq!(bytes, latin1, "{text:?}"),
|
|
85
|
+
(Some(Decoded::TwoByte(units)), None) => assert_eq!(units, utf16, "{text:?}"),
|
|
86
|
+
(None, _) => assert!(text.len() < SHORT, "{text:?} left to V8"),
|
|
87
|
+
_ => panic!("{text:?} decoded in the wrong form"),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#[test]
|
|
92
|
+
fn valid_text_decodes_to_the_units_v8_stores() {
|
|
93
|
+
let mut transcoder = Transcoder::default();
|
|
94
|
+
let pad = "the quick brown fox ";
|
|
95
|
+
for text in [
|
|
96
|
+
"caf\u{e9}",
|
|
97
|
+
"caf\u{e9} na\u{ef}ve Z\u{fc}rich cr\u{e8}me \u{ff}",
|
|
98
|
+
"\u{6771}\u{4eac} \u{5927}\u{962a} \u{540d}\u{53e4}\u{5c4b}",
|
|
99
|
+
"\u{1f600} party \u{1f389} fun \u{1f680} go \u{10ffff}",
|
|
100
|
+
"0123456789abcde\u{e9}",
|
|
101
|
+
"0123456789abc\u{e9}",
|
|
102
|
+
"0123456789abcd\u{e9}",
|
|
103
|
+
] {
|
|
104
|
+
check(&mut transcoder, text);
|
|
105
|
+
}
|
|
106
|
+
let ranges = [
|
|
107
|
+
0x80..0x100,
|
|
108
|
+
0x100..0x800,
|
|
109
|
+
0x800..0xD800,
|
|
110
|
+
0xE000..0x1_0000,
|
|
111
|
+
0x1_0000..0x11_0000,
|
|
112
|
+
];
|
|
113
|
+
for range in ranges {
|
|
114
|
+
for start in range.clone().step_by(61) {
|
|
115
|
+
let text: String = (start..range.end.min(start + 23))
|
|
116
|
+
.filter_map(char::from_u32)
|
|
117
|
+
.collect();
|
|
118
|
+
check(&mut transcoder, &format!("{pad}{text}"));
|
|
119
|
+
check(&mut transcoder, &format!("{text}{pad}"));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
let latin1: String = (0x80..0x100).filter_map(char::from_u32).collect();
|
|
123
|
+
check(&mut transcoder, &latin1.repeat(9));
|
|
124
|
+
check(&mut transcoder, &format!("{}\u{100}", latin1.repeat(9)));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Values of a megabyte, the longest measured, are decoded; past `LONG`
|
|
128
|
+
// text goes as UTF-8.
|
|
129
|
+
#[test]
|
|
130
|
+
fn text_up_to_long_is_decoded_and_longer_is_left_to_v8() {
|
|
131
|
+
let mut transcoder = Transcoder::default();
|
|
132
|
+
let megabyte = "caf\u{e9} ".repeat((1 << 20) / 6);
|
|
133
|
+
assert!(matches!(
|
|
134
|
+
transcoder.decode(megabyte.as_bytes()),
|
|
135
|
+
Some(Decoded::OneByte(_))
|
|
136
|
+
));
|
|
137
|
+
let megabyte = "\u{1f600} go ".repeat((1 << 20) / 8);
|
|
138
|
+
assert!(matches!(
|
|
139
|
+
transcoder.decode(megabyte.as_bytes()),
|
|
140
|
+
Some(Decoded::TwoByte(_))
|
|
141
|
+
));
|
|
142
|
+
let wide = "\u{6771}";
|
|
143
|
+
let longest = format!("{wide}{}", "a".repeat(LONG - wide.len()));
|
|
144
|
+
assert_eq!(longest.len(), LONG);
|
|
145
|
+
match transcoder.decode(longest.as_bytes()) {
|
|
146
|
+
Some(Decoded::TwoByte(units)) => assert_eq!(units.len(), LONG - 2),
|
|
147
|
+
_ => panic!("text of `LONG` bytes is decoded"),
|
|
148
|
+
}
|
|
149
|
+
assert!(
|
|
150
|
+
transcoder
|
|
151
|
+
.decode(format!("{longest}a").as_bytes())
|
|
152
|
+
.is_none()
|
|
153
|
+
);
|
|
154
|
+
let latin1 = "\u{e9}".repeat(LONG / 2 + 1);
|
|
155
|
+
assert!(transcoder.decode(latin1.as_bytes()).is_none());
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#[test]
|
|
159
|
+
fn invalid_or_short_text_is_left_to_v8() {
|
|
160
|
+
let mut transcoder = Transcoder::default();
|
|
161
|
+
let pad = b"the quick brown fox ";
|
|
162
|
+
for invalid in [
|
|
163
|
+
&b"\xff"[..],
|
|
164
|
+
b"\x80",
|
|
165
|
+
b"\xc0\x80",
|
|
166
|
+
b"\xc3",
|
|
167
|
+
b"\xed\xa0\x80",
|
|
168
|
+
b"\xed\xbf\xbf",
|
|
169
|
+
b"\xf4\x90\x80\x80",
|
|
170
|
+
b"\xf5\x80\x80\x80",
|
|
171
|
+
b"\xe0\x80\xaf",
|
|
172
|
+
b"\xf0\x9f\x98",
|
|
173
|
+
b"\xe6\x9d",
|
|
174
|
+
] {
|
|
175
|
+
for text in [[pad, invalid].concat(), [invalid, pad].concat()] {
|
|
176
|
+
assert!(transcoder.decode(&text).is_none(), "{text:x?}");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
assert!(transcoder.decode("caf\u{e9}".as_bytes()).is_none());
|
|
180
|
+
assert!(transcoder.decode(b"").is_none());
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -157,9 +157,14 @@ impl<'a> Writer<'a> {
|
|
|
157
157
|
}
|
|
158
158
|
self.out.push(tag::TWO_BYTE_STRING);
|
|
159
159
|
self.len(bytes);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
// Written into slots made for them at once, which the compiler turns
|
|
161
|
+
// into wide copies; a unit at a time ran ten times slower.
|
|
162
|
+
let start = self.out.len();
|
|
163
|
+
self.out.resize(start.saturating_add(bytes), 0);
|
|
164
|
+
if let Some(slots) = self.out.get_mut(start..) {
|
|
165
|
+
for (slot, unit) in slots.as_chunks_mut::<2>().0.iter_mut().zip(units) {
|
|
166
|
+
*slot = unit.to_le_bytes();
|
|
167
|
+
}
|
|
163
168
|
}
|
|
164
169
|
}
|
|
165
170
|
|
|
@@ -9,7 +9,7 @@ use magnus::{
|
|
|
9
9
|
Error, Fixnum, Float, Module, Object, RArray, RClass, RHash, RModule, RObject, RString, Ruby,
|
|
10
10
|
Time, Value,
|
|
11
11
|
};
|
|
12
|
-
use mini_racer_engine::CallRequest;
|
|
12
|
+
use mini_racer_engine::{CallRequest, Decoded, Transcoder};
|
|
13
13
|
use mini_racer_wire::Writer;
|
|
14
14
|
|
|
15
15
|
use crate::gc_root::{OnceRoot, Pins, StaticRoot};
|
|
@@ -56,6 +56,8 @@ impl From<Error> for EncodeError {
|
|
|
56
56
|
enum Item {
|
|
57
57
|
// A pinned value, by its index in the holder.
|
|
58
58
|
Value(usize),
|
|
59
|
+
// A pinned key of a hash.
|
|
60
|
+
Key(usize),
|
|
59
61
|
// An element read when its turn comes, as serialize1 reads them, so a
|
|
60
62
|
// to_time that changes the array is seen.
|
|
61
63
|
Element { array: usize, index: isize },
|
|
@@ -101,6 +103,7 @@ struct Encoder<'r, 'o> {
|
|
|
101
103
|
// whether to build a call's arguments itself.
|
|
102
104
|
objects: usize,
|
|
103
105
|
properties: usize,
|
|
106
|
+
transcoder: Transcoder,
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
/// Writes `value` as `mini_racer`'s `serialize` does, with the object ids V8
|
|
@@ -131,7 +134,7 @@ pub fn call(
|
|
|
131
134
|
let mut encoder = Encoder::new(ruby, &mut bytes);
|
|
132
135
|
let count = length(args.len().saturating_add(1));
|
|
133
136
|
encoder.writer.begin_array(count);
|
|
134
|
-
encoder.string(name);
|
|
137
|
+
encoder.string(name, Text::Name);
|
|
135
138
|
args.iter()
|
|
136
139
|
.try_for_each(|&arg| encoder.top(arg))
|
|
137
140
|
.map_err(|error| error.raise(unsupported))?;
|
|
@@ -153,6 +156,22 @@ impl EncodeError {
|
|
|
153
156
|
}
|
|
154
157
|
}
|
|
155
158
|
|
|
159
|
+
// How mini_racer sends a String, by its encoding's name.
|
|
160
|
+
enum Sent {
|
|
161
|
+
Utf8,
|
|
162
|
+
Latin1,
|
|
163
|
+
Utf16,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// A key or a call's name goes in the form mini_racer sends unless it is
|
|
167
|
+
// ASCII: the runner builds only calls whose keys are UTF-8 or ASCII, and one
|
|
168
|
+
// is seldom long enough to be worth decoding.
|
|
169
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
170
|
+
enum Text {
|
|
171
|
+
Value,
|
|
172
|
+
Name,
|
|
173
|
+
}
|
|
174
|
+
|
|
156
175
|
fn length(len: usize) -> u32 {
|
|
157
176
|
u32::try_from(len).unwrap_or(u32::MAX)
|
|
158
177
|
}
|
|
@@ -181,6 +200,7 @@ impl<'r, 'o> Encoder<'r, 'o> {
|
|
|
181
200
|
seen: HashMap::default(),
|
|
182
201
|
objects: 0,
|
|
183
202
|
properties: 0,
|
|
203
|
+
transcoder: Transcoder::default(),
|
|
184
204
|
}
|
|
185
205
|
}
|
|
186
206
|
}
|
|
@@ -214,6 +234,10 @@ impl Encoder<'_, '_> {
|
|
|
214
234
|
Item::EndArray(length) => self.writer.end_array(length),
|
|
215
235
|
Item::EndObject(count) => self.writer.end_object(count),
|
|
216
236
|
Item::Value(index) => self.value(self.pinned(index))?,
|
|
237
|
+
Item::Key(index) => {
|
|
238
|
+
let key = self.pinned(index);
|
|
239
|
+
self.key(key, rb_raw::ruby_type(self.ruby, key));
|
|
240
|
+
}
|
|
217
241
|
Item::Element { array, index } => {
|
|
218
242
|
let element = RArray::from_value(self.pinned(array))
|
|
219
243
|
.map(|array| array.entry::<Value>(index))
|
|
@@ -239,14 +263,30 @@ impl Encoder<'_, '_> {
|
|
|
239
263
|
RubyType::True => self.writer.bool(true),
|
|
240
264
|
RubyType::False => self.writer.bool(false),
|
|
241
265
|
RubyType::String => {
|
|
242
|
-
|
|
266
|
+
let text = RString::from_value(value).unwrap_or_else(|| ruby.str_new(""));
|
|
267
|
+
self.string(text, Text::Value);
|
|
243
268
|
}
|
|
244
|
-
RubyType::Symbol => self.string(rb_raw::sym2str(ruby, value)),
|
|
269
|
+
RubyType::Symbol => self.string(rb_raw::sym2str(ruby, value), Text::Value),
|
|
245
270
|
_ => return false,
|
|
246
271
|
}
|
|
247
272
|
true
|
|
248
273
|
}
|
|
249
274
|
|
|
275
|
+
// A String, a Symbol or an Integer, the keys `hash` sends.
|
|
276
|
+
fn key(&mut self, key: Value, kind: RubyType) {
|
|
277
|
+
let ruby = self.ruby;
|
|
278
|
+
match kind {
|
|
279
|
+
RubyType::String => {
|
|
280
|
+
let text = RString::from_value(key).unwrap_or_else(|| ruby.str_new(""));
|
|
281
|
+
self.string(text, Text::Name);
|
|
282
|
+
}
|
|
283
|
+
RubyType::Symbol => self.string(rb_raw::sym2str(ruby, key), Text::Name),
|
|
284
|
+
_ => {
|
|
285
|
+
self.immediate(key, kind);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
250
290
|
fn value(&mut self, value: Value) -> Result<(), EncodeError> {
|
|
251
291
|
let kind = rb_raw::ruby_type(self.ruby, value);
|
|
252
292
|
if self.immediate(value, kind) {
|
|
@@ -338,7 +378,7 @@ impl Encoder<'_, '_> {
|
|
|
338
378
|
);
|
|
339
379
|
let item_kind = rb_raw::ruby_type(self.ruby, item);
|
|
340
380
|
if keys_fit && pairs.is_empty() && immediate(item_kind) {
|
|
341
|
-
self.
|
|
381
|
+
self.key(key, key_kind);
|
|
342
382
|
self.immediate(item, item_kind);
|
|
343
383
|
written = written.saturating_add(1);
|
|
344
384
|
} else {
|
|
@@ -357,7 +397,7 @@ impl Encoder<'_, '_> {
|
|
|
357
397
|
self.work.push(Item::EndObject(count));
|
|
358
398
|
for (key, item) in pairs.into_iter().rev() {
|
|
359
399
|
self.work.push(Item::Value(item));
|
|
360
|
-
self.work.push(Item::
|
|
400
|
+
self.work.push(Item::Key(key));
|
|
361
401
|
}
|
|
362
402
|
Ok(())
|
|
363
403
|
}
|
|
@@ -465,21 +505,43 @@ impl Encoder<'_, '_> {
|
|
|
465
505
|
|
|
466
506
|
// mini_racer tells encodings apart by name. The ones it sends as UTF-8
|
|
467
507
|
// that strings most often have are told by index, without the lookup.
|
|
468
|
-
|
|
508
|
+
// Bytes it sends as UTF-8 that are all ASCII are the same characters as
|
|
509
|
+
// one-byte text, which V8 copies where it would decode UTF-8. A value's
|
|
510
|
+
// other valid UTF-8 goes as the one-byte or two-byte text V8 would decode
|
|
511
|
+
// it to.
|
|
512
|
+
fn string(&mut self, text: RString, kind: Text) {
|
|
469
513
|
let ruby = self.ruby;
|
|
470
514
|
let index = text.enc_get();
|
|
471
515
|
let utf8 = index == ruby.utf8_encindex()
|
|
472
516
|
|| index == ruby.usascii_encindex()
|
|
473
517
|
|| index == ruby.ascii8bit_encindex();
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
518
|
+
let sent = if utf8 {
|
|
519
|
+
Sent::Utf8
|
|
520
|
+
} else {
|
|
478
521
|
match RbEncoding::from(index).name() {
|
|
479
|
-
"ISO-8859-1" =>
|
|
480
|
-
"UTF-16LE" =>
|
|
481
|
-
_ =>
|
|
522
|
+
"ISO-8859-1" => Sent::Latin1,
|
|
523
|
+
"UTF-16LE" => Sent::Utf16,
|
|
524
|
+
_ => Sent::Utf8,
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
let writer = &mut self.writer;
|
|
528
|
+
match sent {
|
|
529
|
+
Sent::Latin1 => rb_raw::with_string_bytes(ruby, text, |bytes| writer.latin1(bytes)),
|
|
530
|
+
Sent::Utf16 => rb_raw::with_string_bytes(ruby, text, |bytes| writer.utf16le(bytes)),
|
|
531
|
+
Sent::Utf8 => {
|
|
532
|
+
let ascii = rb_raw::cached_ascii(ruby, text);
|
|
533
|
+
let transcoder = (kind == Text::Value).then_some(&mut self.transcoder);
|
|
534
|
+
rb_raw::with_string_bytes(ruby, text, |bytes| {
|
|
535
|
+
if ascii.unwrap_or_else(|| bytes.is_ascii()) {
|
|
536
|
+
return writer.latin1(bytes);
|
|
537
|
+
}
|
|
538
|
+
match transcoder.and_then(|transcoder| transcoder.decode(bytes)) {
|
|
539
|
+
Some(Decoded::OneByte(latin1)) => writer.latin1(latin1),
|
|
540
|
+
Some(Decoded::TwoByte(units)) => writer.utf16_aligned(units),
|
|
541
|
+
None => writer.utf8(bytes),
|
|
542
|
+
}
|
|
543
|
+
});
|
|
482
544
|
}
|
|
483
|
-
}
|
|
545
|
+
}
|
|
484
546
|
}
|
|
485
547
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
use std::ffi::{c_int, c_long, c_void};
|
|
2
2
|
|
|
3
|
+
use magnus::encoding::Coderange;
|
|
3
4
|
use magnus::rb_sys::{AsRawId, AsRawValue, FromRawValue, protect};
|
|
4
5
|
use magnus::value::{IntoId, LazyId, ReprValue};
|
|
5
6
|
use magnus::{Error, RArray, RHash, RString, Ruby, Value};
|
|
@@ -343,6 +344,18 @@ pub fn sym2str(ruby: &Ruby, symbol: Value) -> RString {
|
|
|
343
344
|
RString::from_value(name).unwrap_or_else(|| ruby.str_new(""))
|
|
344
345
|
}
|
|
345
346
|
|
|
347
|
+
/// Whether every byte of `s` is ASCII, from the coderange Ruby keeps in the
|
|
348
|
+
/// String's flags; `None` while Ruby has not worked it out. Working it out
|
|
349
|
+
/// here would check every character of non-ASCII text one at a time, slower
|
|
350
|
+
/// than reading the bytes. Never 7-bit in an encoding that is not
|
|
351
|
+
/// ASCII-compatible.
|
|
352
|
+
pub fn cached_ascii(_ruby: &Ruby, s: RString) -> Option<bool> {
|
|
353
|
+
match s.enc_coderange() {
|
|
354
|
+
Coderange::Unknown => None,
|
|
355
|
+
known => Some(known == Coderange::SevenBit),
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
346
359
|
/// Runs `read` over the string's bytes without copying them; `read` must not
|
|
347
360
|
/// call Ruby, which could change or move the string.
|
|
348
361
|
pub fn with_string_bytes<R>(_ruby: &Ruby, s: RString, read: impl FnOnce(&[u8]) -> R) -> R {
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mini_racer_rs
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.
|
|
4
|
+
version: 1.0.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andy Aragon
|
|
@@ -64,6 +64,7 @@ files:
|
|
|
64
64
|
- crates/mini_racer_engine/src/snapshot.rs
|
|
65
65
|
- crates/mini_racer_engine/src/spin.rs
|
|
66
66
|
- crates/mini_racer_engine/src/sys.rs
|
|
67
|
+
- crates/mini_racer_engine/src/text.rs
|
|
67
68
|
- crates/mini_racer_sync/Cargo.toml
|
|
68
69
|
- crates/mini_racer_sync/src/channel.rs
|
|
69
70
|
- crates/mini_racer_sync/src/fork.rs
|