Skip to main content

macho_cpp/
strict_vtable_decoder.rs

1//! Strict absolute-pointer Itanium vtable and VTT decoding.
2
3use std::collections::{BTreeMap, BTreeSet};
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::strict_rtti::decoder::{StrictDecoder, checked_add};
11use crate::strict_rtti::{StrictPointerTarget, StrictRttiGapCode};
12use crate::{Error, Result};
13
14#[derive(Clone, Copy)]
15struct Extent {
16    byte_length: u64,
17    source: ItaniumVtableExtentSource,
18}
19
20struct VtableDecoder<'a, 'data> {
21    reader: StrictDecoder<'a, 'data>,
22    limits: StrictVtableLimits,
23    symbols_by_va: BTreeMap<u64, Vec<String>>,
24    defined_vas: Vec<u64>,
25    total_words: u64,
26}
27
28impl<'a, 'data> VtableDecoder<'a, 'data> {
29    fn new(
30        macho: &'a MachoFile<'data>,
31        limits: StrictVtableLimits,
32        symbols: &[Symbol<'_>],
33    ) -> Result<Self> {
34        let mut symbols_by_va = BTreeMap::<u64, Vec<String>>::new();
35        let mut defined_vas = Vec::new();
36        for symbol in symbols
37            .iter()
38            .filter(|value| value.is_defined() && value.value != 0)
39        {
40            symbols_by_va
41                .entry(symbol.value)
42                .or_default()
43                .push(symbol.name.to_owned());
44            defined_vas.push(symbol.value);
45        }
46        for names in symbols_by_va.values_mut() {
47            names.sort();
48            names.dedup();
49        }
50        defined_vas.sort_unstable();
51        defined_vas.dedup();
52        let mut reader = StrictDecoder::new(macho, limits.reader_limits())?;
53        reader.symbols_by_va = symbols_by_va
54            .iter()
55            .filter_map(|(va, names)| names.first().cloned().map(|name| (*va, name)))
56            .collect();
57        Ok(Self {
58            reader,
59            limits,
60            symbols_by_va,
61            defined_vas,
62            total_words: 0,
63        })
64    }
65
66    fn extent(&self, symbol: &Symbol<'_>) -> Result<Extent> {
67        let section = self
68            .reader
69            .macho
70            .all_sections()
71            .find(|section| {
72                let start = section.addr().0;
73                start
74                    .checked_add(section.size())
75                    .is_some_and(|end| symbol.value >= start && symbol.value < end)
76            })
77            .ok_or_else(|| Error::address("vtable symbol is not inside a section"))?;
78        if section.section_type().is_zerofill() {
79            return Err(Error::format("vtable symbol is in a zero-fill section"));
80        }
81        let section_end = section
82            .addr()
83            .0
84            .checked_add(section.size())
85            .ok_or_else(|| Error::format("vtable section end overflows"))?;
86        let next = self
87            .defined_vas
88            .iter()
89            .copied()
90            .find(|va| *va > symbol.value && *va <= section_end);
91        let (end, source) = next
92            .map_or((section_end, ItaniumVtableExtentSource::SectionEnd), |va| {
93                (va, ItaniumVtableExtentSource::NextDefinedSymbol)
94            });
95        let byte_length = end
96            .checked_sub(symbol.value)
97            .ok_or_else(|| Error::format("vtable symbol extent underflows"))?;
98        let pointer_width = self.reader.pointer_size;
99        if byte_length == 0 || byte_length % pointer_width != 0 {
100            return Err(Error::format(
101                "vtable symbol extent is empty or not pointer aligned",
102            ));
103        }
104        Ok(Extent {
105            byte_length,
106            source,
107        })
108    }
109
110    fn admit_words(&mut self, extent: Extent) -> Result<u64> {
111        let words = extent.byte_length / self.reader.pointer_size;
112        self.total_words = self
113            .total_words
114            .checked_add(words)
115            .ok_or_else(|| Error::format("strict vtable word count overflows"))?;
116        if self.total_words > self.limits.max_words {
117            return Err(Error::format("strict vtable word limit exceeded"));
118        }
119        Ok(words)
120    }
121
122    fn decode_group(
123        &mut self,
124        symbol: &Symbol<'_>,
125        kind: ItaniumVtableSymbolKind,
126    ) -> Result<ItaniumVtableGroupRecord> {
127        let extent = self.extent(symbol)?;
128        let word_count = self.admit_words(extent)?;
129        let ptr = self.reader.pointer_size;
130        let raw = (0..word_count)
131            .map(|ordinal| {
132                let va = checked_add(
133                    symbol.value,
134                    ordinal
135                        .checked_mul(ptr)
136                        .ok_or_else(|| Error::format("vtable word address overflows"))?,
137                )?;
138                self.reader.peek_word(va)
139            })
140            .collect::<Result<Vec<_>>>()?;
141        let mut headers = Vec::new();
142        for offset_word in 0..word_count.saturating_sub(1) {
143            let typeinfo_word = offset_word + 1;
144            let va = checked_add(symbol.value, typeinfo_word * ptr)?;
145            if self.is_typeinfo_target(va)? {
146                headers.push((offset_word, ItaniumVtableAddressPointSource::TypeinfoSymbol));
147            }
148        }
149        if headers.is_empty() && word_count >= 2 && raw[1] == 0 {
150            headers.push((
151                0,
152                ItaniumVtableAddressPointSource::NullTypeinfoAtSymbolStart,
153            ));
154        }
155        if headers.is_empty() {
156            return Err(Error::format(
157                "vtable group has no structurally proven address point",
158            ));
159        }
160        let header_words = headers
161            .iter()
162            .flat_map(|(word, _)| [*word, *word + 1])
163            .collect::<BTreeSet<_>>();
164        if header_words.len() != headers.len() * 2 {
165            return Err(Error::format("vtable address-point headers overlap"));
166        }
167
168        let mut address_points = Vec::new();
169        let mut ambiguous_words = Vec::new();
170        let first_header = headers[0].0;
171        let mut first_prefix = Vec::new();
172        for word in 0..first_header {
173            first_prefix.push(self.offset_record(symbol, word, raw[word as usize])?);
174        }
175        for (index, (offset_word, source)) in headers.iter().copied().enumerate() {
176            let offset_va = checked_add(symbol.value, offset_word * ptr)?;
177            let offset_observation = self.reader.observe(
178                symbol.name,
179                format!("address_point[{index}].offset_to_top"),
180                offset_va,
181                ptr,
182                StrictRttiObservationKind::Integer,
183            )?;
184            let typeinfo = self.reader.pointer(
185                symbol.name,
186                format!("address_point[{index}].typeinfo"),
187                checked_add(offset_va, ptr)?,
188            )?;
189            let next_header = headers
190                .get(index + 1)
191                .map(|(word, _)| *word)
192                .unwrap_or(word_count);
193            let slot_start = offset_word + 2;
194            let mut slots = Vec::new();
195            let mut cursor = slot_start;
196            while cursor < next_header {
197                let va = checked_add(symbol.value, cursor * ptr)?;
198                if index + 1 < headers.len() && !self.is_exact_callable_or_null(va)? {
199                    break;
200                }
201                slots.push(self.slot_record(symbol, index as u64, slots.len() as u64, cursor)?);
202                cursor += 1;
203            }
204            while cursor < next_header {
205                let va = checked_add(symbol.value, cursor * ptr)?;
206                let observation_ordinal = self.reader.observe(
207                    symbol.name,
208                    format!("inter_address_point[{cursor}]"),
209                    va,
210                    ptr,
211                    StrictRttiObservationKind::Integer,
212                )?;
213                ambiguous_words.push(ItaniumVtableAmbiguousWordRecord {
214                    word_ordinal: cursor,
215                    observation_ordinal,
216                    raw_value: raw[cursor as usize],
217                });
218                cursor += 1;
219            }
220            address_points.push(ItaniumVtableAddressPointRecord {
221                ordinal: index as u64,
222                va: checked_add(symbol.value, (offset_word + 2) * ptr)?,
223                source,
224                offset_to_top_word: offset_word,
225                prefix_offsets: if index == 0 {
226                    std::mem::take(&mut first_prefix)
227                } else {
228                    Vec::new()
229                },
230                offset_to_top_observation_ordinal: offset_observation,
231                offset_to_top: sign_extend(raw[offset_word as usize], ptr as u8),
232                typeinfo,
233                slots,
234            });
235        }
236        Ok(ItaniumVtableGroupRecord {
237            symbol: symbol.name.to_owned(),
238            kind,
239            va: symbol.value,
240            file_offset: self
241                .reader
242                .macho
243                .address_map()
244                .va_to_thin_offset(Va(symbol.value))?
245                .0,
246            byte_length: extent.byte_length,
247            extent_source: extent.source,
248            pointer_width: ptr as u8,
249            address_points,
250            ambiguous_words,
251            weak_definition: symbol.is_weak_def(),
252        })
253    }
254
255    fn decode_vtt(&mut self, symbol: &Symbol<'_>) -> Result<ItaniumVttRecord> {
256        let extent = self.extent(symbol)?;
257        let word_count = self.admit_words(extent)?;
258        let ptr = self.reader.pointer_size;
259        let mut entries = Vec::new();
260        for ordinal in 0..word_count {
261            let pointer = self.reader.pointer(
262                symbol.name,
263                format!("vtt[{ordinal}]"),
264                checked_add(symbol.value, ordinal * ptr)?,
265            )?;
266            if matches!(pointer.target, StrictPointerTarget::Null) {
267                return Err(Error::format("VTT entry is null"));
268            }
269            entries.push(ItaniumVttEntryRecord {
270                ordinal,
271                address_point: pointer,
272            });
273        }
274        Ok(ItaniumVttRecord {
275            symbol: symbol.name.to_owned(),
276            va: symbol.value,
277            file_offset: self
278                .reader
279                .macho
280                .address_map()
281                .va_to_thin_offset(Va(symbol.value))?
282                .0,
283            byte_length: extent.byte_length,
284            extent_source: extent.source,
285            entries,
286            weak_definition: symbol.is_weak_def(),
287        })
288    }
289
290    fn is_typeinfo_target(&self, va: u64) -> Result<bool> {
291        Ok(match self.reader.peek_pointer_target(va) {
292            Ok(StrictPointerTarget::Local { va }) => self
293                .symbols_by_va
294                .get(&va)
295                .is_some_and(|names| names.iter().any(|name| is_typeinfo_symbol(name))),
296            Ok(StrictPointerTarget::External { symbol, .. }) => is_typeinfo_symbol(&symbol),
297            Ok(StrictPointerTarget::Null) | Err(_) => false,
298        })
299    }
300
301    fn is_exact_callable_or_null(&self, va: u64) -> Result<bool> {
302        Ok(match self.reader.peek_pointer_target(va) {
303            Ok(StrictPointerTarget::Null) => true,
304            Ok(StrictPointerTarget::External { symbol, .. }) => !is_typeinfo_symbol(&symbol),
305            Ok(StrictPointerTarget::Local { va }) => self
306                .symbols_by_va
307                .get(&va)
308                .is_some_and(|names| names.iter().any(|name| !is_data_special_name(name))),
309            Err(_) => false,
310        })
311    }
312
313    fn offset_record(
314        &mut self,
315        symbol: &Symbol<'_>,
316        word_ordinal: u64,
317        raw_value: u64,
318    ) -> Result<ItaniumVtableOffsetRecord> {
319        let va = checked_add(symbol.value, word_ordinal * self.reader.pointer_size)?;
320        let observation_ordinal = self.reader.observe(
321            symbol.name,
322            format!("prefix_offset[{word_ordinal}]"),
323            va,
324            self.reader.pointer_size,
325            StrictRttiObservationKind::Integer,
326        )?;
327        Ok(ItaniumVtableOffsetRecord {
328            word_ordinal,
329            observation_ordinal,
330            raw_value,
331            signed_value: sign_extend(raw_value, self.reader.pointer_size as u8),
332            role: ItaniumVtableOffsetRole::VcallOrVbase,
333        })
334    }
335
336    fn slot_record(
337        &mut self,
338        symbol: &Symbol<'_>,
339        point: u64,
340        ordinal: u64,
341        word_ordinal: u64,
342    ) -> Result<ItaniumVtableSlotRecord> {
343        let pointer = self.reader.pointer(
344            symbol.name,
345            format!("address_point[{point}].slot[{ordinal}]"),
346            checked_add(symbol.value, word_ordinal * self.reader.pointer_size)?,
347        )?;
348        let target_symbol = exact_target_symbol(&pointer.target, &self.symbols_by_va);
349        let (role, this_adjustment, return_adjustment) =
350            classify_slot(target_symbol.as_deref(), &pointer.target)?;
351        Ok(ItaniumVtableSlotRecord {
352            ordinal,
353            word_ordinal,
354            pointer,
355            target_symbol,
356            role,
357            this_adjustment,
358            return_adjustment,
359        })
360    }
361}
362
363/// Decode one selected thin Mach-O into strict absolute-pointer vtable records.
364pub fn decode_strict_vtables(
365    macho: &MachoFile<'_>,
366    limits: StrictVtableLimits,
367) -> Result<StrictVtableBatch> {
368    limits.validate()?;
369    let input_bytes = macho.bytes().len() as u64;
370    if input_bytes > limits.max_input_bytes {
371        return preflight_batch("input_bytes", limits);
372    }
373    let symtab = macho.ext::<SymbolTable<'_>>()?;
374    if symtab.symbols().len() as u64 > limits.max_symbols {
375        return preflight_batch("symbol_registry", limits);
376    }
377    let mut candidates = symtab
378        .symbols()
379        .iter()
380        .filter_map(|symbol| classify_symbol(symbol.name).map(|kind| (symbol, kind)))
381        .collect::<Vec<_>>();
382    candidates.sort_by(|(left, _), (right, _)| {
383        left.name
384            .cmp(right.name)
385            .then_with(|| left.value.cmp(&right.value))
386            .then_with(|| left.index.cmp(&right.index))
387    });
388    let attempted = candidates.len() as u64;
389    if attempted == 0 {
390        let batch = StrictVtableBatch {
391            outcome: StrictRttiOutcome::Absent,
392            records: Vec::new(),
393            observations: Vec::new(),
394            gaps: Vec::new(),
395            conservation: StrictRttiConservation {
396                attempted: 0,
397                included: 0,
398                unknown: 0,
399                excluded: 0,
400            },
401        };
402        batch.validate(limits)?;
403        return Ok(batch);
404    }
405    if attempted > limits.max_records {
406        let batch = StrictVtableBatch {
407            outcome: StrictRttiOutcome::Rejected,
408            records: Vec::new(),
409            observations: Vec::new(),
410            gaps: vec![StrictRttiGap {
411                symbol: None,
412                field: "candidate_registry".into(),
413                code: StrictRttiGapCode::StructuralLimitExceeded,
414                source_code: None,
415            }],
416            conservation: StrictRttiConservation {
417                attempted,
418                included: 0,
419                unknown: 0,
420                excluded: attempted,
421            },
422        };
423        batch.validate(limits)?;
424        return Ok(batch);
425    }
426    let mut decoder = VtableDecoder::new(macho, limits, symtab.symbols())?;
427    let mut records = Vec::new();
428    let mut gaps = Vec::new();
429    let mut included = 0;
430    let mut unknown = 0;
431    for (symbol, kind) in candidates {
432        if symbol.is_undefined() {
433            records.push(StrictVtableRecord::External {
434                symbol_kind: kind,
435                symbol: symbol.name.to_owned(),
436                library_ordinal: i32::from(symbol.library_ordinal() as i8),
437                weak: symbol.is_weak_ref(),
438            });
439            included += 1;
440            continue;
441        }
442        let decoded = if !symbol.is_defined() || symbol.value == 0 {
443            Err(Error::format("vtable symbol is not a nonzero definition"))
444        } else {
445            match kind {
446                ItaniumVtableSymbolKind::CompleteGroup
447                | ItaniumVtableSymbolKind::ConstructionGroup => decoder
448                    .decode_group(symbol, kind)
449                    .map(|record| StrictVtableRecord::Group {
450                        record: Box::new(record),
451                    }),
452                ItaniumVtableSymbolKind::Vtt => {
453                    decoder
454                        .decode_vtt(symbol)
455                        .map(|record| StrictVtableRecord::Vtt {
456                            record: Box::new(record),
457                        })
458                }
459            }
460        };
461        match decoded {
462            Ok(record) => {
463                records.push(record);
464                included += 1;
465            }
466            Err(error) => {
467                let code = if error.message().contains("limit") {
468                    StrictRttiGapCode::StructuralLimitExceeded
469                } else if error.kind == crate::CppErrorKind::InvalidAddress {
470                    StrictRttiGapCode::PointerUnresolved
471                } else if error.kind == crate::CppErrorKind::Unsupported {
472                    StrictRttiGapCode::FamilyUnsupported
473                } else {
474                    StrictRttiGapCode::RecordMalformed
475                };
476                gaps.push(StrictRttiGap {
477                    symbol: Some(symbol.name.to_owned()),
478                    field: match kind {
479                        ItaniumVtableSymbolKind::Vtt => "vtt",
480                        _ => "vtable_group",
481                    }
482                    .into(),
483                    code,
484                    source_code: Some(error.code().to_owned()),
485                });
486                unknown += 1;
487            }
488        }
489    }
490    let batch = StrictVtableBatch {
491        outcome: if gaps.is_empty() {
492            StrictRttiOutcome::Complete
493        } else {
494            StrictRttiOutcome::Rejected
495        },
496        records,
497        observations: decoder.reader.observations,
498        gaps,
499        conservation: StrictRttiConservation {
500            attempted,
501            included,
502            unknown,
503            excluded: 0,
504        },
505    };
506    batch.validate(limits)?;
507    Ok(batch)
508}
509
510fn preflight_batch(field: &str, limits: StrictVtableLimits) -> Result<StrictVtableBatch> {
511    let batch = StrictVtableBatch {
512        outcome: StrictRttiOutcome::Rejected,
513        records: Vec::new(),
514        observations: Vec::new(),
515        gaps: vec![StrictRttiGap {
516            symbol: None,
517            field: field.into(),
518            code: StrictRttiGapCode::StructuralLimitExceeded,
519            source_code: None,
520        }],
521        conservation: StrictRttiConservation {
522            attempted: 0,
523            included: 0,
524            unknown: 0,
525            excluded: 0,
526        },
527    };
528    batch.validate(limits)?;
529    Ok(batch)
530}
531
532/// Decode strict vtables from one borrowed thin-image byte source.
533pub fn decode_strict_vtables_from_source<S>(
534    source: &S,
535    limits: StrictVtableLimits,
536) -> Result<StrictVtableBatch>
537where
538    S: AsRef<[u8]> + ?Sized,
539{
540    let macho = crate::parse_source(source)?;
541    decode_strict_vtables(&macho, limits)
542}
543
544pub(super) fn classify_symbol(name: &str) -> Option<ItaniumVtableSymbolKind> {
545    let value = name.strip_prefix('_').unwrap_or(name);
546    if value.starts_with("_ZTV") {
547        Some(ItaniumVtableSymbolKind::CompleteGroup)
548    } else if value.starts_with("_ZTC") {
549        Some(ItaniumVtableSymbolKind::ConstructionGroup)
550    } else if value.starts_with("_ZTT") {
551        Some(ItaniumVtableSymbolKind::Vtt)
552    } else {
553        None
554    }
555}
556
557fn is_typeinfo_symbol(name: &str) -> bool {
558    let value = name.strip_prefix('_').unwrap_or(name);
559    value.starts_with("_ZTI")
560}
561
562fn is_data_special_name(name: &str) -> bool {
563    let value = name.strip_prefix('_').unwrap_or(name);
564    ["_ZTI", "_ZTS", "_ZTV", "_ZTT", "_ZTC"]
565        .iter()
566        .any(|prefix| value.starts_with(prefix))
567}
568
569fn exact_target_symbol(
570    target: &StrictPointerTarget,
571    symbols: &BTreeMap<u64, Vec<String>>,
572) -> Option<String> {
573    match target {
574        StrictPointerTarget::Local { va } => symbols.get(va).and_then(|names| {
575            names
576                .iter()
577                .find(|name| !is_data_special_name(name))
578                .or_else(|| names.first())
579                .cloned()
580        }),
581        StrictPointerTarget::External { symbol, .. } => Some(symbol.clone()),
582        StrictPointerTarget::Null => None,
583    }
584}
585
586fn classify_slot(
587    symbol: Option<&str>,
588    target: &StrictPointerTarget,
589) -> Result<(
590    ItaniumVtableSlotRole,
591    Option<ItaniumThunkAdjustment>,
592    Option<ItaniumThunkAdjustment>,
593)> {
594    if matches!(target, StrictPointerTarget::Null) {
595        return Ok((ItaniumVtableSlotRole::Null, None, None));
596    }
597    let Some(symbol) = symbol else {
598        return Ok((ItaniumVtableSlotRole::Unknown, None, None));
599    };
600    let abi = symbol.strip_prefix('_').unwrap_or(symbol);
601    if abi == "__cxa_pure_virtual" {
602        return Ok((ItaniumVtableSlotRole::PureVirtual, None, None));
603    }
604    if abi == "__cxa_deleted_virtual" {
605        return Ok((ItaniumVtableSlotRole::DeletedVirtual, None, None));
606    }
607    if abi.ends_with("D0Ev") {
608        return Ok((ItaniumVtableSlotRole::DeletingDestructor, None, None));
609    }
610    if abi.ends_with("D1Ev") {
611        return Ok((ItaniumVtableSlotRole::CompleteDestructor, None, None));
612    }
613    if abi.ends_with("D2Ev") {
614        return Ok((ItaniumVtableSlotRole::BaseDestructor, None, None));
615    }
616    if let Some(rest) = abi.strip_prefix("_ZTh") {
617        let (offset, _) = parse_number(rest)?;
618        return Ok((
619            ItaniumVtableSlotRole::NonVirtualThunk,
620            Some(ItaniumThunkAdjustment::NonVirtual { offset }),
621            None,
622        ));
623    }
624    if let Some(rest) = abi.strip_prefix("_ZTv") {
625        let (offset, rest) = parse_number(rest)?;
626        let rest = rest
627            .strip_prefix('_')
628            .ok_or_else(|| Error::format("virtual thunk lacks adjustment separator"))?;
629        let (virtual_offset, _) = parse_number(rest)?;
630        return Ok((
631            ItaniumVtableSlotRole::VirtualThunk,
632            Some(ItaniumThunkAdjustment::Virtual {
633                offset,
634                virtual_offset,
635            }),
636            None,
637        ));
638    }
639    if let Some(rest) = abi.strip_prefix("_ZTc") {
640        let (this_adjustment, rest) = parse_call_offset(rest)?;
641        let (return_adjustment, _) = parse_call_offset(rest)?;
642        return Ok((
643            ItaniumVtableSlotRole::CovariantThunk,
644            Some(this_adjustment),
645            Some(return_adjustment),
646        ));
647    }
648    Ok((ItaniumVtableSlotRole::Function, None, None))
649}
650
651fn parse_call_offset(value: &str) -> Result<(ItaniumThunkAdjustment, &str)> {
652    if let Some(rest) = value.strip_prefix('h') {
653        let (offset, rest) = parse_number(rest)?;
654        let rest = rest
655            .strip_prefix('_')
656            .ok_or_else(|| Error::format("non-virtual call offset lacks terminator"))?;
657        Ok((ItaniumThunkAdjustment::NonVirtual { offset }, rest))
658    } else if let Some(rest) = value.strip_prefix('v') {
659        let (offset, rest) = parse_number(rest)?;
660        let rest = rest
661            .strip_prefix('_')
662            .ok_or_else(|| Error::format("virtual call offset lacks first separator"))?;
663        let (virtual_offset, rest) = parse_number(rest)?;
664        let rest = rest
665            .strip_prefix('_')
666            .ok_or_else(|| Error::format("virtual call offset lacks terminator"))?;
667        Ok((
668            ItaniumThunkAdjustment::Virtual {
669                offset,
670                virtual_offset,
671            },
672            rest,
673        ))
674    } else {
675        Err(Error::format("unknown thunk call-offset discriminator"))
676    }
677}
678
679fn parse_number(value: &str) -> Result<(i64, &str)> {
680    let (negative, digits) = value
681        .strip_prefix('n')
682        .map_or((false, value), |rest| (true, rest));
683    let length = digits.bytes().take_while(u8::is_ascii_digit).count();
684    if length == 0 {
685        return Err(Error::format("thunk adjustment lacks a number"));
686    }
687    let magnitude = digits[..length]
688        .parse::<i64>()
689        .map_err(|_| Error::format("thunk adjustment overflows Int64"))?;
690    let number = if negative {
691        magnitude
692            .checked_neg()
693            .ok_or_else(|| Error::format("thunk adjustment negation overflows"))?
694    } else {
695        magnitude
696    };
697    Ok((number, &digits[length..]))
698}
699
700pub(super) fn validate_slot_semantics(slot: &ItaniumVtableSlotRecord) -> Result<()> {
701    let expected = classify_slot(slot.target_symbol.as_deref(), &slot.pointer.target)?;
702    if (
703        slot.role,
704        slot.this_adjustment.clone(),
705        slot.return_adjustment.clone(),
706    ) != expected
707    {
708        return Err(Error::format(
709            "strict vtable slot role disagrees with its target symbol",
710        ));
711    }
712    Ok(())
713}
714
715#[cfg(test)]
716mod tests {
717    use super::*;
718
719    #[test]
720    fn parses_itanium_thunk_adjustments_and_destructor_roles() {
721        let local = StrictPointerTarget::Local { va: 1 };
722        assert_eq!(
723            classify_slot(Some("__ZThn16_N1A1fEv"), &local).unwrap().1,
724            Some(ItaniumThunkAdjustment::NonVirtual { offset: -16 })
725        );
726        assert_eq!(
727            classify_slot(Some("__ZTvn8_n24_N1A1fEv"), &local)
728                .unwrap()
729                .1,
730            Some(ItaniumThunkAdjustment::Virtual {
731                offset: -8,
732                virtual_offset: -24
733            })
734        );
735        let covariant = classify_slot(Some("__ZTchn8_h16_N1A1fEv"), &local).unwrap();
736        assert_eq!(
737            covariant.1,
738            Some(ItaniumThunkAdjustment::NonVirtual { offset: -8 })
739        );
740        assert_eq!(
741            covariant.2,
742            Some(ItaniumThunkAdjustment::NonVirtual { offset: 16 })
743        );
744        assert_eq!(
745            classify_slot(Some("__ZN1AD0Ev"), &local).unwrap().0,
746            ItaniumVtableSlotRole::DeletingDestructor
747        );
748        assert_eq!(
749            classify_slot(Some("___cxa_pure_virtual"), &local)
750                .unwrap()
751                .0,
752            ItaniumVtableSlotRole::PureVirtual
753        );
754        assert_eq!(
755            classify_slot(Some("___cxa_deleted_virtual"), &local)
756                .unwrap()
757                .0,
758            ItaniumVtableSlotRole::DeletedVirtual
759        );
760    }
761}