Skip to content

Commit db55a15

Browse files
authored
Merge pull request #9 from Rhea-OS/documentation
Expression parser had a bug where returning null values would leak other data
2 parents 7672246 + 05f736d commit db55a15

File tree

10 files changed

+173
-183
lines changed

10 files changed

+173
-183
lines changed

expression-js/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ expression = { path = ".." }
1111
wasm-bindgen = { version = "0.2.95" }
1212
serde = { version = "1.0.210", features = ["derive"] }
1313
js-sys = "0.3.72"
14-
web-sys = "0.3.72"
14+
web-sys = { version = "0.3.72", features = ["console"] }

expression-js/examples/repl.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ class Table implements expr.DataSource {
99
this.columns = columns;
1010
}
1111

12-
query(cx: any, query: string) {
12+
query(query: string) {
1313
const [, col, row] = query.match(/^(.+):(\d+)$/) ?? [];
14-
return this.data[Number(row) * this.columns.length + this.columns.indexOf(col)] ?? null;
14+
15+
if (this.columns.indexOf(col) === -1)
16+
return undefined;
17+
18+
return this.data[Number(row) * this.columns.length + this.columns.indexOf(col)] ?? undefined;
1519
}
1620

1721
set(address: string, value: any) {

expression-js/lib.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,6 @@ declare module 'expression' {
4242
}
4343

4444
interface DataSource {
45-
query(cx: any, query: string): any;
45+
query(query: string, cx: any): any;
4646
}
4747
}

expression-js/lib.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import mod from '#mod/main.wasm';
22
import * as util from '#mod/util.js';
33

44
const wasm = new WebAssembly.Instance(new WebAssembly.Module(mod), {
5-
"./expression_js_bg.js": util
5+
"./expression_js_bg.js": util as any,
66
});
77

88
util.__wbg_set_wasm(wasm.exports);
9+
util.__wbindgen_init_externref_table();
910

1011
export * from '#mod/util.js';

expression-js/src/context.rs

Lines changed: 17 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -7,125 +7,24 @@ use expression::Object;
77
use expression::Value;
88
use expression::parse;
99
use expression::parse::literal::Literal;
10+
use crate::convert::{js_value_to_object, value_to_js_object};
1011
use crate::DataSource;
1112

1213
#[wasm_bindgen(js_name=Context)]
1314
pub struct Context {
14-
cx: expression::Context<DataSource>,
15+
expr: expression::Context<DataSource>,
1516
global_context: Rc<WasmRefCell<Object>>
1617
}
1718

18-
/// This function attempts to convert a JS value into its native equivalent.
19-
///
20-
/// * `js_sys::String`s => `expression::Object::String`
21-
/// * `js_sys::Array`s => `expression::Object::List`
22-
/// * `js_sys::Object`s => `expression::Object::AssociativeArray`
23-
/// * `js_sys::Object + { [Symbol.address]: Address }`s => `expression::Object::Address` !
24-
/// * `js_sys::Null` => `expression::Object::Nothing`
25-
/// * `js_sys::falsy()` => `expression::Object::Boolean(false)`
26-
/// * `js_sys::truthy()` => `expression::Object::Boolean(true)`
27-
///
28-
/// > **Note:** Nested values such as those in lists or associative arrays which
29-
/// fail automatic conversation will be dropped silently.
30-
pub(crate) fn js_value_to_object(value: JsValue) -> Option<Object> {
31-
if let Some(number) = value.as_f64() {
32-
return Some(Object::Number(number));
33-
}
34-
35-
Some(match value {
36-
value if value.is_null() || value.is_undefined() => Object::Nothing,
37-
38-
value if value.is_string() => Object::String(value.as_string()?),
39-
value if value.is_array() => Object::List(js_sys::Array::from(&value)
40-
.into_iter()
41-
.flat_map(js_value_to_object)
42-
.collect()),
43-
44-
value if value.is_function() => {
45-
let value = value.clone();
46-
Object::function(move |args| {
47-
let args = js_sys::Array::from_iter(args.into_iter()
48-
.map(value_to_js_object)
49-
.map(|i| i.ok_or(Into::<Error>::into(ManualError::ConversionFailed)))
50-
.collect::<Result<Vec<_>>>()?
51-
.into_iter());
52-
let result = js_sys::Function::from(value.clone()).call1(&JsValue::null(), &JsValue::from(args))
53-
.unwrap_throw();
54-
js_value_to_object(result)
55-
.ok_or(ManualError::ConversionFailed.into())
56-
})
57-
},
58-
59-
// TODO: Detect Addresses
60-
value if value.is_object() => expression::Object::AssociativeArray(js_sys::Reflect::own_keys(&value).ok()?
61-
.into_iter()
62-
.flat_map(|i| match i {
63-
value if value.is_string() || value.is_symbol() => value.as_string(),
64-
_ => None
65-
})
66-
.flat_map(|key| js_value_to_object(js_sys::Reflect::get(&value, &JsValue::from_str(&key)).ok()?)
67-
.map(|value| (key.clone(), value)))
68-
.collect()),
69-
70-
value if value.is_falsy() => Object::Boolean(false),
71-
value if value.is_truthy() => Object::Boolean(true),
72-
_ => None?,
73-
})
74-
}
75-
76-
77-
/// This function attempts to convert a native value into its JS equivalent.
78-
///
79-
/// * `js_sys::String`s => `expression::Object::String`
80-
/// * `js_sys::Array`s => `expression::Object::List`
81-
/// * `js_sys::Object`s => `expression::Object::AssociativeArray`
82-
/// * `js_sys::Object + { [Symbol.address]: Address }`s => `expression::Object::Address` !
83-
/// * `js_sys::Null` => `expression::Object::Nothing`
84-
/// * `js_sys::falsy()` => `expression::Object::Boolean(false)`
85-
/// * `js_sys::truthy()` => `expression::Object::Boolean(true)`
86-
///
87-
/// > **Note:** Nested values such as those in lists or associative arrays which
88-
/// fail automatic conversation will be dropped silently.
89-
pub(crate) fn value_to_js_object(value: Object) -> Option<JsValue> {
90-
Some(match value {
91-
Object::Number(num) => js_sys::Number::from(num).into(),
92-
Object::Boolean(bool) => js_sys::Boolean::from(bool).into(),
93-
Object::String(str) => JsValue::from_str(&str),
94-
Object::List(list) => JsValue::from(js_sys::Array::from_iter(list.into_iter()
95-
.flat_map(value_to_js_object))),
96-
Object::AssociativeArray(arr) => {
97-
let key_map = js_sys::Object::new();
98-
99-
for (key, value) in arr {
100-
js_sys::Reflect::set(&key_map, &JsValue::from_str(&key), &value_to_js_object(value)?)
101-
.unwrap_throw();
102-
}
103-
104-
JsValue::from(key_map)
105-
},
106-
Object::Nothing => JsValue::null(),
107-
Object::Function(function) => JsClosure::new(move |_args| -> JsValue {
108-
wasm_bindgen::throw_str("Fuck you");
109-
function(vec![])
110-
.map(value_to_js_object)
111-
.unwrap_throw()
112-
.unwrap_throw()
113-
}).into(),
114-
})
115-
}
116-
11719
#[wasm_bindgen(js_class=Context)]
11820
impl Context {
11921
#[wasm_bindgen(constructor)]
120-
pub fn new(config: js_sys::Object) -> Context {
121-
let cx = DataSource::from_js(config);
22+
pub fn new(provider: js_sys::Object) -> Context {
23+
let provider = DataSource::from_js(provider);
12224

12325
Self {
124-
global_context: cx.cx.clone(),
125-
cx: expression::Context::new(cx)
126-
.with_fn("cx", move |cx, _| {
127-
Ok(cx.provider().cx.borrow().clone())
128-
})
26+
global_context: provider.ephemeral_cx.clone(),
27+
expr: expression::Context::new(provider)
12928
}
13029
}
13130

@@ -141,21 +40,21 @@ impl Context {
14140
wasm_bindgen::throw_str("Object could not be cast to target");
14241
};
14342

144-
self.cx.push_global(name, obj);
43+
self.expr.push_global(name, obj);
14544
}
14645

147-
#[deprecated(note = "Use push_global instead")]
46+
#[deprecated(note = "Use pushGlobal instead")]
14847
#[wasm_bindgen(js_name = withGlobal)]
14948
pub fn with_global(self, name: js_sys::JsString, global: JsValue) -> Self {
15049
unimplemented!("Use the `pushGlobal` function instead`")
15150
}
15251

15352
#[wasm_bindgen(js_name = pushOperator)]
15453
pub fn set_operator(&mut self, operator: crate::Operator) {
155-
self.cx.push_operator(operator.into_operator());
54+
self.expr.push_operator(operator.into_operator());
15655
}
15756

158-
#[deprecated(note = "Use push_operator instead")]
57+
#[deprecated(note = "Use pushOperator instead")]
15958
#[wasm_bindgen(js_name = withOperator)]
16059
pub fn with_operator(self, name: js_sys::JsString, global: JsValue) -> Self {
16160
unimplemented!("Use the `pushOperator` function instead");
@@ -167,10 +66,9 @@ impl Context {
16766
wasm_bindgen::throw_str("Expression could not be cast to native string");
16867
};
16968

170-
*self.global_context.borrow_mut() = js_value_to_object(cx)
171-
.unwrap_throw();
69+
*self.global_context.borrow_mut() = js_value_to_object(cx).unwrap_throw();
17270

173-
let error_message_or_result = self.cx.evaluate(expr)
71+
let error_message_or_result = self.expr.evaluate(expr)
17472
.map_err(|error| match error.into_inner() {
17573
global::Inner::ManualError(ManualError::InsufficientOperands(op)) => format!("The operator '{}' did not receive the required number of operands.", op),
17674
global::Inner::ManualError(ManualError::CannotCallNonFunctionObject()) => format!("Object not callable"),
@@ -186,11 +84,8 @@ impl Context {
18684
Err(err) => wasm_bindgen::throw_str(&err)
18785
};
18886

189-
if let Some(res) = value_to_js_object(res) {
190-
res
191-
} else {
192-
wasm_bindgen::throw_str("Unable to convert result back into JS");
193-
}
87+
return value_to_js_object(res)
88+
.unwrap_throw();
19489
}
19590

19691
#[wasm_bindgen(js_name="parseStr")]
@@ -235,7 +130,7 @@ impl Context {
235130
}
236131
}
237132

238-
flatten(&mut match self.cx.parse(&expr) {
133+
flatten(&mut match self.expr.parse(&expr) {
239134
Ok(value) => value,
240135
Err(err) => wasm_bindgen::throw_str(&format!("{:#?}", err))
241136
}).unwrap_or(vec![])
@@ -244,27 +139,14 @@ impl Context {
244139
#[wasm_bindgen(js_name="clone")]
245140
pub fn clone(&self) -> Self {
246141
Self {
247-
cx: self.cx.clone(),
142+
expr: self.expr.clone(),
248143
global_context: self.global_context.clone(),
249144
}
250145
}
251146

252147
#[wasm_bindgen(js_name="provider")]
253148
pub fn provider(&self) -> JsValue {
254-
self.cx.provider().inner.clone().into()
255-
}
256-
}
257-
258-
#[wasm_bindgen]
259-
struct JsClosure {
260-
closure: Closure<dyn Fn()>,
261-
}
262-
263-
impl JsClosure {
264-
pub fn new(handler: impl Fn(JsValue) -> JsValue + 'static) -> Self {
265-
Self {
266-
closure: Closure::new(|| {})
267-
}
149+
self.expr.provider().inner.clone().into()
268150
}
269151
}
270152

expression-js/src/convert.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
use wasm_bindgen::{JsValue, UnwrapThrowExt};
2+
use wasm_bindgen::closure::Closure;
3+
use wasm_bindgen::prelude::wasm_bindgen;
4+
use expression::{Error, ManualError, Object};
5+
6+
/// This function attempts to convert a JS value into its native equivalent.
7+
///
8+
/// * `js_sys::String`s => `expression::Object::String`
9+
/// * `js_sys::Array`s => `expression::Object::List`
10+
/// * `js_sys::Object`s => `expression::Object::AssociativeArray`
11+
/// * `js_sys::Object + { [Symbol.address]: Address }`s => `expression::Object::Address` !
12+
/// * `js_sys::Null` => `expression::Object::Nothing`
13+
/// * `js_sys::falsy()` => `expression::Object::Boolean(false)`
14+
/// * `js_sys::truthy()` => `expression::Object::Boolean(true)`
15+
///
16+
/// > **Note:** Nested values such as those in lists or associative arrays which
17+
/// fail automatic conversation will be dropped silently.
18+
pub(crate) fn js_value_to_object(value: JsValue) -> Option<Object> {
19+
if let Some(number) = value.as_f64() {
20+
return Some(Object::Number(number));
21+
}
22+
23+
Some(match value {
24+
value if value.is_null() || value.is_undefined() => Object::Nothing,
25+
26+
value if value.is_string() => Object::String(value.as_string()?),
27+
value if value.is_array() => Object::List(js_sys::Array::from(&value)
28+
.into_iter()
29+
.flat_map(js_value_to_object)
30+
.collect()),
31+
32+
value if value.is_function() => {
33+
let value = value.clone();
34+
Object::function(move |args| {
35+
let args = js_sys::Array::from_iter(args.into_iter()
36+
.map(value_to_js_object)
37+
.map(|i| i.ok_or(Into::<Error>::into(ManualError::ConversionFailed)))
38+
.collect::<expression::Result<Vec<_>>>()?
39+
.into_iter());
40+
let result = js_sys::Function::from(value.clone()).call1(&JsValue::null(), &JsValue::from(args))
41+
.unwrap_throw();
42+
js_value_to_object(result)
43+
.ok_or(ManualError::ConversionFailed.into())
44+
})
45+
},
46+
47+
// TODO: Detect Addresses
48+
value if value.is_object() => expression::Object::AssociativeArray(js_sys::Reflect::own_keys(&value).ok()?
49+
.into_iter()
50+
.flat_map(|i| match i {
51+
value if value.is_string() || value.is_symbol() => value.as_string(),
52+
_ => None
53+
})
54+
.flat_map(|key| js_value_to_object(js_sys::Reflect::get(&value, &JsValue::from_str(&key)).ok()?)
55+
.map(|value| (key.clone(), value)))
56+
.collect()),
57+
58+
value if value.is_falsy() => Object::Boolean(false),
59+
value if value.is_truthy() => Object::Boolean(true),
60+
_ => None?,
61+
})
62+
}
63+
64+
65+
/// This function attempts to convert a native value into its JS equivalent.
66+
///
67+
/// * `js_sys::String`s => `expression::Object::String`
68+
/// * `js_sys::Array`s => `expression::Object::List`
69+
/// * `js_sys::Object`s => `expression::Object::AssociativeArray`
70+
/// * `js_sys::Object + { [Symbol.address]: Address }`s => `expression::Object::Address` !
71+
/// * `js_sys::Null` => `expression::Object::Nothing`
72+
/// * `js_sys::falsy()` => `expression::Object::Boolean(false)`
73+
/// * `js_sys::truthy()` => `expression::Object::Boolean(true)`
74+
///
75+
/// > **Note:** Nested values such as those in lists or associative arrays which
76+
/// fail automatic conversation will be dropped silently.
77+
pub(crate) fn value_to_js_object(value: Object) -> Option<JsValue> {
78+
Some(match value {
79+
Object::Number(num) => js_sys::Number::from(num).into(),
80+
Object::Boolean(bool) => js_sys::Boolean::from(bool).into(),
81+
Object::String(str) => JsValue::from_str(&str),
82+
Object::List(list) => JsValue::from(js_sys::Array::from_iter(list.into_iter()
83+
.flat_map(value_to_js_object))),
84+
Object::AssociativeArray(arr) => {
85+
let key_map = js_sys::Object::new();
86+
87+
for (key, value) in arr {
88+
js_sys::Reflect::set(&key_map, &JsValue::from_str(&key), &value_to_js_object(value)?)
89+
.unwrap_throw();
90+
}
91+
92+
JsValue::from(key_map)
93+
},
94+
Object::Nothing => JsValue::null(),
95+
Object::Function(function) => JsClosure::new(move |_args| -> JsValue {
96+
wasm_bindgen::throw_str("Fuck you");
97+
function(vec![])
98+
.map(value_to_js_object)
99+
.unwrap_throw()
100+
.unwrap_throw()
101+
}).into(),
102+
})
103+
}
104+
105+
106+
#[wasm_bindgen]
107+
struct JsClosure {
108+
closure: Closure<dyn Fn()>,
109+
}
110+
111+
impl JsClosure {
112+
pub fn new(handler: impl Fn(JsValue) -> JsValue + 'static) -> Self {
113+
Self {
114+
closure: Closure::new(|| {})
115+
}
116+
}
117+
}

0 commit comments

Comments
 (0)