Skip to main content

ferridriver_script/
error.rs

1//! Script execution errors with source-level diagnostics.
2
3use std::fmt;
4
5/// Kind of failure a script can produce.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
7#[serde(rename_all = "snake_case")]
8pub enum ScriptErrorKind {
9  /// Source failed to parse.
10  Syntax,
11  /// Script threw an exception during execution.
12  Runtime,
13  /// Wall-clock timeout was exceeded.
14  Timeout,
15  /// `QuickJS` memory quota was exceeded.
16  MemoryLimit,
17  /// A sandboxed operation (e.g., `fs.readFile` with a traversal path) was rejected.
18  SandboxViolation,
19  /// Engine-level failure unrelated to user script (binding setup, module loader, etc.).
20  Internal,
21}
22
23impl fmt::Display for ScriptErrorKind {
24  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25    match self {
26      Self::Syntax => write!(f, "syntax_error"),
27      Self::Runtime => write!(f, "runtime_error"),
28      Self::Timeout => write!(f, "timeout"),
29      Self::MemoryLimit => write!(f, "memory_limit"),
30      Self::SandboxViolation => write!(f, "sandbox_violation"),
31      Self::Internal => write!(f, "internal_error"),
32    }
33  }
34}
35
36/// Structured error returned when a script fails.
37///
38/// `line`, `column`, and `source_snippet` are filled in whenever the `QuickJS`
39/// runtime exposes them (syntax and runtime errors); they are `None` for
40/// engine-level failures like timeouts.
41#[derive(Debug, Clone, serde::Serialize)]
42pub struct ScriptError {
43  pub kind: ScriptErrorKind,
44  pub message: String,
45  #[serde(skip_serializing_if = "Option::is_none")]
46  pub stack: Option<String>,
47  #[serde(skip_serializing_if = "Option::is_none")]
48  pub line: Option<u32>,
49  #[serde(skip_serializing_if = "Option::is_none")]
50  pub column: Option<u32>,
51  #[serde(skip_serializing_if = "Option::is_none")]
52  pub source_snippet: Option<String>,
53}
54
55impl ScriptError {
56  #[must_use]
57  pub fn internal(message: impl Into<String>) -> Self {
58    Self {
59      kind: ScriptErrorKind::Internal,
60      message: message.into(),
61      stack: None,
62      line: None,
63      column: None,
64      source_snippet: None,
65    }
66  }
67
68  #[must_use]
69  pub fn timeout(elapsed_ms: u64, limit_ms: u64) -> Self {
70    Self {
71      kind: ScriptErrorKind::Timeout,
72      message: format!("script exceeded timeout: {elapsed_ms}ms > {limit_ms}ms"),
73      stack: None,
74      line: None,
75      column: None,
76      source_snippet: None,
77    }
78  }
79
80  #[must_use]
81  pub fn memory_limit(limit_bytes: usize) -> Self {
82    Self {
83      kind: ScriptErrorKind::MemoryLimit,
84      message: format!("script exceeded memory limit of {limit_bytes} bytes"),
85      stack: None,
86      line: None,
87      column: None,
88      source_snippet: None,
89    }
90  }
91
92  #[must_use]
93  pub fn sandbox(message: impl Into<String>) -> Self {
94    Self {
95      kind: ScriptErrorKind::SandboxViolation,
96      message: message.into(),
97      stack: None,
98      line: None,
99      column: None,
100      source_snippet: None,
101    }
102  }
103}
104
105impl fmt::Display for ScriptError {
106  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107    write!(f, "[{}] {}", self.kind, self.message)?;
108    if let (Some(l), Some(c)) = (self.line, self.column) {
109      write!(f, " (at {l}:{c})")?;
110    }
111    Ok(())
112  }
113}
114
115impl std::error::Error for ScriptError {}