From aac6e34a3e8748370d7876f2012147658c1dcc6f Mon Sep 17 00:00:00 2001 From: Darya Parygina Date: Fri, 6 Oct 2023 18:49:56 +0300 Subject: [PATCH] Parsing refinement --- libcasr/src/constants.rs | 16 +++ libcasr/src/js.rs | 242 +++++++++++++++++++++++---------------- libcasr/src/report.rs | 26 +++++ 3 files changed, 184 insertions(+), 100 deletions(-) diff --git a/libcasr/src/constants.rs b/libcasr/src/constants.rs index 8eb23b12..e0f1291d 100644 --- a/libcasr/src/constants.rs +++ b/libcasr/src/constants.rs @@ -31,6 +31,12 @@ pub const STACK_FRAME_FUNCTION_IGNORE_REGEXES_JAVA: &[&str] = &[ r"^javax\.", ]; +/// Regular expressions for JS functions to be ignored. +pub const STACK_FRAME_FUNCTION_IGNORE_REGEXES_JS: &[&str] = &[ + // TODO + r"^$", +]; + /// Regular expressions for python functions to be ignored. pub const STACK_FRAME_FUNCTION_IGNORE_REGEXES_PYTHON: &[&str] = &[ // TODO @@ -228,6 +234,16 @@ pub const STACK_FRAME_FILEPATH_IGNORE_REGEXES_JAVA: &[&str] = &[ r"^[^.]$", ]; +/// Regular expressions for paths to JS files that should be ignored. +pub const STACK_FRAME_FILEPATH_IGNORE_REGEXES_JS: &[&str] = &[ + // TODO + r"^$", + r"^native$", + // JS internal modules + r"^(|node:)internal/?", + r"^(|node:)events/?", +]; + /// Regular expressions for paths to python files that should be ignored. pub const STACK_FRAME_FILEPATH_IGNORE_REGEXES_PYTHON: &[&str] = &[ // TODO diff --git a/libcasr/src/js.rs b/libcasr/src/js.rs index afd1accf..7b285214 100644 --- a/libcasr/src/js.rs +++ b/libcasr/src/js.rs @@ -1,8 +1,7 @@ //! JS module implements `ParseStacktrace` and `Exception` traits for JS reports. -use crate::stacktrace::ParseStacktrace; +use crate::error::{Error, Result}; +use crate::stacktrace::{ParseStacktrace, Stacktrace, StacktraceEntry}; -use crate::error::*; -use crate::stacktrace::*; use regex::Regex; /// Structure provides an interface for processing the stack trace. @@ -24,7 +23,7 @@ impl ParseStacktrace for JSStacktrace { .as_str() .split('\n') .filter(|l| !l.is_empty()) - .map(|l| l.to_string()) + .map(|l| l.trim().to_string()) .collect::>(); Ok(stacktrace) @@ -40,46 +39,45 @@ impl ParseStacktrace for JSStacktrace { Regex::new(r"^(?:\s|\t)*at(?:\s|\t)+(.+?)(?:(?:(?:\s|\t)+(\[as.*?\]))?(?:\s|\t)*)$") .unwrap(); - fn parse_location( - entry: &str, - stentry: &mut StacktraceEntry, - loc: &str, - can_be_func: bool, - ) -> Result<()> { + fn parse_location(entry: &str, stentry: &mut StacktraceEntry, loc: &str) -> Result<()> { + if loc.is_empty() { + return Err(Error::Casr(format!( + "Couldn't parse location. Entry: {entry}" + ))); + } + let mut debug: Vec = loc.split(':').map(|s| s.to_string()).collect(); - if debug.len() > 1 { - if debug.len() > 3 { - let tail = debug[debug.len() - 2..].to_vec(); - debug = [debug[..debug.len() - 2].join(":")].to_vec(); - debug.append(&mut tail.to_vec()); - } + if debug.len() == 1 { + // Location contains filename only stentry.debug.file = debug[0].to_string(); - stentry.debug.line = if let Ok(line) = debug[1].parse::() { - line + return Ok(()); + } + if debug.len() > 3 { + // Filename contains ':' so all the elements except + // the last 2 belong to filename + let tail = debug[debug.len() - 2..].to_vec(); + debug = [debug[..debug.len() - 2].join(":")].to_vec(); + debug.append(&mut tail.to_vec()); + } + + stentry.debug.file = debug[0].to_string(); + stentry.debug.line = if let Ok(line) = debug[1].parse::() { + line + } else { + return Err(Error::Casr(format!( + "Couldn't parse line number {}. Entry: {entry}", + debug[1] + ))); + }; + if debug.len() == 3 { + stentry.debug.column = if let Ok(column) = debug[2].parse::() { + column } else { return Err(Error::Casr(format!( - "Couldn't parse line number {}. Entry: {entry}", - debug[1] + "Couldn't parse column number {}. Entry: {entry}", + debug[2] ))); - }; - if debug.len() == 3 { - stentry.debug.column = if let Ok(column) = debug[2].parse::() { - column - } else { - return Err(Error::Casr(format!( - "Couldn't parse column number {}. Entry: {entry}", - debug[2] - ))); - } - } - } else if can_be_func { - if debug[0].ends_with(".js") || debug[0] == "native" { - stentry.debug.file = debug[0].to_string(); - } else { - stentry.function = debug[0].to_string(); } - } else { - stentry.debug.file = debug[0].to_string(); } Ok(()) } @@ -97,28 +95,33 @@ impl ParseStacktrace for JSStacktrace { let debug = cap.get(1).unwrap().as_str().to_string(); if debug == "" { + // Eval is located in anonymous function stentry.function = "eval".to_string(); } else { + // Can append function name where eval is located stentry.function = "eval at ".to_string() + debug.as_str(); } if let Some(method_name) = cap.get(2) { + // at eval (eval at func [as method] (file:line[:column])) stentry.function += (" ".to_string() + method_name.as_str()).as_str(); } let debug = cap.get(3).unwrap().as_str().to_string(); if debug.contains('(') || debug.contains(')') { - // Irrelevant entry that contains nested evals - // Fill to filter this ectry from stacktrace + // Entry contains nested evals + // Fill to filter this entry from stacktrace // after parsing stentry.function = "".to_string(); return Ok(()); } - parse_location(entry, stentry, &debug, false)?; + parse_location(entry, stentry, &debug)?; + // Recalculate location adding offset inside eval function let debug = cap.get(4).unwrap().as_str().to_string(); let mut eval_stentry = StacktraceEntry::default(); - parse_location(entry, &mut eval_stentry, &debug, false)?; - if eval_stentry.debug.line != 0 { + parse_location(entry, &mut eval_stentry, &debug)?; + if eval_stentry.debug.line >= 3 { + // Line number inside eval function starts with 3 stentry.debug.line += eval_stentry.debug.line - 3; if eval_stentry.debug.column != 0 { stentry.debug.column = eval_stentry.debug.column; @@ -129,21 +132,25 @@ impl ParseStacktrace for JSStacktrace { if let Some(cap) = re_full.captures(entry) { if entry.starts_with("at eval") && entry.contains("eval at") { + // at eval (eval at func (file:line[:column]), :line[:column]) // Parse eval parse_eval(entry, &mut stentry)?; } else { - // at function ([file][:line[:column]]) - // Not eval + // at func (file:line[:column]) + // Parse function with location stentry.function = cap.get(1).unwrap().as_str().to_string(); if let Some(method_name) = cap.get(2) { + // at func [as method] (file:line[:column]) stentry.function += (" ".to_string() + method_name.as_str()).as_str(); } let debug = cap.get(3).unwrap().as_str().to_string(); - parse_location(entry, &mut stentry, &debug, false)?; + parse_location(entry, &mut stentry, &debug)?; } } else if let Some(cap) = re_without_pars.captures(entry) { + // at file:line[:column] + // Parse location only let debug = cap.get(1).unwrap().as_str().to_string(); - parse_location(entry, &mut stentry, &debug, true)?; + parse_location(entry, &mut stentry, &debug)?; } else { return Err(Error::Casr(format!( "Couldn't parse stacktrace line: {entry}" @@ -159,13 +166,6 @@ impl ParseStacktrace for JSStacktrace { .map(String::as_str) .filter(|entry| !entry.contains("unknown location")) .map(Self::parse_stacktrace_entry) - .filter(|entry| { - entry.as_ref().is_ok_and(|x| { - x.debug.file != "native" - && x.debug.file != "" - && x.function != "" - }) - }) .collect() } } @@ -173,37 +173,90 @@ impl ParseStacktrace for JSStacktrace { #[cfg(test)] mod tests { use super::*; + use crate::{ + constants::{ + STACK_FRAME_FILEPATH_IGNORE_REGEXES_CPP, STACK_FRAME_FILEPATH_IGNORE_REGEXES_GO, + STACK_FRAME_FILEPATH_IGNORE_REGEXES_JAVA, STACK_FRAME_FILEPATH_IGNORE_REGEXES_JS, + STACK_FRAME_FILEPATH_IGNORE_REGEXES_PYTHON, STACK_FRAME_FILEPATH_IGNORE_REGEXES_RUST, + STACK_FRAME_FUNCTION_IGNORE_REGEXES_CPP, STACK_FRAME_FUNCTION_IGNORE_REGEXES_GO, + STACK_FRAME_FUNCTION_IGNORE_REGEXES_JAVA, STACK_FRAME_FUNCTION_IGNORE_REGEXES_JS, + STACK_FRAME_FUNCTION_IGNORE_REGEXES_PYTHON, STACK_FRAME_FUNCTION_IGNORE_REGEXES_RUST, + }, + init_ignored_frames, + stacktrace::{ + Filter, STACK_FRAME_FILEPATH_IGNORE_REGEXES, STACK_FRAME_FUNCTION_IGNORE_REGEXES, + }, + }; #[test] fn test_js_stacktrace() { + let stream = r#"Uncaught ReferenceError: var is not defined + at new Uint8Array () + at Object.decode (/fuzz/node_modules/jpeg-js/lib/decoder.js:1110:13) + at fuzz (/fuzz/FuzzTarget.js:6:14) + at result (/fuzz/node_modules/@jazzer.js/core/core.ts:335:15) + at Worker.fuzz [as fn] (/home/user/test_js_stacktrace/main.js:1:2017) + at process. (/home/user/.nvm/versions/node/v16.15.1/lib/node_modules/jsfuzz/build/src/worker.js:55:30) + at process.emit (node:events:527:28) + at + at bootstrap_node.js:609:3 + at file:///home/user/node/offset.js:3:37 + at async Loader.import (internal/modules/esm/loader.js:178:24) + at eval (eval at (eval at g (/fuzz/FuzzTarget.js:7:7)), :4:23) + at eval (eval at (file:///home/user/node/offset.js:3:3), :3:7) + at eval (eval at g (/fuzz/FuzzTarget.js:7:7), :8:13) + at eval (/.svelte-kit/runtime/components/layout.svelte:8:41) + +Uncaught ReferenceError: var is not defined + at Object.decode (/fuzz/node_modules/jpeg-js/lib/decoder.js:1110:13) + at fuzz (/fuzz/FuzzTarget.js:6:14) + at result (/fuzz/node_modules/@jazzer.js/core/core.ts:335:15) + at Worker.fuzz [as fn] (/home/user/test_js_stacktrace/main.js:1:2017) + at process. (/home/user/.nvm/versions/node/v16.15.1/lib/node_modules/jsfuzz/build/src/worker.js:55:30) + at process.emit (node:events:527:28) + at + at bootstrap_node.js:609:3 + at file:///home/user/node/offset.js:3:37 + at async Loader.import (internal/modules/esm/loader.js:178:24) + at eval (eval at (eval at g (/fuzz/FuzzTarget.js:7:7)), :4:23) + at eval (eval at (file:///home/user/node/offset.js:3:3), :3:7) + at eval (eval at g (/fuzz/FuzzTarget.js:7:7), :8:13) + at eval (/.svelte-kit/runtime/components/layout.svelte:8:41)"#; + let raw_stacktrace = &[ "at new Uint8Array ()", "at Object.decode (/fuzz/node_modules/jpeg-js/lib/decoder.js:1110:13)", "at fuzz (/fuzz/FuzzTarget.js:6:14)", "at result (/fuzz/node_modules/@jazzer.js/core/core.ts:335:15)", - "at Worker.fuzz [as fn] (/home/pa_darochek/Documents/SandBox/test_js_stacktrace/main.js:1:2017)", - "at process. (/home/pa_darochek/.nvm/versions/node/v16.15.1/lib/node_modules/jsfuzz/build/src/worker.js:55:30)", + "at Worker.fuzz [as fn] (/home/user/test_js_stacktrace/main.js:1:2017)", + "at process. (/home/user/.nvm/versions/node/v16.15.1/lib/node_modules/jsfuzz/build/src/worker.js:55:30)", "at process.emit (node:events:527:28)", - "at func (:1:2)", "at ", "at bootstrap_node.js:609:3", - "at file:///offset.js:3:37", + "at file:///home/user/node/offset.js:3:37", "at async Loader.import (internal/modules/esm/loader.js:178:24)", "at eval (eval at (eval at g (/fuzz/FuzzTarget.js:7:7)), :4:23)", - "at eval (eval at (file:///offset.js:3:3), :3:7)", + "at eval (eval at (file:///home/user/node/offset.js:3:3), :3:7)", "at eval (eval at g (/fuzz/FuzzTarget.js:7:7), :8:13)", "at eval (/.svelte-kit/runtime/components/layout.svelte:8:41)", ]; + let trace = raw_stacktrace .iter() .map(|e| e.to_string()) .collect::>(); - let sttr = JSStacktrace::parse_stacktrace(&trace); + let bt = JSStacktrace::extract_stacktrace(stream).unwrap(); + assert_eq!(bt, trace); + + let sttr = JSStacktrace::parse_stacktrace(&bt); if sttr.is_err() { panic!("{}", sttr.err().unwrap()); } + let mut stacktrace = sttr.unwrap(); + + init_ignored_frames!("js"); + stacktrace.filter(); - let stacktrace = sttr.unwrap(); assert_eq!( stacktrace[0].debug.file, "/fuzz/node_modules/jpeg-js/lib/decoder.js".to_string() @@ -224,58 +277,47 @@ mod tests { assert_eq!(stacktrace[2].function, "result".to_string()); assert_eq!( stacktrace[3].debug.file, - "/home/pa_darochek/Documents/SandBox/test_js_stacktrace/main.js".to_string() + "/home/user/test_js_stacktrace/main.js".to_string() ); assert_eq!(stacktrace[3].debug.line, 1); assert_eq!(stacktrace[3].debug.column, 2017); assert_eq!(stacktrace[3].function, "Worker.fuzz [as fn]".to_string()); - assert_eq!(stacktrace[4].debug.file, "/home/pa_darochek/.nvm/versions/node/v16.15.1/lib/node_modules/jsfuzz/build/src/worker.js".to_string()); + assert_eq!( + stacktrace[4].debug.file, + "/home/user/.nvm/versions/node/v16.15.1/lib/node_modules/jsfuzz/build/src/worker.js" + .to_string() + ); assert_eq!(stacktrace[4].debug.line, 55); assert_eq!(stacktrace[4].debug.column, 30); assert_eq!(stacktrace[4].function, "process.".to_string()); - assert_eq!(stacktrace[5].debug.file, "node:events".to_string()); - assert_eq!(stacktrace[5].debug.line, 527); - assert_eq!(stacktrace[5].debug.column, 28); - assert_eq!(stacktrace[5].function, "process.emit".to_string()); - assert_eq!(stacktrace[6].debug.file, "".to_string()); - assert_eq!(stacktrace[6].debug.line, 1); - assert_eq!(stacktrace[6].debug.column, 2); - assert_eq!(stacktrace[6].function, "func".to_string()); - assert_eq!(stacktrace[7].debug.file, "bootstrap_node.js".to_string()); - assert_eq!(stacktrace[7].debug.line, 609); - assert_eq!(stacktrace[7].debug.column, 3); - assert_eq!(stacktrace[7].function, "".to_string()); + assert_eq!(stacktrace[5].debug.file, "bootstrap_node.js".to_string()); + assert_eq!(stacktrace[5].debug.line, 609); + assert_eq!(stacktrace[5].debug.column, 3); + assert_eq!(stacktrace[5].function, "".to_string()); assert_eq!( - stacktrace[8].debug.file, - "file:///offset.js".to_string() + stacktrace[6].debug.file, + "file:///home/user/node/offset.js".to_string() ); - assert_eq!(stacktrace[8].debug.line, 3); - assert_eq!(stacktrace[8].debug.column, 37); - assert_eq!(stacktrace[8].function, "".to_string()); + assert_eq!(stacktrace[6].debug.line, 3); + assert_eq!(stacktrace[6].debug.column, 37); + assert_eq!(stacktrace[6].function, "".to_string()); assert_eq!( - stacktrace[9].debug.file, - "internal/modules/esm/loader.js".to_string() + stacktrace[7].debug.file, + "file:///home/user/node/offset.js".to_string() ); - assert_eq!(stacktrace[9].debug.line, 178); - assert_eq!(stacktrace[9].debug.column, 24); - assert_eq!(stacktrace[9].function, "async Loader.import".to_string()); + assert_eq!(stacktrace[7].debug.line, 3); + assert_eq!(stacktrace[7].debug.column, 7); + assert_eq!(stacktrace[7].function, "eval".to_string()); + assert_eq!(stacktrace[8].debug.file, "/fuzz/FuzzTarget.js".to_string()); + assert_eq!(stacktrace[8].debug.line, 12); + assert_eq!(stacktrace[8].debug.column, 13); + assert_eq!(stacktrace[8].function, "eval at g".to_string()); assert_eq!( - stacktrace[10].debug.file, - "file:///offset.js".to_string() - ); - assert_eq!(stacktrace[10].debug.line, 3); - assert_eq!(stacktrace[10].debug.column, 7); - assert_eq!(stacktrace[10].function, "eval".to_string()); - assert_eq!(stacktrace[11].debug.file, "/fuzz/FuzzTarget.js".to_string()); - assert_eq!(stacktrace[11].debug.line, 12); - assert_eq!(stacktrace[11].debug.column, 13); - assert_eq!(stacktrace[11].function, "eval at g".to_string()); - assert_eq!( - stacktrace[12].debug.file, + stacktrace[9].debug.file, "/.svelte-kit/runtime/components/layout.svelte".to_string() ); - assert_eq!(stacktrace[12].debug.line, 8); - assert_eq!(stacktrace[12].debug.column, 41); - assert_eq!(stacktrace[12].function, "eval".to_string()); + assert_eq!(stacktrace[9].debug.line, 8); + assert_eq!(stacktrace[9].debug.column, 41); + assert_eq!(stacktrace[9].function, "eval".to_string()); } } diff --git a/libcasr/src/report.rs b/libcasr/src/report.rs index 14790c01..f8d495ae 100644 --- a/libcasr/src/report.rs +++ b/libcasr/src/report.rs @@ -6,6 +6,7 @@ use crate::execution_class::*; use crate::gdb::GdbStacktrace; use crate::go::GoStacktrace; use crate::java::JavaStacktrace; +use crate::js::JSStacktrace; use crate::python::PythonStacktrace; use crate::rust::RustStacktrace; use crate::stacktrace::*; @@ -218,6 +219,13 @@ pub struct CrashReport { )] #[cfg_attr(feature = "serde", serde(default))] pub rust_report: Vec, + /// JS report. + #[cfg_attr( + feature = "serde", + serde(rename(serialize = "JSReport", deserialize = "JSReport")) + )] + #[cfg_attr(feature = "serde", serde(default))] + pub js_report: Vec, /// Crash line from stack trace: source:line or binary+offset. #[cfg_attr( feature = "serde", @@ -546,6 +554,8 @@ impl CrashReport { GoStacktrace::parse_stacktrace(&self.stacktrace)? } else if !self.rust_report.is_empty() { RustStacktrace::parse_stacktrace(&self.stacktrace)? + } else if !self.js_report.is_empty() { + JSStacktrace::parse_stacktrace(&self.stacktrace)? } else { GdbStacktrace::parse_stacktrace(&self.stacktrace)? }; @@ -727,6 +737,14 @@ impl fmt::Display for CrashReport { } } + // JSReport + if !self.js_report.is_empty() { + report += "\n===JSReport===\n"; + for e in self.js_report.iter() { + report += &format!("{e}\n"); + } + } + // Source if !self.source.is_empty() { report += "\n===Source===\n"; @@ -842,6 +860,10 @@ mod tests { "index out of bounds: the len is 0 but the index is 10".to_string(), "stack backtrace:".to_string(), ]; + report.js_report = vec![ + "Uncaught ReferenceError: var is not defined".to_string(), + " at Worker.fuzz [as fn] (/home/user/test_js_stacktrace/main.js:1:2017)".to_string(), + ]; report.source = vec![ "--->83 return utf16_to_utf8(std::u16string(name_array.begin()," .to_string(), @@ -916,6 +938,10 @@ mod tests { "index out of bounds: the len is 0 but the index is 10".to_string(), "stack backtrace:".to_string(), "".to_string(), + "===JSReport===".to_string(), + "Uncaught ReferenceError: var is not defined".to_string(), + " at Worker.fuzz [as fn] (/home/user/test_js_stacktrace/main.js:1:2017)".to_string(), + "".to_string(), "===Source===".to_string(), "--->83 return utf16_to_utf8(std::u16string(name_array.begin(),".to_string(), ].join("\n"),