1#![deny(missing_docs)]
2use std::borrow::Cow;
5use std::collections::HashMap;
6use swift_demangler::raw::{Node, NodeKind};
7use swift_demangler::{Context, MetadataKind, Symbol, TypeKind};
8
9pub mod swift_evidence;
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14pub enum SwiftNominalMetadataKind {
15 Class,
17 Struct,
19 Enum,
21}
22
23#[derive(Clone, Debug, Eq, PartialEq)]
25pub struct SwiftTypeMetadataIdentity {
26 pub name: String,
28 pub kind: SwiftNominalMetadataKind,
30}
31
32#[derive(Clone, Debug, Eq, PartialEq)]
38pub enum SwiftTypeMetadataSymbolEvidence {
39 NotTypeMetadata,
41 Metadata(SwiftTypeMetadataIdentity),
43 Unsupported {
45 detail: String,
47 },
48 Malformed {
50 detail: String,
52 },
53}
54
55#[derive(Debug, Default)]
57pub struct SymbolDemangler {
58 enabled: bool,
59 cache: HashMap<String, Option<String>>,
60}
61
62impl SymbolDemangler {
63 pub fn new(enabled: bool) -> Self {
65 Self {
66 enabled,
67 cache: HashMap::new(),
68 }
69 }
70
71 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 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
113pub 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
120pub fn demangle_cpp_symbol(name: &str) -> Option<String> {
122 demangle_cpp_symbol_with_options(name, cpp_demangle::DemangleOptions::default())
123}
124
125pub 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
133pub 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
139pub 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
153pub 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
327pub 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}