use crate::{qjs, Ctx, FromJs, IntoJs, Object, StdResult, StdString, Type, Value};
use std::{
error::Error as StdError,
ffi::{CString, NulError},
fmt::{Display, Formatter, Result as FmtResult},
io::Error as IoError,
ops::Range,
panic,
panic::UnwindSafe,
str::{FromStr, Utf8Error},
string::FromUtf8Error,
};
pub type Result<T> = StdResult<T, Error>;
#[derive(Debug)]
pub enum Error {
Allocation,
InvalidString(NulError),
Utf8(Utf8Error),
Io(IoError),
Exception {
message: StdString,
file: StdString,
line: i32,
stack: StdString,
},
FromJs {
from: &'static str,
to: &'static str,
message: Option<StdString>,
},
IntoJs {
from: &'static str,
to: &'static str,
message: Option<StdString>,
},
NumArgs {
expected: Range<usize>,
given: usize,
},
#[cfg(feature = "loader")]
Resolving {
base: StdString,
name: StdString,
message: Option<StdString>,
},
#[cfg(feature = "loader")]
Loading {
name: StdString,
message: Option<StdString>,
},
Unknown,
}
impl Error {
#[cfg(feature = "loader")]
pub fn new_resolving<B, N>(base: B, name: N) -> Self
where
StdString: From<B> + From<N>,
{
Error::Resolving {
base: base.into(),
name: name.into(),
message: None,
}
}
#[cfg(feature = "loader")]
pub fn new_resolving_message<B, N, M>(base: B, name: N, msg: M) -> Self
where
StdString: From<B> + From<N> + From<M>,
{
Error::Resolving {
base: base.into(),
name: name.into(),
message: Some(msg.into()),
}
}
#[cfg(feature = "loader")]
pub fn is_resolving(&self) -> bool {
matches!(self, Error::Resolving { .. })
}
#[cfg(feature = "loader")]
pub fn new_loading<N>(name: N) -> Self
where
StdString: From<N>,
{
Error::Loading {
name: name.into(),
message: None,
}
}
#[cfg(feature = "loader")]
pub fn new_loading_message<N, M>(name: N, msg: M) -> Self
where
StdString: From<N> + From<M>,
{
Error::Loading {
name: name.into(),
message: Some(msg.into()),
}
}
#[cfg(feature = "loader")]
pub fn is_loading(&self) -> bool {
matches!(self, Error::Loading { .. })
}
pub fn is_exception(&self) -> bool {
matches!(self, Error::Exception{..})
}
pub fn new_from_js(from: &'static str, to: &'static str) -> Self {
Error::FromJs {
from,
to,
message: None,
}
}
pub fn new_from_js_message<M>(from: &'static str, to: &'static str, msg: M) -> Self
where
StdString: From<M>,
{
Error::FromJs {
from,
to,
message: Some(msg.into()),
}
}
pub fn new_into_js(from: &'static str, to: &'static str) -> Self {
Error::IntoJs {
from,
to,
message: None,
}
}
pub fn new_into_js_message<M>(from: &'static str, to: &'static str, msg: M) -> Self
where
StdString: From<M>,
{
Error::IntoJs {
from,
to,
message: Some(msg.into()),
}
}
pub fn is_from_js(&self) -> bool {
matches!(self, Self::FromJs { .. })
}
pub fn is_from_js_to_js(&self) -> bool {
matches!(self, Self::FromJs { to, .. } if Type::from_str(*to).is_ok())
}
pub fn is_into_js(&self) -> bool {
matches!(self, Self::IntoJs { .. })
}
pub fn new_num_args(expected: Range<usize>, given: usize) -> Self {
Self::NumArgs { expected, given }
}
pub fn is_num_args(&self) -> bool {
matches!(self, Self::NumArgs { .. })
}
pub(crate) fn to_cstring(&self) -> CString {
let mut message = format!("{}\0", self).into_bytes();
message.pop();
unsafe { CString::from_vec_unchecked(message) }
}
pub(crate) fn throw(&self, ctx: Ctx) -> qjs::JSValue {
use Error::*;
match self {
Allocation => unsafe { qjs::JS_ThrowOutOfMemory(ctx.ctx) },
InvalidString(_) | Utf8(_) | FromJs { .. } | IntoJs { .. } | NumArgs { .. } => {
let message = self.to_cstring();
unsafe { qjs::JS_ThrowTypeError(ctx.ctx, message.as_ptr()) }
}
#[cfg(feature = "loader")]
Resolving { .. } | Loading { .. } => {
let message = self.to_cstring();
unsafe { qjs::JS_ThrowReferenceError(ctx.ctx, message.as_ptr()) }
}
Unknown => {
let message = self.to_cstring();
unsafe { qjs::JS_ThrowInternalError(ctx.ctx, message.as_ptr()) }
}
_ => {
let value = self.into_js(ctx).unwrap();
unsafe { qjs::JS_Throw(ctx.ctx, value.into_js_value()) }
}
}
}
}
impl StdError for Error {}
impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
use Error::*;
match self {
Allocation => "Allocation failed while creating object".fmt(f)?,
InvalidString(error) => {
"String contained internal null bytes: ".fmt(f)?;
error.fmt(f)?;
}
Utf8(error) => {
"Conversion from string failed: ".fmt(f)?;
error.fmt(f)?;
}
Unknown => "quickjs library created a unknown error".fmt(f)?,
Exception {
file,
line,
message,
stack,
} => {
"Exception generated by quickjs: ".fmt(f)?;
if !file.is_empty() {
'['.fmt(f)?;
file.fmt(f)?;
']'.fmt(f)?;
}
if *line >= 0 {
':'.fmt(f)?;
line.fmt(f)?;
}
if !message.is_empty() {
' '.fmt(f)?;
message.fmt(f)?;
}
if !stack.is_empty() {
'\n'.fmt(f)?;
stack.fmt(f)?;
}
}
FromJs { from, to, message } => {
"Error converting from js '".fmt(f)?;
from.fmt(f)?;
"' into type '".fmt(f)?;
to.fmt(f)?;
"'".fmt(f)?;
if let Some(message) = message {
if !message.is_empty() {
": ".fmt(f)?;
message.fmt(f)?;
}
}
}
IntoJs { from, to, message } => {
"Error converting from '".fmt(f)?;
from.fmt(f)?;
"' into js '".fmt(f)?;
to.fmt(f)?;
"'".fmt(f)?;
if let Some(message) = message {
if !message.is_empty() {
": ".fmt(f)?;
message.fmt(f)?;
}
}
}
NumArgs { expected, given } => {
"Error calling function with ".fmt(f)?;
given.fmt(f)?;
" argument(s) while ".fmt(f)?;
expected.start.fmt(f)?;
"..".fmt(f)?;
if expected.end < usize::MAX {
expected.end.fmt(f)?;
}
" expected".fmt(f)?;
}
#[cfg(feature = "loader")]
Resolving {
base,
name,
message,
} => {
"Error resolving module '".fmt(f)?;
name.fmt(f)?;
"' from '".fmt(f)?;
base.fmt(f)?;
"'".fmt(f)?;
if let Some(message) = message {
if !message.is_empty() {
": ".fmt(f)?;
message.fmt(f)?;
}
}
}
#[cfg(feature = "loader")]
Loading { name, message } => {
"Error loading module '".fmt(f)?;
name.fmt(f)?;
"'".fmt(f)?;
if let Some(message) = message {
if !message.is_empty() {
": ".fmt(f)?;
message.fmt(f)?;
}
}
}
Io(error) => {
"IO Error: ".fmt(f)?;
error.fmt(f)?;
}
}
Ok(())
}
}
macro_rules! from_impls {
($($type:ty => $variant:ident,)*) => {
$(
impl From<$type> for Error {
fn from(error: $type) -> Self {
Error::$variant(error)
}
}
)*
};
}
from_impls! {
NulError => InvalidString,
Utf8Error => Utf8,
IoError => Io,
}
impl From<FromUtf8Error> for Error {
fn from(error: FromUtf8Error) -> Self {
Error::Utf8(error.utf8_error())
}
}
impl<'js> FromJs<'js> for Error {
fn from_js(ctx: Ctx<'js>, value: Value<'js>) -> Result<Self> {
let obj = Object::from_js(ctx, value)?;
if obj.is_error() {
Ok(Error::Exception {
message: obj.get("message").unwrap_or_else(|_| "".into()),
file: obj.get("fileName").unwrap_or_else(|_| "".into()),
line: obj.get("lineNumber").unwrap_or(-1),
stack: obj.get("stack").unwrap_or_else(|_| "".into()),
})
} else {
Err(Error::new_from_js("object", "error"))
}
}
}
impl<'js> IntoJs<'js> for &Error {
fn into_js(self, ctx: Ctx<'js>) -> Result<Value<'js>> {
use Error::*;
let value = unsafe {
Object::from_js_value(ctx, handle_exception(ctx, qjs::JS_NewError(ctx.ctx))?)
};
match self {
Exception {
message,
file,
line,
stack,
} => {
if !message.is_empty() {
value.set("message", message)?;
}
if !file.is_empty() {
value.set("fileName", file)?;
}
if *line >= 0 {
value.set("lineNumber", *line)?;
}
if !stack.is_empty() {
value.set("stack", stack)?;
}
}
error => {
value.set("message", error.to_string())?;
}
}
Ok(value.0)
}
}
pub(crate) fn handle_panic<F: FnOnce() -> qjs::JSValue + UnwindSafe>(
ctx: *mut qjs::JSContext,
f: F,
) -> qjs::JSValue {
unsafe {
match panic::catch_unwind(f) {
Ok(x) => x,
Err(e) => {
Ctx::from_ptr(ctx).get_opaque().panic = Some(e);
qjs::JS_Throw(ctx, qjs::JS_MKVAL(qjs::JS_TAG_EXCEPTION, 0))
}
}
}
}
pub(crate) unsafe fn handle_exception<'js>(
ctx: Ctx<'js>,
js_val: qjs::JSValue,
) -> Result<qjs::JSValue> {
if qjs::JS_VALUE_GET_NORM_TAG(js_val) != qjs::JS_TAG_EXCEPTION {
Ok(js_val)
} else {
Err(get_exception(ctx))
}
}
pub(crate) unsafe fn get_exception<'js>(ctx: Ctx<'js>) -> Error {
let exception_val = qjs::JS_GetException(ctx.ctx);
if let Some(x) = ctx.get_opaque().panic.take() {
panic::resume_unwind(x);
}
let exception = Value::from_js_value(ctx, exception_val);
Error::from_js(ctx, exception).unwrap()
}