Skip to content

Commit 2c41224

Browse files
authored
fix: use line-based heuristic for auto-export detection (#39) (#46)
The naive .contains("export") check in register_handler() would false-positive on string literals (e.g. '<config mode="export">'), comments (e.g. // TODO: export data), and identifiers (e.g. exportPath), skipping auto-export and causing handler registration to fail. Replace with has_export_statement() which checks whether any source line starts with 'export' (after leading whitespace), correctly ignoring occurrences inside strings, comments, and variable names. Closes #39 Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
1 parent ad4530b commit 2c41224

File tree

2 files changed

+144
-4
lines changed

2 files changed

+144
-4
lines changed

src/hyperlight-js-runtime/src/lib.rs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,15 @@ impl JsRuntime {
155155
let handler_script = handler_script.into();
156156
let handler_pwd = handler_pwd.into();
157157

158-
// If the handler script doesn't already export the handler function, we export it for the user.
159-
// This is a convenience for the common case where the handler script is just a single file that defines
160-
// the handler function, without needing to explicitly export it.
161-
let handler_script = if !handler_script.contains("export") {
158+
// If the handler script doesn't already contain an ES export statement,
159+
// append one for the user. This is a convenience for the common case where
160+
// the handler script defines a handler function without explicitly exporting it.
161+
//
162+
// We check whether any line *starts* with `export` (after leading whitespace)
163+
// rather than using a naive `.contains("export")`, which would false-positive
164+
// on string literals (e.g. '<config mode="export">'), comments
165+
// (e.g. // TODO: export data), or identifiers (e.g. exportPath).
166+
let handler_script = if !has_export_statement(&handler_script) {
162167
format!("{}\nexport {{ handler }};", handler_script)
163168
} else {
164169
handler_script
@@ -315,6 +320,20 @@ fn make_handler_path(function_name: &str, handler_dir: &str) -> String {
315320
handler_path
316321
}
317322

323+
/// Returns `true` if the script contains an actual ES `export` statement
324+
/// (as opposed to the word "export" inside a string literal, comment, or
325+
/// identifier like `exportPath`).
326+
///
327+
/// The heuristic checks whether any source line begins with `export` (after
328+
/// optional leading whitespace). This avoids the false positives from a
329+
/// naive `.contains("export")` while staying `no_std`-compatible.
330+
fn has_export_statement(script: &str) -> bool {
331+
script.lines().any(|line| {
332+
let trimmed = line.trim_start();
333+
trimmed.starts_with("export ") || trimmed.starts_with("export{")
334+
})
335+
}
336+
318337
// RAII guard that flushes the output buffer of libc when dropped.
319338
// This is used to make sure we flush all output after running a handler, without needing to manually call it in every code path.
320339
struct FlushGuard;

src/hyperlight-js/src/sandbox/js_sandbox.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,4 +248,125 @@ mod tests {
248248
let res = sandbox.get_loaded_sandbox();
249249
assert!(res.is_ok());
250250
}
251+
252+
// ── Auto-export heuristic tests (issue #39) ──────────────────────────
253+
// The auto-export logic must only detect actual ES export statements,
254+
// not the word "export" inside string literals, comments, or identifiers.
255+
256+
#[test]
257+
fn handler_with_export_in_string_literal() {
258+
// "export" appears inside a string — auto-export should still fire
259+
let handler = Script::from_content(
260+
r#"
261+
function handler(event) {
262+
const xml = '<config mode="export">value</config>';
263+
return { result: xml };
264+
}
265+
"#,
266+
);
267+
268+
let proto = SandboxBuilder::new().build().unwrap();
269+
let mut sandbox = proto.load_runtime().unwrap();
270+
sandbox.add_handler("handler", handler).unwrap();
271+
let mut loaded = sandbox.get_loaded_sandbox().unwrap();
272+
273+
let res = loaded
274+
.handle_event("handler", "{}".to_string(), None)
275+
.unwrap();
276+
assert_eq!(
277+
res,
278+
r#"{"result":"<config mode=\"export\">value</config>"}"#
279+
);
280+
}
281+
282+
#[test]
283+
fn handler_with_export_in_comment() {
284+
// "export" appears in a comment — auto-export should still fire
285+
let handler = Script::from_content(
286+
r#"
287+
function handler(event) {
288+
// TODO: export this data to CSV
289+
return { result: 42 };
290+
}
291+
"#,
292+
);
293+
294+
let proto = SandboxBuilder::new().build().unwrap();
295+
let mut sandbox = proto.load_runtime().unwrap();
296+
sandbox.add_handler("handler", handler).unwrap();
297+
let mut loaded = sandbox.get_loaded_sandbox().unwrap();
298+
299+
let res = loaded
300+
.handle_event("handler", "{}".to_string(), None)
301+
.unwrap();
302+
assert_eq!(res, r#"{"result":42}"#);
303+
}
304+
305+
#[test]
306+
fn handler_with_export_in_identifier() {
307+
// "export" is part of an identifier — auto-export should still fire
308+
let handler = Script::from_content(
309+
r#"
310+
function handler(event) {
311+
const exportPath = "/tmp/out.csv";
312+
return { result: exportPath };
313+
}
314+
"#,
315+
);
316+
317+
let proto = SandboxBuilder::new().build().unwrap();
318+
let mut sandbox = proto.load_runtime().unwrap();
319+
sandbox.add_handler("handler", handler).unwrap();
320+
let mut loaded = sandbox.get_loaded_sandbox().unwrap();
321+
322+
let res = loaded
323+
.handle_event("handler", "{}".to_string(), None)
324+
.unwrap();
325+
assert_eq!(res, r#"{"result":"/tmp/out.csv"}"#);
326+
}
327+
328+
#[test]
329+
fn handler_with_explicit_export_is_not_doubled() {
330+
// Script already has an export statement — auto-export should be skipped
331+
let handler = Script::from_content(
332+
r#"
333+
function handler(event) {
334+
return { result: "explicit" };
335+
}
336+
export { handler };
337+
"#,
338+
);
339+
340+
let proto = SandboxBuilder::new().build().unwrap();
341+
let mut sandbox = proto.load_runtime().unwrap();
342+
sandbox.add_handler("handler", handler).unwrap();
343+
let mut loaded = sandbox.get_loaded_sandbox().unwrap();
344+
345+
let res = loaded
346+
.handle_event("handler", "{}".to_string(), None)
347+
.unwrap();
348+
assert_eq!(res, r#"{"result":"explicit"}"#);
349+
}
350+
351+
#[test]
352+
fn handler_with_export_default_function() {
353+
// `export function` — auto-export should be skipped
354+
let handler = Script::from_content(
355+
r#"
356+
export function handler(event) {
357+
return { result: "inline-export" };
358+
}
359+
"#,
360+
);
361+
362+
let proto = SandboxBuilder::new().build().unwrap();
363+
let mut sandbox = proto.load_runtime().unwrap();
364+
sandbox.add_handler("handler", handler).unwrap();
365+
let mut loaded = sandbox.get_loaded_sandbox().unwrap();
366+
367+
let res = loaded
368+
.handle_event("handler", "{}".to_string(), None)
369+
.unwrap();
370+
assert_eq!(res, r#"{"result":"inline-export"}"#);
371+
}
251372
}

0 commit comments

Comments
 (0)