use crate::error::*;
use crate::execution_class::ExecutionClass;
use crate::severity::Severity;
use crate::stacktrace::{CrashLine, CrashLineExt, DebugInfo};
use crate::stacktrace::{ParseStacktrace, Stacktrace, StacktraceEntry};
use regex::Regex;
#[derive(Clone, Debug)]
pub struct LuaException {
message: String,
}
impl LuaException {
pub fn new(stream: &str) -> Option<Self> {
let re = Regex::new(r#"\S+: .+\n\s*stack traceback:\s*\n(?:.*\n)*.+: .+"#).unwrap();
let mat = re.find(stream)?;
Some(LuaException {
message: mat.as_str().to_string(),
})
}
pub fn extract_stacktrace(&self) -> Result<Vec<String>> {
LuaStacktrace::extract_stacktrace(&self.message)
}
pub fn parse_stacktrace(&self) -> Result<Stacktrace> {
LuaStacktrace::parse_stacktrace(&self.extract_stacktrace()?)
}
pub fn lua_report(&self) -> Vec<String> {
self.message
.split('\n')
.map(|s| s.trim().to_string())
.collect()
}
}
pub struct LuaStacktrace;
impl ParseStacktrace for LuaStacktrace {
fn extract_stacktrace(stream: &str) -> Result<Vec<String>> {
let stacktrace = stream
.split('\n')
.map(|l| l.trim().to_string())
.collect::<Vec<String>>();
let Some(first) = stacktrace
.iter()
.position(|line| line.ends_with("stack traceback:"))
else {
return Err(Error::Casr(
"Couldn't find traceback in lua report".to_string(),
));
};
let re = Regex::new(r#".+: in .+"#).unwrap();
Ok(stacktrace[first..]
.iter()
.map(|s| s.to_string())
.filter(|s| re.is_match(s))
.collect::<Vec<String>>())
}
fn parse_stacktrace_entry(entry: &str) -> Result<StacktraceEntry> {
let mut stentry = StacktraceEntry::default();
let re = Regex::new(r#"(.+):(\d+): in (\S+ )(\S+)"#).unwrap();
let Some(cap) = re.captures(entry) else {
return Err(Error::Casr(format!(
"Couldn't parse stacktrace line: {entry}"
)));
};
stentry.debug.file = cap.get(1).unwrap().as_str().to_string();
if let Ok(line) = cap.get(2).unwrap().as_str().parse::<u64>() {
stentry.debug.line = line;
} else {
return Err(Error::Casr(format!(
"Couldn't parse stacktrace line number: {entry}"
)));
};
stentry.function = if let Some(func) = cap.get(4) {
func.as_str()
.to_string()
.trim_matches('\'')
.trim_start_matches('<')
.trim_end_matches('>')
.to_string()
} else {
return Err(Error::Casr(format!(
"Couldn't parse stacktrace function: {entry}"
)));
};
if let Some(appendix) = cap.get(3) {
let appendix = appendix.as_str().to_string();
if appendix.starts_with("main") {
stentry.function = appendix + &stentry.function;
}
}
Ok(stentry)
}
fn parse_stacktrace(entries: &[String]) -> Result<Stacktrace> {
let mut stacktrace = Stacktrace::new();
for entry in entries.iter() {
if entry.starts_with("[C]:") {
continue;
}
stacktrace.push(Self::parse_stacktrace_entry(entry)?);
}
Ok(stacktrace)
}
}
impl CrashLineExt for LuaException {
fn crash_line(&self) -> Result<CrashLine> {
let lines = self.lua_report();
let re = Regex::new(r#"\S+: (.+):(\d+):"#).unwrap();
let mut cap = re.captures(&lines[0]);
if cap.is_none() {
let Some(index) = lines
.iter()
.rposition(|line| line.ends_with("stack traceback:"))
else {
return Err(Error::Casr(
"Couldn't find traceback in lua report".to_string(),
));
};
let re = Regex::new(r#"(.+):(\d+):"#).unwrap();
for line in &lines[index + 1..] {
cap = re.captures(line);
if cap.is_some() {
break;
}
}
}
let Some(cap) = cap else {
return Err(Error::Casr(format!("Crashline is not found: {:?}", lines)));
};
let file = cap.get(1).unwrap().as_str().to_string();
let line = cap.get(2).unwrap().as_str();
let Ok(line) = line.parse::<u64>() else {
return Err(Error::Casr(format!(
"Couldn't crashline line number: {line}"
)));
};
Ok(CrashLine::Source(DebugInfo {
file,
line,
column: 0,
}))
}
}
impl Severity for LuaException {
fn severity(&self) -> Result<ExecutionClass> {
let re = Regex::new(r#"\S+:(?: .+:)? (.+)"#).unwrap();
let lines = self.lua_report();
let description = lines.first().unwrap();
let Some(cap) = re.captures(description) else {
return Err(Error::Casr(format!(
"Couldn't parse exception description: {description}"
)));
};
let description = cap.get(1).unwrap().as_str().to_string();
Ok(ExecutionClass::new((
"NOT_EXPLOITABLE",
&description,
"",
"",
)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_lua_exception() {
let stream = "
lua: (error object is a table value)
stack traceback:
[C]: in function 'error'
/usr/local/share/lua/5.4/luacheck/parser.lua:28: in function 'luacheck.parser.syntax_error'
/usr/local/share/lua/5.4/luacheck/parser.lua:96: in upvalue 'parse_error'
/usr/local/share/lua/5.4/luacheck/parser.lua:535: in upvalue 'parse_simple_expression'
/usr/local/share/lua/5.4/luacheck/parser.lua:894: in function </usr/local/share/lua/5.4/luacheck/parser.lua:886>
(...tail calls...)
/usr/local/share/lua/5.4/luacheck/parser.lua:974: in upvalue 'parse_block'
/usr/local/share/lua/5.4/luacheck/parser.lua:1020: in function 'luacheck.parser.parse'
luacheck_parser_parse.lua:6: in local 'TestOneInput'
luacheck_parser_parse.lua:18: in main chunk
[C]: in ?
";
let exception = LuaException::new(stream);
let Some(exception) = exception else {
panic!("{:?}", exception);
};
let lines = exception.lua_report();
assert_eq!(lines.len(), 13);
let sttr = exception.extract_stacktrace();
let Ok(sttr) = sttr else {
panic!("{}", sttr.err().unwrap());
};
assert_eq!(sttr.len(), 10);
let sttr = exception.parse_stacktrace();
let Ok(sttr) = sttr else {
panic!("{}", sttr.err().unwrap());
};
assert_eq!(sttr.len(), 8);
assert_eq!(
sttr[0].debug.file,
"/usr/local/share/lua/5.4/luacheck/parser.lua".to_string()
);
assert_eq!(sttr[0].debug.line, 28);
assert_eq!(sttr[0].function, "luacheck.parser.syntax_error".to_string());
assert_eq!(
sttr[1].debug.file,
"/usr/local/share/lua/5.4/luacheck/parser.lua".to_string()
);
assert_eq!(sttr[1].debug.line, 96);
assert_eq!(sttr[1].function, "parse_error".to_string());
assert_eq!(
sttr[2].debug.file,
"/usr/local/share/lua/5.4/luacheck/parser.lua".to_string()
);
assert_eq!(sttr[2].debug.line, 535);
assert_eq!(sttr[2].function, "parse_simple_expression".to_string());
assert_eq!(
sttr[3].debug.file,
"/usr/local/share/lua/5.4/luacheck/parser.lua".to_string()
);
assert_eq!(sttr[3].debug.line, 894);
assert_eq!(
sttr[3].function,
"/usr/local/share/lua/5.4/luacheck/parser.lua:886".to_string()
);
assert_eq!(
sttr[4].debug.file,
"/usr/local/share/lua/5.4/luacheck/parser.lua".to_string()
);
assert_eq!(sttr[4].debug.line, 974);
assert_eq!(sttr[4].function, "parse_block".to_string());
assert_eq!(
sttr[5].debug.file,
"/usr/local/share/lua/5.4/luacheck/parser.lua".to_string()
);
assert_eq!(sttr[5].debug.line, 1020);
assert_eq!(sttr[5].function, "luacheck.parser.parse".to_string());
assert_eq!(sttr[6].debug.file, "luacheck_parser_parse.lua".to_string());
assert_eq!(sttr[6].debug.line, 6);
assert_eq!(sttr[6].function, "TestOneInput".to_string());
assert_eq!(sttr[7].debug.file, "luacheck_parser_parse.lua".to_string());
assert_eq!(sttr[7].debug.line, 18);
assert_eq!(sttr[7].function, "main chunk".to_string());
let crashline = exception.crash_line();
let Ok(crashline) = crashline else {
panic!("{}", crashline.err().unwrap());
};
assert_eq!(
crashline.to_string(),
"/usr/local/share/lua/5.4/luacheck/parser.lua:28"
);
let execution_class = exception.severity();
let Ok(execution_class) = execution_class else {
panic!("{}", execution_class.err().unwrap());
};
assert_eq!(execution_class.severity, "NOT_EXPLOITABLE");
assert_eq!(
execution_class.short_description,
"(error object is a table value)"
);
assert_eq!(execution_class.description, "");
assert_eq!(execution_class.explanation, "");
let stream = "
custom-lua-interpreter: (command line):1: crash
stack traceback:
some custom error message
stack traceback:
[C]: in function 'error'
(command line):1: in main chunk
[C]: at 0x607f3df872e0
";
let exception = LuaException::new(stream);
let Some(exception) = exception else {
panic!("{:?}", exception);
};
let lines = exception.lua_report();
assert_eq!(lines.len(), 7);
let sttr = exception.extract_stacktrace();
let Ok(sttr) = sttr else {
panic!("{}", sttr.err().unwrap());
};
assert_eq!(sttr.len(), 2);
let sttr = exception.parse_stacktrace();
let Ok(sttr) = sttr else {
panic!("{}", sttr.err().unwrap());
};
assert_eq!(sttr.len(), 1);
assert_eq!(sttr[0].debug.file, "(command line)".to_string());
assert_eq!(sttr[0].debug.line, 1);
assert_eq!(sttr[0].function, "main chunk".to_string());
let crashline = exception.crash_line();
let Ok(crashline) = crashline else {
panic!("{}", crashline.err().unwrap());
};
assert_eq!(crashline.to_string(), "(command line):1");
let execution_class = exception.severity();
let Ok(execution_class) = execution_class else {
panic!("{}", execution_class.err().unwrap());
};
assert_eq!(execution_class.severity, "NOT_EXPLOITABLE");
assert_eq!(execution_class.short_description, "crash");
assert_eq!(execution_class.description, "");
assert_eq!(execution_class.explanation, "");
}
}