Skip to main content

ferrijs_std/exceptions/
mod.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3use core::fmt;
4use std::fmt::Debug;
5
6use crate::utils::{
7    object::define_subclass,
8    option::Undefined,
9    primordials::{BasePrimordials, Primordial},
10};
11use rquickjs::{
12    atom::PredefinedAtom,
13    class::{
14        impl_::{CloneTrait, CloneWrapper},
15        JsClass, Trace,
16    },
17    function::{Constructor, Opt},
18    object::{Accessor, Property},
19    prelude::{Func, This},
20    qjs, Class, Coerced, Ctx, Error, Exception, FromJs, IntoJs, JsLifetime, Object, Result, Value,
21};
22
23#[derive(JsLifetime)]
24struct ExceptionPrimordials<'js> {
25    constructor_dom_exception: Constructor<'js>,
26    constructor_quota_exceeded_error: Constructor<'js>,
27}
28
29impl<'js> Primordial<'js> for ExceptionPrimordials<'js> {
30    fn new(ctx: &Ctx<'js>) -> Result<Self> {
31        let globals = ctx.globals();
32        Ok(Self {
33            constructor_dom_exception: globals.get(DOMException::NAME)?,
34            constructor_quota_exceeded_error: globals.get("QuotaExceededError")?,
35        })
36    }
37}
38
39#[derive(Trace, JsLifetime, Debug)]
40pub struct DOMException {
41    name: String,
42    message: String,
43    stack: String,
44    code: u8,
45}
46
47impl fmt::Display for DOMException {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        f.debug_struct("DOMException")
50            .field("name", &self.name())
51            .field("message", &self.message())
52            .field("stack", &self.stack)
53            .finish()
54    }
55}
56
57fn add_constants(obj: &Object<'_>) -> Result<()> {
58    const CONSTANTS: [(&str, u8); 25] = [
59        ("INDEX_SIZE_ERR", 1),
60        ("DOMSTRING_SIZE_ERR", 2),
61        ("HIERARCHY_REQUEST_ERR", 3),
62        ("WRONG_DOCUMENT_ERR", 4),
63        ("INVALID_CHARACTER_ERR", 5),
64        ("NO_DATA_ALLOWED_ERR", 6),
65        ("NO_MODIFICATION_ALLOWED_ERR", 7),
66        ("NOT_FOUND_ERR", 8),
67        ("NOT_SUPPORTED_ERR", 9),
68        ("INUSE_ATTRIBUTE_ERR", 10),
69        ("INVALID_STATE_ERR", 11),
70        ("SYNTAX_ERR", 12),
71        ("INVALID_MODIFICATION_ERR", 13),
72        ("NAMESPACE_ERR", 14),
73        ("INVALID_ACCESS_ERR", 15),
74        ("VALIDATION_ERR", 16),
75        ("TYPE_MISMATCH_ERR", 17),
76        ("SECURITY_ERR", 18),
77        ("NETWORK_ERR", 19),
78        ("ABORT_ERR", 20),
79        ("URL_MISMATCH_ERR", 21),
80        ("QUOTA_EXCEEDED_ERR", 22),
81        ("TIMEOUT_ERR", 23),
82        ("INVALID_NODE_TYPE_ERR", 24),
83        ("DATA_CLONE_ERR", 25),
84    ];
85
86    for (key, value) in CONSTANTS {
87        obj.prop(key, Property::from(value).enumerable())?;
88    }
89
90    Ok(())
91}
92
93impl<'js> JsClass<'js> for DOMException {
94    const NAME: &'static str = "DOMException";
95    type Mutable = rquickjs::class::Writable;
96    fn prototype(ctx: &Ctx<'js>) -> rquickjs::Result<Option<Object<'js>>> {
97        use rquickjs::class::impl_::{MethodImpl, MethodImplementor};
98        let proto = Object::new(ctx.clone())?;
99        let implementor = MethodImpl::<Self>::new();
100        implementor.implement(&proto)?;
101        add_constants(&proto)?;
102
103        Ok(Some(proto))
104    }
105    fn constructor(ctx: &Ctx<'js>) -> Result<Option<Constructor<'js>>> {
106        use rquickjs::class::impl_::{ConstructorCreate, ConstructorCreator};
107        let implementor = ConstructorCreate::<Self>::new();
108        let constructor = implementor
109            .create_constructor(ctx)?
110            .expect("DOMException must have a constructor");
111        add_constants(&constructor)?;
112
113        Ok(Some(constructor))
114    }
115}
116
117impl<'js> IntoJs<'js> for DOMException {
118    fn into_js(self, ctx: &rquickjs::Ctx<'js>) -> Result<Value<'js>> {
119        let cls = Class::<Self>::instance(ctx.clone(), self)?;
120        IntoJs::into_js(cls, ctx)
121    }
122}
123
124impl<'js> FromJs<'js> for DOMException
125where
126    for<'a> CloneWrapper<'a, Self>: CloneTrait<Self>,
127{
128    fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result<Self> {
129        let value = Class::<Self>::from_js(ctx, value)?;
130        let borrow = value.try_borrow()?;
131        Ok(CloneWrapper(&*borrow).wrap_clone())
132    }
133}
134
135#[rquickjs::methods]
136impl DOMException {
137    #[qjs(constructor)]
138    pub fn new<'js>(
139        ctx: Ctx<'js>,
140        this: This<Value<'js>>,
141        message: Opt<Undefined<Coerced<String>>>,
142        name: Opt<Undefined<Coerced<String>>>,
143    ) -> Result<Self> {
144        // When called with `new`, rquickjs passes the constructor function
145        // as `this`. Without `new` this is undefined or the global object.
146        if this.0.as_function().is_none() {
147            return Err(Exception::throw_type(
148                &ctx,
149                "Cannot call the DOMException constructor without 'new'",
150            ));
151        }
152
153        let message = match message.0 {
154            Some(Undefined(Some(message))) => message.0,
155            _ => String::new(),
156        };
157
158        let name = match name.0 {
159            Some(Undefined(Some(message))) => DOMExceptionName::from(message.0),
160            _ => DOMExceptionName::Error,
161        };
162
163        Self::new_with_name(&ctx, name, message)
164    }
165
166    #[qjs(skip)]
167    pub fn new_with_name(ctx: &Ctx<'_>, name: DOMExceptionName, message: String) -> Result<Self> {
168        let primordials = BasePrimordials::get(ctx)?;
169
170        let new: Object = primordials
171            .constructor_error
172            .construct((message.clone(),))?;
173
174        Ok(Self {
175            name: name.as_str().to_string(),
176            code: name.code(),
177            message,
178            stack: new.get::<_, String>(PredefinedAtom::Stack)?,
179        })
180    }
181
182    #[qjs(get, enumerable, configurable)]
183    fn message(&self) -> &str {
184        self.message.as_str()
185    }
186
187    #[qjs(get, enumerable, configurable)]
188    pub fn name(&self) -> &str {
189        self.name.as_str()
190    }
191
192    #[qjs(get, enumerable, configurable)]
193    pub fn code(&self) -> u8 {
194        self.code
195    }
196
197    #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)]
198    pub fn to_string_tag() -> &'static str {
199        stringify!(DOMException)
200    }
201}
202
203impl<'js> DOMException {
204    fn create(
205        ctx: &Ctx<'js>,
206        name: DOMExceptionName,
207        message: impl Into<String>,
208    ) -> Result<Value<'js>> {
209        let primordials = ExceptionPrimordials::get(ctx)?;
210        let ctor = match name {
211            DOMExceptionName::QuotaExceededError => &primordials.constructor_quota_exceeded_error,
212            _ => &primordials.constructor_dom_exception,
213        };
214        ctor.construct((message.into(), name.as_str()))
215    }
216
217    fn throw_value(ctx: &Ctx<'js>, value: Value<'js>) -> Error {
218        unsafe {
219            let dup = qjs::JS_DupValue(ctx.as_raw().as_ptr(), value.as_raw());
220            qjs::JS_Throw(ctx.as_raw().as_ptr(), dup);
221        }
222        Error::Exception
223    }
224
225    fn create_error(ctx: &Ctx<'js>, name: DOMExceptionName, message: impl Into<String>) -> Error {
226        let value = Self::create(ctx, name, message).expect("failed to create DOMException");
227        Self::throw_value(ctx, value)
228    }
229
230    pub fn not_supported_error(ctx: &Ctx<'js>, message: impl Into<String>) -> Error {
231        Self::create_error(ctx, DOMExceptionName::NotSupportedError, message)
232    }
233
234    pub fn type_mismatch_error(ctx: &Ctx<'js>, message: impl Into<String>) -> Error {
235        Self::create_error(ctx, DOMExceptionName::TypeMismatchError, message)
236    }
237
238    pub fn operation_error(ctx: &Ctx<'js>, message: impl Into<String>) -> Error {
239        Self::create_error(ctx, DOMExceptionName::OperationError, message)
240    }
241
242    pub fn quota_exceeded_error(ctx: &Ctx<'js>, message: impl Into<String>) -> Error {
243        Self::create_error(ctx, DOMExceptionName::QuotaExceededError, message)
244    }
245
246    pub fn data_error(ctx: &Ctx<'js>, message: impl Into<String>) -> Error {
247        Self::create_error(ctx, DOMExceptionName::DataError, message)
248    }
249
250    pub fn invalid_access_error(ctx: &Ctx<'js>, message: impl Into<String>) -> Error {
251        Self::create_error(ctx, DOMExceptionName::InvalidAccessError, message)
252    }
253
254    pub fn syntax_error(ctx: &Ctx<'js>, message: impl Into<String>) -> Error {
255        Self::create_error(ctx, DOMExceptionName::SyntaxError, message)
256    }
257
258    fn define_quota_exceeded_error(ctx: &Ctx<'js>) -> Result<()> {
259        let dom_exception: Constructor = ctx.globals().get(Self::NAME)?;
260        let quota_exceeded_error = define_subclass(
261            ctx,
262            "QuotaExceededError",
263            &dom_exception,
264            |ctx, message: Opt<Undefined<Coerced<String>>>| {
265                let message = match message.0 {
266                    Some(Undefined(Some(m))) => m.0,
267                    _ => String::new(),
268                };
269                Self::new_with_name(&ctx, DOMExceptionName::QuotaExceededError, message)
270            },
271        )?;
272        let null = Value::new_null(ctx.clone());
273        let proto: Object = quota_exceeded_error.get(PredefinedAtom::Prototype)?;
274        proto.prop(
275            "requested",
276            Property::from(null.clone()).enumerable().configurable(),
277        )?;
278        proto.prop("quota", Property::from(null).enumerable().configurable())?;
279        ctx.globals().prop(
280            "QuotaExceededError",
281            Property::from(quota_exceeded_error)
282                .writable()
283                .configurable(),
284        )
285    }
286}
287
288macro_rules! create_dom_exception {
289    ($name:ident, $($variant:ident),+ $(,)?) => {
290        #[derive(Debug)]
291        pub enum $name {
292            $(
293                $variant,
294            )+
295            Other(String),
296        }
297
298        impl $name {
299            pub fn as_str(&self) -> &str {
300                match self {
301                    $(
302                        Self::$variant => stringify!($variant),
303                    )+
304                    Self::Other(value) => value,
305                }
306            }
307        }
308
309        impl From<String> for $name {
310            fn from(value: String) -> Self {
311                match value.as_str() {
312                    $(
313                        stringify!($variant) => Self::$variant,
314                    )+
315                    _ => Self::Other(value),
316                }
317            }
318        }
319    };
320}
321
322// https://webidl.spec.whatwg.org/#dfn-error-names-table
323create_dom_exception!(
324    DOMExceptionName,
325    IndexSizeError,
326    HierarchyRequestError,
327    WrongDocumentError,
328    InvalidCharacterError,
329    NoModificationAllowedError,
330    NotFoundError,
331    NotSupportedError,
332    InUseAttributeError,
333    InvalidStateError,
334    SyntaxError,
335    InvalidModificationError,
336    NamespaceError,
337    InvalidAccessError,
338    TypeMismatchError,
339    SecurityError,
340    NetworkError,
341    AbortError,
342    URLMismatchError,
343    QuotaExceededError,
344    TimeoutError,
345    InvalidNodeTypeError,
346    DataCloneError,
347    EncodingError,
348    NotReadableError,
349    UnknownError,
350    ConstraintError,
351    DataError,
352    TransactionInactiveError,
353    ReadOnlyError,
354    VersionError,
355    OperationError,
356    NotAllowedError,
357    Error,
358);
359
360impl DOMExceptionName {
361    fn code(&self) -> u8 {
362        match self {
363            DOMExceptionName::IndexSizeError => 1,
364            DOMExceptionName::HierarchyRequestError => 3,
365            DOMExceptionName::WrongDocumentError => 4,
366            DOMExceptionName::InvalidCharacterError => 5,
367            DOMExceptionName::NoModificationAllowedError => 7,
368            DOMExceptionName::NotFoundError => 8,
369            DOMExceptionName::NotSupportedError => 9,
370            DOMExceptionName::InUseAttributeError => 10,
371            DOMExceptionName::InvalidStateError => 11,
372            DOMExceptionName::SyntaxError => 12,
373            DOMExceptionName::InvalidModificationError => 13,
374            DOMExceptionName::NamespaceError => 14,
375            DOMExceptionName::InvalidAccessError => 15,
376            DOMExceptionName::TypeMismatchError => 17,
377            DOMExceptionName::SecurityError => 18,
378            DOMExceptionName::NetworkError => 19,
379            DOMExceptionName::AbortError => 20,
380            DOMExceptionName::URLMismatchError => 21,
381            DOMExceptionName::QuotaExceededError => 22,
382            DOMExceptionName::TimeoutError => 23,
383            DOMExceptionName::InvalidNodeTypeError => 24,
384            DOMExceptionName::DataCloneError => 25,
385            _ => 0,
386        }
387    }
388}
389
390pub fn init(ctx: &Ctx<'_>) -> Result<()> {
391    let globals = ctx.globals();
392
393    BasePrimordials::init(ctx)?;
394
395    if let Some(constructor) = Class::<DOMException>::create_constructor(ctx)? {
396        // the wpt tests expect this particular property descriptor
397        globals.prop(
398            DOMException::NAME,
399            Property::from(constructor).writable().configurable(),
400        )?;
401    }
402
403    let dom_ex_proto = Class::<DOMException>::prototype(ctx)?.unwrap();
404    dom_ex_proto.set_prototype(Some(&BasePrimordials::get(ctx)?.prototype_error))?;
405
406    DOMException::define_quota_exceeded_error(ctx)?;
407    ExceptionPrimordials::init(ctx)?;
408
409    // `Error.isError(v)` only returns `true` for objects with QuickJS's
410    // `[[ErrorData]]` internal slot (class id `JS_CLASS_ERROR`). There is
411    // no public rquickjs API to tag a class-derived instance with that
412    // slot, so we replace `Error.isError` with a version that also
413    // recognizes `DOMException` instances (and its subclasses) via
414    // `instanceof`.
415    BasePrimordials::get(ctx)?
416        .constructor_error
417        .set("isError", Func::from(is_error))?;
418
419    define_error_stack_accessor(ctx)?;
420
421    Ok(())
422}
423
424// https://tc39.es/proposal-error-stack-accessor/ moves `stack` to an accessor
425// on `Error.prototype`, and the engine behind rquickjs 0.13 implements it:
426// `Error.prototype.stack` is already a getter and instances carry no own
427// `stack` to shadow it.
428//
429// So the accessor goes on `DOMException.prototype`, not on `Error.prototype`.
430// Defining it upstream would replace the engine's getter rather than be
431// shadowed by it, and every ordinary error would report an empty stack --
432// which is exactly what happened when this crate first moved to 0.13.
433// Capturing the engine's getter to delegate to is not an option either: a
434// native closure holding a JS value is a cycle the collector cannot see,
435// and the runtime aborts at teardown with the function still alive.
436fn define_error_stack_accessor<'js>(ctx: &Ctx<'js>) -> Result<()> {
437    let dom_ex_proto = Class::<DOMException>::prototype(ctx)?
438        .expect("DOMException prototype is defined by the call above");
439    dom_ex_proto.prop(
440        PredefinedAtom::Stack,
441        Accessor::new(
442            |this: This<Value<'js>>| -> Result<String> {
443                let stack = Class::<DOMException>::from_value(&this.0)
444                    .ok()
445                    .map(|cls| cls.borrow().stack.clone());
446                Ok(stack.unwrap_or_default())
447            },
448            |ctx: Ctx<'js>, this: This<Value<'js>>, value: Value<'js>| -> Result<()> {
449                // SetterThatIgnoresPrototypeProperties: never install on the
450                // home object itself.
451                let Some(obj) = this.0.as_object() else {
452                    return Ok(());
453                };
454                if let Ok(Some(home)) = Class::<DOMException>::prototype(&ctx) {
455                    if *obj == home {
456                        return Ok(());
457                    }
458                }
459                obj.prop(
460                    PredefinedAtom::Stack,
461                    Property::from(value).writable().enumerable().configurable(),
462                )
463            },
464        )
465        .configurable(),
466    )
467}
468
469fn is_error<'js>(ctx: Ctx<'js>, value: Value<'js>) -> Result<bool> {
470    if value.is_error() {
471        return Ok(true);
472    }
473    let Some(obj) = value.as_object() else {
474        return Ok(false);
475    };
476    let dom_exception: Value = ctx.globals().get(DOMException::NAME)?;
477    Ok(obj.is_instance_of(&dom_exception))
478}