Skip to main content

macho_cpp/
strict_rtti_decoder.rs

1//! Bounded Itanium RTTI decoding and pointer-fixup reconstruction.
2
3use 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
439/// Decode one selected thin Mach-O into strict Itanium RTTI leaf records.
440pub 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
613/// Decode strict Itanium RTTI from a borrowed thin-image byte source.
614pub 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}