#![warn(missing_docs)]
use std::borrow::Cow;
#[cfg(feature = "swift")]
use std::ffi::{CStr, CString};
#[cfg(feature = "swift")]
use std::os::raw::{c_char, c_int};
use symbolic_common::{Language, Name, NameMangling};
#[cfg(feature = "swift")]
const SYMBOLIC_SWIFT_FEATURE_RETURN_TYPE: c_int = 0x1;
#[cfg(feature = "swift")]
const SYMBOLIC_SWIFT_FEATURE_PARAMETERS: c_int = 0x2;
#[cfg(feature = "swift")]
extern "C" {
fn symbolic_demangle_swift(
sym: *const c_char,
buf: *mut c_char,
buf_len: usize,
features: c_int,
) -> c_int;
fn symbolic_demangle_is_swift_symbol(sym: *const c_char) -> c_int;
}
#[derive(Clone, Copy, Debug)]
pub struct DemangleOptions {
return_type: bool,
parameters: bool,
}
impl DemangleOptions {
pub const fn complete() -> Self {
Self {
return_type: true,
parameters: true,
}
}
pub const fn name_only() -> Self {
Self {
return_type: false,
parameters: false,
}
}
pub const fn return_type(mut self, return_type: bool) -> Self {
self.return_type = return_type;
self
}
pub const fn parameters(mut self, parameters: bool) -> Self {
self.parameters = parameters;
self
}
}
fn is_maybe_objc(ident: &str) -> bool {
(ident.starts_with("-[") || ident.starts_with("+[")) && ident.ends_with(']')
}
fn is_maybe_cpp(ident: &str) -> bool {
ident.starts_with("_Z")
|| ident.starts_with("__Z")
|| ident.starts_with("___Z")
|| ident.starts_with("____Z")
}
fn is_maybe_msvc(ident: &str) -> bool {
ident.starts_with('?') || ident.starts_with("@?")
}
fn is_maybe_md5(ident: &str) -> bool {
if ident.len() != 36 {
return false;
}
ident.starts_with("??@")
&& ident.ends_with('@')
&& ident[3..35].chars().all(|c| c.is_ascii_hexdigit())
}
#[cfg(feature = "swift")]
fn is_maybe_swift(ident: &str) -> bool {
CString::new(ident)
.map(|cstr| unsafe { symbolic_demangle_is_swift_symbol(cstr.as_ptr()) != 0 })
.unwrap_or(false)
}
#[cfg(not(feature = "swift"))]
fn is_maybe_swift(_ident: &str) -> bool {
false
}
#[cfg(feature = "msvc")]
fn try_demangle_msvc(ident: &str, opts: DemangleOptions) -> Option<String> {
use msvc_demangler::DemangleFlags as MsvcFlags;
let mut flags = MsvcFlags::COMPLETE
| MsvcFlags::SPACE_AFTER_COMMA
| MsvcFlags::HUG_TYPE
| MsvcFlags::NO_MS_KEYWORDS
| MsvcFlags::NO_CLASS_TYPE;
if !opts.return_type {
flags |= MsvcFlags::NO_FUNCTION_RETURNS;
}
if !opts.parameters {
flags |= MsvcFlags::NAME_ONLY;
}
msvc_demangler::demangle(ident, flags).ok()
}
#[cfg(not(feature = "msvc"))]
fn try_demangle_msvc(_ident: &str, _opts: DemangleOptions) -> Option<String> {
None
}
fn strip_hash_suffix(ident: &str) -> &str {
let len = ident.len();
if len >= 33 {
let mut char_iter = ident.char_indices();
while let Some((pos, c)) = char_iter.next_back() {
if (len - pos) == 33 && c == '$' {
return &ident[..pos];
} else if (len - pos) > 33 || !c.is_ascii_hexdigit() {
return ident;
}
}
}
ident
}
struct BoundedString {
str: String,
bound: usize,
}
impl BoundedString {
fn new(bound: usize) -> Self {
Self {
str: String::new(),
bound,
}
}
pub fn into_inner(self) -> String {
self.str
}
}
impl std::fmt::Write for BoundedString {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
if self.str.len().saturating_add(s.len()) > self.bound {
return Err(std::fmt::Error);
}
self.str.write_str(s)
}
}
fn try_demangle_cpp(ident: &str, opts: DemangleOptions) -> Option<String> {
if is_maybe_msvc(ident) {
return try_demangle_msvc(ident, opts);
}
if !is_maybe_cpp(ident) {
return None;
}
#[cfg(feature = "cpp")]
{
use cpp_demangle::{DemangleOptions as CppOptions, ParseOptions, Symbol as CppSymbol};
let stripped = strip_hash_suffix(ident);
let parse_options = ParseOptions::default().recursion_limit(160); let symbol = match CppSymbol::new_with_options(stripped, &parse_options) {
Ok(symbol) => symbol,
Err(_) => return None,
};
let mut cpp_options = CppOptions::new().recursion_limit(192); if !opts.parameters {
cpp_options = cpp_options.no_params();
}
if !opts.return_type {
cpp_options = cpp_options.no_return_type();
}
let mut buf = BoundedString::new(4096);
symbol
.structured_demangle(&mut buf, &cpp_options)
.ok()
.map(|_| buf.into_inner())
}
#[cfg(not(feature = "cpp"))]
{
None
}
}
#[cfg(feature = "rust")]
fn try_demangle_rust(ident: &str, _opts: DemangleOptions) -> Option<String> {
match rustc_demangle::try_demangle(ident) {
Ok(demangled) => Some(format!("{demangled:#}")),
Err(_) => None,
}
}
#[cfg(not(feature = "rust"))]
fn try_demangle_rust(_ident: &str, _opts: DemangleOptions) -> Option<String> {
None
}
#[cfg(feature = "swift")]
fn try_demangle_swift(ident: &str, opts: DemangleOptions) -> Option<String> {
let mut buf = vec![0; 4096];
let sym = match CString::new(ident) {
Ok(sym) => sym,
Err(_) => return None,
};
let mut features = 0;
if opts.return_type {
features |= SYMBOLIC_SWIFT_FEATURE_RETURN_TYPE;
}
if opts.parameters {
features |= SYMBOLIC_SWIFT_FEATURE_PARAMETERS;
}
unsafe {
match symbolic_demangle_swift(sym.as_ptr(), buf.as_mut_ptr(), buf.len(), features) {
0 => None,
_ => Some(CStr::from_ptr(buf.as_ptr()).to_string_lossy().to_string()),
}
}
}
#[cfg(not(feature = "swift"))]
fn try_demangle_swift(_ident: &str, _opts: DemangleOptions) -> Option<String> {
None
}
fn demangle_objc(ident: &str, _opts: DemangleOptions) -> String {
ident.to_string()
}
fn try_demangle_objcpp(ident: &str, opts: DemangleOptions) -> Option<String> {
if is_maybe_objc(ident) {
Some(demangle_objc(ident, opts))
} else if is_maybe_cpp(ident) {
try_demangle_cpp(ident, opts)
} else {
None
}
}
pub trait Demangle {
fn detect_language(&self) -> Language;
fn demangle(&self, opts: DemangleOptions) -> Option<String>;
fn try_demangle(&self, opts: DemangleOptions) -> Cow<'_, str>;
}
impl Demangle for Name<'_> {
fn detect_language(&self) -> Language {
if self.language() != Language::Unknown {
return self.language();
}
if is_maybe_objc(self.as_str()) {
return Language::ObjC;
}
#[cfg(feature = "rust")]
{
if rustc_demangle::try_demangle(self.as_str()).is_ok() {
return Language::Rust;
}
}
if is_maybe_cpp(self.as_str()) || is_maybe_msvc(self.as_str()) {
return Language::Cpp;
}
if is_maybe_swift(self.as_str()) {
return Language::Swift;
}
Language::Unknown
}
fn demangle(&self, opts: DemangleOptions) -> Option<String> {
if matches!(self.mangling(), NameMangling::Unmangled) || is_maybe_md5(self.as_str()) {
return Some(self.to_string());
}
match self.detect_language() {
Language::ObjC => Some(demangle_objc(self.as_str(), opts)),
Language::ObjCpp => try_demangle_objcpp(self.as_str(), opts),
Language::Rust => try_demangle_rust(self.as_str(), opts),
Language::Cpp => try_demangle_cpp(self.as_str(), opts),
Language::Swift => try_demangle_swift(self.as_str(), opts),
_ => None,
}
}
fn try_demangle(&self, opts: DemangleOptions) -> Cow<'_, str> {
if matches!(self.mangling(), NameMangling::Unmangled) {
return Cow::Borrowed(self.as_str());
}
match self.demangle(opts) {
Some(demangled) => Cow::Owned(demangled),
None => Cow::Borrowed(self.as_str()),
}
}
}
pub fn demangle(ident: &str) -> Cow<'_, str> {
match Name::from(ident).demangle(DemangleOptions::complete()) {
Some(demangled) => Cow::Owned(demangled),
None => Cow::Borrowed(ident),
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn simple_md5() {
let md5_mangled = "??@8ba8d245c9eca390356129098dbe9f73@";
assert_eq!(
Name::from(md5_mangled)
.demangle(DemangleOptions::name_only())
.unwrap(),
md5_mangled
);
}
#[test]
fn test_strip_hash_suffix() {
assert_eq!(
strip_hash_suffix("hello$0123456789abcdef0123456789abcdef"),
"hello"
);
assert_eq!(
strip_hash_suffix("hello_0123456789abcdef0123456789abcdef"),
"hello_0123456789abcdef0123456789abcdef",
);
assert_eq!(
strip_hash_suffix("hello\u{1000}0123456789abcdef0123456789abcdef"),
"hello\u{1000}0123456789abcdef0123456789abcdef"
);
assert_eq!(
strip_hash_suffix("hello$0123456789abcdef0123456789abcdxx"),
"hello$0123456789abcdef0123456789abcdxx"
);
assert_eq!(
strip_hash_suffix("hello$\u{1000}0123456789abcdef0123456789abcde"),
"hello$\u{1000}0123456789abcdef0123456789abcde"
);
}
}