Skip to main content

vitaminc_aead_napi/
js_ref.rs

1//! [`JsRef`]: a live JavaScript value as an [`Encrypt`] source.
2//!
3//! The walk runs on the JavaScript thread, reading each value as it goes:
4//! no tree is built, and a scalar leaves the engine only to go straight to
5//! the sink. ADR 0001 section 6 gives the mapping:
6//!
7//! | JavaScript             | `Sink` call                                     |
8//! | ---------------------- | ----------------------------------------------- |
9//! | `null`                 | `scalar(None, Null)`                            |
10//! | `undefined`            | `scalar(Some(Undefined), Null)`                 |
11//! | `boolean`              | `scalar(None, Bool)`                            |
12//! | `number`               | `scalar(None, Float(F64))`                      |
13//! | `bigint`               | `scalar(None, Int(narrowest fitting width))`    |
14//! | `string`               | `scalar(None, Text)`                            |
15//! | `Buffer`, `Uint8Array` | `scalar(None, Bytes)`; any other view is refused |
16//! | `Date`                 | `scalar(Some(Timestamp(Millis)), Int(I64(ms)))` |
17//! | `Array`                | `seq`, then `element()` per index               |
18//! | plain object           | `map`, then `entry(key)` per own property       |
19//!
20//! The wrapper classes carry the scalars JavaScript has no type for:
21//! `VcDate` is `Date` over the day count, `VcTimestamp` is
22//! `Timestamp(Nanos)` over the nanosecond count, `VcDecimal` is `Decimal`
23//! over the mantissa and `VcUuid` is `Uuid` over sixteen bytes.
24//!
25//! A getter that throws, a string with an unpaired surrogate, a BigInt out
26//! of range or a `Date` outside chrono's range stops the walk through
27//! [`Error::source`](vitaminc_aead::plaintext::Error::source), and comes
28//! back to JavaScript as that error: see [`JsFault`].
29
30use napi::bindgen_prelude::{
31    Array, BigInt, FromNapiValue, JsObjectValue, JsValue, Object, Uint8ArraySlice, Unknown,
32};
33use napi::{Env, Error, Status, ValueType};
34use vitaminc_aead::frame::FrameWriteError;
35use vitaminc_aead::plaintext::{Error as _, SourceError};
36use vitaminc_aead::{
37    to_frame, Encrypt, Float, Logical, MapSink, Physical, SeqSink, Sink, TimeUnit,
38};
39use vitaminc_protected::Protected;
40
41use crate::error::JsFault;
42use crate::js::{
43    depth_error, forbidden_key, forbidden_key_error, get_own, lossless_string, not_plain_error,
44    own_enumerable_keys, MAX_DEPTH,
45};
46use crate::scalar::{self, ConversionError};
47use crate::wrappers::{Wrapper, Wrappers};
48
49/// A live JavaScript value, walked in place on the JavaScript thread.
50pub struct JsRef<'e> {
51    value: Unknown<'e>,
52    depth: usize,
53}
54
55impl<'e> JsRef<'e> {
56    /// The value to walk, as the root of the structure.
57    pub fn new(value: Unknown<'e>) -> Self {
58        Self { value, depth: 0 }
59    }
60
61    /// A value that sits `depth` levels down in a larger structure, so
62    /// the nesting limit counts the whole structure.
63    pub(crate) fn at_depth(value: Unknown<'e>, depth: usize) -> Self {
64        Self { value, depth }
65    }
66
67    /// Snapshot the value as a frame, on the JavaScript thread, so it can be
68    /// encrypted on a worker with no JavaScript access. The frame is
69    /// plaintext and comes back wiped on drop.
70    pub fn to_frame(self) -> napi::Result<Protected<Vec<u8>>> {
71        to_frame(self).map_err(frame_error)
72    }
73}
74
75impl Encrypt for JsRef<'_> {
76    fn visit<S: Sink>(self, sink: S) -> Result<S::Ok, S::Error> {
77        js_walk(self.value, self.depth, sink)
78    }
79}
80
81/// A frame writer's refusal as the napi error to hand back.
82pub(crate) fn frame_error(error: FrameWriteError) -> Error {
83    match error {
84        FrameWriteError::Source(source) => JsFault::from_source(&source),
85        FrameWriteError::TooDeep => depth_error(),
86        other => Error::new(Status::InvalidArg, other.to_string()),
87    }
88}
89
90/// A JavaScript-side failure, as the sink's error.
91fn js_fault<S: Sink>(error: Error) -> S::Error {
92    S::Error::source(SourceError::new(JsFault::from(error)))
93}
94
95/// A scalar conversion failure: thrown into JavaScript as a `TypeError`
96/// with the error's stable code, and carried as the pending exception.
97fn js_conversion<S: Sink>(env: &Env, error: ConversionError) -> S::Error {
98    let error = match env.throw_type_error(&error.to_string(), Some(error.code())) {
99        Ok(()) => Error::new(Status::PendingException, error.to_string()),
100        Err(error) => error,
101    };
102    js_fault::<S>(error)
103}
104
105fn js_walk<S: Sink>(value: Unknown<'_>, depth: usize, sink: S) -> Result<S::Ok, S::Error> {
106    if depth > MAX_DEPTH {
107        return Err(js_fault::<S>(depth_error()));
108    }
109    match value.get_type().map_err(js_fault::<S>)? {
110        ValueType::Null => sink.scalar(None, Physical::Null),
111        ValueType::Undefined => sink.scalar(Some(Logical::Undefined), Physical::Null),
112        ValueType::Boolean => {
113            let b = bool::from_unknown(value).map_err(js_fault::<S>)?;
114            sink.scalar(None, Physical::Bool(b))
115        }
116        ValueType::Number => {
117            let n = f64::from_unknown(value).map_err(js_fault::<S>)?;
118            sink.scalar(None, Physical::Float(Float::F64(n)))
119        }
120        ValueType::BigInt => {
121            let env = Env::from_raw(value.value().env);
122            let big = BigInt::from_unknown(value).map_err(js_fault::<S>)?;
123            let int = scalar::bigint(big.sign_bit, &big.words)
124                .map_err(|e| js_conversion::<S>(&env, e))?;
125            sink.scalar(None, Physical::Int(int))
126        }
127        ValueType::String => {
128            // The text is copied out of the engine to check it and goes
129            // straight to the sink; the V8 original cannot be wiped from here.
130            let text = lossless_string(value, "string").map_err(js_fault::<S>)?;
131            sink.scalar(None, Physical::Text(&text))
132        }
133        ValueType::Object => js_object(value, depth, sink),
134        other => Err(js_fault::<S>(Error::new(
135            Status::InvalidArg,
136            format!("JS type cannot be encrypted: {other}"),
137        ))),
138    }
139}
140
141/// Dispatch an object. The scalar shapes are read in helpers whose frames
142/// are gone before the containers recurse, so the stack a nested level
143/// costs is the recursion's own and a document at the depth limit walks
144/// in a debug build too.
145fn js_object<S: Sink>(value: Unknown<'_>, depth: usize, sink: S) -> Result<S::Ok, S::Error> {
146    // Typed arrays before anything else: `napi_is_buffer` is true for every
147    // `ArrayBufferView`, so a `Buffer` check first would take a
148    // `Float64Array` as raw bytes.
149    if value.is_typedarray().map_err(js_fault::<S>)? {
150        return js_bytes(value, sink);
151    }
152    if value.is_dataview().map_err(js_fault::<S>)? {
153        return Err(js_fault::<S>(not_bytes_error("DataView")));
154    }
155    if value.is_array().map_err(js_fault::<S>)? {
156        return js_array(value, depth, sink);
157    }
158    if value.is_date().map_err(js_fault::<S>)? {
159        return js_date(value, sink);
160    }
161    match js_classify(value).map_err(js_fault::<S>)? {
162        Some(wrapper) => js_wrapped(value, wrapper, sink),
163        None => js_map(value, depth, sink),
164    }
165}
166
167/// A `Buffer` or `Uint8Array` as bytes. Only byte views are meaningful
168/// plaintext: a `Float64Array`, an `Int16Array`, a `BigInt64Array` or a
169/// `Uint8ClampedArray` would seal as its raw bytes and silently decrypt
170/// as a `Buffer`, so the cast, which checks the element type, refuses
171/// them. A `Buffer` is a `Uint8Array` and passes.
172///
173/// The view is borrowed as a slice, which holds no reference. napi-rs's
174/// `Uint8Array` takes a reference before it checks the element type and
175/// never releases it when the check fails, so every refused array would
176/// stay alive until the environment exits.
177fn js_bytes<S: Sink>(value: Unknown<'_>, sink: S) -> Result<S::Ok, S::Error> {
178    let arr = Uint8ArraySlice::from_unknown(value)
179        .map_err(|_| js_fault::<S>(not_bytes_error("typed array")))?;
180    sink.scalar(None, Physical::Bytes(&arr))
181}
182
183fn not_bytes_error(what: &str) -> Error {
184    Error::new(
185        Status::InvalidArg,
186        format!("only a Buffer or a Uint8Array can be encrypted as bytes, not a {what}"),
187    )
188}
189
190fn js_date<S: Sink>(value: Unknown<'_>, sink: S) -> Result<S::Ok, S::Error> {
191    let env = Env::from_raw(value.value().env);
192    let date = napi::JsDate::from_unknown(value).map_err(js_fault::<S>)?;
193    let ms = date.value_of().map_err(js_fault::<S>)?;
194    let count = scalar::date_millis(ms).map_err(|e| js_conversion::<S>(&env, e))?;
195    sink.scalar(
196        Some(Logical::Timestamp(TimeUnit::Millis)),
197        Physical::Int(count),
198    )
199}
200
201/// Which wrapper class an object is an instance of, or none for a plain
202/// object. Anything else is refused.
203fn js_classify(value: Unknown<'_>) -> napi::Result<Option<Wrapper>> {
204    let env = Env::from_raw(value.value().env);
205    let wrappers = Wrappers::get(&env)?;
206    // Most objects are plain, and a plain object has no wrapper prototype
207    // in its chain, so it is asked first and the class checks are skipped.
208    if wrappers.is_plain(value)? {
209        return Ok(None);
210    }
211    match wrappers.classify(&value)? {
212        Some(wrapper) => Ok(Some(wrapper)),
213        None => Err(not_plain_error()),
214    }
215}
216
217fn js_map<S: Sink>(value: Unknown<'_>, depth: usize, sink: S) -> Result<S::Ok, S::Error> {
218    let obj = Object::from_unknown(value).map_err(js_fault::<S>)?;
219    let keys = own_enumerable_keys(&obj).map_err(js_fault::<S>)?;
220    let mut map = sink.map(Some(keys.len()))?;
221    for key in keys {
222        if forbidden_key(&key.name) {
223            return Err(js_fault::<S>(forbidden_key_error(&key.name)));
224        }
225        // Raw fetch so an explicit `undefined` value survives as
226        // `Undefined` instead of reading as a missing property.
227        let value = get_own(&obj, &key).map_err(js_fault::<S>)?;
228        js_walk(value, depth + 1, map.entry(&key.name)?)?;
229    }
230    map.end()
231}
232
233fn js_array<S: Sink>(value: Unknown<'_>, depth: usize, sink: S) -> Result<S::Ok, S::Error> {
234    let arr = Array::from_unknown(value).map_err(js_fault::<S>)?;
235    // The same JS value seen as an object, for per-index own-property
236    // checks: `Array::get` wraps every in-bounds hole in `Some(undefined)`,
237    // so on its own it can neither reject sparseness nor resist a polluted
238    // `Array.prototype` filling holes with inherited elements.
239    let obj = Object::from_unknown(value).map_err(js_fault::<S>)?;
240    let len = arr.len();
241    let mut seq = sink.seq(Some(len as usize))?;
242    for i in 0..len {
243        // A hole has no own property; an explicit `undefined` element does.
244        // `[undefined]` round-trips; `[, , "x"]` and `new Array(n)` are
245        // rejected.
246        let sparse = || Error::new(Status::InvalidArg, "sparse arrays are not supported");
247        if !obj
248            .has_own_property(&i.to_string())
249            .map_err(js_fault::<S>)?
250        {
251            return Err(js_fault::<S>(sparse()));
252        }
253        let element: Unknown = arr
254            .get(i)
255            .map_err(js_fault::<S>)?
256            .ok_or_else(|| js_fault::<S>(sparse()))?;
257        js_walk(element, depth + 1, seq.element()?)?;
258    }
259    seq.end()
260}
261
262/// A wrapper instance as its scalar. The text is read as an own string
263/// property and checked here, so an instance made without the constructor
264/// is held to the same rule.
265fn js_wrapped<S: Sink>(value: Unknown<'_>, wrapper: Wrapper, sink: S) -> Result<S::Ok, S::Error> {
266    let env = &Env::from_raw(value.value().env);
267    let wrappers = Wrappers::get(env).map_err(js_fault::<S>)?;
268    let instance = &Object::from_unknown(value).map_err(js_fault::<S>)?;
269    let invalid = match wrapper {
270        Wrapper::Date => ConversionError::InvalidDate,
271        Wrapper::Timestamp => ConversionError::InvalidTimestamp,
272        Wrapper::Decimal => ConversionError::InvalidDecimal,
273        Wrapper::Uuid => ConversionError::InvalidUuid,
274    };
275    let text = match wrappers.text(instance).map_err(js_fault::<S>)? {
276        Some(text) => lossless_string(text, wrapper.class_name()).map_err(js_fault::<S>)?,
277        None => return Err(js_conversion::<S>(env, invalid)),
278    };
279    let convert = |e| js_conversion::<S>(env, e);
280    match wrapper {
281        Wrapper::Date => {
282            let days = scalar::date(&text).map_err(convert)?;
283            sink.scalar(Some(Logical::Date), Physical::Int(days))
284        }
285        Wrapper::Timestamp => {
286            let nanos = scalar::timestamp(&text).map_err(convert)?;
287            sink.scalar(
288                Some(Logical::Timestamp(TimeUnit::Nanos)),
289                Physical::Int(nanos),
290            )
291        }
292        Wrapper::Decimal => {
293            let (scale, mantissa) = scalar::decimal(&text).map_err(convert)?;
294            sink.scalar(Some(Logical::Decimal { scale }), Physical::Int(mantissa))
295        }
296        Wrapper::Uuid => {
297            let bytes = scalar::uuid(&text).map_err(convert)?;
298            sink.scalar(Some(Logical::Uuid), Physical::Bytes(&bytes))
299        }
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306
307    #[test]
308    fn a_frame_writers_refusal_becomes_the_napi_error_the_host_expects() {
309        let too_deep = frame_error(FrameWriteError::TooDeep);
310        assert_eq!(too_deep.status, Status::InvalidArg);
311        assert!(too_deep.reason.contains("nested too deeply"), "{too_deep}");
312
313        let too_long = frame_error(FrameWriteError::TooLong);
314        assert_eq!(too_long.status, Status::InvalidArg);
315        assert_eq!(too_long.reason, FrameWriteError::TooLong.to_string());
316
317        // A getter that threw comes back as the error it threw.
318        let thrown = Error::new(Status::PendingException, "the getter threw");
319        let source = FrameWriteError::Source(SourceError::new(JsFault::from(thrown)));
320        let back = frame_error(source);
321        assert_eq!(back.status, Status::PendingException);
322        assert_eq!(back.reason, "the getter threw");
323    }
324}