Skip to main content

oxideav_otf/tables/
layout.rs

1//! Common OpenType Layout tables shared by GSUB and GPOS.
2//!
3//! Spec: `docs/text/opentype/otspec-chapter2-common-layout-tables.html`
4//! ("OpenType Layout common table formats"). The chapter defines four
5//! data structures that both the Glyph Substitution (GSUB) and Glyph
6//! Positioning (GPOS) tables consult via header offsets:
7//!
8//! * [`ScriptList`] — array of `ScriptRecord` (`scriptTag`,
9//!   `scriptOffset`), sorted alphabetically by tag.
10//! * [`Script`] — `defaultLangSysOffset` plus an array of
11//!   `LangSysRecord` (`langSysTag`, `langSysOffset`).
12//! * [`LangSys`] — `requiredFeatureIndex` plus an array of
13//!   `featureIndices[]` into the FeatureList.
14//! * [`FeatureList`] — array of `FeatureRecord` (`featureTag`,
15//!   `featureOffset`).
16//! * [`Feature`] — `featureParamsOffset` plus an array of
17//!   `lookupListIndices[]` into the LookupList.
18//! * [`LookupList`] — array of `lookupOffsets[]`.
19//! * [`Lookup`] — `lookupType`, [`LookupFlag`], array of
20//!   `subtableOffsets[]`, and an optional `markFilteringSet` (present
21//!   when `LookupFlag::USE_MARK_FILTERING_SET` is set).
22//!
23//! Every accessor is read-only and zero-copy: each `parse` call
24//! validates the on-disk shape and stashes the borrowed slice; field
25//! lookups decode their two- or four-byte windows on every call. The
26//! types are `Copy` so they can be passed around freely.
27
28use crate::parser::{read_u16, read_u32};
29use crate::Error;
30
31// ---------------------------------------------------------------------------
32// Tag helper
33// ---------------------------------------------------------------------------
34
35/// 4-byte OpenType tag (ScriptTag, LangSysTag, FeatureTag) read from a
36/// fixed offset. The four bytes are returned verbatim — the spec does
37/// not mandate ASCII even though every well-known tag happens to be
38/// ASCII printable.
39#[inline]
40fn read_tag(bytes: &[u8], off: usize) -> Result<[u8; 4], Error> {
41    if bytes.len() < off + 4 {
42        return Err(Error::UnexpectedEof);
43    }
44    Ok([bytes[off], bytes[off + 1], bytes[off + 2], bytes[off + 3]])
45}
46
47// ---------------------------------------------------------------------------
48// ScriptList
49// ---------------------------------------------------------------------------
50
51/// Parsed `ScriptList` table (chapter 2 §"ScriptList table").
52///
53/// Layout:
54/// ```text
55///   0 / 2 / scriptCount
56///   2 / 6 / scriptRecords[scriptCount]   // (tag[4] + scriptOffset[2])
57/// ```
58///
59/// The records are sorted alphabetically by tag, so [`Self::find`]
60/// runs a binary search.
61#[derive(Debug, Clone, Copy)]
62pub struct ScriptList<'a> {
63    bytes: &'a [u8],
64    count: u16,
65}
66
67impl<'a> ScriptList<'a> {
68    /// Validate the header and the trailing `scriptRecords[]` array.
69    pub fn parse(bytes: &'a [u8]) -> Result<Self, Error> {
70        let count = read_u16(bytes, 0)?;
71        let need = 2usize
72            .checked_add(
73                (count as usize)
74                    .checked_mul(6)
75                    .ok_or(Error::BadStructure("ScriptList scriptCount overflow"))?,
76            )
77            .ok_or(Error::BadStructure("ScriptList length overflow"))?;
78        if bytes.len() < need {
79            return Err(Error::UnexpectedEof);
80        }
81        Ok(Self { bytes, count })
82    }
83
84    /// Number of `ScriptRecord`s.
85    pub fn count(&self) -> u16 {
86        self.count
87    }
88
89    /// `true` when the list is empty.
90    pub fn is_empty(&self) -> bool {
91        self.count == 0
92    }
93
94    /// Raw 4-byte tag at record index `i`, or `None` if out of range.
95    pub fn tag(&self, i: u16) -> Option<[u8; 4]> {
96        if i >= self.count {
97            return None;
98        }
99        let off = 2 + (i as usize) * 6;
100        read_tag(self.bytes, off).ok()
101    }
102
103    /// Parse the [`Script`] table referenced by record `i`. Returns
104    /// `None` for an out-of-range index, an `Err` if the offset
105    /// resolves outside the ScriptList or the Script header itself is
106    /// truncated.
107    pub fn script(&self, i: u16) -> Option<Result<Script<'a>, Error>> {
108        if i >= self.count {
109            return None;
110        }
111        let off = 2 + (i as usize) * 6;
112        let script_off = read_u16(self.bytes, off + 4).ok()? as usize;
113        Some(self.script_at(script_off))
114    }
115
116    fn script_at(&self, off: usize) -> Result<Script<'a>, Error> {
117        if off == 0 || off >= self.bytes.len() {
118            return Err(Error::BadStructure("ScriptList: scriptOffset out of range"));
119        }
120        Script::parse(&self.bytes[off..])
121    }
122
123    /// Look up a Script by tag. Returns `None` when the tag is absent.
124    pub fn find(&self, tag: &[u8; 4]) -> Option<Result<Script<'a>, Error>> {
125        // ScriptRecord array is sorted alphabetically by tag.
126        let mut lo = 0i32;
127        let mut hi = self.count as i32 - 1;
128        while lo <= hi {
129            let mid = (lo + hi) / 2;
130            let mid_tag = self.tag(mid as u16)?;
131            match mid_tag.cmp(tag) {
132                std::cmp::Ordering::Equal => {
133                    return self.script(mid as u16);
134                }
135                std::cmp::Ordering::Less => lo = mid + 1,
136                std::cmp::Ordering::Greater => hi = mid - 1,
137            }
138        }
139        None
140    }
141
142    /// Iterate over `(tag, script_result)` pairs in on-disk order.
143    pub fn iter(&self) -> ScriptListIter<'a> {
144        ScriptListIter {
145            list: *self,
146            next: 0,
147        }
148    }
149}
150
151/// Iterator yielded by [`ScriptList::iter`].
152#[derive(Debug, Clone)]
153pub struct ScriptListIter<'a> {
154    list: ScriptList<'a>,
155    next: u16,
156}
157
158impl<'a> Iterator for ScriptListIter<'a> {
159    type Item = ([u8; 4], Result<Script<'a>, Error>);
160    fn next(&mut self) -> Option<Self::Item> {
161        if self.next >= self.list.count {
162            return None;
163        }
164        let i = self.next;
165        self.next += 1;
166        let tag = self.list.tag(i)?;
167        let script = self.list.script(i)?;
168        Some((tag, script))
169    }
170}
171
172// ---------------------------------------------------------------------------
173// Script
174// ---------------------------------------------------------------------------
175
176/// Parsed `Script` table (chapter 2 §"Script table").
177///
178/// Layout:
179/// ```text
180///   0 / 2 / defaultLangSysOffset     (Offset16, may be NULL)
181///   2 / 2 / langSysCount
182///   4 / 6 / langSysRecords[]         (tag[4] + langSysOffset[2])
183/// ```
184#[derive(Debug, Clone, Copy)]
185pub struct Script<'a> {
186    bytes: &'a [u8],
187    default_off: u16,
188    count: u16,
189}
190
191impl<'a> Script<'a> {
192    pub fn parse(bytes: &'a [u8]) -> Result<Self, Error> {
193        let default_off = read_u16(bytes, 0)?;
194        let count = read_u16(bytes, 2)?;
195        let need = 4usize
196            .checked_add(
197                (count as usize)
198                    .checked_mul(6)
199                    .ok_or(Error::BadStructure("Script langSysCount overflow"))?,
200            )
201            .ok_or(Error::BadStructure("Script length overflow"))?;
202        if bytes.len() < need {
203            return Err(Error::UnexpectedEof);
204        }
205        Ok(Self {
206            bytes,
207            default_off,
208            count,
209        })
210    }
211
212    /// `true` iff a default `LangSys` is present (`defaultLangSysOffset != 0`).
213    pub fn has_default_lang_sys(&self) -> bool {
214        self.default_off != 0
215    }
216
217    /// Number of language-system records (excluding the default).
218    pub fn lang_sys_count(&self) -> u16 {
219        self.count
220    }
221
222    /// Parse the default `LangSys`, or `None` if not present.
223    pub fn default_lang_sys(&self) -> Option<Result<LangSys<'a>, Error>> {
224        if self.default_off == 0 {
225            return None;
226        }
227        let off = self.default_off as usize;
228        if off >= self.bytes.len() {
229            return Some(Err(Error::BadStructure(
230                "Script: defaultLangSysOffset out of range",
231            )));
232        }
233        Some(LangSys::parse(&self.bytes[off..]))
234    }
235
236    /// Tag of `LangSysRecord` `i`.
237    pub fn lang_sys_tag(&self, i: u16) -> Option<[u8; 4]> {
238        if i >= self.count {
239            return None;
240        }
241        let off = 4 + (i as usize) * 6;
242        read_tag(self.bytes, off).ok()
243    }
244
245    /// Parse the `LangSys` at record `i`.
246    pub fn lang_sys(&self, i: u16) -> Option<Result<LangSys<'a>, Error>> {
247        if i >= self.count {
248            return None;
249        }
250        let off = 4 + (i as usize) * 6;
251        let lang_off = read_u16(self.bytes, off + 4).ok()? as usize;
252        if lang_off == 0 || lang_off >= self.bytes.len() {
253            return Some(Err(Error::BadStructure(
254                "Script: langSysOffset out of range",
255            )));
256        }
257        Some(LangSys::parse(&self.bytes[lang_off..]))
258    }
259
260    /// Binary-search the `LangSysRecord` array by tag.
261    pub fn find_lang_sys(&self, tag: &[u8; 4]) -> Option<Result<LangSys<'a>, Error>> {
262        let mut lo = 0i32;
263        let mut hi = self.count as i32 - 1;
264        while lo <= hi {
265            let mid = (lo + hi) / 2;
266            let mid_tag = self.lang_sys_tag(mid as u16)?;
267            match mid_tag.cmp(tag) {
268                std::cmp::Ordering::Equal => return self.lang_sys(mid as u16),
269                std::cmp::Ordering::Less => lo = mid + 1,
270                std::cmp::Ordering::Greater => hi = mid - 1,
271            }
272        }
273        None
274    }
275}
276
277// ---------------------------------------------------------------------------
278// LangSys
279// ---------------------------------------------------------------------------
280
281/// "No required feature" sentinel for [`LangSys::required_feature_index`].
282pub const NO_REQUIRED_FEATURE: u16 = 0xFFFF;
283
284/// Parsed `LangSys` table (chapter 2 §"Language system table").
285///
286/// Layout:
287/// ```text
288///   0 / 2 / lookupOrderOffset     (reserved — must be 0)
289///   2 / 2 / requiredFeatureIndex  (0xFFFF = none)
290///   4 / 2 / featureIndexCount
291///   6 / 2 / featureIndices[featureIndexCount]
292/// ```
293#[derive(Debug, Clone, Copy)]
294pub struct LangSys<'a> {
295    bytes: &'a [u8],
296    required: u16,
297    count: u16,
298}
299
300impl<'a> LangSys<'a> {
301    pub fn parse(bytes: &'a [u8]) -> Result<Self, Error> {
302        // lookupOrderOffset is reserved — must be NULL.
303        let _ = read_u16(bytes, 0)?;
304        let required = read_u16(bytes, 2)?;
305        let count = read_u16(bytes, 4)?;
306        let need = 6usize
307            .checked_add(
308                (count as usize)
309                    .checked_mul(2)
310                    .ok_or(Error::BadStructure("LangSys featureIndexCount overflow"))?,
311            )
312            .ok_or(Error::BadStructure("LangSys length overflow"))?;
313        if bytes.len() < need {
314            return Err(Error::UnexpectedEof);
315        }
316        Ok(Self {
317            bytes,
318            required,
319            count,
320        })
321    }
322
323    /// `requiredFeatureIndex`, or `None` for the sentinel `0xFFFF`.
324    pub fn required_feature_index(&self) -> Option<u16> {
325        if self.required == NO_REQUIRED_FEATURE {
326            None
327        } else {
328            Some(self.required)
329        }
330    }
331
332    /// Number of entries in `featureIndices[]`.
333    pub fn feature_count(&self) -> u16 {
334        self.count
335    }
336
337    /// Feature index at position `i` (into the parent FeatureList).
338    pub fn feature_index(&self, i: u16) -> Option<u16> {
339        if i >= self.count {
340            return None;
341        }
342        let off = 6 + (i as usize) * 2;
343        read_u16(self.bytes, off).ok()
344    }
345
346    /// Iterate over the `featureIndices[]` array.
347    pub fn feature_indices(&self) -> impl Iterator<Item = u16> + '_ {
348        (0..self.count).filter_map(move |i| self.feature_index(i))
349    }
350}
351
352// ---------------------------------------------------------------------------
353// FeatureList
354// ---------------------------------------------------------------------------
355
356/// Parsed `FeatureList` table (chapter 2 §"FeatureList table").
357///
358/// Layout:
359/// ```text
360///   0 / 2 / featureCount
361///   2 / 6 / featureRecords[]   // (tag[4] + featureOffset[2])
362/// ```
363///
364/// The records "should be" sorted by tag (the spec phrase
365/// "alphabetically by feature tag" is "should") but ties on a tag are
366/// allowed because a feature implementation may differ per script /
367/// language system. Iteration order is on-disk; tag lookup is linear.
368#[derive(Debug, Clone, Copy)]
369pub struct FeatureList<'a> {
370    bytes: &'a [u8],
371    count: u16,
372}
373
374impl<'a> FeatureList<'a> {
375    pub fn parse(bytes: &'a [u8]) -> Result<Self, Error> {
376        let count = read_u16(bytes, 0)?;
377        let need = 2usize
378            .checked_add(
379                (count as usize)
380                    .checked_mul(6)
381                    .ok_or(Error::BadStructure("FeatureList featureCount overflow"))?,
382            )
383            .ok_or(Error::BadStructure("FeatureList length overflow"))?;
384        if bytes.len() < need {
385            return Err(Error::UnexpectedEof);
386        }
387        Ok(Self { bytes, count })
388    }
389
390    /// Number of feature records.
391    pub fn count(&self) -> u16 {
392        self.count
393    }
394
395    /// `true` iff the list is empty.
396    pub fn is_empty(&self) -> bool {
397        self.count == 0
398    }
399
400    /// Tag of feature record `i`.
401    pub fn tag(&self, i: u16) -> Option<[u8; 4]> {
402        if i >= self.count {
403            return None;
404        }
405        let off = 2 + (i as usize) * 6;
406        read_tag(self.bytes, off).ok()
407    }
408
409    /// Parse the [`Feature`] referenced by record `i`.
410    pub fn feature(&self, i: u16) -> Option<Result<Feature<'a>, Error>> {
411        if i >= self.count {
412            return None;
413        }
414        let off = 2 + (i as usize) * 6;
415        let feat_off = read_u16(self.bytes, off + 4).ok()? as usize;
416        if feat_off == 0 || feat_off >= self.bytes.len() {
417            return Some(Err(Error::BadStructure(
418                "FeatureList: featureOffset out of range",
419            )));
420        }
421        Some(Feature::parse(&self.bytes[feat_off..]))
422    }
423
424    /// Iterate `(tag, feature_result)` pairs in on-disk order.
425    pub fn iter(&self) -> FeatureListIter<'a> {
426        FeatureListIter {
427            list: *self,
428            next: 0,
429        }
430    }
431}
432
433/// Iterator yielded by [`FeatureList::iter`].
434#[derive(Debug, Clone)]
435pub struct FeatureListIter<'a> {
436    list: FeatureList<'a>,
437    next: u16,
438}
439
440impl<'a> Iterator for FeatureListIter<'a> {
441    type Item = ([u8; 4], Result<Feature<'a>, Error>);
442    fn next(&mut self) -> Option<Self::Item> {
443        if self.next >= self.list.count {
444            return None;
445        }
446        let i = self.next;
447        self.next += 1;
448        let tag = self.list.tag(i)?;
449        let feat = self.list.feature(i)?;
450        Some((tag, feat))
451    }
452}
453
454// ---------------------------------------------------------------------------
455// Feature
456// ---------------------------------------------------------------------------
457
458/// Parsed `Feature` table (chapter 2 §"Feature table").
459///
460/// Layout:
461/// ```text
462///   0 / 2 / featureParamsOffset  (Offset16, may be NULL)
463///   2 / 2 / lookupIndexCount
464///   4 / 2 / lookupListIndices[lookupIndexCount]
465/// ```
466#[derive(Debug, Clone, Copy)]
467pub struct Feature<'a> {
468    bytes: &'a [u8],
469    params_off: u16,
470    count: u16,
471}
472
473impl<'a> Feature<'a> {
474    pub fn parse(bytes: &'a [u8]) -> Result<Self, Error> {
475        let params_off = read_u16(bytes, 0)?;
476        let count = read_u16(bytes, 2)?;
477        let need = 4usize
478            .checked_add(
479                (count as usize)
480                    .checked_mul(2)
481                    .ok_or(Error::BadStructure("Feature lookupIndexCount overflow"))?,
482            )
483            .ok_or(Error::BadStructure("Feature length overflow"))?;
484        if bytes.len() < need {
485            return Err(Error::UnexpectedEof);
486        }
487        Ok(Self {
488            bytes,
489            params_off,
490            count,
491        })
492    }
493
494    /// Raw `featureParamsOffset`; `0` means no feature-parameters
495    /// table is attached. The spec defines parameter formats only for
496    /// a handful of features (`size`, `ssXX`, `cvXX`); decoding them
497    /// is deferred to a future round.
498    pub fn feature_params_offset(&self) -> u16 {
499        self.params_off
500    }
501
502    /// Number of lookup-list indices.
503    pub fn lookup_count(&self) -> u16 {
504        self.count
505    }
506
507    /// Lookup index at position `i` (into the parent LookupList).
508    pub fn lookup_index(&self, i: u16) -> Option<u16> {
509        if i >= self.count {
510            return None;
511        }
512        let off = 4 + (i as usize) * 2;
513        read_u16(self.bytes, off).ok()
514    }
515
516    /// Iterate over `lookupListIndices[]`.
517    pub fn lookup_indices(&self) -> impl Iterator<Item = u16> + '_ {
518        (0..self.count).filter_map(move |i| self.lookup_index(i))
519    }
520}
521
522// ---------------------------------------------------------------------------
523// LookupList + Lookup + LookupFlag
524// ---------------------------------------------------------------------------
525
526/// Parsed `LookupList` table (chapter 2 §"LookupList table").
527///
528/// Layout:
529/// ```text
530///   0 / 2 / lookupCount
531///   2 / 2 / lookupOffsets[lookupCount]
532/// ```
533#[derive(Debug, Clone, Copy)]
534pub struct LookupList<'a> {
535    bytes: &'a [u8],
536    count: u16,
537}
538
539impl<'a> LookupList<'a> {
540    pub fn parse(bytes: &'a [u8]) -> Result<Self, Error> {
541        let count = read_u16(bytes, 0)?;
542        let need = 2usize
543            .checked_add(
544                (count as usize)
545                    .checked_mul(2)
546                    .ok_or(Error::BadStructure("LookupList lookupCount overflow"))?,
547            )
548            .ok_or(Error::BadStructure("LookupList length overflow"))?;
549        if bytes.len() < need {
550            return Err(Error::UnexpectedEof);
551        }
552        Ok(Self { bytes, count })
553    }
554
555    /// Number of lookups.
556    pub fn count(&self) -> u16 {
557        self.count
558    }
559
560    /// `true` iff the list is empty.
561    pub fn is_empty(&self) -> bool {
562        self.count == 0
563    }
564
565    /// Parse the [`Lookup`] at index `i`.
566    pub fn lookup(&self, i: u16) -> Option<Result<Lookup<'a>, Error>> {
567        if i >= self.count {
568            return None;
569        }
570        let off = 2 + (i as usize) * 2;
571        let lkup_off = read_u16(self.bytes, off).ok()? as usize;
572        if lkup_off == 0 || lkup_off >= self.bytes.len() {
573            return Some(Err(Error::BadStructure(
574                "LookupList: lookupOffset out of range",
575            )));
576        }
577        Some(Lookup::parse(&self.bytes[lkup_off..]))
578    }
579
580    /// Iterate over every parsed `Lookup` in on-disk order.
581    pub fn iter(&self) -> LookupListIter<'a> {
582        LookupListIter {
583            list: *self,
584            next: 0,
585        }
586    }
587}
588
589/// Iterator yielded by [`LookupList::iter`].
590#[derive(Debug, Clone)]
591pub struct LookupListIter<'a> {
592    list: LookupList<'a>,
593    next: u16,
594}
595
596impl<'a> Iterator for LookupListIter<'a> {
597    type Item = Result<Lookup<'a>, Error>;
598    fn next(&mut self) -> Option<Self::Item> {
599        if self.next >= self.list.count {
600            return None;
601        }
602        let i = self.next;
603        self.next += 1;
604        self.list.lookup(i)
605    }
606}
607
608/// Bit fields of the `LookupFlag` `u16`, per chapter 2 §"LookupFlag
609/// bit enumeration".
610#[derive(Debug, Clone, Copy, PartialEq, Eq)]
611pub struct LookupFlag(pub u16);
612
613impl LookupFlag {
614    /// Cursive-attachment-only direction bit (GPOS lookup type 3).
615    pub const RIGHT_TO_LEFT: u16 = 0x0001;
616    /// Skip base glyphs (consults `GDEF.GlyphClassDef`).
617    pub const IGNORE_BASE_GLYPHS: u16 = 0x0002;
618    /// Skip ligatures (consults `GDEF.GlyphClassDef`).
619    pub const IGNORE_LIGATURES: u16 = 0x0004;
620    /// Skip all marks (consults `GDEF.GlyphClassDef`).
621    pub const IGNORE_MARKS: u16 = 0x0008;
622    /// Lookup header carries a trailing `markFilteringSet` field.
623    pub const USE_MARK_FILTERING_SET: u16 = 0x0010;
624    /// High byte: filter on `GDEF.MarkAttachClassDef`, `0` = no filter.
625    pub const MARK_ATTACHMENT_CLASS_MASK: u16 = 0xFF00;
626
627    /// Raw `u16` bits.
628    pub fn bits(self) -> u16 {
629        self.0
630    }
631
632    /// `true` if the cursive RTL bit is set.
633    pub fn right_to_left(self) -> bool {
634        self.0 & Self::RIGHT_TO_LEFT != 0
635    }
636
637    /// `true` if the lookup is configured to skip base glyphs.
638    pub fn ignore_base_glyphs(self) -> bool {
639        self.0 & Self::IGNORE_BASE_GLYPHS != 0
640    }
641
642    /// `true` if the lookup is configured to skip ligature glyphs.
643    pub fn ignore_ligatures(self) -> bool {
644        self.0 & Self::IGNORE_LIGATURES != 0
645    }
646
647    /// `true` if the lookup is configured to skip all mark glyphs.
648    pub fn ignore_marks(self) -> bool {
649        self.0 & Self::IGNORE_MARKS != 0
650    }
651
652    /// `true` if the lookup carries a trailing `markFilteringSet`
653    /// field (and a [`MarkGlyphSets`](super::gdef::MarkGlyphSets)
654    /// table must exist in `GDEF`).
655    pub fn use_mark_filtering_set(self) -> bool {
656        self.0 & Self::USE_MARK_FILTERING_SET != 0
657    }
658
659    /// `markAttachmentType` filter; `0` = unfiltered. Non-zero values
660    /// index a class in `GDEF.MarkAttachClassDef`.
661    pub fn mark_attachment_type(self) -> u8 {
662        ((self.0 & Self::MARK_ATTACHMENT_CLASS_MASK) >> 8) as u8
663    }
664}
665
666/// Parsed `Lookup` table (chapter 2 §"Lookup table").
667///
668/// Layout:
669/// ```text
670///   0 / 2 / lookupType
671///   2 / 2 / lookupFlag
672///   4 / 2 / subTableCount
673///   6 / 2 / subtableOffsets[subTableCount]
674///   6 + 2*subTableCount / 2 / markFilteringSet  (iff USE_MARK_FILTERING_SET)
675/// ```
676///
677/// `lookupType` is interpreted by the surrounding GSUB / GPOS table;
678/// the chapter-2 form intentionally leaves the enumeration open.
679#[derive(Debug, Clone, Copy)]
680pub struct Lookup<'a> {
681    bytes: &'a [u8],
682    lookup_type: u16,
683    flag: LookupFlag,
684    sub_count: u16,
685    /// Cached `markFilteringSet` when `flag.use_mark_filtering_set()`,
686    /// else `0`. The spec only requires the field to be present, not
687    /// non-zero, so callers should consult [`LookupFlag::use_mark_filtering_set`].
688    mark_filtering_set: u16,
689}
690
691impl<'a> Lookup<'a> {
692    pub fn parse(bytes: &'a [u8]) -> Result<Self, Error> {
693        let lookup_type = read_u16(bytes, 0)?;
694        let flag_bits = read_u16(bytes, 2)?;
695        let flag = LookupFlag(flag_bits);
696        let sub_count = read_u16(bytes, 4)?;
697        let sub_array = 6usize
698            .checked_add(
699                (sub_count as usize)
700                    .checked_mul(2)
701                    .ok_or(Error::BadStructure("Lookup subTableCount overflow"))?,
702            )
703            .ok_or(Error::BadStructure("Lookup length overflow"))?;
704        let need = if flag.use_mark_filtering_set() {
705            sub_array
706                .checked_add(2)
707                .ok_or(Error::BadStructure("Lookup mark-filtering overflow"))?
708        } else {
709            sub_array
710        };
711        if bytes.len() < need {
712            return Err(Error::UnexpectedEof);
713        }
714        let mark_filtering_set = if flag.use_mark_filtering_set() {
715            read_u16(bytes, sub_array)?
716        } else {
717            0
718        };
719        Ok(Self {
720            bytes,
721            lookup_type,
722            flag,
723            sub_count,
724            mark_filtering_set,
725        })
726    }
727
728    /// Lookup-type number. Interpretation depends on whether this
729    /// lookup lives in GSUB (1–8) or GPOS (1–9).
730    pub fn lookup_type(&self) -> u16 {
731        self.lookup_type
732    }
733
734    /// Parsed [`LookupFlag`] bits.
735    pub fn flag(&self) -> LookupFlag {
736        self.flag
737    }
738
739    /// Number of subtables.
740    pub fn subtable_count(&self) -> u16 {
741        self.sub_count
742    }
743
744    /// Borrow the raw bytes of subtable `i` as a sub-slice starting at
745    /// the subtable header. Length runs to the end of the parent
746    /// Lookup byte window — callers are expected to use the
747    /// subtable's own internal length / count fields to bound a read.
748    pub fn subtable_bytes(&self, i: u16) -> Option<&'a [u8]> {
749        if i >= self.sub_count {
750            return None;
751        }
752        let off_off = 6 + (i as usize) * 2;
753        let sub_off = read_u16(self.bytes, off_off).ok()? as usize;
754        if sub_off == 0 || sub_off >= self.bytes.len() {
755            return None;
756        }
757        Some(&self.bytes[sub_off..])
758    }
759
760    /// `markFilteringSet` value (a [`MarkGlyphSets`](super::gdef::MarkGlyphSets)
761    /// index in GDEF), or `None` when the flag bit is unset.
762    pub fn mark_filtering_set(&self) -> Option<u16> {
763        if self.flag.use_mark_filtering_set() {
764            Some(self.mark_filtering_set)
765        } else {
766            None
767        }
768    }
769}
770
771// ---------------------------------------------------------------------------
772// Common GSUB / GPOS header (versions 1.0 + 1.1)
773// ---------------------------------------------------------------------------
774
775/// Parsed common header used by both GSUB and GPOS. Version `1.0`
776/// (10 bytes) carries ScriptList / FeatureList / LookupList offsets;
777/// version `1.1` (14 bytes) adds an `Offset32 featureVariationsOffset`.
778#[derive(Debug, Clone, Copy)]
779pub(crate) struct LayoutHeader {
780    pub(crate) major: u16,
781    pub(crate) minor: u16,
782    pub(crate) script_list_off: u16,
783    pub(crate) feature_list_off: u16,
784    pub(crate) lookup_list_off: u16,
785    /// `0` (= NULL) on a v1.0 header or when the v1.1 offset is the
786    /// "no feature variations" sentinel.
787    pub(crate) feature_variations_off: u32,
788}
789
790impl LayoutHeader {
791    /// Decode a GSUB / GPOS header, accepting major version 1 and
792    /// minor version 0 or 1. Unknown versions surface as
793    /// [`Error::BadStructure`].
794    pub(crate) fn parse(bytes: &[u8]) -> Result<Self, Error> {
795        if bytes.len() < 10 {
796            return Err(Error::UnexpectedEof);
797        }
798        let major = read_u16(bytes, 0)?;
799        let minor = read_u16(bytes, 2)?;
800        if major != 1 || minor > 1 {
801            return Err(Error::BadStructure(
802                "GSUB/GPOS: only version 1.0 / 1.1 are defined",
803            ));
804        }
805        let script_list_off = read_u16(bytes, 4)?;
806        let feature_list_off = read_u16(bytes, 6)?;
807        let lookup_list_off = read_u16(bytes, 8)?;
808        let feature_variations_off = if minor == 1 {
809            if bytes.len() < 14 {
810                return Err(Error::UnexpectedEof);
811            }
812            read_u32(bytes, 10)?
813        } else {
814            0
815        };
816        Ok(Self {
817            major,
818            minor,
819            script_list_off,
820            feature_list_off,
821            lookup_list_off,
822            feature_variations_off,
823        })
824    }
825}
826
827// ---------------------------------------------------------------------------
828// Synthetic-byte tests
829// ---------------------------------------------------------------------------
830
831#[cfg(test)]
832mod tests {
833    use super::*;
834
835    fn be(u: u16) -> [u8; 2] {
836        u.to_be_bytes()
837    }
838    fn be32(u: u32) -> [u8; 4] {
839        u.to_be_bytes()
840    }
841
842    #[test]
843    fn header_v10_round_trip() {
844        let mut bytes = Vec::new();
845        bytes.extend_from_slice(&be(1));
846        bytes.extend_from_slice(&be(0));
847        bytes.extend_from_slice(&be(10));
848        bytes.extend_from_slice(&be(20));
849        bytes.extend_from_slice(&be(30));
850        let h = LayoutHeader::parse(&bytes).unwrap();
851        assert_eq!(h.major, 1);
852        assert_eq!(h.minor, 0);
853        assert_eq!(h.script_list_off, 10);
854        assert_eq!(h.feature_list_off, 20);
855        assert_eq!(h.lookup_list_off, 30);
856        assert_eq!(h.feature_variations_off, 0);
857    }
858
859    #[test]
860    fn header_v11_round_trip() {
861        let mut bytes = Vec::new();
862        bytes.extend_from_slice(&be(1));
863        bytes.extend_from_slice(&be(1));
864        bytes.extend_from_slice(&be(14));
865        bytes.extend_from_slice(&be(24));
866        bytes.extend_from_slice(&be(34));
867        bytes.extend_from_slice(&be32(99_999));
868        let h = LayoutHeader::parse(&bytes).unwrap();
869        assert_eq!(h.minor, 1);
870        assert_eq!(h.feature_variations_off, 99_999);
871    }
872
873    #[test]
874    fn header_rejects_unknown_versions() {
875        let bytes_v2 = {
876            let mut b = vec![0u8; 14];
877            b[0..2].copy_from_slice(&be(2));
878            b
879        };
880        assert!(matches!(
881            LayoutHeader::parse(&bytes_v2),
882            Err(Error::BadStructure(_))
883        ));
884        let bytes_v12 = {
885            let mut b = vec![0u8; 14];
886            b[0..2].copy_from_slice(&be(1));
887            b[2..4].copy_from_slice(&be(2));
888            b
889        };
890        assert!(matches!(
891            LayoutHeader::parse(&bytes_v12),
892            Err(Error::BadStructure(_))
893        ));
894    }
895
896    /// Build a tiny ScriptList + Script + LangSys + FeatureList +
897    /// Feature + LookupList + Lookup synthetic byte tower and confirm
898    /// the parsers walk it.
899    #[test]
900    fn synthetic_round_trip() {
901        // LangSys: lookupOrder=0, requiredFeatureIndex=0xFFFF,
902        // featureIndexCount=2, featureIndices=[0,1]
903        let mut lang_sys = Vec::new();
904        lang_sys.extend_from_slice(&be(0));
905        lang_sys.extend_from_slice(&be(NO_REQUIRED_FEATURE));
906        lang_sys.extend_from_slice(&be(2));
907        lang_sys.extend_from_slice(&be(0));
908        lang_sys.extend_from_slice(&be(1));
909        let ls = LangSys::parse(&lang_sys).unwrap();
910        assert!(ls.required_feature_index().is_none());
911        assert_eq!(ls.feature_count(), 2);
912        assert_eq!(ls.feature_index(0), Some(0));
913        assert_eq!(ls.feature_index(1), Some(1));
914        assert_eq!(ls.feature_index(2), None);
915        let v: Vec<_> = ls.feature_indices().collect();
916        assert_eq!(v, vec![0, 1]);
917
918        // Feature: featureParamsOffset=0, lookupIndexCount=1, lookupListIndices=[0]
919        let mut feature = Vec::new();
920        feature.extend_from_slice(&be(0));
921        feature.extend_from_slice(&be(1));
922        feature.extend_from_slice(&be(0));
923        let f = Feature::parse(&feature).unwrap();
924        assert_eq!(f.feature_params_offset(), 0);
925        assert_eq!(f.lookup_count(), 1);
926        assert_eq!(f.lookup_index(0), Some(0));
927
928        // Lookup: type=1, flag=0, subTableCount=0  (no subtables — fine for the parser)
929        let mut lookup = Vec::new();
930        lookup.extend_from_slice(&be(1));
931        lookup.extend_from_slice(&be(0));
932        lookup.extend_from_slice(&be(0));
933        let l = Lookup::parse(&lookup).unwrap();
934        assert_eq!(l.lookup_type(), 1);
935        assert_eq!(l.subtable_count(), 0);
936        assert!(l.mark_filtering_set().is_none());
937        assert!(!l.flag().right_to_left());
938        assert!(!l.flag().ignore_marks());
939    }
940
941    #[test]
942    fn lookup_with_mark_filtering_set() {
943        let mut lookup = Vec::new();
944        lookup.extend_from_slice(&be(4));
945        lookup.extend_from_slice(&be(LookupFlag::USE_MARK_FILTERING_SET));
946        lookup.extend_from_slice(&be(0));
947        lookup.extend_from_slice(&be(7));
948        let l = Lookup::parse(&lookup).unwrap();
949        assert!(l.flag().use_mark_filtering_set());
950        assert_eq!(l.mark_filtering_set(), Some(7));
951    }
952
953    #[test]
954    fn lookup_flag_helpers() {
955        let f = LookupFlag(0x0A0E);
956        assert!(!f.right_to_left());
957        assert!(f.ignore_base_glyphs());
958        assert!(f.ignore_ligatures());
959        assert!(f.ignore_marks());
960        assert!(!f.use_mark_filtering_set());
961        assert_eq!(f.mark_attachment_type(), 0x0A);
962    }
963
964    #[test]
965    fn truncation_surfaces_unexpected_eof() {
966        // ScriptList claims 3 records but the buffer has 2.
967        let mut bytes = Vec::new();
968        bytes.extend_from_slice(&be(3));
969        bytes.extend_from_slice(b"DFLT");
970        bytes.extend_from_slice(&be(0));
971        bytes.extend_from_slice(b"latn");
972        bytes.extend_from_slice(&be(0));
973        // Missing the third record.
974        assert!(matches!(
975            ScriptList::parse(&bytes),
976            Err(Error::UnexpectedEof)
977        ));
978
979        // Lookup with USE_MARK_FILTERING_SET but missing the trailing word.
980        let mut lookup = Vec::new();
981        lookup.extend_from_slice(&be(4));
982        lookup.extend_from_slice(&be(LookupFlag::USE_MARK_FILTERING_SET));
983        lookup.extend_from_slice(&be(0));
984        assert!(matches!(Lookup::parse(&lookup), Err(Error::UnexpectedEof)));
985    }
986}