Skip to main content

harfrust/hb/ot/
lookup.rs

1use crate::hb::{
2    hb_font_t,
3    ot_layout::TableIndex,
4    ot_layout_gsubgpos::{
5        Apply, SubtableExternalCache, SubtableExternalCacheMode, WouldApply, WouldApplyContext,
6        OT::hb_ot_apply_context_t,
7    },
8    set_digest::hb_set_digest_t,
9    GlyphInfo,
10};
11use alloc::vec::Vec;
12use read_fonts::{
13    tables::{
14        gpos::{
15            CursivePosFormat1, Gpos, MarkBasePosFormat1, MarkLigPosFormat1, MarkMarkPosFormat1,
16            PairPos, PairPosFormat1, PairPosFormat2, SinglePos, SinglePosFormat1, SinglePosFormat2,
17        },
18        gsub::{
19            AlternateSubstFormat1, ExtensionSubstFormat1, Gsub, LigatureSubstFormat1,
20            MultipleSubstFormat1, ReverseChainSingleSubstFormat1, SingleSubst, SingleSubstFormat1,
21            SingleSubstFormat2,
22        },
23        layout::{
24            ChainedSequenceContext, ChainedSequenceContextFormat1, ChainedSequenceContextFormat2,
25            ChainedSequenceContextFormat3, CoverageTable, Lookup, LookupFlag, SequenceContext,
26            SequenceContextFormat1, SequenceContextFormat2, SequenceContextFormat3,
27        },
28    },
29    FontData, FontRead, Offset, ReadError,
30};
31
32pub struct LookupData<'a> {
33    /// Offset of the lookup from the base of the layout table.
34    offset: usize,
35    /// True if the lookup comes from GSUB.
36    is_subst: bool,
37    /// Data of the layout table.
38    table_data: FontData<'a>,
39}
40
41pub trait LookupHost<'a> {
42    fn lookup_count(&self) -> u16;
43    fn lookup_data(&self, index: u16) -> Result<LookupData<'a>, ReadError>;
44}
45
46impl<'a> LookupHost<'a> for Gsub<'a> {
47    fn lookup_count(&self) -> u16 {
48        self.lookup_list()
49            .map(|list| list.lookup_count())
50            .unwrap_or_default()
51    }
52
53    fn lookup_data(&self, index: u16) -> Result<LookupData<'a>, ReadError> {
54        let list = self.lookup_list()?;
55        let offset = list
56            .lookup_offsets()
57            .get(index as usize)
58            .ok_or(ReadError::OutOfBounds)?
59            .get()
60            .to_usize()
61            + self.lookup_list_offset().to_usize();
62        Ok(LookupData {
63            offset,
64            is_subst: true,
65            table_data: self.offset_data(),
66        })
67    }
68}
69
70impl<'a> LookupHost<'a> for Gpos<'a> {
71    fn lookup_count(&self) -> u16 {
72        self.lookup_list()
73            .map(|list| list.lookup_count())
74            .unwrap_or_default()
75    }
76
77    fn lookup_data(&self, index: u16) -> Result<LookupData<'a>, ReadError> {
78        let list = self.lookup_list()?;
79        let offset = list
80            .lookup_offsets()
81            .get(index as usize)
82            .ok_or(ReadError::OutOfBounds)?
83            .get()
84            .to_usize()
85            + self.lookup_list_offset().to_usize();
86        Ok(LookupData {
87            offset,
88            is_subst: false,
89            table_data: self.offset_data(),
90        })
91    }
92}
93
94#[cfg(feature = "std")]
95mod cache {
96    use super::{LookupHost, LookupInfo};
97    use std::sync::OnceLock;
98
99    #[derive(Default)]
100    pub(crate) struct LookupCache {
101        lookups: Vec<OnceLock<Option<Box<LookupInfo>>>>,
102    }
103
104    impl LookupCache {
105        pub fn new<'a>(host: &impl LookupHost<'a>) -> Self {
106            let mut lookups = Vec::new();
107            lookups.resize_with(host.lookup_count() as usize, Default::default);
108            Self { lookups }
109        }
110
111        pub fn get<'a>(&self, host: &impl LookupHost<'a>, index: u16) -> Option<&LookupInfo> {
112            self.lookups
113                .get(index as usize)?
114                .get_or_init(|| {
115                    host.lookup_data(index)
116                        .ok()
117                        .and_then(|data| LookupInfo::new(&data))
118                        .map(Box::new)
119                })
120                .as_ref()
121                .map(|v| &**v)
122        }
123    }
124}
125
126#[cfg(not(feature = "std"))]
127mod cache {
128    use super::{LookupHost, LookupInfo, Vec};
129
130    #[derive(Default)]
131    pub(crate) struct LookupCache {
132        lookups: Vec<Option<LookupInfo>>,
133    }
134
135    impl LookupCache {
136        pub fn new<'a>(host: &impl LookupHost<'a>) -> Self {
137            let count = host.lookup_count();
138            let mut lookups = Vec::with_capacity(count as usize);
139            for i in 0..count {
140                lookups.push(
141                    host.lookup_data(i)
142                        .ok()
143                        .and_then(|data| LookupInfo::new(&data)),
144                );
145            }
146            Self { lookups }
147        }
148
149        pub fn get<'a>(&self, _host: &impl LookupHost<'a>, index: u16) -> Option<&LookupInfo> {
150            self.lookups.get(index as usize)?.as_ref()
151        }
152    }
153}
154
155pub(crate) use cache::LookupCache;
156
157fn is_extension_lookup_type(is_subst: bool, lookup_type: u8) -> bool {
158    (is_subst && lookup_type == 7) || (!is_subst && lookup_type == 9)
159}
160
161fn is_reversed(table_data: FontData, lookup: &Lookup<()>, lookup_offset: usize) -> Option<bool> {
162    match lookup.lookup_type() {
163        // Reverse chain context
164        8 => Some(true),
165        // Extension table
166        7 => {
167            let offset = lookup_offset + lookup.subtable_offsets().first()?.get().to_usize();
168            let data = table_data.split_off(offset)?;
169            let ext = ExtensionSubstFormat1::<()>::read(data).ok()?;
170            if is_extension_lookup_type(true, ext.extension_lookup_type() as u8) {
171                return None;
172            }
173            Some(ext.extension_lookup_type() == 8)
174        }
175        _ => Some(false),
176    }
177}
178
179/// Cached information about a lookup.
180#[derive(Default)]
181pub struct LookupInfo {
182    pub props: u32,
183    pub is_subst: bool,
184    pub is_reversed: bool,
185    pub digest: hb_set_digest_t,
186    pub subtable_cache_user_idx: Option<usize>,
187    pub subtables: Vec<SubtableInfo>,
188}
189
190impl LookupInfo {
191    pub fn new(data: &LookupData) -> Option<Self> {
192        let mut info = Self {
193            is_subst: data.is_subst,
194            ..Default::default()
195        };
196        let lookup_data = data.table_data.split_off(data.offset)?;
197        let lookup: Lookup<()> = Lookup::read(lookup_data).ok()?;
198        let lookup_type = lookup.lookup_type();
199        let lookup_flag = lookup.lookup_flag();
200        info.props = u32::from(lookup.lookup_flag().to_bits());
201        if lookup_flag.to_bits() & LookupFlag::USE_MARK_FILTERING_SET.to_bits() != 0 {
202            info.props |= (lookup.mark_filtering_set().unwrap_or_default() as u32) << 16;
203        }
204        if data.is_subst {
205            info.is_reversed =
206                is_reversed(data.table_data, &lookup, data.offset).unwrap_or_default();
207        }
208        let mut subtable_cache_user_cost = 0;
209        info.subtables.reserve(lookup.sub_table_count() as usize);
210        for (idx, subtable_offset) in lookup.subtable_offsets().iter().enumerate() {
211            let cache_mode = if idx < 8 {
212                SubtableExternalCacheMode::Full
213            } else {
214                SubtableExternalCacheMode::Small
215            };
216            let subtable_offset = subtable_offset.get().to_usize() + data.offset;
217            if let Some((subtable_info, cache_cost)) = SubtableInfo::new(
218                data.table_data,
219                subtable_offset as u32,
220                data.is_subst,
221                lookup_type as u8,
222                cache_mode,
223            ) {
224                info.digest.union(&subtable_info.digest);
225                if cache_cost > subtable_cache_user_cost {
226                    info.subtable_cache_user_idx = Some(info.subtables.len());
227                    subtable_cache_user_cost = cache_cost;
228                }
229                info.subtables.push(subtable_info);
230            }
231        }
232        info.subtables.shrink_to_fit();
233        Some(info)
234    }
235
236    pub fn props(&self) -> u32 {
237        self.props
238    }
239
240    pub fn is_reverse(&self) -> bool {
241        self.is_reversed
242    }
243
244    pub fn digest(&self) -> &hb_set_digest_t {
245        &self.digest
246    }
247}
248
249impl LookupInfo {
250    #[inline]
251    pub(crate) fn apply(
252        &self,
253        ctx: &mut hb_ot_apply_context_t,
254        table_data: &[u8],
255        use_hot_subtable_cache: bool,
256    ) -> Option<()> {
257        let glyph = ctx.buffer.cur(0).glyph_id;
258        for (subtable_idx, subtable_info) in self.subtables.iter().enumerate() {
259            if !subtable_info.digest.may_have(glyph) {
260                continue;
261            }
262            let is_cached =
263                use_hot_subtable_cache && (self.subtable_cache_user_idx == Some(subtable_idx));
264            if subtable_info.apply(ctx, table_data, is_cached).is_some() {
265                return Some(());
266            }
267        }
268        None
269    }
270
271    pub(crate) fn cache_enter(&self, ctx: &mut hb_ot_apply_context_t) -> bool {
272        let Some(idx) = self.subtable_cache_user_idx else {
273            return false;
274        };
275        let Some(subtable_info) = self.subtables.get(idx) else {
276            return false;
277        };
278        if matches!(
279            subtable_info.kind,
280            SubtableKind::ContextFormat2 | SubtableKind::ChainedContextFormat2
281        ) {
282            cache_enter(ctx)
283        } else {
284            false
285        }
286    }
287    pub(crate) fn cache_leave(&self, ctx: &mut hb_ot_apply_context_t) {
288        let Some(idx) = self.subtable_cache_user_idx else {
289            return;
290        };
291        let Some(subtable_info) = self.subtables.get(idx) else {
292            return;
293        };
294        if matches!(
295            subtable_info.kind,
296            SubtableKind::ContextFormat2 | SubtableKind::ChainedContextFormat2
297        ) {
298            cache_leave(ctx);
299        }
300    }
301}
302
303impl LookupInfo {
304    pub fn would_apply(&self, face: &hb_font_t, ctx: &WouldApplyContext) -> Option<bool> {
305        let glyph = ctx.glyphs[0];
306        if !self.digest.may_have(glyph.into()) {
307            return Some(false);
308        }
309        let table_index = if self.is_subst {
310            TableIndex::GSUB
311        } else {
312            TableIndex::GPOS
313        };
314        let table_data = face.ot_tables.table_data(table_index)?;
315        for subtable_info in &self.subtables {
316            if !subtable_info.digest.may_have(glyph.into()) {
317                continue;
318            }
319            let Some(data) = table_data.get(subtable_info.offset as usize..) else {
320                continue;
321            };
322            let data = FontData::new(data);
323            let result = match subtable_info.kind {
324                SubtableKind::SingleSubst1 => {
325                    SingleSubstFormat1::read(data).map(|t| t.would_apply(ctx))
326                }
327                SubtableKind::SingleSubst2 => {
328                    SingleSubstFormat2::read(data).map(|t| t.would_apply(ctx))
329                }
330                SubtableKind::MultipleSubst1 => {
331                    MultipleSubstFormat1::read(data).map(|t| t.would_apply(ctx))
332                }
333                SubtableKind::AlternateSubst1 => {
334                    AlternateSubstFormat1::read(data).map(|t| t.would_apply(ctx))
335                }
336                SubtableKind::LigatureSubst1 => {
337                    LigatureSubstFormat1::read(data).map(|t| t.would_apply(ctx))
338                }
339                SubtableKind::ReverseChainContext => {
340                    ReverseChainSingleSubstFormat1::read(data).map(|t| t.would_apply(ctx))
341                }
342                SubtableKind::ContextFormat1 => {
343                    SequenceContextFormat1::read(data).map(|t| t.would_apply(ctx))
344                }
345                SubtableKind::ContextFormat2 => {
346                    SequenceContextFormat2::read(data).map(|t| t.would_apply(ctx))
347                }
348                SubtableKind::ContextFormat3 => {
349                    SequenceContextFormat3::read(data).map(|t| t.would_apply(ctx))
350                }
351                SubtableKind::ChainedContextFormat1 => {
352                    ChainedSequenceContextFormat1::read(data).map(|t| t.would_apply(ctx))
353                }
354                SubtableKind::ChainedContextFormat2 => {
355                    ChainedSequenceContextFormat2::read(data).map(|t| t.would_apply(ctx))
356                }
357                SubtableKind::ChainedContextFormat3 => {
358                    ChainedSequenceContextFormat3::read(data).map(|t| t.would_apply(ctx))
359                }
360                _ => continue,
361            };
362            if result == Ok(true) {
363                return Some(true);
364            }
365        }
366        None
367    }
368}
369
370/// Cached information about a subtable.
371pub struct SubtableInfo {
372    /// The fully resolved type of the subtable.
373    pub kind: SubtableKind,
374    /// Byte offset to the subtable from the base of the GSUB or GPOS
375    /// table.
376    pub offset: u32,
377    pub digest: hb_set_digest_t,
378    pub apply_fns: [SubtableApplyFn; 2],
379    pub external_cache: SubtableExternalCache,
380}
381
382pub type SubtableApplyFn =
383    fn(&mut hb_ot_apply_context_t, &SubtableExternalCache, &[u8]) -> Option<()>;
384
385impl SubtableInfo {
386    #[inline]
387    pub(crate) fn apply(
388        &self,
389        ctx: &mut hb_ot_apply_context_t,
390        table_data: &[u8],
391        is_cached: bool,
392    ) -> Option<()> {
393        let subtable_data = table_data.get(self.offset as usize..)?;
394        self.apply_fns[is_cached as usize](ctx, &self.external_cache, subtable_data)
395    }
396}
397
398macro_rules! apply_fns {
399    ($apply:ident, $apply_cached:ident, $ty:ident) => {
400        fn $apply(
401            ctx: &mut hb_ot_apply_context_t,
402            external_cache: &SubtableExternalCache,
403            table_data: &[u8],
404        ) -> Option<()> {
405            let t = $ty::read(FontData::new(table_data)).ok()?;
406            t.apply_with_external_cache(ctx, external_cache)
407        }
408
409        fn $apply_cached(
410            ctx: &mut hb_ot_apply_context_t,
411            external_cache: &SubtableExternalCache,
412            table_data: &[u8],
413        ) -> Option<()> {
414            let t = $ty::read(FontData::new(table_data)).ok()?;
415            t.apply_cached(ctx, external_cache)
416        }
417    };
418}
419
420apply_fns!(single_subst1, single_subst1_cached, SingleSubstFormat1);
421apply_fns!(single_subst2, single_subst2_cached, SingleSubstFormat2);
422apply_fns!(
423    multiple_subst1,
424    multiple_subst1_cached,
425    MultipleSubstFormat1
426);
427apply_fns!(
428    alternate_subst1,
429    alternate_subst1_cached,
430    AlternateSubstFormat1
431);
432apply_fns!(
433    ligature_subst1,
434    ligature_subst1_cached,
435    LigatureSubstFormat1
436);
437apply_fns!(single_pos1, single_pos1_cached, SinglePosFormat1);
438apply_fns!(single_pos2, single_pos2_cached, SinglePosFormat2);
439apply_fns!(pair_pos1, pair_pos1_cached, PairPosFormat1);
440apply_fns!(pair_pos2, pair_pos2_cached, PairPosFormat2);
441apply_fns!(cursive_pos1, cursive_pos1_cached, CursivePosFormat1);
442apply_fns!(mark_base_pos1, mark_base_pos1_cached, MarkBasePosFormat1);
443apply_fns!(mark_mark_pos1, mark_mark_pos1_cached, MarkMarkPosFormat1);
444apply_fns!(mark_lig_pos1, mark_lig_pos1_cached, MarkLigPosFormat1);
445apply_fns!(context1, context1_cached, SequenceContextFormat1);
446apply_fns!(context2, context2_cached, SequenceContextFormat2);
447apply_fns!(context3, context3_cached, SequenceContextFormat3);
448apply_fns!(
449    chained_context1,
450    chained_context1_cached,
451    ChainedSequenceContextFormat1
452);
453apply_fns!(
454    chained_context2,
455    chained_context2_cached,
456    ChainedSequenceContextFormat2
457);
458apply_fns!(
459    chained_context3,
460    chained_context3_cached,
461    ChainedSequenceContextFormat3
462);
463apply_fns!(
464    rev_chain_single_subst1,
465    rev_chain_single_subst1_cached,
466    ReverseChainSingleSubstFormat1
467);
468
469/// All possible subtables in a lookup.
470#[derive(Copy, Clone, PartialEq, Eq, Debug)]
471pub enum SubtableKind {
472    SingleSubst1,
473    SingleSubst2,
474    MultipleSubst1,
475    AlternateSubst1,
476    LigatureSubst1,
477    SinglePos1,
478    SinglePos2,
479    PairPos1,
480    PairPos2,
481    CursivePos1,
482    MarkBasePos1,
483    MarkMarkPos1,
484    MarkLigPos1,
485    ContextFormat1,
486    ContextFormat2,
487    ContextFormat3,
488    ChainedContextFormat1,
489    ChainedContextFormat2,
490    ChainedContextFormat3,
491    ReverseChainContext,
492}
493
494impl SubtableInfo {
495    fn new(
496        table_data: FontData,
497        subtable_offset: u32,
498        is_subst: bool,
499        lookup_type: u8,
500        cache_mode: SubtableExternalCacheMode,
501    ) -> Option<(Self, u32)> {
502        let data = table_data.split_off(subtable_offset as usize)?;
503        let maybe_external_cache = |s: &dyn Apply| s.external_cache_create(cache_mode);
504        let (kind, (external_cache, cache_cost, coverage), apply_fns): (
505            SubtableKind,
506            (SubtableExternalCache, u32, CoverageTable),
507            [SubtableApplyFn; 2],
508        ) = match (is_subst, lookup_type) {
509            (true, 1) => match SingleSubst::read(data).ok()? {
510                SingleSubst::Format1(s) => (
511                    SubtableKind::SingleSubst1,
512                    (maybe_external_cache(&s), s.cache_cost(), s.coverage().ok()?),
513                    [single_subst1, single_subst1_cached as _],
514                ),
515                SingleSubst::Format2(s) => (
516                    SubtableKind::SingleSubst2,
517                    (maybe_external_cache(&s), s.cache_cost(), s.coverage().ok()?),
518                    [single_subst2, single_subst2_cached as _],
519                ),
520            },
521            (false, 1) => match SinglePos::read(data).ok()? {
522                SinglePos::Format1(s) => (
523                    SubtableKind::SinglePos1,
524                    (maybe_external_cache(&s), s.cache_cost(), s.coverage().ok()?),
525                    [single_pos1, single_pos1_cached as _],
526                ),
527                SinglePos::Format2(s) => (
528                    SubtableKind::SinglePos2,
529                    (maybe_external_cache(&s), s.cache_cost(), s.coverage().ok()?),
530                    [single_pos2, single_pos2_cached as _],
531                ),
532            },
533            (true, 2) => (
534                SubtableKind::MultipleSubst1,
535                MultipleSubstFormat1::read(data).ok().and_then(|t| {
536                    Some((maybe_external_cache(&t), t.cache_cost(), t.coverage().ok()?))
537                })?,
538                [multiple_subst1, multiple_subst1_cached as _],
539            ),
540            (false, 2) => match PairPos::read(data).ok()? {
541                PairPos::Format1(s) => (
542                    SubtableKind::PairPos1,
543                    (maybe_external_cache(&s), s.cache_cost(), s.coverage().ok()?),
544                    [pair_pos1, pair_pos1_cached as _],
545                ),
546                PairPos::Format2(s) => (
547                    SubtableKind::PairPos2,
548                    (maybe_external_cache(&s), s.cache_cost(), s.coverage().ok()?),
549                    [pair_pos2, pair_pos2_cached as _],
550                ),
551            },
552            (true, 3) => (
553                SubtableKind::AlternateSubst1,
554                AlternateSubstFormat1::read(data).ok().and_then(|t| {
555                    Some((maybe_external_cache(&t), t.cache_cost(), t.coverage().ok()?))
556                })?,
557                [alternate_subst1, alternate_subst1_cached as _],
558            ),
559            (false, 3) => (
560                SubtableKind::CursivePos1,
561                CursivePosFormat1::read(data).ok().and_then(|t| {
562                    Some((maybe_external_cache(&t), t.cache_cost(), t.coverage().ok()?))
563                })?,
564                [cursive_pos1, cursive_pos1_cached as _],
565            ),
566            (true, 4) => (
567                SubtableKind::LigatureSubst1,
568                LigatureSubstFormat1::read(data).ok().and_then(|t| {
569                    Some((maybe_external_cache(&t), t.cache_cost(), t.coverage().ok()?))
570                })?,
571                [ligature_subst1, ligature_subst1_cached as _],
572            ),
573            (false, 4) => (
574                SubtableKind::MarkBasePos1,
575                MarkBasePosFormat1::read(data).ok().and_then(|t| {
576                    Some((
577                        maybe_external_cache(&t),
578                        t.cache_cost(),
579                        t.mark_coverage().ok()?,
580                    ))
581                })?,
582                [mark_base_pos1, mark_base_pos1_cached as _],
583            ),
584            (true, 5) | (false, 7) => match SequenceContext::read(data).ok()? {
585                SequenceContext::Format1(s) => (
586                    SubtableKind::ContextFormat1,
587                    (maybe_external_cache(&s), s.cache_cost(), s.coverage().ok()?),
588                    [context1, context1_cached as _],
589                ),
590                SequenceContext::Format2(s) => (
591                    SubtableKind::ContextFormat2,
592                    (maybe_external_cache(&s), s.cache_cost(), s.coverage().ok()?),
593                    [context2, context2_cached as _],
594                ),
595                SequenceContext::Format3(s) => (
596                    SubtableKind::ContextFormat3,
597                    (
598                        maybe_external_cache(&s),
599                        s.cache_cost(),
600                        s.coverages().get(0).ok()?,
601                    ),
602                    [context3, context3_cached as _],
603                ),
604            },
605            (false, 5) => (
606                SubtableKind::MarkLigPos1,
607                MarkLigPosFormat1::read(data).ok().and_then(|t| {
608                    Some((
609                        maybe_external_cache(&t),
610                        t.cache_cost(),
611                        t.mark_coverage().ok()?,
612                    ))
613                })?,
614                [mark_lig_pos1, mark_lig_pos1_cached as _],
615            ),
616            (true, 6) | (false, 8) => match ChainedSequenceContext::read(data).ok()? {
617                ChainedSequenceContext::Format1(s) => (
618                    SubtableKind::ChainedContextFormat1,
619                    (maybe_external_cache(&s), s.cache_cost(), s.coverage().ok()?),
620                    [chained_context1, chained_context1_cached as _],
621                ),
622                ChainedSequenceContext::Format2(s) => (
623                    SubtableKind::ChainedContextFormat2,
624                    (maybe_external_cache(&s), s.cache_cost(), s.coverage().ok()?),
625                    [chained_context2, chained_context2_cached as _],
626                ),
627                ChainedSequenceContext::Format3(s) => (
628                    SubtableKind::ChainedContextFormat3,
629                    (
630                        maybe_external_cache(&s),
631                        s.cache_cost(),
632                        s.input_coverages().get(0).ok()?,
633                    ),
634                    [chained_context3, chained_context3_cached as _],
635                ),
636            },
637            (true, 7) | (false, 9) => {
638                let ext = ExtensionSubstFormat1::<'_, ()>::read(data).ok()?;
639                let ext_type = ext.extension_lookup_type() as u8;
640                if is_extension_lookup_type(is_subst, ext_type) {
641                    return None;
642                }
643                let ext_offset = ext.extension_offset().to_u32();
644                return Self::new(
645                    table_data,
646                    subtable_offset.checked_add(ext_offset)?,
647                    is_subst,
648                    ext_type,
649                    cache_mode,
650                );
651            }
652            (false, 6) => (
653                SubtableKind::MarkMarkPos1,
654                MarkMarkPosFormat1::read(data).ok().and_then(|t| {
655                    Some((
656                        maybe_external_cache(&t),
657                        t.cache_cost(),
658                        t.mark1_coverage().ok()?,
659                    ))
660                })?,
661                [mark_mark_pos1, mark_mark_pos1_cached as _],
662            ),
663            (true, 8) => (
664                SubtableKind::ReverseChainContext,
665                ReverseChainSingleSubstFormat1::read(data)
666                    .ok()
667                    .and_then(|t| {
668                        Some((maybe_external_cache(&t), t.cache_cost(), t.coverage().ok()?))
669                    })?,
670                [rev_chain_single_subst1, rev_chain_single_subst1_cached as _],
671            ),
672            _ => return None,
673        };
674        let mut digest = hb_set_digest_t::new();
675        digest.add_coverage(&coverage);
676        Some((
677            SubtableInfo {
678                kind,
679                offset: subtable_offset,
680                digest,
681                apply_fns,
682                external_cache,
683            },
684            cache_cost,
685        ))
686    }
687}
688
689fn cache_enter(ctx: &mut hb_ot_apply_context_t) -> bool {
690    if !ctx.buffer.try_allocate_var(GlyphInfo::SYLLABLE_VAR) {
691        return false;
692    }
693    for info in &mut ctx.buffer.info {
694        info.set_syllable(255);
695    }
696    ctx.new_syllables = Some(255);
697    true
698}
699
700fn cache_leave(ctx: &mut hb_ot_apply_context_t) {
701    ctx.new_syllables = None;
702    ctx.buffer.deallocate_var(GlyphInfo::SYLLABLE_VAR);
703}
704
705#[cfg(test)]
706mod tests {
707    use super::*;
708
709    fn lookup_with_recursive_extension(lookup_type: u16) -> [u8; 16] {
710        let mut data = [0; 16];
711        // Lookup table.
712        data[0..2].copy_from_slice(&lookup_type.to_be_bytes());
713        data[4..6].copy_from_slice(&1u16.to_be_bytes());
714        data[6..8].copy_from_slice(&8u16.to_be_bytes());
715        // ExtensionSubstFormat1/ExtensionPosFormat1 subtable.
716        data[8..10].copy_from_slice(&1u16.to_be_bytes());
717        data[10..12].copy_from_slice(&lookup_type.to_be_bytes());
718        data
719    }
720
721    #[test]
722    fn gsub_extension_lookup_cannot_target_extension_lookup() {
723        let data = lookup_with_recursive_extension(7);
724        let lookup = LookupData {
725            offset: 0,
726            is_subst: true,
727            table_data: FontData::new(&data),
728        };
729        let info = LookupInfo::new(&lookup).unwrap();
730
731        assert!(info.subtables.is_empty());
732    }
733
734    #[test]
735    fn gpos_extension_lookup_cannot_target_extension_lookup() {
736        let data = lookup_with_recursive_extension(9);
737        let lookup = LookupData {
738            offset: 0,
739            is_subst: false,
740            table_data: FontData::new(&data),
741        };
742        let info = LookupInfo::new(&lookup).unwrap();
743
744        assert!(info.subtables.is_empty());
745    }
746}