Skip to main content

macho_demangle/
lib.rs

1#![deny(missing_docs)]
2//! Process-free Rust, C++, and Swift symbol demangling and normalization.
3
4use std::borrow::Cow;
5use std::collections::HashMap;
6use swift_demangler::raw::{Node, NodeKind};
7use swift_demangler::{Context, MetadataKind, Symbol, TypeKind};
8
9/// Owned, process-free Swift mangling evidence.
10pub mod swift_evidence;
11
12/// Closed nominal kinds carried by an emitted Swift type-metadata symbol.
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub enum SwiftNominalMetadataKind {
15    /// Class metadata.
16    Class,
17    /// Structure metadata.
18    Struct,
19    /// Enumeration metadata.
20    Enum,
21}
22
23/// Typed identity carried by an emitted Swift type-metadata symbol.
24#[derive(Clone, Debug, Eq, PartialEq)]
25pub struct SwiftTypeMetadataIdentity {
26    /// Fully-qualified nominal type name.
27    pub name: String,
28    /// Nominal ABI kind.
29    pub kind: SwiftNominalMetadataKind,
30}
31
32/// Strict classification of one possible emitted Swift type-metadata symbol.
33///
34/// The parser's third-party syntax tree remains private to this crate. Callers
35/// receive only Macho-owned, forward-closed evidence and cannot accidentally
36/// couple policy code to parser implementation details.
37#[derive(Clone, Debug, Eq, PartialEq)]
38pub enum SwiftTypeMetadataSymbolEvidence {
39    /// The name is not an emitted type-metadata symbol.
40    NotTypeMetadata,
41    /// The name carries one supported, already-materialized nominal identity.
42    Metadata(SwiftTypeMetadataIdentity),
43    /// The symbol is structurally metadata, but its nominal kind is unsupported.
44    Unsupported {
45        /// Stable, non-sensitive rejection detail.
46        detail: String,
47    },
48    /// The name has the emitted-metadata shape but cannot be decoded.
49    Malformed {
50        /// Stable, non-sensitive rejection detail.
51        detail: String,
52    },
53}
54
55/// Cached demangler for symbol-heavy CLI output.
56#[derive(Debug, Default)]
57pub struct SymbolDemangler {
58    enabled: bool,
59    cache: HashMap<String, Option<String>>,
60}
61
62impl SymbolDemangler {
63    /// Performs new.
64    pub fn new(enabled: bool) -> Self {
65        Self {
66            enabled,
67            cache: HashMap::new(),
68        }
69    }
70
71    /// Performs precompute.
72    pub fn precompute<'a>(&mut self, names: impl IntoIterator<Item = &'a str>) {
73        if !self.enabled {
74            return;
75        }
76
77        for name in names {
78            if self.cache.contains_key(name) {
79                continue;
80            }
81
82            if let Some(demangled) = demangle_rust_or_cpp(name) {
83                self.cache.insert(name.to_owned(), Some(demangled));
84                continue;
85            }
86
87            if let Some(demangled) = demangle_swift_symbol(name) {
88                self.cache.insert(name.to_owned(), Some(demangled));
89                continue;
90            }
91
92            self.cache.insert(name.to_owned(), None);
93        }
94    }
95
96    /// Performs format.
97    pub fn format<'a>(&mut self, name: &'a str) -> Cow<'a, str> {
98        if !self.enabled {
99            return Cow::Borrowed(name);
100        }
101
102        if !self.cache.contains_key(name) {
103            self.precompute([name]);
104        }
105
106        match self.cache.get(name).and_then(|entry| entry.as_ref()) {
107            Some(demangled) => Cow::Owned(demangled.clone()),
108            None => Cow::Borrowed(name),
109        }
110    }
111}
112
113/// Demangle a Rust, C++, or Swift symbol name when possible.
114pub fn demangle_symbol(name: &str) -> Option<String> {
115    let mut demangler = SymbolDemangler::new(true);
116    demangler.precompute([name]);
117    demangler.cache.remove(name).flatten()
118}
119
120/// Demangle a C++ symbol, including a return type when the ABI encoding carries one.
121pub fn demangle_cpp_symbol(name: &str) -> Option<String> {
122    demangle_cpp_symbol_with_options(name, cpp_demangle::DemangleOptions::default())
123}
124
125/// Demangle a C++ symbol while omitting any encoded return type.
126pub fn demangle_cpp_symbol_without_return_type(name: &str) -> Option<String> {
127    demangle_cpp_symbol_with_options(
128        name,
129        cpp_demangle::DemangleOptions::default().no_return_type(),
130    )
131}
132
133/// Demangle a legacy Swift Objective-C runtime name in-process.
134pub fn demangle_swift_symbol(name: &str) -> Option<String> {
135    let candidate = swift_candidate(name)?;
136    swift_demangler::demangle(candidate).or_else(|| demangle_legacy_swift_runtime_name(candidate))
137}
138
139/// Decode the typed identity of an emitted Swift type-metadata symbol.
140///
141/// Narrower than display demangling: accessors, descriptors, protocols,
142/// functions, and unsupported nominal kinds are not admitted as
143/// already-materialized type metadata.
144pub fn swift_type_metadata_identity(name: &str) -> Option<SwiftTypeMetadataIdentity> {
145    match classify_swift_type_metadata_symbol(name) {
146        SwiftTypeMetadataSymbolEvidence::Metadata(identity) => Some(identity),
147        SwiftTypeMetadataSymbolEvidence::NotTypeMetadata
148        | SwiftTypeMetadataSymbolEvidence::Unsupported { .. }
149        | SwiftTypeMetadataSymbolEvidence::Malformed { .. } => None,
150    }
151}
152
153/// Strictly classify an emitted Swift type-metadata symbol.
154///
155/// Only the `...N` value-metadata production is considered a candidate.
156/// Accessors (`...Ma`), descriptors (`...Mn`), functions, and unrelated Swift
157/// symbols are outside this evidence surface.
158pub fn classify_swift_type_metadata_symbol(name: &str) -> SwiftTypeMetadataSymbolEvidence {
159    let Some(candidate) = swift_candidate(name) else {
160        return SwiftTypeMetadataSymbolEvidence::NotTypeMetadata;
161    };
162    if !candidate.ends_with('N') {
163        return SwiftTypeMetadataSymbolEvidence::NotTypeMetadata;
164    }
165    let context = Context::new();
166    let Some(root) = Node::parse(&context, candidate) else {
167        return SwiftTypeMetadataSymbolEvidence::Malformed {
168            detail: "Swift emitted type-metadata mangling is malformed".into(),
169        };
170    };
171    let Some(symbol) = Symbol::from_node(root) else {
172        return SwiftTypeMetadataSymbolEvidence::Malformed {
173            detail: "Swift emitted type-metadata mangling has no typed symbol".into(),
174        };
175    };
176    let Symbol::Metadata(metadata) = symbol else {
177        return SwiftTypeMetadataSymbolEvidence::Malformed {
178            detail: "Swift emitted type-metadata mangling decoded as a different symbol kind"
179                .into(),
180        };
181    };
182    if metadata.kind() != MetadataKind::Type {
183        return SwiftTypeMetadataSymbolEvidence::Unsupported {
184            detail: "Swift emitted metadata is not type metadata".into(),
185        };
186    }
187    let Some(metadata_type) = metadata.metadata_type() else {
188        return SwiftTypeMetadataSymbolEvidence::Malformed {
189            detail: "Swift emitted type metadata has no metadata type".into(),
190        };
191    };
192    let TypeKind::Named(named) = metadata_type.kind() else {
193        return SwiftTypeMetadataSymbolEvidence::Unsupported {
194            detail: "Swift emitted type metadata is not a named nominal type".into(),
195        };
196    };
197    let kind = match named.raw().kind() {
198        NodeKind::Class => SwiftNominalMetadataKind::Class,
199        NodeKind::Structure => SwiftNominalMetadataKind::Struct,
200        NodeKind::Enum => SwiftNominalMetadataKind::Enum,
201        _ => {
202            return SwiftTypeMetadataSymbolEvidence::Unsupported {
203                detail: "Swift emitted type metadata has an unsupported nominal kind".into(),
204            };
205        }
206    };
207    let full_name = named.full_name();
208    let name = if full_name.starts_with('(') {
209        let Some(boundary) = full_name.find("):") else {
210            return SwiftTypeMetadataSymbolEvidence::Malformed {
211                detail: "Swift extension metadata identity has no declaration boundary".into(),
212            };
213        };
214        full_name[boundary + 2..].to_owned()
215    } else {
216        full_name
217    };
218    if name.is_empty() {
219        return SwiftTypeMetadataSymbolEvidence::Malformed {
220            detail: "Swift emitted type metadata has an invalid qualified name".into(),
221        };
222    }
223    SwiftTypeMetadataSymbolEvidence::Metadata(SwiftTypeMetadataIdentity { name, kind })
224}
225
226fn demangle_legacy_swift_runtime_name(name: &str) -> Option<String> {
227    let mut parser = LegacySwiftRuntimeName::new(name.strip_prefix("_Tt")?);
228    let components = parser.parse_nominal_type()?;
229    if parser.remaining() == "_" {
230        parser.advance(1)?;
231    }
232    parser.remaining().is_empty().then(|| components.join("."))
233}
234
235struct LegacySwiftRuntimeName<'a> {
236    input: &'a str,
237    offset: usize,
238}
239
240impl<'a> LegacySwiftRuntimeName<'a> {
241    fn new(input: &'a str) -> Self {
242        Self { input, offset: 0 }
243    }
244
245    fn parse_nominal_type(&mut self) -> Option<Vec<&'a str>> {
246        match self.next_byte()? {
247            b'C' | b'V' | b'O' | b'P' => {}
248            _ => return None,
249        }
250
251        let mut components = self.parse_context()?;
252        components.push(self.parse_decl_name()?);
253        Some(components)
254    }
255
256    fn parse_context(&mut self) -> Option<Vec<&'a str>> {
257        match self.peek_byte()? {
258            b'C' | b'V' | b'O' | b'P' => self.parse_nominal_type(),
259            b's' => {
260                self.advance(1)?;
261                Some(vec!["Swift"])
262            }
263            _ => Some(vec![self.parse_identifier()?]),
264        }
265    }
266
267    fn parse_decl_name(&mut self) -> Option<&'a str> {
268        if self.peek_byte() == Some(b'P') {
269            self.advance(1)?;
270            self.parse_identifier()?;
271        }
272        self.parse_identifier()
273    }
274
275    fn parse_identifier(&mut self) -> Option<&'a str> {
276        let start = self.offset;
277        while self.peek_byte().is_some_and(|byte| byte.is_ascii_digit()) {
278            self.offset += 1;
279        }
280        if self.offset == start {
281            return None;
282        }
283        let length = self.input[start..self.offset].parse::<usize>().ok()?;
284        let identifier_start = self.offset;
285        self.advance(length)?;
286        let identifier = &self.input[identifier_start..self.offset];
287        (!identifier.is_empty()).then_some(identifier)
288    }
289
290    fn peek_byte(&self) -> Option<u8> {
291        self.input.as_bytes().get(self.offset).copied()
292    }
293
294    fn next_byte(&mut self) -> Option<u8> {
295        let byte = self.peek_byte()?;
296        self.offset += 1;
297        Some(byte)
298    }
299
300    fn advance(&mut self, length: usize) -> Option<()> {
301        let end = self.offset.checked_add(length)?;
302        self.input.get(self.offset..end)?;
303        self.offset = end;
304        Some(())
305    }
306
307    fn remaining(&self) -> &'a str {
308        &self.input[self.offset..]
309    }
310}
311
312fn demangle_cpp_symbol_with_options(
313    name: &str,
314    options: cpp_demangle::DemangleOptions,
315) -> Option<String> {
316    for candidate in symbol_candidates(name) {
317        if !looks_like_cpp_symbol(candidate) {
318            continue;
319        }
320        let symbol = cpp_demangle::Symbol::new(candidate).ok()?;
321        let demangled = symbol.demangle_with_options(&options).ok()?;
322        return Some(simplify_cpp_demangled(&demangled));
323    }
324    None
325}
326
327/// Return either the original name or a demangled replacement.
328pub fn format_symbol<'a>(name: &'a str, demangle: bool) -> Cow<'a, str> {
329    if !demangle {
330        return Cow::Borrowed(name);
331    }
332
333    match demangle_symbol(name) {
334        Some(demangled) => Cow::Owned(demangled),
335        None => Cow::Borrowed(name),
336    }
337}
338
339fn demangle_rust_or_cpp(name: &str) -> Option<String> {
340    for candidate in symbol_candidates(name) {
341        if is_rust_v0_symbol(candidate)
342            && let Some(demangled) = demangle_rust_symbol(candidate)
343        {
344            return Some(demangled);
345        }
346
347        if is_likely_legacy_rust_symbol(candidate)
348            && let Some(demangled) = demangle_rust_symbol(candidate)
349        {
350            return Some(demangled);
351        }
352
353        if looks_like_cpp_symbol(candidate)
354            && let Ok(symbol) = cpp_demangle::Symbol::new(candidate)
355            && let Ok(demangled) = symbol.demangle()
356        {
357            return Some(simplify_cpp_demangled(&demangled));
358        }
359
360        if candidate.starts_with("_ZN")
361            && let Some(demangled) = demangle_rust_symbol(candidate)
362        {
363            return Some(demangled);
364        }
365    }
366
367    None
368}
369
370fn demangle_rust_symbol(name: &str) -> Option<String> {
371    const MACHO_TLV_INIT_SUFFIX: &str = "$tlv$init";
372
373    let (mangled, suffix) = match name.strip_suffix(MACHO_TLV_INIT_SUFFIX) {
374        Some(mangled) => (mangled, MACHO_TLV_INIT_SUFFIX),
375        None => (name, ""),
376    };
377    let demangled = rustc_demangle::try_demangle(mangled).ok()?;
378
379    Some(format!("{demangled}{suffix}"))
380}
381
382fn symbol_candidates(name: &str) -> impl Iterator<Item = &str> {
383    [Some(name), macho_stripped_candidate(name)]
384        .into_iter()
385        .flatten()
386}
387
388fn macho_stripped_candidate(name: &str) -> Option<&str> {
389    let stripped = name.strip_prefix('_')?;
390    if is_rust_v0_symbol(stripped)
391        || stripped.starts_with("_ZN")
392        || looks_like_cpp_symbol(stripped)
393        || looks_like_swift_symbol(stripped)
394    {
395        Some(stripped)
396    } else {
397        None
398    }
399}
400
401fn is_rust_v0_symbol(name: &str) -> bool {
402    name.starts_with("_R")
403}
404
405fn is_likely_legacy_rust_symbol(name: &str) -> bool {
406    name.starts_with("_ZN") && has_legacy_rust_hash(name)
407}
408
409fn has_legacy_rust_hash(name: &str) -> bool {
410    let bytes = name.as_bytes();
411    for index in 0..bytes.len() {
412        if bytes[index] != b'h' {
413            continue;
414        }
415
416        let mut end = index + 1;
417        while end < bytes.len() && bytes[end].is_ascii_hexdigit() {
418            end += 1;
419        }
420
421        if end > index + 8 && end <= index + 17 {
422            return true;
423        }
424    }
425
426    false
427}
428
429fn looks_like_cpp_symbol(name: &str) -> bool {
430    name.starts_with("_Z")
431}
432
433fn swift_candidate(name: &str) -> Option<&str> {
434    symbol_candidates(name).find(|candidate| looks_like_swift_symbol(candidate))
435}
436
437fn looks_like_swift_symbol(name: &str) -> bool {
438    name.starts_with("$s")
439        || name.starts_with("$S")
440        || name.starts_with("$e")
441        || name.starts_with("_Tt")
442}
443
444fn simplify_cpp_demangled(text: &str) -> String {
445    let normalized = text.replace("std::__1::", "std::");
446    let simplified = simplify_cpp_fragment(&normalized);
447    simplify_cpp_abi_wrapper(&simplified)
448}
449
450fn simplify_cpp_fragment(input: &str) -> String {
451    let mut out = String::with_capacity(input.len());
452    let mut index = 0;
453
454    while index < input.len() {
455        let Some(ch) = input[index..].chars().next() else {
456            break;
457        };
458
459        if is_template_name_start(ch) {
460            let mut name_end = index + ch.len_utf8();
461            while let Some(next) = input[name_end..].chars().next() {
462                if is_template_name_char(next) {
463                    name_end += next.len_utf8();
464                } else {
465                    break;
466                }
467            }
468
469            if input[name_end..].starts_with('<')
470                && let Some(template_end) = find_matching_angle(input, name_end)
471            {
472                let name = &input[index..name_end];
473                let inner = &input[name_end + 1..template_end];
474                let args: Vec<String> = split_top_level_args(inner)
475                    .into_iter()
476                    .map(|arg| simplify_cpp_fragment(arg.trim()))
477                    .collect();
478                out.push_str(&simplify_template(name, &args));
479                index = template_end + 1;
480                continue;
481            }
482        }
483
484        out.push(ch);
485        index += ch.len_utf8();
486    }
487
488    out
489}
490
491fn is_template_name_start(ch: char) -> bool {
492    ch.is_ascii_alphabetic() || ch == '_'
493}
494
495fn is_template_name_char(ch: char) -> bool {
496    ch.is_ascii_alphanumeric() || matches!(ch, '_' | ':')
497}
498
499fn find_matching_angle(input: &str, open_index: usize) -> Option<usize> {
500    let mut depth = 0usize;
501
502    for (offset, ch) in input[open_index..].char_indices() {
503        match ch {
504            '<' => depth += 1,
505            '>' => {
506                depth = depth.checked_sub(1)?;
507                if depth == 0 {
508                    return Some(open_index + offset);
509                }
510            }
511            _ => {}
512        }
513    }
514
515    None
516}
517
518fn split_top_level_args(input: &str) -> Vec<&str> {
519    let mut args = Vec::new();
520    let mut start = 0usize;
521    let mut angle_depth = 0usize;
522    let mut paren_depth = 0usize;
523
524    for (index, ch) in input.char_indices() {
525        match ch {
526            '<' => angle_depth += 1,
527            '>' => angle_depth = angle_depth.saturating_sub(1),
528            '(' => paren_depth += 1,
529            ')' => paren_depth = paren_depth.saturating_sub(1),
530            ',' if angle_depth == 0 && paren_depth == 0 => {
531                args.push(&input[start..index]);
532                start = index + 1;
533            }
534            _ => {}
535        }
536    }
537
538    args.push(&input[start..]);
539    args
540}
541
542fn simplify_template(name: &str, args: &[String]) -> String {
543    if let Some(alias) = simplify_string_like(name, args) {
544        return alias;
545    }
546
547    if let Some(alias) = simplify_stream_like(name, args) {
548        return alias;
549    }
550
551    if let Some(simplified) = simplify_allocator_container(name, args) {
552        return simplified;
553    }
554
555    if let Some(simplified) = simplify_set_like(name, args) {
556        return simplified;
557    }
558
559    if let Some(simplified) = simplify_map_like(name, args) {
560        return simplified;
561    }
562
563    format!("{name}<{}>", args.join(", "))
564}
565
566fn simplify_string_like(name: &str, args: &[String]) -> Option<String> {
567    let alias_base = match name {
568        "std::basic_string" => "string",
569        "std::basic_string_view" => "string_view",
570        _ => return None,
571    };
572
573    if args.len() != 3 && args.len() != 2 {
574        return None;
575    }
576
577    let kind = char_type_prefix(args[0].as_str())?;
578
579    let traits = format!("std::char_traits<{}>", args[0]);
580    if args[1] != traits {
581        return None;
582    }
583
584    if name == "std::basic_string" && args[2] != format!("std::allocator<{}>", args[0]) {
585        return None;
586    }
587
588    Some(format!("std::{kind}{alias_base}"))
589}
590
591fn simplify_stream_like(name: &str, args: &[String]) -> Option<String> {
592    let alias_base = match name {
593        "std::basic_stringbuf" => "stringbuf",
594        "std::basic_stringstream" => "stringstream",
595        "std::basic_istringstream" => "istringstream",
596        "std::basic_ostringstream" => "ostringstream",
597        "std::basic_filebuf" => "filebuf",
598        "std::basic_ifstream" => "ifstream",
599        "std::basic_ofstream" => "ofstream",
600        "std::basic_fstream" => "fstream",
601        _ => return None,
602    };
603
604    let kind = match args.first().map(String::as_str) {
605        Some("char") => "",
606        Some("wchar_t") => "w",
607        _ => return None,
608    };
609
610    let traits = format!("std::char_traits<{}>", args[0]);
611    match name {
612        "std::basic_stringbuf"
613        | "std::basic_stringstream"
614        | "std::basic_istringstream"
615        | "std::basic_ostringstream" => {
616            if args.len() != 3 || args[1] != traits {
617                return None;
618            }
619            if args[2] != format!("std::allocator<{}>", args[0]) {
620                return None;
621            }
622        }
623        "std::basic_filebuf"
624        | "std::basic_ifstream"
625        | "std::basic_ofstream"
626        | "std::basic_fstream" => {
627            if args.len() != 2 || args[1] != traits {
628                return None;
629            }
630        }
631        _ => return None,
632    }
633
634    Some(format!("std::{kind}{alias_base}"))
635}
636
637fn char_type_prefix(arg: &str) -> Option<&'static str> {
638    match arg {
639        "char" => Some(""),
640        "wchar_t" => Some("w"),
641        "char8_t" => Some("u8"),
642        "char16_t" => Some("u16"),
643        "char32_t" => Some("u32"),
644        _ => None,
645    }
646}
647
648fn simplify_allocator_container(name: &str, args: &[String]) -> Option<String> {
649    let container = match name {
650        "std::vector" | "std::list" | "std::deque" | "std::forward_list" => name,
651        _ => return None,
652    };
653
654    if args.len() != 2 {
655        return None;
656    }
657
658    if args[1] != format!("std::allocator<{}>", args[0]) {
659        return None;
660    }
661
662    Some(format!("{container}<{}>", args[0]))
663}
664
665fn simplify_set_like(name: &str, args: &[String]) -> Option<String> {
666    match name {
667        "std::set" | "std::multiset" => {
668            if args.len() != 3 {
669                return None;
670            }
671            if args[1] == format!("std::less<{}>", args[0])
672                && args[2] == format!("std::allocator<{}>", args[0])
673            {
674                return Some(format!("{name}<{}>", args[0]));
675            }
676        }
677        "std::unordered_set" | "std::unordered_multiset" => {
678            if args.len() != 4 {
679                return None;
680            }
681            if args[1] == format!("std::hash<{}>", args[0])
682                && args[2] == format!("std::equal_to<{}>", args[0])
683                && args[3] == format!("std::allocator<{}>", args[0])
684            {
685                return Some(format!("{name}<{}>", args[0]));
686            }
687        }
688        _ => {}
689    }
690
691    None
692}
693
694fn simplify_map_like(name: &str, args: &[String]) -> Option<String> {
695    match name {
696        "std::map" | "std::multimap" => {
697            if args.len() != 4 {
698                return None;
699            }
700            if args[2] == format!("std::less<{}>", args[0])
701                && args[3] == format!("std::allocator<std::pair<{} const, {}>>", args[0], args[1])
702            {
703                return Some(format!("{name}<{}, {}>", args[0], args[1]));
704            }
705        }
706        "std::unordered_map" | "std::unordered_multimap" => {
707            if args.len() != 5 {
708                return None;
709            }
710            if args[2] == format!("std::hash<{}>", args[0])
711                && args[3] == format!("std::equal_to<{}>", args[0])
712                && args[4] == format!("std::allocator<std::pair<{} const, {}>>", args[0], args[1])
713            {
714                return Some(format!("{name}<{}, {}>", args[0], args[1]));
715            }
716        }
717        _ => {}
718    }
719
720    None
721}
722
723fn simplify_cpp_abi_wrapper(text: &str) -> String {
724    if let Some(name) = text
725        .strip_prefix("{vtable(")
726        .and_then(|inner| inner.strip_suffix(")}"))
727    {
728        return format!("vtable for {}", name.trim());
729    }
730
731    if let Some(name) = text
732        .strip_prefix("{typeinfo(")
733        .and_then(|inner| inner.strip_suffix(")}"))
734    {
735        return format!("typeinfo for {}", name.trim());
736    }
737
738    if let Some(name) = text
739        .strip_prefix("{vtt(")
740        .and_then(|inner| inner.strip_suffix(")}"))
741    {
742        return format!("vtt for {}", name.trim());
743    }
744
745    if let Some(inner) = text
746        .strip_prefix("{virtual override thunk(")
747        .and_then(|value| value.strip_suffix(")}"))
748    {
749        return simplify_virtual_override_thunk(inner);
750    }
751
752    text.trim().to_owned()
753}
754
755fn simplify_virtual_override_thunk(inner: &str) -> String {
756    if let Some(rest) = inner.strip_prefix("{offset(")
757        && let Some((offset, target)) = rest.split_once(")},")
758    {
759        return format!(
760            "virtual override thunk [offset {}] {}",
761            offset.trim(),
762            target.trim()
763        );
764    }
765
766    format!("virtual override thunk {}", inner.trim())
767}
768
769#[cfg(test)]
770mod tests {
771    use super::{
772        SwiftNominalMetadataKind, SwiftTypeMetadataSymbolEvidence, SymbolDemangler,
773        classify_swift_type_metadata_symbol, demangle_swift_symbol, demangle_symbol, format_symbol,
774        simplify_cpp_demangled, swift_type_metadata_identity,
775    };
776
777    #[test]
778    fn demangles_macho_cpp_symbol() {
779        let demangled = demangle_symbol("__Z3foov").expect("expected C++ demangling");
780        assert_eq!(demangled, "foo()");
781    }
782
783    #[test]
784    fn demangles_legacy_rust_symbol() {
785        let demangled =
786            demangle_symbol("__ZN3foo17h05af221e174051e9E").expect("expected Rust demangling");
787        assert_eq!(demangled, "foo::h05af221e174051e9");
788    }
789
790    #[test]
791    fn demangles_macho_rust_thread_local_initializer() {
792        let demangled = demangle_symbol(
793            "__RNvNCNKNvNtCsfTOUOv1Xnuk_12tracing_core10dispatcher13CURRENT_STATE0023___RUST_STD_INTERNAL_VAL$tlv$init",
794        )
795        .expect("expected Rust thread-local initializer demangling");
796
797        assert_eq!(
798            demangled,
799            "tracing_core[b9337ccb037ca08e]::dispatcher::CURRENT_STATE::{K#0}::{closure#0}::__RUST_STD_INTERNAL_VAL$tlv$init"
800        );
801    }
802
803    #[test]
804    fn demangles_legacy_swift_objc_runtime_names_in_process() {
805        assert_eq!(
806            demangle_swift_symbol("_TtC7iMazing10BackupData").as_deref(),
807            Some("iMazing.BackupData")
808        );
809        assert_eq!(
810            demangle_swift_symbol("_TtP7iMazing10Switchable_").as_deref(),
811            Some("iMazing.Switchable")
812        );
813        assert_eq!(
814            demangle_swift_symbol("_TtCC7iMazing12LottieHelper10LottieInfo").as_deref(),
815            Some("iMazing.LottieHelper.LottieInfo")
816        );
817        assert_eq!(
818            demangle_swift_symbol("_$s4Demo6WidgetCMn").as_deref(),
819            Some("nominal type descriptor for Demo.Widget")
820        );
821    }
822
823    #[test]
824    fn retains_typed_swift_metadata_identity() {
825        let identity = swift_type_metadata_identity("_$s4Demo6WidgetVN")
826            .expect("expected emitted structure metadata");
827        assert_eq!(identity.name, "Demo.Widget");
828        assert_eq!(identity.kind, SwiftNominalMetadataKind::Struct);
829        assert!(swift_type_metadata_identity("_$s4Demo6WidgetVMa").is_none());
830        assert!(matches!(
831            classify_swift_type_metadata_symbol("_$s4Demo6WidgetVMa"),
832            SwiftTypeMetadataSymbolEvidence::NotTypeMetadata
833        ));
834        assert!(matches!(
835            classify_swift_type_metadata_symbol("_$s4Demo6WidgetVN"),
836            SwiftTypeMetadataSymbolEvidence::Metadata(_)
837        ));
838        assert!(matches!(
839            classify_swift_type_metadata_symbol("_$s4Demo6WidgetVxN"),
840            SwiftTypeMetadataSymbolEvidence::Malformed { .. }
841        ));
842        let extension = swift_type_metadata_identity("_$sSo8NSNumberC6plutilE15BetterSwiftTypeON")
843            .expect("expected metadata nested in an extension");
844        assert_eq!(extension.name, "__C.NSNumber.BetterSwiftType");
845        let private = swift_type_metadata_identity(
846            "_$s13SwiftPlan00040aB7Private33_C754C3B558BDB8C9E83DFE94C8941DB3LLVN",
847        )
848        .expect("expected private emitted structure metadata");
849        assert_eq!(
850            private.name,
851            "SwiftPlan0004.(SwiftPlan0004Private in _C754C3B558BDB8C9E83DFE94C8941DB3)"
852        );
853        assert_eq!(private.kind, SwiftNominalMetadataKind::Struct);
854    }
855
856    #[test]
857    fn leaves_plain_c_symbol_untouched() {
858        assert!(demangle_symbol("_printf").is_none());
859        assert_eq!(format_symbol("_printf", false), "_printf");
860        assert_eq!(format_symbol("_printf", true), "_printf");
861    }
862
863    #[test]
864    fn caches_batch_demangles_for_cpp_and_rust() {
865        let mut demangler = SymbolDemangler::new(true);
866        demangler.precompute(["__Z3foov", "__ZN3foo17h05af221e174051e9E"]);
867
868        assert_eq!(demangler.format("__Z3foov"), "foo()");
869        assert_eq!(
870            demangler.format("__ZN3foo17h05af221e174051e9E"),
871            "foo::h05af221e174051e9"
872        );
873    }
874
875    #[test]
876    fn simplifies_libcxx_string_and_list_spellings() {
877        let simplified = simplify_cpp_demangled(
878            "FileMgr::GetDirInfo(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >&, std::__1::list<file_info, std::__1::allocator<file_info> >&, bool*)",
879        );
880
881        assert_eq!(
882            simplified,
883            "FileMgr::GetDirInfo(std::string&, std::list<file_info>&, bool*)"
884        );
885    }
886
887    #[test]
888    fn simplifies_typeinfo_stream_aliases() {
889        let simplified = simplify_cpp_demangled(
890            "typeinfo name for std::__1::basic_stringstream<char, std::__1::char_traits<char>, std::__1::allocator<char> >",
891        );
892
893        assert_eq!(simplified, "typeinfo name for std::stringstream");
894    }
895
896    #[test]
897    fn simplifies_vtable_and_thunk_wrappers() {
898        assert_eq!(
899            simplify_cpp_demangled("{vtable(CWeChatMgr)}"),
900            "vtable for CWeChatMgr"
901        );
902        assert_eq!(
903            simplify_cpp_demangled(
904                "{virtual override thunk({offset(-16)}, MainWindow::~MainWindow())}"
905            ),
906            "virtual override thunk [offset -16] MainWindow::~MainWindow()"
907        );
908    }
909}