mini_racer_rs 1.0.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.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/AI-DISCLOSURE.md +13 -0
  3. data/CHANGELOG.md +45 -0
  4. data/COMPATIBILITY.md +235 -0
  5. data/Cargo.lock +1263 -0
  6. data/Cargo.toml +62 -0
  7. data/LICENSE +21 -0
  8. data/README.md +254 -0
  9. data/THIRD_PARTY_NOTICES.md +4501 -0
  10. data/crates/mini_racer_engine/Cargo.toml +25 -0
  11. data/crates/mini_racer_engine/build.rs +13 -0
  12. data/crates/mini_racer_engine/src/context.rs +802 -0
  13. data/crates/mini_racer_engine/src/control.rs +239 -0
  14. data/crates/mini_racer_engine/src/error.rs +88 -0
  15. data/crates/mini_racer_engine/src/fast_calls.rs +681 -0
  16. data/crates/mini_racer_engine/src/lib.rs +20 -0
  17. data/crates/mini_racer_engine/src/notify.rs +107 -0
  18. data/crates/mini_racer_engine/src/process/code_cache.rs +412 -0
  19. data/crates/mini_racer_engine/src/process/fork.rs +261 -0
  20. data/crates/mini_racer_engine/src/process/platform.rs +194 -0
  21. data/crates/mini_racer_engine/src/process/tasks.rs +96 -0
  22. data/crates/mini_racer_engine/src/process/watchdog.rs +185 -0
  23. data/crates/mini_racer_engine/src/process/workers.rs +165 -0
  24. data/crates/mini_racer_engine/src/process.rs +69 -0
  25. data/crates/mini_racer_engine/src/runner.rs +1560 -0
  26. data/crates/mini_racer_engine/src/serialize.rs +253 -0
  27. data/crates/mini_racer_engine/src/snapshot.rs +221 -0
  28. data/crates/mini_racer_engine/src/spin.rs +23 -0
  29. data/crates/mini_racer_engine/src/sys.rs +566 -0
  30. data/crates/mini_racer_sync/Cargo.toml +18 -0
  31. data/crates/mini_racer_sync/src/channel.rs +621 -0
  32. data/crates/mini_racer_sync/src/fork.rs +116 -0
  33. data/crates/mini_racer_sync/src/lib.rs +20 -0
  34. data/crates/mini_racer_sync/src/lock.rs +49 -0
  35. data/crates/mini_racer_sync/src/parker.rs +44 -0
  36. data/crates/mini_racer_sync/src/primitives.rs +10 -0
  37. data/crates/mini_racer_sync/src/tasks.rs +345 -0
  38. data/crates/mini_racer_sync/src/termination.rs +1234 -0
  39. data/crates/mini_racer_sync/src/workers.rs +283 -0
  40. data/crates/mini_racer_wire/Cargo.toml +18 -0
  41. data/crates/mini_racer_wire/src/decode.rs +926 -0
  42. data/crates/mini_racer_wire/src/lib.rs +67 -0
  43. data/crates/mini_racer_wire/src/marker.rs +43 -0
  44. data/crates/mini_racer_wire/src/text.rs +106 -0
  45. data/crates/mini_racer_wire/src/write.rs +187 -0
  46. data/ext/mini_racer_rs/Cargo.toml +26 -0
  47. data/ext/mini_racer_rs/build.rs +16 -0
  48. data/ext/mini_racer_rs/extconf.rb +78 -0
  49. data/ext/mini_racer_rs/src/context.rs +485 -0
  50. data/ext/mini_racer_rs/src/decode.rs +450 -0
  51. data/ext/mini_racer_rs/src/encode.rs +333 -0
  52. data/ext/mini_racer_rs/src/errors.rs +156 -0
  53. data/ext/mini_racer_rs/src/fork.rs +31 -0
  54. data/ext/mini_racer_rs/src/gc_root.rs +117 -0
  55. data/ext/mini_racer_rs/src/guard.rs +7 -0
  56. data/ext/mini_racer_rs/src/host.rs +339 -0
  57. data/ext/mini_racer_rs/src/lib.rs +44 -0
  58. data/ext/mini_racer_rs/src/options.rs +269 -0
  59. data/ext/mini_racer_rs/src/platform.rs +42 -0
  60. data/ext/mini_racer_rs/src/rb_raw.rs +368 -0
  61. data/ext/mini_racer_rs/src/snapshot.rs +149 -0
  62. data/ext/mini_racer_rs/v8-archives.sha256 +8 -0
  63. data/lib/mini_racer/shared.rb +420 -0
  64. data/lib/mini_racer/truffleruby.rb +439 -0
  65. data/lib/mini_racer/version.rb +6 -0
  66. data/lib/mini_racer.rb +3 -0
  67. data/lib/mini_racer_rs/internal_error.rb +7 -0
  68. data/lib/mini_racer_rs/version.rb +5 -0
  69. data/lib/mini_racer_rs/vocabulary.rb +105 -0
  70. data/lib/mini_racer_rs.rb +33 -0
  71. data/licenses/mini_racer.txt +21 -0
  72. data/licenses/rust/SOURCES.tsv +6 -0
  73. data/licenses/rust/std/COPYRIGHT +34 -0
  74. data/licenses/rust/std/COPYRIGHT-library.html +29357 -0
  75. data/licenses/rust/std/LICENSE-APACHE +176 -0
  76. data/licenses/rust/std/LICENSE-MIT +25 -0
  77. data/licenses/v8/SOURCES.tsv +20 -0
  78. data/licenses/v8/abseil-cpp/LICENSE +203 -0
  79. data/licenses/v8/chromium-build/LICENSE +27 -0
  80. data/licenses/v8/components.tsv +29 -0
  81. data/licenses/v8/dragonbox/LICENSE-Boost +23 -0
  82. data/licenses/v8/fast_float/LICENSE-MIT +27 -0
  83. data/licenses/v8/highway/LICENSE +371 -0
  84. data/licenses/v8/icu/LICENSE +568 -0
  85. data/licenses/v8/inspector_protocol/LICENSE +27 -0
  86. data/licenses/v8/libc++/LICENSE.TXT +311 -0
  87. data/licenses/v8/libc++abi/LICENSE.TXT +311 -0
  88. data/licenses/v8/llvm-libc/LICENSE.TXT +278 -0
  89. data/licenses/v8/rapidhash/LICENSE +34 -0
  90. data/licenses/v8/simdutf/LICENSE +18 -0
  91. data/licenses/v8/siphash/LICENSE +116 -0
  92. data/licenses/v8/utf8-decoder/LICENSE +19 -0
  93. data/licenses/v8/v8/LICENSE +61 -0
  94. data/licenses/v8/v8/LICENSE.fdlibm +6 -0
  95. data/licenses/v8/v8/LICENSE.strongtalk +29 -0
  96. data/licenses/v8/v8/LICENSE.v8 +26 -0
  97. metadata +156 -0
@@ -0,0 +1,681 @@
1
+ /// A value an eval source carries as a literal, which a fast call hands its
2
+ /// template instead of compiling.
3
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
4
+ pub enum Hoisted<'a> {
5
+ /// JSON text for an array or object, which V8's JSON parser decides on.
6
+ Json(&'a [u8]),
7
+ /// What a single-quoted string literal with no escapes holds.
8
+ Text(&'a [u8]),
9
+ }
10
+
11
+ /// A source split around the literals of the values it carries: the source
12
+ /// is `around[0]`, the first value's literal, `around[1]`, and so on.
13
+ #[derive(Debug, PartialEq, Eq)]
14
+ pub struct Shape<'a> {
15
+ pub around: Vec<&'a [u8]>,
16
+ pub values: Vec<Hoisted<'a>>,
17
+ /// The code a render runs before and after its call. The template reads
18
+ /// its values as the source would only if the call runs once, at the
19
+ /// wrapper's top level, so each must leave the call alone
20
+ /// (`leaves_the_call_alone`) and compile alone as a function body.
21
+ pub code: Vec<&'a [u8]>,
22
+ }
23
+
24
+ impl Shape<'_> {
25
+ /// The source with each value's literal replaced by `<reader>(<index>)`,
26
+ /// unless the source already names `reader`.
27
+ pub fn template(&self, reader: &[u8]) -> Option<Vec<u8>> {
28
+ let mut text = Vec::new();
29
+ for (index, piece) in self.around.iter().enumerate() {
30
+ if find(piece, reader).is_some() {
31
+ return None;
32
+ }
33
+ if let Some(value) = index.checked_sub(1) {
34
+ let digit = u8::try_from(value).ok().filter(|digit| *digit < 10)?;
35
+ text.extend_from_slice(reader);
36
+ text.extend_from_slice(&[b'(', b'0' + digit, b')']);
37
+ }
38
+ text.extend_from_slice(piece);
39
+ }
40
+ Some(text)
41
+ }
42
+ }
43
+
44
+ /// The shape of `source` if it is, in UTF-8, one of the two evals sent once
45
+ /// per call: `ExecJS` 2.10.2's `call`, `(<path>.apply(this, <JSON array>))`
46
+ /// with `<path>` ASCII identifiers joined by dots, or a react-rails 3.2.1 or
47
+ /// 3.3.1 render, `(` and `compose_js`'s wrapper around its one
48
+ /// `this.ReactRailsUJS.serverRender('<function>', '<component>', <props>)`
49
+ /// call, with JSON props on one line. Anything else is `None`.
50
+ pub fn recognize(source: &[u8]) -> Option<Shape<'_>> {
51
+ call(source)
52
+ .or_else(|| render(source))
53
+ .filter(|_| std::str::from_utf8(source).is_ok())
54
+ }
55
+
56
+ const APPLY: &[u8] = b".apply(this, ";
57
+
58
+ fn call(source: &[u8]) -> Option<Shape<'_>> {
59
+ let inner = source.strip_prefix(b"(")?.strip_suffix(b"))")?;
60
+ let mut path = 0;
61
+ let args = loop {
62
+ path += identifier_len(inner.get(path..)?)?;
63
+ let rest = inner.get(path..)?;
64
+ if let Some(args) = rest.strip_prefix(APPLY) {
65
+ break args;
66
+ }
67
+ if rest.first() != Some(&b'.') {
68
+ return None;
69
+ }
70
+ path += 1;
71
+ };
72
+ if args.first() != Some(&b'[') || json_extent(args)? != args.len() {
73
+ return None;
74
+ }
75
+ let args_at = 1 + path + APPLY.len();
76
+ Some(Shape {
77
+ around: vec![source.get(..args_at)?, source.get(args_at + args.len()..)?],
78
+ values: vec![Hoisted::Json(args)],
79
+ code: Vec::new(),
80
+ })
81
+ }
82
+
83
+ fn identifier_len(text: &[u8]) -> Option<usize> {
84
+ let name = |byte: u8| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'$';
85
+ if text
86
+ .first()
87
+ .is_none_or(|first| first.is_ascii_digit() || !name(*first))
88
+ {
89
+ return None;
90
+ }
91
+ Some(
92
+ text.iter()
93
+ .position(|&byte| !name(byte))
94
+ .unwrap_or(text.len()),
95
+ )
96
+ }
97
+
98
+ // react-rails' `compose_js` heredoc, indentation and all, inside ExecJS's
99
+ // parentheses; its `before` and `after` code sits between these.
100
+ const OPEN: &[u8] = b"( (function () {\n ";
101
+ const CALL: &[u8] = b"\n var result = this.ReactRailsUJS.serverRender(";
102
+ const RESULT: &[u8] = b");\n ";
103
+ const CLOSE: &[u8] = b"\n return result;\n })()\n)";
104
+ const SERVER_RENDER: &[u8] = b"ReactRailsUJS.serverRender";
105
+
106
+ fn render(source: &[u8]) -> Option<Shape<'_>> {
107
+ let body = source.strip_prefix(OPEN)?.strip_suffix(CLOSE)?;
108
+ let call = find(body, CALL)?;
109
+ let before = body.get(..call)?;
110
+ if find(before, SERVER_RENDER).is_some() {
111
+ return None;
112
+ }
113
+ let function_at = OPEN.len() + call + CALL.len();
114
+ let function = literal(source.get(function_at..)?)?;
115
+ let component_at = function_at + function.len() + 4;
116
+ let component = literal(source.get(component_at..)?)?;
117
+ let props_at = component_at + component.len() + 4;
118
+ let props_end = props_at + json_extent(source.get(props_at..)?)?;
119
+ let tail = source.get(props_end..)?;
120
+ let after = tail.strip_prefix(RESULT)?.strip_suffix(CLOSE)?;
121
+ let between = [
122
+ source.get(function_at + function.len() + 2..component_at)?,
123
+ source.get(component_at + component.len() + 2..props_at)?,
124
+ ];
125
+ let props = source.get(props_at..props_end)?;
126
+ if between != [b", ", b", "] || find(after, SERVER_RENDER).is_some() || breaks_line(props) {
127
+ return None;
128
+ }
129
+ Some(Shape {
130
+ around: vec![source.get(..function_at)?, between[0], between[1], tail],
131
+ values: vec![
132
+ Hoisted::Text(function),
133
+ Hoisted::Text(component),
134
+ Hoisted::Json(props),
135
+ ],
136
+ code: vec![before, after],
137
+ })
138
+ }
139
+
140
+ /// Whether a render's `code` names nothing that reaches the wrapper or what V8
141
+ /// keeps per script: no template literal (V8 keeps a tagged one's strings per
142
+ /// script), no `eval` or `caller`, no escape that could spell one of them, and
143
+ /// `arguments` only as a property after a `.` on the same line. With the code
144
+ /// also compiling alone as a function body, the call line runs once,
145
+ /// synchronously, at the wrapper's top level, short of reflection:
146
+ /// `CallSite#getFunction` through `Error.prepareStackTrace`, or `caller` and
147
+ /// `arguments.callee` through names built at run time.
148
+ pub fn leaves_the_call_alone(code: &[u8]) -> bool {
149
+ !(code.contains(&b'`')
150
+ || find(code, b"\\u").is_some()
151
+ || names(code, b"eval", false)
152
+ || names(code, b"caller", false)
153
+ || names(code, b"arguments", true))
154
+ }
155
+
156
+ // Whether `code` holds `name` as a whole word, unless `as_property` lets one
157
+ // read as a property after a single `.` pass. A byte past ASCII never joins a
158
+ // word, as V8 takes some of those for spaces. The `.` must be on the same
159
+ // line: one that ends a line comment or a number is no property access.
160
+ fn names(code: &[u8], name: &[u8], as_property: bool) -> bool {
161
+ let joins = |byte: &u8| byte.is_ascii_alphanumeric() || *byte == b'_' || *byte == b'$';
162
+ let mut from = 0;
163
+ while let Some(at) = code.get(from..).and_then(|rest| find(rest, name)) {
164
+ let at = from + at;
165
+ from = at + 1;
166
+ let start = at.checked_sub(1).and_then(|before| code.get(before));
167
+ if start.is_some_and(joins) || code.get(at + name.len()).is_some_and(joins) {
168
+ continue;
169
+ }
170
+ let mut before = code.get(..at).unwrap_or_default().iter().rev();
171
+ let property = before.find(|byte| !matches!(byte, b' ' | b'\t' | b'\x0C')) == Some(&b'.')
172
+ && before.next() != Some(&b'.');
173
+ if !(as_property && property) {
174
+ return true;
175
+ }
176
+ }
177
+ false
178
+ }
179
+
180
+ // A string literal that holds its own text: no escapes, and no line
181
+ // terminator, which would move every line after it.
182
+ fn literal(text: &[u8]) -> Option<&[u8]> {
183
+ let text = text.strip_prefix(b"'")?;
184
+ let content = text.get(..text.iter().position(|&byte| byte == b'\'')?)?;
185
+ (!content.contains(&b'\\') && !breaks_line(content)).then_some(content)
186
+ }
187
+
188
+ // V8 numbers lines at U+2028 and U+2029 as well, inside literals too. Each
189
+ // `contains` reads a word at a time, as most text holds none of these bytes.
190
+ fn breaks_line(text: &[u8]) -> bool {
191
+ text.contains(&b'\n')
192
+ || text.contains(&b'\r')
193
+ || (text.contains(&0xE2)
194
+ && text
195
+ .windows(3)
196
+ .any(|window| matches!(window, [0xE2, 0x80, 0xA8 | 0xA9])))
197
+ }
198
+
199
+ fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
200
+ haystack
201
+ .windows(needle.len())
202
+ .position(|window| window == needle)
203
+ }
204
+
205
+ // V8's JSON parser keeps its own stack where its JavaScript parser recurses,
206
+ // so a value nested deeper than this takes the normal path, which fails on it
207
+ // the way mini_racer does once the nesting overflows the stack.
208
+ const MAX_DEPTH: usize = 128;
209
+
210
+ // The length of the JSON array or object `text` starts with, if it nests no
211
+ // deeper than `MAX_DEPTH` and cannot name a `__proto__` key, which an object
212
+ // literal makes its prototype and `JSON.parse` an own property. It reads
213
+ // `text` as JSON; V8's parser decides whether it is, though a byte JSON never
214
+ // has outside a string ends it here, cheaper than V8's error.
215
+ fn json_extent(text: &[u8]) -> Option<usize> {
216
+ if !matches!(text.first(), Some(b'[' | b'{')) {
217
+ return None;
218
+ }
219
+ let mut depth = 0_usize;
220
+ let mut at = 0;
221
+ while let Some(&byte) = text.get(at) {
222
+ match CLASSES[usize::from(byte)] {
223
+ Class::Quote => {
224
+ at = string_end(text, at + 1)?;
225
+ continue;
226
+ }
227
+ Class::Open => {
228
+ depth += 1;
229
+ if depth > MAX_DEPTH {
230
+ return None;
231
+ }
232
+ }
233
+ Class::Close => {
234
+ depth = depth.checked_sub(1)?;
235
+ if depth == 0 {
236
+ return Some(at + 1);
237
+ }
238
+ }
239
+ Class::Plain => {}
240
+ Class::Other => return None,
241
+ }
242
+ at += 1;
243
+ }
244
+ None
245
+ }
246
+
247
+ #[derive(Clone, Copy)]
248
+ enum Class {
249
+ Quote,
250
+ Open,
251
+ Close,
252
+ // Whitespace, `:`, `,`, what numbers are written with, and the letters of
253
+ // `true`, `false` and `null`.
254
+ Plain,
255
+ Other,
256
+ }
257
+
258
+ const CLASSES: [Class; 256] = {
259
+ let mut classes = [Class::Other; 256];
260
+ let plain = b" \t\n\r:,0123456789-+.Eaeflnrstu";
261
+ let mut at = 0;
262
+ while at < plain.len() {
263
+ classes[plain[at] as usize] = Class::Plain;
264
+ at += 1;
265
+ }
266
+ classes[b'"' as usize] = Class::Quote;
267
+ classes[b'[' as usize] = Class::Open;
268
+ classes[b'{' as usize] = Class::Open;
269
+ classes[b']' as usize] = Class::Close;
270
+ classes[b'}' as usize] = Class::Close;
271
+ classes
272
+ };
273
+
274
+ // The index after the closing quote of the string whose contents start at
275
+ // `at`, unless it holds `__proto__` or an escaped letter, digit, `_` or `$`,
276
+ // which could spell it.
277
+ fn string_end(text: &[u8], mut at: usize) -> Option<usize> {
278
+ let mut plain = 0;
279
+ loop {
280
+ // Most strings are short keys, which a word at a time only slows.
281
+ if plain == 16 {
282
+ at = plain_run(text, at);
283
+ plain = 0;
284
+ }
285
+ match *text.get(at)? {
286
+ b'"' => return Some(at + 1),
287
+ b'\\' if text.get(at + 1) == Some(&b'u') => {
288
+ let unit = std::str::from_utf8(text.get(at + 2..at + 6)?).ok()?;
289
+ let unit = u8::try_from(u32::from_str_radix(unit, 16).ok()?).unwrap_or(0x80);
290
+ if unit.is_ascii_alphanumeric() || unit == b'_' || unit == b'$' {
291
+ return None;
292
+ }
293
+ at += 6;
294
+ }
295
+ b'\\' => at += 2,
296
+ b'_' if text
297
+ .get(at..)
298
+ .is_some_and(|rest| rest.starts_with(b"__proto__")) =>
299
+ {
300
+ return None;
301
+ }
302
+ _ => {
303
+ at += 1;
304
+ plain += 1;
305
+ }
306
+ }
307
+ }
308
+ }
309
+
310
+ // Past eight bytes at a time that hold no quote, backslash or `_`: most of a
311
+ // long string, read a word at a time.
312
+ fn plain_run(text: &[u8], mut at: usize) -> usize {
313
+ const ONES: u64 = u64::from_ne_bytes([1; 8]);
314
+ const HIGHS: u64 = u64::from_ne_bytes([0x80; 8]);
315
+ let holds = |word: u64, byte: u8| {
316
+ let matched = word ^ (ONES * u64::from(byte));
317
+ matched.wrapping_sub(ONES) & !matched & HIGHS != 0
318
+ };
319
+ while let Some(word) = text
320
+ .get(at..at + 8)
321
+ .and_then(|bytes| <[u8; 8]>::try_from(bytes).ok())
322
+ .map(u64::from_ne_bytes)
323
+ {
324
+ if holds(word, b'"') || holds(word, b'\\') || holds(word, b'_') {
325
+ break;
326
+ }
327
+ at += 8;
328
+ }
329
+ at
330
+ }
331
+
332
+ #[cfg(test)]
333
+ mod tests {
334
+ use super::{Hoisted, MAX_DEPTH, Shape, leaves_the_call_alone, recognize};
335
+
336
+ // Code a renderer runs around each render, as react-rails' console
337
+ // replay does.
338
+ const BEFORE: &str = "var logged = [];\nconsole.history = logged;\n";
339
+ const AFTER: &str = "if (logged.length > 0) {\n result += \"\\n<!--\" + \
340
+ logged.map(function (entry) {\n \
341
+ return \" \" + entry.level + \" \" + JSON.stringify(entry.arguments);\n \
342
+ }).join(\"\") + \" -->\";\n}\n";
343
+
344
+ // What react-rails 3.2.1 and 3.3.1 hand ExecJS's eval, which wraps it in
345
+ // parentheses.
346
+ fn compose_js(before: &str, main: &str, after: &str) -> String {
347
+ format!(
348
+ "( (function () {{\n {before}\n var result = {main};\n \
349
+ {after}\n return result;\n }})()\n)"
350
+ )
351
+ }
352
+
353
+ fn render(function: &str, component: &str, props: &str) -> String {
354
+ compose_js(
355
+ BEFORE,
356
+ &format!("this.ReactRailsUJS.serverRender('{function}', '{component}', {props})"),
357
+ AFTER,
358
+ )
359
+ }
360
+
361
+ fn values(source: &str) -> Option<Vec<Hoisted<'_>>> {
362
+ recognize(source.as_bytes()).map(|shape| shape.values)
363
+ }
364
+
365
+ fn json(text: &str) -> Hoisted<'_> {
366
+ Hoisted::Json(text.as_bytes())
367
+ }
368
+
369
+ fn text(text: &str) -> Hoisted<'_> {
370
+ Hoisted::Text(text.as_bytes())
371
+ }
372
+
373
+ fn nested(depth: usize) -> String {
374
+ format!("{}{}", "[".repeat(depth), "]".repeat(depth))
375
+ }
376
+
377
+ #[test]
378
+ fn an_execjs_call_hoists_its_arguments() {
379
+ let source = "(add.apply(this, [1,\"two\",{\"3\":null}]))";
380
+ assert_eq!(
381
+ recognize(source.as_bytes()),
382
+ Some(Shape {
383
+ around: vec![b"(add.apply(this, ", b"))"],
384
+ values: vec![json("[1,\"two\",{\"3\":null}]")],
385
+ code: vec![],
386
+ })
387
+ );
388
+ }
389
+
390
+ #[test]
391
+ fn a_call_names_its_function_by_a_dotted_path() {
392
+ for path in [
393
+ "a.b.c",
394
+ "$_.x9",
395
+ "_",
396
+ "$",
397
+ "foo.apply",
398
+ "this.add",
399
+ "arguments",
400
+ ] {
401
+ let source = format!("({path}.apply(this, []))");
402
+ let shape = recognize(source.as_bytes());
403
+ assert_eq!(
404
+ shape.map(|shape| shape.around[0].to_vec()),
405
+ Some(format!("({path}.apply(this, ").into_bytes()),
406
+ "{path}"
407
+ );
408
+ }
409
+ }
410
+
411
+ #[test]
412
+ fn a_call_template_reads_its_arguments_back() {
413
+ let shape = recognize(b"(add.apply(this, [1]))");
414
+ assert_eq!(
415
+ shape.and_then(|shape| shape.template(b"read")),
416
+ Some(b"(add.apply(this, read(0)))".to_vec())
417
+ );
418
+ }
419
+
420
+ #[test]
421
+ fn a_source_that_names_the_reader_has_no_template() {
422
+ for source in ["(read.apply(this, [1]))", "(a.read.apply(this, [1]))"] {
423
+ let shape = recognize(source.as_bytes());
424
+ assert!(shape.is_some(), "{source}");
425
+ assert_eq!(shape.and_then(|shape| shape.template(b"read")), None);
426
+ }
427
+ }
428
+
429
+ #[test]
430
+ fn near_misses_of_a_call_are_not_recognized() {
431
+ for source in [
432
+ " (add.apply(this, [1]))",
433
+ "( add.apply(this, [1]))",
434
+ "(add .apply(this, [1]))",
435
+ "(add.apply( this, [1]))",
436
+ "(add.apply(this, [1]))",
437
+ "(add.apply(this,[1]))",
438
+ "(add.apply(this, [1]) )",
439
+ "(add.apply(this, [1]));",
440
+ "add.apply(this, [1])",
441
+ "(add.call(this, [1]))",
442
+ "(add.apply(globalThis, [1]))",
443
+ "(a[0].apply(this, [1]))",
444
+ "(a['b'].apply(this, [1]))",
445
+ "(a..b.apply(this, [1]))",
446
+ "(.a.apply(this, [1]))",
447
+ "(9a.apply(this, [1]))",
448
+ "(\u{e4}.apply(this, [1]))",
449
+ "(f().apply(this, [1]))",
450
+ "(add.apply(this, \"[1]\"))",
451
+ "(add.apply(this, {\"a\":1}))",
452
+ "(add.apply(this, [1]) + add.apply(this, [2]))",
453
+ "(add.apply(this, [1] ))",
454
+ "(add.apply(this, [1]\n))",
455
+ "(add.apply(this, [1))",
456
+ "(add.apply(this, [\"1]))",
457
+ ] {
458
+ assert_eq!(values(source), None, "{source:?}");
459
+ }
460
+ }
461
+
462
+ #[test]
463
+ fn brackets_and_parentheses_in_strings_do_not_end_the_arguments() {
464
+ let source = "(f.apply(this, [\"])\\\"[\",\"))\"]))";
465
+ assert_eq!(values(source), Some(vec![json("[\"])\\\"[\",\"))\"]")]));
466
+ }
467
+
468
+ #[test]
469
+ fn a_proto_key_in_any_spelling_takes_the_normal_path() {
470
+ for args in [
471
+ "[{\"__proto__\":{}}]",
472
+ "[\"__proto__\"]",
473
+ "[{\"\\u005f_proto__\":1}]",
474
+ "[{\"_\\u005Fproto__\":1}]",
475
+ "[{\"__\\u0070roto__\":1}]",
476
+ "[{\"__pr\\u006Fto__\":1}]",
477
+ ] {
478
+ assert_eq!(values(&format!("(f.apply(this, {args}))")), None, "{args}");
479
+ }
480
+ let escaped = "[\"\\u003c\\u2028\\\\u005f\\\"\"]";
481
+ assert_eq!(
482
+ values(&format!("(f.apply(this, {escaped}))")),
483
+ Some(vec![json(escaped)])
484
+ );
485
+ }
486
+
487
+ #[test]
488
+ fn nesting_past_the_limit_takes_the_normal_path() {
489
+ let deepest = nested(MAX_DEPTH);
490
+ let source = format!("(f.apply(this, {deepest}))");
491
+ assert_eq!(values(&source), Some(vec![json(&deepest)]));
492
+ assert_eq!(
493
+ values(&format!("(f.apply(this, {}))", nested(MAX_DEPTH + 1))),
494
+ None
495
+ );
496
+ }
497
+
498
+ #[test]
499
+ fn a_source_that_is_not_utf8_is_not_recognized() {
500
+ assert_eq!(recognize(b"(f.apply(this, [\"\xff\"]))"), None);
501
+ }
502
+
503
+ #[test]
504
+ fn a_react_rails_render_hoists_its_function_component_and_props() {
505
+ let source = render(
506
+ "renderToString",
507
+ "Components.Hello",
508
+ "{\"name\":\"W\u{e9}\"}",
509
+ );
510
+ let shape = recognize(source.as_bytes());
511
+ assert_eq!(
512
+ shape.as_ref().map(|shape| shape.values.clone()),
513
+ Some(vec![
514
+ text("renderToString"),
515
+ text("Components.Hello"),
516
+ json("{\"name\":\"W\u{e9}\"}"),
517
+ ])
518
+ );
519
+ let expected = compose_js(
520
+ BEFORE,
521
+ "this.ReactRailsUJS.serverRender(read(0), read(1), read(2))",
522
+ AFTER,
523
+ );
524
+ assert_eq!(
525
+ shape.and_then(|shape| shape.template(b"read")),
526
+ Some(expected.into_bytes())
527
+ );
528
+ }
529
+
530
+ #[test]
531
+ fn a_render_with_no_code_around_its_call_is_recognized() {
532
+ let source = compose_js(
533
+ "",
534
+ "this.ReactRailsUJS.serverRender('renderToStaticMarkup', 'A', [])",
535
+ "",
536
+ );
537
+ assert_eq!(
538
+ values(&source),
539
+ Some(vec![text("renderToStaticMarkup"), text("A"), json("[]")])
540
+ );
541
+ }
542
+
543
+ #[test]
544
+ fn a_renders_code_is_what_runs_before_and_after_its_call() {
545
+ let source = render("renderToString", "A", "{}");
546
+ let shape = recognize(source.as_bytes());
547
+ assert_eq!(
548
+ shape.map(|shape| shape.code),
549
+ Some(vec![BEFORE.as_bytes(), AFTER.as_bytes()])
550
+ );
551
+ assert_eq!(
552
+ recognize(b"(f.apply(this, []))").map(|shape| shape.code),
553
+ Some(vec![])
554
+ );
555
+ }
556
+
557
+ #[test]
558
+ fn code_that_could_reach_past_the_call_does_not_leave_it_alone() {
559
+ for code in [
560
+ "var s = `x`;",
561
+ "eval('1');",
562
+ "var f = window.eval;",
563
+ "f.caller;",
564
+ "var a = arguments;",
565
+ "arguments.callee;",
566
+ "f(...arguments);",
567
+ "x = (arguments);",
568
+ "\\u0061rguments;",
569
+ "ev\\u0061l('1');",
570
+ "\u{a0}arguments;",
571
+ "// x.\narguments.callee();",
572
+ "x = 1.\narguments;",
573
+ "x.\r\narguments;",
574
+ "x.\rarguments;",
575
+ "x.\u{2028}arguments;",
576
+ ] {
577
+ assert!(!leaves_the_call_alone(code.as_bytes()), "{code}");
578
+ }
579
+ }
580
+
581
+ #[test]
582
+ fn longer_names_and_properties_called_arguments_leave_the_call_alone() {
583
+ for code in [
584
+ BEFORE,
585
+ AFTER,
586
+ "msg.arguments;",
587
+ "msg . arguments;",
588
+ "msg.\targuments;",
589
+ "myarguments;",
590
+ "arguments_;",
591
+ "$eval();",
592
+ "evaluate();",
593
+ "callers;",
594
+ "'\\n';",
595
+ ] {
596
+ assert!(leaves_the_call_alone(code.as_bytes()), "{code}");
597
+ }
598
+ }
599
+
600
+ #[test]
601
+ fn near_misses_of_a_render_are_not_recognized() {
602
+ let call = "this.ReactRailsUJS.serverRender('renderToString', 'A', {})";
603
+ let second = "this.ReactRailsUJS.serverRender('renderToString', 'B', {});";
604
+ let sources = [
605
+ compose_js(BEFORE, call, AFTER).replacen("( (", "( (", 1),
606
+ compose_js(BEFORE, call, AFTER).replacen("var result = ", "var result = ", 1),
607
+ compose_js(BEFORE, call, AFTER).replacen("return result;\n", "return result; \n", 1),
608
+ compose_js(BEFORE, call, AFTER).replacen("{});", "{} );", 1),
609
+ compose_js(BEFORE, call, AFTER).replacen("'A', {}", "'A',{}", 1),
610
+ compose_js(BEFORE, call, AFTER)
611
+ .trim_end_matches(')')
612
+ .to_owned(),
613
+ compose_js(second, call, AFTER),
614
+ compose_js(BEFORE, call, second),
615
+ compose_js(BEFORE, &format!("{call} + {call}"), AFTER),
616
+ compose_js(
617
+ BEFORE,
618
+ "ReactRailsUJS.serverRender('renderToString', 'A', {})",
619
+ AFTER,
620
+ ),
621
+ render("renderToString", "He'llo", "{}"),
622
+ render("renderToString", "A\\u0041", "{}"),
623
+ render("renderToString", "A\u{2028}", "{}"),
624
+ render("render\nToString", "A", "{}"),
625
+ render("renderToString", "A", "{name: 'js'} || {}"),
626
+ render("renderToString", "A", "null"),
627
+ render("renderToString", "A", "\"{}\""),
628
+ render("renderToString", "A", "{\n \"n\": 4\n}"),
629
+ render("renderToString", "A", "{\"n\":\"\u{2029}\"}"),
630
+ render("renderToString", "A", "{\"__proto__\":{}}"),
631
+ ];
632
+ for source in &sources {
633
+ assert_eq!(values(source), None, "{source:?}");
634
+ }
635
+ }
636
+
637
+ #[test]
638
+ fn props_with_a_byte_json_keeps_in_strings_take_the_normal_path() {
639
+ for props in [
640
+ "{name: 'js'}",
641
+ "{\"a\":undefined}",
642
+ "{\"a\":1}/*x*/",
643
+ "{\"a\":`b`}",
644
+ ] {
645
+ assert_eq!(
646
+ values(&render("renderToString", "A", props)),
647
+ None,
648
+ "{props}"
649
+ );
650
+ }
651
+ }
652
+
653
+ // Whether props are JSON at all is V8's to decide.
654
+ #[test]
655
+ fn props_of_json_bytes_are_left_to_the_json_parser() {
656
+ let source = render("renderToString", "A", "{n: true}");
657
+ assert_eq!(
658
+ values(&source),
659
+ Some(vec![text("renderToString"), text("A"), json("{n: true}")])
660
+ );
661
+ }
662
+
663
+ // Past its first 16 bytes a string is read eight bytes at a time, so each
664
+ // byte that matters is tried before that and at every offset into a word.
665
+ #[test]
666
+ fn quotes_escapes_and_proto_are_found_at_any_offset_in_a_string() {
667
+ for offset in 0..48 {
668
+ let pad = "x".repeat(offset);
669
+ let plain = format!("[\"{pad}\",\"{pad}\\\"]\"]");
670
+ let source = format!("(f.apply(this, {plain}))");
671
+ assert_eq!(values(&source), Some(vec![json(&plain)]), "{offset}");
672
+ for args in [
673
+ format!("[{{\"{pad}__proto__\":1}}]"),
674
+ format!("[\"{pad}\\u005f\"]"),
675
+ format!("[\"{pad}]"),
676
+ ] {
677
+ assert_eq!(values(&format!("(f.apply(this, {args}))")), None, "{args}");
678
+ }
679
+ }
680
+ }
681
+ }
@@ -0,0 +1,20 @@
1
+ mod context;
2
+ mod control;
3
+ mod error;
4
+ mod fast_calls;
5
+ mod notify;
6
+ pub mod process;
7
+ mod runner;
8
+ mod serialize;
9
+ mod snapshot;
10
+ mod spin;
11
+ #[allow(
12
+ unsafe_code,
13
+ reason = "the engine's only FFI fence: symbols the linker routes into V8"
14
+ )]
15
+ mod sys;
16
+
17
+ pub use context::{ContextHandle, Host, Unparker, WaitHandle, WaitOutcome};
18
+ pub use control::{CallbackReply, ContextConfig, JsText};
19
+ pub use error::{EngineError, FlagError, InvalidSnapshot, JsError, JsErrorKind, TerminationCause};
20
+ pub use snapshot::{SnapshotBlob, WeakSnapshotBlob};