1use std::collections::BTreeMap;
4
5use macho_core::model::addr::Va;
6use macho_core::model::macho_file::MachoFile;
7use macho_core::model::symbol::{Symbol, SymbolTable};
8
9use super::*;
10use crate::{Error, Result};
11
12#[path = "strict_rtti_fixups.rs"]
13mod fixups;
14use fixups::{PointerFixup, build_pointer_fixups};
15
16pub(crate) struct StrictDecoder<'a, 'data> {
17 pub(crate) macho: &'a MachoFile<'data>,
18 limits: StrictRttiLimits,
19 pub(crate) pointer_size: u64,
20 fixups: BTreeMap<u64, PointerFixup>,
21 pub(crate) symbols_by_va: BTreeMap<u64, String>,
22 pub(crate) observations: Vec<StrictRttiObservation>,
23 base_count: u64,
24 evidence_bytes: u64,
25}
26
27impl<'a, 'data> StrictDecoder<'a, 'data> {
28 pub(crate) fn new(macho: &'a MachoFile<'data>, limits: StrictRttiLimits) -> Result<Self> {
29 Ok(Self {
30 macho,
31 limits,
32 pointer_size: if macho.is_64bit() { 8 } else { 4 },
33 fixups: build_pointer_fixups(macho)?,
34 symbols_by_va: BTreeMap::new(),
35 observations: Vec::new(),
36 base_count: 0,
37 evidence_bytes: 0,
38 })
39 }
40
41 pub(crate) fn observe(
42 &mut self,
43 symbol: &str,
44 field: impl Into<String>,
45 va: u64,
46 length: u64,
47 kind: StrictRttiObservationKind,
48 ) -> Result<u64> {
49 let file_offset = self.macho.address_map().va_to_thin_offset(Va(va))?.0;
50 self.evidence_bytes = self
51 .evidence_bytes
52 .checked_add(length)
53 .ok_or_else(|| Error::format("strict RTTI evidence byte count overflows"))?;
54 if self.evidence_bytes > self.limits.max_evidence_bytes {
55 self.evidence_bytes -= length;
56 return Err(Error::format("strict RTTI evidence byte limit exceeded"));
57 }
58 let ordinal = u64::try_from(self.observations.len())
59 .map_err(|_| Error::format("strict RTTI observation count exceeds UInt64"))?;
60 self.observations.push(StrictRttiObservation {
61 ordinal,
62 symbol: symbol.to_owned(),
63 field: field.into(),
64 va,
65 file_offset,
66 length,
67 kind,
68 });
69 Ok(ordinal)
70 }
71
72 pub(crate) fn pointer(
73 &mut self,
74 symbol: &str,
75 field: impl Into<String>,
76 va: u64,
77 ) -> Result<StrictPointerObservation> {
78 let field = field.into();
79 let file_offset = self.macho.address_map().va_to_thin_offset(Va(va))?.0;
80 let raw_value = self.peek_word(va)?;
81 let observation_ordinal = self.observe(
82 symbol,
83 field,
84 va,
85 self.pointer_size,
86 StrictRttiObservationKind::Pointer,
87 )?;
88 let (encoding, authentication, target) =
89 self.pointer_value(file_offset, raw_value, u64::MAX)?;
90 Ok(StrictPointerObservation {
91 observation_ordinal,
92 raw_value,
93 width: self.pointer_size as u8,
94 encoding,
95 authentication,
96 target,
97 })
98 }
99
100 fn type_name_pointer(
101 &mut self,
102 symbol: &str,
103 va: u64,
104 ) -> Result<(StrictPointerObservation, bool)> {
105 let file_offset = self.macho.address_map().va_to_thin_offset(Va(va))?.0;
106 let raw_value = self.peek_word(va)?;
107 let shift = u32::try_from(self.pointer_size * 8 - 1)
108 .map_err(|_| Error::format("strict RTTI type-name tag width is invalid"))?;
109 let tag = 1_u64
110 .checked_shl(shift)
111 .ok_or_else(|| Error::format("strict RTTI type-name tag width is invalid"))?;
112 let observation_ordinal = self.observe(
113 symbol,
114 "type_name",
115 va,
116 self.pointer_size,
117 StrictRttiObservationKind::Pointer,
118 )?;
119 let (encoding, authentication, target) =
120 self.pointer_value(file_offset, raw_value, !tag)?;
121 Ok((
122 StrictPointerObservation {
123 observation_ordinal,
124 raw_value,
125 width: self.pointer_size as u8,
126 encoding,
127 authentication,
128 target,
129 },
130 raw_value & tag != 0,
131 ))
132 }
133
134 pub(crate) fn peek_word(&self, va: u64) -> Result<u64> {
135 let bytes = self
136 .macho
137 .read_bytes_at_va(Va(va), self.pointer_size as usize)?;
138 if self.pointer_size == 8 {
139 Ok(self.macho.endian().read_u64(
140 bytes.try_into().map_err(|_| {
141 Error::format("strict RTTI pointer read returned the wrong width")
142 })?,
143 ))
144 } else {
145 Ok(u64::from(self.macho.endian().read_u32(
146 bytes.try_into().map_err(|_| {
147 Error::format("strict RTTI pointer read returned the wrong width")
148 })?,
149 )))
150 }
151 }
152
153 pub(crate) fn peek_pointer_target(&self, va: u64) -> Result<StrictPointerTarget> {
154 let file_offset = self.macho.address_map().va_to_thin_offset(Va(va))?.0;
155 let raw_value = self.peek_word(va)?;
156 self.pointer_value(file_offset, raw_value, u64::MAX)
157 .map(|(_, _, target)| target)
158 }
159
160 fn pointer_value(
161 &self,
162 file_offset: u64,
163 raw_value: u64,
164 local_address_mask: u64,
165 ) -> Result<(
166 StrictPointerEncoding,
167 StrictPointerAuthentication,
168 StrictPointerTarget,
169 )> {
170 let fixup = self.fixups.get(&file_offset).cloned();
171 let (encoding, authentication, target) = fixup.map_or_else(
172 || {
173 let target = if raw_value == 0 {
174 StrictPointerTarget::Null
175 } else {
176 StrictPointerTarget::Local { va: raw_value }
177 };
178 (
179 StrictPointerEncoding::Direct,
180 StrictPointerAuthentication::NotApplicable,
181 target,
182 )
183 },
184 |value| (value.encoding, value.authentication, value.target),
185 );
186 let target = match target {
187 StrictPointerTarget::Local { va } => StrictPointerTarget::Local {
188 va: va & local_address_mask,
189 },
190 value => value,
191 };
192 if let StrictPointerTarget::Local { va: target } = target {
193 if target != 0
194 && self
195 .macho
196 .address_map()
197 .va_to_thin_offset(Va(target))
198 .is_err()
199 {
200 return Err(Error::address(format!(
201 "strict RTTI pointer targets unmapped VA {target:#x}"
202 )));
203 }
204 }
205 Ok((encoding, authentication, target))
206 }
207
208 fn integer_u32(&mut self, symbol: &str, field: impl Into<String>, va: u64) -> Result<u32> {
209 let bytes = self.macho.read_bytes_at_va(Va(va), 4)?;
210 let value = self.macho.endian().read_u32(
211 bytes
212 .try_into()
213 .map_err(|_| Error::format("strict RTTI u32 read returned the wrong width"))?,
214 );
215 self.observe(symbol, field, va, 4, StrictRttiObservationKind::Integer)?;
216 Ok(value)
217 }
218
219 pub(crate) fn integer_word(
220 &mut self,
221 symbol: &str,
222 field: impl Into<String>,
223 va: u64,
224 ) -> Result<u64> {
225 if self.pointer_size == 8 {
226 let bytes = self.macho.read_bytes_at_va(Va(va), 8)?;
227 let value =
228 self.macho.endian().read_u64(bytes.try_into().map_err(|_| {
229 Error::format("strict RTTI word read returned the wrong width")
230 })?);
231 self.observe(symbol, field, va, 8, StrictRttiObservationKind::Integer)?;
232 Ok(value)
233 } else {
234 self.integer_u32(symbol, field, va).map(u64::from)
235 }
236 }
237
238 fn type_name(&mut self, symbol: &str, pointer: &StrictPointerObservation) -> Result<String> {
239 let StrictPointerTarget::Local { va } = pointer.target else {
240 return Err(Error::address("strict RTTI type-name pointer is not local"));
241 };
242 if va == 0 {
243 return Err(Error::address("strict RTTI type-name pointer is null"));
244 }
245 let offset = self.macho.address_map().va_to_thin_offset(Va(va))?.0;
246 let start = usize::try_from(offset)
247 .map_err(|_| Error::format("strict RTTI type-name offset exceeds usize"))?;
248 let maximum = usize::try_from(self.limits.max_name_bytes)
249 .map_err(|_| Error::format("strict RTTI name limit exceeds usize"))?;
250 let available = self
251 .macho
252 .bytes()
253 .get(start..)
254 .ok_or_else(|| Error::bounds(offset, 1, self.macho.bytes().len() as u64))?;
255 let bounded = &available[..available.len().min(maximum.saturating_add(1))];
256 let length = bounded
257 .iter()
258 .position(|byte| *byte == 0)
259 .ok_or_else(|| Error::format("strict RTTI type name is unterminated or oversized"))?;
260 if length == 0 {
261 return Err(Error::format("strict RTTI type name is empty"));
262 }
263 let value = std::str::from_utf8(&bounded[..length])
264 .map_err(|_| Error::format("strict RTTI type name is not UTF-8"))?;
265 self.observe(
266 symbol,
267 "type_name_bytes",
268 va,
269 u64::try_from(length + 1)
270 .map_err(|_| Error::format("strict RTTI type-name length exceeds UInt64"))?,
271 StrictRttiObservationKind::TypeName,
272 )?;
273 Ok(value.to_owned())
274 }
275
276 fn family(&self, pointer: &StrictPointerObservation) -> Result<ItaniumTypeInfoFamily> {
277 let name = match &pointer.target {
278 StrictPointerTarget::Local { va } => {
279 let (symbol_va, name) = self
280 .symbols_by_va
281 .range(..=*va)
282 .next_back()
283 .ok_or_else(|| Error::format("RTTI runtime vtable target lacks a symbol"))?;
284 let maximum_delta = self
285 .pointer_size
286 .checked_mul(2)
287 .ok_or_else(|| Error::format("RTTI vtable address-point bound overflows"))?;
288 if va
289 .checked_sub(*symbol_va)
290 .is_none_or(|delta| delta > maximum_delta)
291 {
292 return Err(Error::format(
293 "RTTI runtime vtable target is not an exact ABI address point",
294 ));
295 }
296 name.as_str()
297 }
298 StrictPointerTarget::External { symbol, .. } => symbol.as_str(),
299 StrictPointerTarget::Null => {
300 return Err(Error::format("RTTI runtime vtable pointer is null"));
301 }
302 };
303 classify_family(name)
304 .ok_or_else(|| Error::unsupported(format!("unsupported RTTI runtime family {name}")))
305 }
306
307 fn decode(&mut self, symbol: &Symbol<'_>) -> Result<ItaniumTypeInfoRecord> {
308 let ptr = self.pointer_size;
309 let runtime_vtable = self.pointer(symbol.name, "runtime_vtable", symbol.value)?;
310 let family = self.family(&runtime_vtable)?;
311 let (type_name_pointer, type_name_non_unique) =
312 self.type_name_pointer(symbol.name, checked_add(symbol.value, ptr)?)?;
313 let type_name = self.type_name(symbol.name, &type_name_pointer)?;
314 let mut class_flags = 0;
315 let mut bases = Vec::new();
316 let mut pointee = None;
317 match family {
318 ItaniumTypeInfoFamily::SingleInheritanceClass => {
319 let base = self.pointer(
320 symbol.name,
321 "base[0].typeinfo",
322 checked_add(
323 symbol.value,
324 ptr.checked_mul(2)
325 .ok_or_else(|| Error::format("RTTI field offset overflows"))?,
326 )?,
327 )?;
328 bases.push(ItaniumBaseRecord {
329 ordinal: 0,
330 typeinfo: base,
331 offset_flags: 0x2,
332 signed_offset: 0,
333 is_virtual: false,
334 is_public: true,
335 });
336 self.base_count = self
337 .base_count
338 .checked_add(1)
339 .ok_or_else(|| Error::format("RTTI base count overflows"))?;
340 }
341 ItaniumTypeInfoFamily::VirtualMultipleInheritanceClass => {
342 let header = checked_add(
343 symbol.value,
344 ptr.checked_mul(2)
345 .ok_or_else(|| Error::format("RTTI field offset overflows"))?,
346 )?;
347 class_flags = self.integer_u32(symbol.name, "vmi.flags", header)?;
348 let count = u64::from(self.integer_u32(
349 symbol.name,
350 "vmi.base_count",
351 checked_add(header, 4)?,
352 )?);
353 self.base_count = self
354 .base_count
355 .checked_add(count)
356 .ok_or_else(|| Error::format("RTTI base count overflows"))?;
357 if self.base_count > self.limits.max_bases {
358 return Err(Error::format("strict RTTI base limit exceeded"));
359 }
360 let entry_size = ptr
361 .checked_mul(2)
362 .ok_or_else(|| Error::format("RTTI base entry size overflows"))?;
363 let start = checked_add(header, 8)?;
364 for ordinal in 0..count {
365 let entry = checked_add(
366 start,
367 ordinal
368 .checked_mul(entry_size)
369 .ok_or_else(|| Error::format("RTTI base table offset overflows"))?,
370 )?;
371 let typeinfo =
372 self.pointer(symbol.name, format!("base[{ordinal}].typeinfo"), entry)?;
373 let offset_flags = self.integer_word(
374 symbol.name,
375 format!("base[{ordinal}].offset_flags"),
376 checked_add(entry, ptr)?,
377 )?;
378 bases.push(ItaniumBaseRecord {
379 ordinal,
380 typeinfo,
381 offset_flags,
382 signed_offset: (offset_flags as i64) >> 8,
383 is_virtual: offset_flags & 1 != 0,
384 is_public: offset_flags & 2 != 0,
385 });
386 }
387 }
388 ItaniumTypeInfoFamily::Pointer | ItaniumTypeInfoFamily::PointerToMember => {
389 let flags_va = checked_add(
390 symbol.value,
391 ptr.checked_mul(2)
392 .ok_or_else(|| Error::format("RTTI field offset overflows"))?,
393 )?;
394 let flags = self.integer_u32(symbol.name, "pbase.flags", flags_va)?;
395 let pointee_va = align_up(checked_add(flags_va, 4)?, ptr)?;
396 let pointee_pointer = self.pointer(symbol.name, "pbase.pointee", pointee_va)?;
397 let member_of = if family == ItaniumTypeInfoFamily::PointerToMember {
398 Some(self.pointer(
399 symbol.name,
400 "pointer_to_member.context",
401 checked_add(pointee_va, ptr)?,
402 )?)
403 } else {
404 None
405 };
406 pointee = Some(ItaniumPointeeRecord {
407 flags,
408 pointee: pointee_pointer,
409 member_of,
410 });
411 }
412 _ => {}
413 }
414 if self.base_count > self.limits.max_bases {
415 return Err(Error::format("strict RTTI base limit exceeded"));
416 }
417 let file_offset = self
418 .macho
419 .address_map()
420 .va_to_thin_offset(Va(symbol.value))?
421 .0;
422 Ok(ItaniumTypeInfoRecord {
423 symbol: symbol.name.to_owned(),
424 va: symbol.value,
425 file_offset,
426 family,
427 type_name,
428 type_name_non_unique,
429 runtime_vtable,
430 type_name_pointer,
431 class_flags,
432 bases,
433 pointee,
434 weak_definition: symbol.is_weak_def(),
435 })
436 }
437}
438
439pub fn decode_strict_rtti(
441 macho: &MachoFile<'_>,
442 limits: StrictRttiLimits,
443) -> Result<StrictRttiBatch> {
444 limits.validate()?;
445 let input_bytes = u64::try_from(macho.bytes().len())
446 .map_err(|_| Error::format("strict RTTI input length exceeds UInt64"))?;
447 if input_bytes > limits.max_input_bytes {
448 return preflight_limit_batch("input_bytes", limits);
449 }
450 let symtab = macho.ext::<SymbolTable<'_>>()?;
451 let symbol_count = u64::try_from(symtab.symbols().len())
452 .map_err(|_| Error::format("strict RTTI symbol count exceeds UInt64"))?;
453 if symbol_count > limits.max_symbols {
454 return preflight_limit_batch("symbol_registry", limits);
455 }
456 let mut candidates = symtab
457 .symbols()
458 .iter()
459 .filter(|symbol| is_typeinfo_symbol(symbol.name))
460 .collect::<Vec<_>>();
461 candidates.sort_by(|left, right| {
462 left.name
463 .cmp(right.name)
464 .then_with(|| left.value.cmp(&right.value))
465 .then_with(|| left.index.cmp(&right.index))
466 });
467 let attempted = u64::try_from(candidates.len())
468 .map_err(|_| Error::format("strict RTTI candidate count exceeds UInt64"))?;
469 if attempted == 0 {
470 let batch = StrictRttiBatch {
471 outcome: StrictRttiOutcome::Absent,
472 records: Vec::new(),
473 observations: Vec::new(),
474 gaps: Vec::new(),
475 conservation: StrictRttiConservation {
476 attempted: 0,
477 included: 0,
478 unknown: 0,
479 excluded: 0,
480 },
481 };
482 batch.validate(limits)?;
483 return Ok(batch);
484 }
485 if attempted > limits.max_records {
486 let batch = StrictRttiBatch {
487 outcome: StrictRttiOutcome::Rejected,
488 records: Vec::new(),
489 observations: Vec::new(),
490 gaps: vec![StrictRttiGap {
491 symbol: None,
492 field: "candidate_registry".into(),
493 code: StrictRttiGapCode::StructuralLimitExceeded,
494 source_code: None,
495 }],
496 conservation: StrictRttiConservation {
497 attempted,
498 included: 0,
499 unknown: 0,
500 excluded: attempted,
501 },
502 };
503 batch.validate(limits)?;
504 return Ok(batch);
505 }
506
507 let mut decoder = StrictDecoder::new(macho, limits)?;
508 decoder.symbols_by_va = symtab
509 .symbols()
510 .iter()
511 .filter(|symbol| symbol.is_defined() && symbol.value != 0)
512 .map(|symbol| (symbol.value, symbol.name.to_owned()))
513 .collect();
514 let mut records = Vec::new();
515 let mut gaps = Vec::new();
516 let mut included = 0_u64;
517 let mut unknown = 0_u64;
518 for symbol in candidates {
519 if symbol.is_undefined() {
520 records.push(StrictRttiRecord::ExternalTypeInfo {
521 symbol: symbol.name.to_owned(),
522 library_ordinal: i32::from(symbol.library_ordinal() as i8),
523 weak: symbol.is_weak_ref(),
524 });
525 included += 1;
526 continue;
527 }
528 if !symbol.is_defined() || symbol.value == 0 {
529 gaps.push(StrictRttiGap {
530 symbol: Some(symbol.name.to_owned()),
531 field: "symbol".into(),
532 code: StrictRttiGapCode::RecordMalformed,
533 source_code: None,
534 });
535 unknown += 1;
536 continue;
537 }
538 match decoder.decode(symbol) {
539 Ok(record) => {
540 records.push(StrictRttiRecord::TypeInfo {
541 record: Box::new(record),
542 });
543 included += 1;
544 }
545 Err(error) => {
546 let code = match error.kind {
547 crate::CppErrorKind::Unsupported => StrictRttiGapCode::FamilyUnsupported,
548 crate::CppErrorKind::InvalidAddress => StrictRttiGapCode::PointerUnresolved,
549 crate::CppErrorKind::OutOfBounds | crate::CppErrorKind::InvalidFormat => {
550 if error.message().contains("type name") {
551 StrictRttiGapCode::TypeNameInvalid
552 } else if error.message().contains("limit") {
553 StrictRttiGapCode::StructuralLimitExceeded
554 } else {
555 StrictRttiGapCode::RecordMalformed
556 }
557 }
558 _ => StrictRttiGapCode::RecordMalformed,
559 };
560 gaps.push(StrictRttiGap {
561 symbol: Some(symbol.name.to_owned()),
562 field: "typeinfo".into(),
563 code,
564 source_code: Some(error.code().to_owned()),
565 });
566 unknown += 1;
567 }
568 }
569 }
570 let outcome = if gaps.is_empty() {
571 StrictRttiOutcome::Complete
572 } else {
573 StrictRttiOutcome::Rejected
574 };
575 let batch = StrictRttiBatch {
576 outcome,
577 records,
578 observations: decoder.observations,
579 gaps,
580 conservation: StrictRttiConservation {
581 attempted,
582 included,
583 unknown,
584 excluded: 0,
585 },
586 };
587 batch.validate(limits)?;
588 Ok(batch)
589}
590
591fn preflight_limit_batch(field: &str, limits: StrictRttiLimits) -> Result<StrictRttiBatch> {
592 let batch = StrictRttiBatch {
593 outcome: StrictRttiOutcome::Rejected,
594 records: Vec::new(),
595 observations: Vec::new(),
596 gaps: vec![StrictRttiGap {
597 symbol: None,
598 field: field.to_owned(),
599 code: StrictRttiGapCode::StructuralLimitExceeded,
600 source_code: None,
601 }],
602 conservation: StrictRttiConservation {
603 attempted: 0,
604 included: 0,
605 unknown: 0,
606 excluded: 0,
607 },
608 };
609 batch.validate(limits)?;
610 Ok(batch)
611}
612
613pub fn decode_strict_rtti_from_source<S>(
615 source: &S,
616 limits: StrictRttiLimits,
617) -> Result<StrictRttiBatch>
618where
619 S: AsRef<[u8]> + ?Sized,
620{
621 let macho = crate::parse_source(source)?;
622 decode_strict_rtti(&macho, limits)
623}
624
625pub(super) fn is_typeinfo_symbol(name: &str) -> bool {
626 name.starts_with("__ZTI") || name.starts_with("_ZTI")
627}
628
629fn classify_family(name: &str) -> Option<ItaniumTypeInfoFamily> {
630 [
631 (
632 "__fundamental_type_info",
633 ItaniumTypeInfoFamily::Fundamental,
634 ),
635 ("__array_type_info", ItaniumTypeInfoFamily::Array),
636 ("__function_type_info", ItaniumTypeInfoFamily::Function),
637 ("__enum_type_info", ItaniumTypeInfoFamily::Enum),
638 (
639 "__si_class_type_info",
640 ItaniumTypeInfoFamily::SingleInheritanceClass,
641 ),
642 (
643 "__vmi_class_type_info",
644 ItaniumTypeInfoFamily::VirtualMultipleInheritanceClass,
645 ),
646 ("__class_type_info", ItaniumTypeInfoFamily::Class),
647 ("__pointer_type_info", ItaniumTypeInfoFamily::Pointer),
648 (
649 "__pointer_to_member_type_info",
650 ItaniumTypeInfoFamily::PointerToMember,
651 ),
652 ("__qualified_type_info", ItaniumTypeInfoFamily::Qualified),
653 ]
654 .into_iter()
655 .find_map(|(needle, family)| name.contains(needle).then_some(family))
656}
657
658pub(crate) fn checked_add(left: u64, right: u64) -> Result<u64> {
659 left.checked_add(right)
660 .ok_or_else(|| Error::format("strict RTTI address arithmetic overflows"))
661}
662
663fn align_up(value: u64, alignment: u64) -> Result<u64> {
664 let mask = alignment
665 .checked_sub(1)
666 .ok_or_else(|| Error::format("strict RTTI alignment is zero"))?;
667 value
668 .checked_add(mask)
669 .map(|candidate| candidate & !mask)
670 .ok_or_else(|| Error::format("strict RTTI alignment overflows"))
671}
672
673#[cfg(test)]
674mod tests {
675 use super::*;
676
677 #[test]
678 fn admitted_itanium_family_registry_is_exact() {
679 for (symbol, expected) in [
680 (
681 "__ZTVN10__cxxabiv123__fundamental_type_infoE",
682 ItaniumTypeInfoFamily::Fundamental,
683 ),
684 (
685 "__ZTVN10__cxxabiv117__array_type_infoE",
686 ItaniumTypeInfoFamily::Array,
687 ),
688 (
689 "__ZTVN10__cxxabiv120__function_type_infoE",
690 ItaniumTypeInfoFamily::Function,
691 ),
692 (
693 "__ZTVN10__cxxabiv116__enum_type_infoE",
694 ItaniumTypeInfoFamily::Enum,
695 ),
696 (
697 "__ZTVN10__cxxabiv117__class_type_infoE",
698 ItaniumTypeInfoFamily::Class,
699 ),
700 (
701 "__ZTVN10__cxxabiv120__si_class_type_infoE",
702 ItaniumTypeInfoFamily::SingleInheritanceClass,
703 ),
704 (
705 "__ZTVN10__cxxabiv121__vmi_class_type_infoE",
706 ItaniumTypeInfoFamily::VirtualMultipleInheritanceClass,
707 ),
708 (
709 "__ZTVN10__cxxabiv119__pointer_type_infoE",
710 ItaniumTypeInfoFamily::Pointer,
711 ),
712 (
713 "__ZTVN10__cxxabiv129__pointer_to_member_type_infoE",
714 ItaniumTypeInfoFamily::PointerToMember,
715 ),
716 (
717 "__ZTVN10__cxxabiv121__qualified_type_infoE",
718 ItaniumTypeInfoFamily::Qualified,
719 ),
720 ] {
721 assert_eq!(classify_family(symbol), Some(expected));
722 }
723 assert_eq!(classify_family("__ZTV7Mystery"), None);
724 }
725}