Skip to main content

vitaminc_aead_napi/
wrappers.rs

1//! The wrapper classes: `VcDate`, `VcTimestamp`, `VcDecimal` and `VcUuid`.
2//!
3//! JavaScript has no type of its own for a calendar date, a timestamp finer
4//! than a millisecond, a decimal or a UUID, so the binding represents each
5//! as an instance of a small class holding its text. A class instance is
6//! not a plain object, so it can never be confused with data another
7//! language wrote as an ordinary one-key object (#382).
8//!
9//! The classes are defined by the JavaScript in `wrappers.js`, evaluated
10//! once per environment. The object it returns (the classes and the
11//! plain-object check, frozen) is kept in a registry keyed by the
12//! environment and released by an environment cleanup hook, so the
13//! addon's own instance data (`napi_set_instance_data`) is never touched.
14//! An addon exports the classes with [`Wrappers::exports`] so applications
15//! can construct them; the walk recognises instances with `instanceof`,
16//! and the build constructs them.
17
18use std::collections::HashMap;
19use std::sync::{LazyLock, Mutex};
20
21use napi::bindgen_prelude::{Function, JsObjectValue, JsValue, Object, ObjectRef, Unknown};
22use napi::{Env, Error, Result, Status};
23
24/// Which wrapper class a value is an instance of.
25///
26/// A logical type JavaScript has no value for needs a new class, so more
27/// variants may come.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29#[non_exhaustive]
30pub enum Wrapper {
31    /// `VcDate`: a calendar date as `YYYY-MM-DD`.
32    Date,
33    /// `VcTimestamp`: an instant as RFC 3339 text, to the nanosecond.
34    Timestamp,
35    /// `VcDecimal`: a fixed-point decimal as text.
36    Decimal,
37    /// `VcUuid`: a UUID as hyphenated hexadecimal.
38    Uuid,
39}
40
41impl Wrapper {
42    /// Every wrapper class, in the order the walk asks them.
43    pub const ALL: &'static [Wrapper] = &[
44        Wrapper::Date,
45        Wrapper::Timestamp,
46        Wrapper::Decimal,
47        Wrapper::Uuid,
48    ];
49
50    /// The class name, as JavaScript sees it.
51    pub fn class_name(self) -> &'static str {
52        match self {
53            Wrapper::Date => "VcDate",
54            Wrapper::Timestamp => "VcTimestamp",
55            Wrapper::Decimal => "VcDecimal",
56            Wrapper::Uuid => "VcUuid",
57        }
58    }
59}
60
61const SOURCE: &str = include_str!("wrappers.js");
62
63/// The own property a wrapper instance holds its text in.
64const VALUE: &str = "value";
65
66/// The property of the script's result that answers the plain-object check.
67const IS_PLAIN: &str = "isPlain";
68
69/// One environment's evaluated `wrappers.js`, by the environment's raw
70/// handle. An entry is added on first use and removed by the cleanup hook
71/// registered then, so a handle a later environment reuses starts fresh.
72/// The reference is dropped without the leak warning: it is released by
73/// the hook while the environment is still live.
74static REALMS: LazyLock<Mutex<HashMap<usize, ObjectRef<false>>>> =
75    LazyLock::new(|| Mutex::new(HashMap::new()));
76
77fn registry_poisoned() -> Error {
78    Error::new(Status::GenericFailure, "wrapper class registry is poisoned")
79}
80
81/// The wrapper classes of one environment, for one call: a borrowed view
82/// that cannot outlive the environment it was taken from.
83pub struct Wrappers<'e> {
84    classes: Object<'e>,
85}
86
87impl<'e> Wrappers<'e> {
88    /// The wrapper classes of `env`, defined on first use.
89    pub fn get(env: &'e Env) -> Result<Self> {
90        let key = env.raw() as usize;
91        let existing = REALMS
92            .lock()
93            .map_err(|_| registry_poisoned())?
94            .get(&key)
95            .map(|classes| classes.get_value(env))
96            .transpose()?;
97        let classes = match existing {
98            Some(classes) => classes,
99            // The script runs outside the lock: it is self-contained, but
100            // nothing that runs JavaScript should hold a lock the
101            // JavaScript thread may take again.
102            None => {
103                let classes: Object<'e> = env.run_script(SOURCE)?;
104                let reference = classes.create_ref::<false>()?;
105                REALMS
106                    .lock()
107                    .map_err(|_| registry_poisoned())?
108                    .insert(key, reference);
109                env.add_env_cleanup_hook(key, release_wrappers)?;
110                classes
111            }
112        };
113        Ok(Self { classes })
114    }
115
116    /// An object holding every class under its name, for an addon to
117    /// re-export: `VcDate`, `VcTimestamp`, `VcDecimal` and `VcUuid`.
118    pub fn exports(env: &'e Env) -> Result<Object<'e>> {
119        let wrappers = Self::get(env)?;
120        let mut exports = Object::new(env)?;
121        for &wrapper in Wrapper::ALL {
122            exports.set_named_property(wrapper.class_name(), wrappers.constructor(wrapper)?)?;
123        }
124        Ok(exports)
125    }
126
127    fn constructor(&self, wrapper: Wrapper) -> Result<Function<'e, String, Unknown<'e>>> {
128        self.classes.get_named_property(wrapper.class_name())
129    }
130
131    /// Which wrapper class `value` is an instance of, if any.
132    pub fn classify(&self, value: &Unknown<'e>) -> Result<Option<Wrapper>> {
133        for &wrapper in Wrapper::ALL {
134            if value.instanceof(self.constructor(wrapper)?)? {
135                return Ok(Some(wrapper));
136            }
137        }
138        Ok(None)
139    }
140
141    /// Whether `value` is a plain object: prototype `Object.prototype` or
142    /// `null` (`Object.create(null)`).
143    ///
144    /// Anything else stores its state in internal slots or carries class
145    /// behaviour that key/value enumeration silently loses: a `Map`'s or
146    /// `Set`'s contents enumerate as `{}`, a `RegExp` drops its pattern and
147    /// flags, an `ArrayBuffer` or `DataView` its bytes. Encrypting such a
148    /// value would destroy the payload with no error at either end, so the
149    /// walk refuses it instead, unless it is an instance of a wrapper class.
150    /// The question is put to the `Object.getPrototypeOf` captured when the
151    /// script was evaluated, which runs a `Proxy`'s trap; `napi_get_prototype`
152    /// reports `null` for every proxy, which would pass a proxied `Map` as
153    /// `Object.create(null)`.
154    ///
155    /// A trap that throws leaves its exception pending, so the caller hands
156    /// back the very value thrown, as it would for a getter that throws.
157    pub fn is_plain(&self, value: Unknown<'e>) -> Result<bool> {
158        let is_plain: Function<'e, Unknown<'e>, bool> =
159            self.classes.get_named_property(IS_PLAIN)?;
160        // napi-rs clears the exception a called function throws and keeps
161        // it inside the error, which a `JsFault` cannot carry, so it is
162        // thrown again here.
163        is_plain.call(value).map_err(|error| {
164            let reason = error.reason.clone();
165            match Env::from_raw(self.classes.value().env).throw(error) {
166                Ok(()) => Error::new(Status::PendingException, reason),
167                Err(error) => error,
168            }
169        })
170    }
171
172    /// The text a wrapper instance holds. The class keeps `value` as an
173    /// own, read-only string, but an instance can be made without the
174    /// constructor (`Object.create`), so the property is read as an own
175    /// property and its type checked.
176    pub fn text(&self, instance: &Object<'e>) -> Result<Option<Unknown<'e>>> {
177        if !instance.has_own_property(VALUE)? {
178            return Ok(None);
179        }
180        let text = crate::js::get_property_unknown(instance, VALUE)?;
181        Ok((text.get_type()? == napi::ValueType::String).then_some(text))
182    }
183
184    /// A new instance of `wrapper` holding `text`.
185    pub fn construct(&self, wrapper: Wrapper, text: &str) -> Result<Unknown<'e>> {
186        self.constructor(wrapper)?.new_instance(text.to_owned())
187    }
188}
189
190/// The environment cleanup hook: delete the reference while the
191/// environment can still do so, and forget the entry.
192///
193/// Only a live environment can run this, so no unit test reaches it; the
194/// Worker teardown check in `tests/object-safety.cjs` does.
195#[cfg_attr(test, mutants::skip)]
196fn release_wrappers(key: usize) {
197    let Ok(mut realms) = REALMS.lock() else {
198        return;
199    };
200    if let Some(reference) = realms.remove(&key) {
201        let env = Env::from_raw(key as napi::sys::napi_env);
202        // A failure here leaves a reference the environment is about to
203        // reclaim anyway; there is nobody to report it to.
204        let _ = reference.unref(&env);
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn every_wrapper_has_the_class_name_javascript_sees() {
214        let names: Vec<&str> = Wrapper::ALL.iter().map(|w| w.class_name()).collect();
215        assert_eq!(names, ["VcDate", "VcTimestamp", "VcDecimal", "VcUuid"]);
216        for &wrapper in Wrapper::ALL {
217            assert_eq!(wrapper.class_name(), format!("Vc{wrapper:?}"));
218        }
219    }
220}