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}