#![deny(clippy::arithmetic_side_effects)]
mod itanium;
mod output;
mod rust_legacy;
mod rust_v0;
use std::borrow::Cow;
use output::Output;
pub const MAX_INPUT: usize = 1 << 16;
pub const MAX_DEPTH: usize = 192;
pub const MAX_NODES: usize = 1 << 17;
pub const MAX_OUTPUT: usize = 1 << 18;
pub(crate) const PRINT_FUEL: usize = 1 << 20;
pub(crate) const PARSE_FUEL: usize = 1 << 18;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Options {
pub verbose: bool,
}
impl Options {
#[must_use]
pub const fn new() -> Self {
Self { verbose: false }
}
#[must_use]
pub const fn verbose() -> Self {
Self { verbose: true }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Scheme {
Itanium,
RustLegacy,
RustV0,
}
#[must_use]
pub fn demangle(name: &[u8]) -> Cow<'_, str> {
demangle_with(name, Options::new())
}
#[must_use]
pub fn demangle_with(name: &[u8], options: Options) -> Cow<'_, str> {
match try_demangle_with(name, options) {
Some(text) => Cow::Owned(text),
None => String::from_utf8_lossy(name),
}
}
#[must_use]
pub fn try_demangle(name: &[u8]) -> Option<String> {
try_demangle_with(name, Options::new())
}
#[must_use]
pub fn try_demangle_with(name: &[u8], options: Options) -> Option<String> {
try_demangle_scheme(name, options).map(|(text, _)| text)
}
#[must_use]
pub fn try_demangle_scheme(name: &[u8], options: Options) -> Option<(String, Scheme)> {
if name.len() > MAX_INPUT {
return None;
}
if name.starts_with(b"_R") || name.starts_with(b"__R") {
let mut out = Output::for_input(name.len());
rust_v0::demangle(name, options.verbose, &mut out)?;
return out.finish().map(|text| (text, Scheme::RustV0));
}
let mut out = Output::for_input(name.len());
if rust_legacy::demangle(name, options.verbose, &mut out).is_some() {
if let Some(text) = out.finish() {
return Some((text, Scheme::RustLegacy));
}
return None;
}
itanium::demangle(name).map(|text| (text, Scheme::Itanium))
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Parts {
pub scheme: Scheme,
pub full: String,
pub name: String,
pub scope: String,
pub base: String,
pub params: Option<String>,
pub qualifiers: String,
}
#[must_use]
pub fn parts(name: &[u8], options: Options) -> Option<Parts> {
if name.len() > MAX_INPUT {
return None;
}
let (full, scheme) = try_demangle_scheme(name, options)?;
if scheme == Scheme::Itanium {
let pieces = itanium::pieces(name)?;
return Some(Parts {
scheme,
full: pieces.full,
name: pieces.name,
scope: pieces.scope,
base: pieces.base,
params: pieces.params,
qualifiers: pieces.qualifiers,
});
}
let segments = split_path(&full);
let (last, scope) = segments.split_last()?;
let base = last
.find('<')
.map_or(*last, |at| last.get(..at).unwrap_or(last))
.trim_end_matches("::")
.to_string();
Some(Parts {
scheme,
name: full.clone(),
scope: scope.join("::"),
base,
full,
params: None,
qualifiers: String::new(),
})
}
fn split_path(path: &str) -> Vec<&str> {
let mut segments = Vec::new();
let mut depth = 0usize;
let mut start = 0usize;
let bytes = path.as_bytes();
let mut i = 0usize;
while let Some(&c) = bytes.get(i) {
match c {
b'<' | b'(' | b'[' | b'{' => depth = depth.saturating_add(1),
b'>' | b')' | b']' | b'}' => depth = depth.saturating_sub(1),
b':' if depth == 0
&& bytes.get(i.saturating_add(1)) == Some(&b':')
&& bytes.get(i.saturating_add(2)) != Some(&b'<') =>
{
segments.push(path.get(start..i).unwrap_or_default());
i = i.saturating_add(2);
start = i;
continue;
}
_ => {}
}
i = i.saturating_add(1);
}
segments.push(path.get(start..).unwrap_or_default());
segments
}
#[must_use]
pub fn scheme(name: &[u8]) -> Option<Scheme> {
let name = name
.strip_prefix(b"_")
.filter(|n| n.starts_with(b"_"))
.unwrap_or(name);
if name.starts_with(b"_R") {
Some(Scheme::RustV0)
} else if name.starts_with(b"_ZN") && is_legacy_rust_shape(name) {
Some(Scheme::RustLegacy)
} else if name.starts_with(b"_Z") || name.starts_with(b"_GLOBAL_") {
Some(Scheme::Itanium)
} else {
None
}
}
fn is_legacy_rust_shape(name: &[u8]) -> bool {
let end = name.iter().rposition(|&c| c == b'E').unwrap_or(name.len());
name.get(..end)
.and_then(|body| body.len().checked_sub(19).and_then(|at| body.get(at..)))
.is_some_and(|tail| tail.starts_with(b"17h"))
}
#[must_use]
pub fn is_mangled(name: &[u8]) -> bool {
scheme(name).is_some()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn api_basics() {
assert_eq!(demangle(b"_ZN3foo3barEv"), "foo::bar()");
assert_eq!(demangle(b"main"), "main");
assert_eq!(demangle(b"_Z"), "_Z");
assert_eq!(try_demangle(b"memcpy"), None);
assert_eq!(demangle(b"\xff_Zx"), "\u{fffd}_Zx");
assert_eq!(scheme(b"_R3foo"), Some(Scheme::RustV0));
assert_eq!(
scheme(b"_ZN3foo17h0123456789abcdefE"),
Some(Scheme::RustLegacy)
);
assert_eq!(scheme(b"_ZN3fooE"), Some(Scheme::Itanium));
assert!(!is_mangled(b"foo"));
}
#[test]
fn parts_of_cpp_names() {
let p = parts(b"_ZNK2ns3Foo3barIiEEvv", Options::new()).unwrap();
assert_eq!(p.full, "void ns::Foo::bar<int>() const");
assert_eq!(p.name, "ns::Foo::bar<int>");
assert_eq!(p.scope, "ns::Foo");
assert_eq!(p.base, "bar");
assert_eq!(p.params.as_deref(), Some("()"));
assert_eq!(p.qualifiers, "const");
let p = parts(b"_ZN1AIiE1fIcEET_", Options::new());
assert!(p.is_none(), "{p:?}");
let p = parts(b"_ZN1AIiE1fIcEEvT_", Options::new()).unwrap();
assert_eq!(p.params.as_deref(), Some("(char)"));
assert_eq!(p.scope, "A<int>");
let p = parts(b"_Z3fooi", Options::new()).unwrap();
assert_eq!(
(p.name.as_str(), p.scope.as_str(), p.base.as_str()),
("foo", "", "foo")
);
assert_eq!(p.params.as_deref(), Some("(int)"));
let p = parts(b"_ZZ4mainENKUlvE_clEv", Options::new()).unwrap();
assert_eq!(p.name, "main::{lambda()#1}::operator()");
assert_eq!(p.scope, "main::{lambda()#1}");
let p = parts(b"_ZZ1fvENKUlvE_clEv", Options::new()).unwrap();
assert_eq!(p.scope, "f()::{lambda()#1}");
assert_eq!(p.base, "operator()");
assert_eq!(p.qualifiers, "const");
let p = parts(b"_ZNO1S1gEv", Options::new()).unwrap();
assert_eq!(p.qualifiers, "&&");
let p = parts(b"_ZN2ns1xE", Options::new()).unwrap();
assert_eq!(
(p.scope.as_str(), p.base.as_str(), p.params),
("ns", "x", None)
);
let p = parts(b"_ZTVN2ns1AE", Options::new()).unwrap();
assert_eq!(p.name, "vtable for ns::A");
let p = parts(b"_ZN2ns1AC2Ev", Options::new()).unwrap();
assert_eq!((p.scope.as_str(), p.base.as_str()), ("ns::A", "A"));
}
#[test]
fn parts_of_rust_names() {
let p = parts(
b"_ZN4core3ptr13drop_in_place17h0123456789abcdefE",
Options::new(),
)
.unwrap();
assert_eq!(
(p.scope.as_str(), p.base.as_str()),
("core::ptr", "drop_in_place")
);
let p = parts(
b"_RINvNtCs3XFJfFEDSOQ_4core3ptr13drop_in_placeNtNtCs5MFCHAZFjYk_12regex_syntax3hir5ClassEBK_",
Options::new(),
)
.unwrap();
assert_eq!(p.base, "drop_in_place");
assert_eq!(p.scope, "core::ptr");
}
#[test]
fn rust_hash_option() {
let name = b"_ZN4core3fmt5write17h0123456789abcdefE";
assert_eq!(demangle(name), "core::fmt::write");
assert_eq!(
demangle_with(name, Options::verbose()),
"core::fmt::write::h0123456789abcdef"
);
}
}