pub(super) struct ParsedFrame {
pub(super) raw: String,
pub(super) symbol: Option<String>,
pub(super) path: Option<String>,
pub(super) line: Option<u32>,
pub(super) column: Option<u32>,
}
pub(super) fn parse(text: &str, max_frames: usize) -> Vec<ParsedFrame> {
let mut frames = Vec::new();
let mut pending_symbol: Option<String> = None;
for raw_line in text.lines() {
if frames.len() >= max_frames {
break;
}
let line = raw_line.trim();
if line.is_empty() {
continue;
}
if let Some(frame) = python_frame(line) {
pending_symbol = None;
frames.push(frame);
} else if let Some(frame) = panic_frame(line) {
pending_symbol = None;
frames.push(frame);
} else if let Some(symbol) = rust_numbered_symbol(line) {
pending_symbol = Some(symbol);
} else if let Some(rest) = line.strip_prefix("at ") {
let mut frame = at_frame(raw_line, rest);
if frame.symbol.is_none() {
frame.symbol = pending_symbol.take();
}
pending_symbol = None;
frames.push(frame);
}
}
frames
}
fn python_frame(line: &str) -> Option<ParsedFrame> {
let rest = line.strip_prefix("File \"")?;
let (path, rest) = rest.split_once('"')?;
let line_number = rest
.split_once("line ")
.and_then(|(_, tail)| numeric_prefix(tail));
let symbol = rest
.split_once(", in ")
.map(|(_, name)| name.trim().to_owned())
.filter(|name| !name.is_empty());
Some(ParsedFrame {
raw: line.to_owned(),
symbol,
path: Some(path.to_owned()),
line: line_number,
column: None,
})
}
fn panic_frame(line: &str) -> Option<ParsedFrame> {
let rest = line.split_once("panicked at ")?.1.trim();
let candidate = rest
.rsplit(|character: char| character.is_whitespace() || character == ',')
.find(|token| !token.is_empty() && token.matches(':').count() >= 1)?;
let (path, line_number, column) = location(candidate)?;
line_number?;
Some(ParsedFrame {
raw: line.to_owned(),
symbol: None,
path: Some(path),
line: line_number,
column,
})
}
fn rust_numbered_symbol(line: &str) -> Option<String> {
let (number, rest) = line.split_once(':')?;
if number.is_empty() || !number.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
let symbol = rest.trim();
(!symbol.is_empty() && !symbol.contains(' ')).then(|| symbol.to_owned())
}
fn at_frame(raw: &str, rest: &str) -> ParsedFrame {
let rest = rest.trim();
let (symbol, location_text) = if let Some(open) = rest.rfind('(') {
let inside = rest[open + 1..].trim_end_matches(')');
let symbol = rest[..open].trim();
(
(!symbol.is_empty()).then(|| strip_symbol_qualifiers(symbol)),
inside,
)
} else {
(None, rest)
};
let (path, line, column) = location(location_text).unwrap_or((String::new(), None, None));
ParsedFrame {
raw: raw.trim().to_owned(),
symbol,
path: (!path.is_empty()).then_some(path),
line,
column,
}
}
fn strip_symbol_qualifiers(symbol: &str) -> String {
symbol
.trim_start_matches("async ")
.trim_start_matches("new ")
.trim()
.to_owned()
}
fn location(text: &str) -> Option<(String, Option<u32>, Option<u32>)> {
let text = text.trim().trim_end_matches(':');
if text.is_empty() || text == "native" || text.starts_with('<') {
return None;
}
let text = text
.strip_prefix("file:///")
.or_else(|| text.strip_prefix("file://"))
.unwrap_or(text);
let mut path = text;
let mut line = None;
let mut column = None;
if let Some((rest, last)) = path.rsplit_once(':')
&& let Some(number) = parse_u32(last)
&& !rest.is_empty()
{
path = rest;
line = Some(number);
if let Some((rest, middle)) = path.rsplit_once(':')
&& let Some(inner) = parse_u32(middle)
&& !rest.is_empty()
{
path = rest;
column = line;
line = Some(inner);
}
}
let path = path.split('?').next().unwrap_or(path).replace('\\', "/");
(!path.is_empty()).then_some((path, line, column))
}
fn parse_u32(text: &str) -> Option<u32> {
(!text.is_empty() && text.bytes().all(|byte| byte.is_ascii_digit()))
.then(|| text.parse().ok())
.flatten()
}
fn numeric_prefix(text: &str) -> Option<u32> {
let digits = text.bytes().take_while(u8::is_ascii_digit).count();
(digits > 0).then(|| text[..digits].parse().ok()).flatten()
}