Skip to main content

oxideav_ttf/tables/
post.rs

1//! `post` — PostScript metadata + glyph names.
2//!
3//! Decoded per ISO/IEC 14496-22:2019 §5.2.10 / MS Learn `otspec-post`
4//! (`docs/text/opentype/otspec-post.html`). The fixed 32-byte header
5//! is identical across every version and carries `italicAngle`,
6//! underline geometry, the `isFixedPitch` boolean, and the four
7//! PostScript memory-usage hints. After the header the layout
8//! diverges per `version`:
9//!
10//! - **`0x00010000` (v1.0)** — no trailing data. The font is asserted
11//!   to contain exactly the 258 glyphs of the standard Macintosh
12//!   TrueType font in the standard order; glyph names are looked up
13//!   from the system Macintosh glyph table by glyph id.
14//! - **`0x00020000` (v2.0)** — `uint16 numGlyphs` + `uint16
15//!   glyphNameIndex[numGlyphs]` + Pascal-format `stringData[…]`. Each
16//!   glyph id selects an index `nameIdx`. If `nameIdx < 258` the
17//!   glyph name is the corresponding standard Macintosh name; if
18//!   `nameIdx >= 258` the name is the `(nameIdx - 258)`th Pascal
19//!   string in `stringData`.
20//! - **`0x00025000` (v2.5, deprecated)** — `uint16 numGlyphs` +
21//!   `int8 offset[numGlyphs]`. For each glyph id `gid` the standard
22//!   Macintosh glyph index is `gid + offset[gid]`. Used by legacy
23//!   fonts whose glyph set is a permutation or subset of the
24//!   standard Macintosh order.
25//! - **`0x00030000` (v3.0)** — no trailing data, no glyph names.
26//!   Required form for CFF v1 outline fonts; permitted for any font
27//!   that does not wish to publish glyph names.
28//!
29//! Spec v4.0 is defined by Apple for non-OpenType use and is out of
30//! scope per §5.2.10 ("not supported in OpenType"); it is rejected
31//! here as an unsupported version.
32//!
33//! ## Standard-Macintosh-glyph-name list
34//!
35//! ISO §5.2.10.1 defers to "Reference [2]" (Apple's TrueType
36//! Reference Manual, Chap 6 — the `RM06/Chap6post.html` page) for the
37//! list of the 258 standard Macintosh glyph names. The MS Learn
38//! `otspec-post` page does the same. That 258-name array is now staged
39//! at `docs/text/opentype/post-standard-mac-glyph-names.md` and
40//! transcribed verbatim into [`STANDARD_MAC_GLYPH_NAMES`]. This module:
41//!
42//! - decodes the post-table **structure** for all four versions
43//!   (header + numGlyphs + index array + Pascal strings);
44//! - exposes the per-glyph name index ([`GlyphNameRef::StandardMac`]
45//!   `{ index }`) and the per-glyph Pascal string
46//!   ([`GlyphNameRef::Custom`] `(&str)`) for callers that want the raw
47//!   reference;
48//! - resolves both branches into a single `Option<&str>` through
49//!   [`PostTable::resolved_glyph_name`] and the convenience
50//!   [`Font::glyph_name`](crate::Font::glyph_name) accessor, looking a
51//!   `StandardMac` index up in [`STANDARD_MAC_GLYPH_NAMES`].
52
53use crate::parser::{read_i16, read_i32, read_u16, read_u32, read_u8};
54use crate::Error;
55
56/// `post` table tag (`b"post"`).
57pub const POST_TABLE_TAG: [u8; 4] = *b"post";
58
59/// Fixed 32-byte common header length.
60// internal — exposed for tests/fuzz; not part of the stable API
61#[doc(hidden)]
62pub const POST_HEADER_LEN: usize = 32;
63
64/// Version 1.0 (`0x00010000`). All names come from the standard
65/// Macintosh order, addressed by glyph id.
66pub const POST_VERSION_10: u32 = 0x0001_0000;
67
68/// Version 2.0 (`0x00020000`). The most common form: per-glyph index
69/// into the standard-Mac set or the local Pascal-string pool.
70pub const POST_VERSION_20: u32 = 0x0002_0000;
71
72/// Version 2.5 (`0x00025000`, deprecated). Per-glyph signed `int8`
73/// offset into the standard-Macintosh glyph order.
74pub const POST_VERSION_25: u32 = 0x0002_5000;
75
76/// Version 3.0 (`0x00030000`). No glyph names; the only form
77/// permitted for CFF v1 fonts.
78pub const POST_VERSION_30: u32 = 0x0003_0000;
79
80/// Inclusive upper bound on the standard-Macintosh glyph-name index
81/// space. `glyphNameIndex` values strictly below this address the
82/// 258-name standard set; values at or above it address the Pascal
83/// string pool offset by this constant.
84pub const STANDARD_MAC_GLYPH_COUNT: u16 = 258;
85
86/// Maximum length of a v2.0 PostScript glyph name in bytes, per
87/// §5.2.10.2: "Names must be no longer than 63 characters; some older
88/// implementations can assume a length limit of 31 characters." This
89/// is a tolerance ceiling — names up to and including 63 bytes are
90/// accepted; longer names are tolerated (the Pascal length byte is
91/// itself a `u8`, capping at 255) but flagged through
92/// [`PostTable::has_oversize_glyph_name`] so callers can downgrade
93/// gracefully.
94pub const RECOMMENDED_GLYPH_NAME_MAX_LEN: usize = 63;
95
96/// The 258 standard Macintosh glyph names, in their canonical ordering,
97/// as referenced by `post` table formats 1.0, 2.0 (for
98/// `glyphNameIndex < 258`), and 2.5.
99///
100/// Index 0 = `.notdef`, 1 = `.null`, 2 = `nonmarkingreturn`, …,
101/// 257 = `dcroat`. All 258 entries are distinct. The ordering and the
102/// names themselves are standards data defined by Apple's *TrueType
103/// Reference Manual*, Chapter 6 `post` table, "`'post'` Format 1"
104/// (Microsoft's OpenType `post` spec defers to Apple for the list) —
105/// transcribed verbatim from
106/// `docs/text/opentype/post-standard-mac-glyph-names.md`.
107pub static STANDARD_MAC_GLYPH_NAMES: [&str; STANDARD_MAC_GLYPH_COUNT as usize] = [
108    ".notdef",
109    ".null",
110    "nonmarkingreturn",
111    "space",
112    "exclam",
113    "quotedbl",
114    "numbersign",
115    "dollar",
116    "percent",
117    "ampersand",
118    "quotesingle",
119    "parenleft",
120    "parenright",
121    "asterisk",
122    "plus",
123    "comma",
124    "hyphen",
125    "period",
126    "slash",
127    "zero",
128    "one",
129    "two",
130    "three",
131    "four",
132    "five",
133    "six",
134    "seven",
135    "eight",
136    "nine",
137    "colon",
138    "semicolon",
139    "less",
140    "equal",
141    "greater",
142    "question",
143    "at",
144    "A",
145    "B",
146    "C",
147    "D",
148    "E",
149    "F",
150    "G",
151    "H",
152    "I",
153    "J",
154    "K",
155    "L",
156    "M",
157    "N",
158    "O",
159    "P",
160    "Q",
161    "R",
162    "S",
163    "T",
164    "U",
165    "V",
166    "W",
167    "X",
168    "Y",
169    "Z",
170    "bracketleft",
171    "backslash",
172    "bracketright",
173    "asciicircum",
174    "underscore",
175    "grave",
176    "a",
177    "b",
178    "c",
179    "d",
180    "e",
181    "f",
182    "g",
183    "h",
184    "i",
185    "j",
186    "k",
187    "l",
188    "m",
189    "n",
190    "o",
191    "p",
192    "q",
193    "r",
194    "s",
195    "t",
196    "u",
197    "v",
198    "w",
199    "x",
200    "y",
201    "z",
202    "braceleft",
203    "bar",
204    "braceright",
205    "asciitilde",
206    "Adieresis",
207    "Aring",
208    "Ccedilla",
209    "Eacute",
210    "Ntilde",
211    "Odieresis",
212    "Udieresis",
213    "aacute",
214    "agrave",
215    "acircumflex",
216    "adieresis",
217    "atilde",
218    "aring",
219    "ccedilla",
220    "eacute",
221    "egrave",
222    "ecircumflex",
223    "edieresis",
224    "iacute",
225    "igrave",
226    "icircumflex",
227    "idieresis",
228    "ntilde",
229    "oacute",
230    "ograve",
231    "ocircumflex",
232    "odieresis",
233    "otilde",
234    "uacute",
235    "ugrave",
236    "ucircumflex",
237    "udieresis",
238    "dagger",
239    "degree",
240    "cent",
241    "sterling",
242    "section",
243    "bullet",
244    "paragraph",
245    "germandbls",
246    "registered",
247    "copyright",
248    "trademark",
249    "acute",
250    "dieresis",
251    "notequal",
252    "AE",
253    "Oslash",
254    "infinity",
255    "plusminus",
256    "lessequal",
257    "greaterequal",
258    "yen",
259    "mu",
260    "partialdiff",
261    "summation",
262    "product",
263    "pi",
264    "integral",
265    "ordfeminine",
266    "ordmasculine",
267    "Omega",
268    "ae",
269    "oslash",
270    "questiondown",
271    "exclamdown",
272    "logicalnot",
273    "radical",
274    "florin",
275    "approxequal",
276    "Delta",
277    "guillemotleft",
278    "guillemotright",
279    "ellipsis",
280    "nonbreakingspace",
281    "Agrave",
282    "Atilde",
283    "Otilde",
284    "OE",
285    "oe",
286    "endash",
287    "emdash",
288    "quotedblleft",
289    "quotedblright",
290    "quoteleft",
291    "quoteright",
292    "divide",
293    "lozenge",
294    "ydieresis",
295    "Ydieresis",
296    "fraction",
297    "currency",
298    "guilsinglleft",
299    "guilsinglright",
300    "fi",
301    "fl",
302    "daggerdbl",
303    "periodcentered",
304    "quotesinglbase",
305    "quotedblbase",
306    "perthousand",
307    "Acircumflex",
308    "Ecircumflex",
309    "Aacute",
310    "Edieresis",
311    "Egrave",
312    "Iacute",
313    "Icircumflex",
314    "Idieresis",
315    "Igrave",
316    "Oacute",
317    "Ocircumflex",
318    "apple",
319    "Ograve",
320    "Uacute",
321    "Ucircumflex",
322    "Ugrave",
323    "dotlessi",
324    "circumflex",
325    "tilde",
326    "macron",
327    "breve",
328    "dotaccent",
329    "ring",
330    "cedilla",
331    "hungarumlaut",
332    "ogonek",
333    "caron",
334    "Lslash",
335    "lslash",
336    "Scaron",
337    "scaron",
338    "Zcaron",
339    "zcaron",
340    "brokenbar",
341    "Eth",
342    "eth",
343    "Yacute",
344    "yacute",
345    "Thorn",
346    "thorn",
347    "minus",
348    "multiply",
349    "onesuperior",
350    "twosuperior",
351    "threesuperior",
352    "onehalf",
353    "onequarter",
354    "threequarters",
355    "franc",
356    "Gbreve",
357    "gbreve",
358    "Idotaccent",
359    "Scedilla",
360    "scedilla",
361    "Cacute",
362    "cacute",
363    "Ccaron",
364    "ccaron",
365    "dcroat",
366];
367
368/// Resolve a standard-Macintosh glyph-name index to its name.
369///
370/// Returns `None` for `index >= 258` (outside the standard set). The
371/// [`GlyphNameRef::StandardMac`] variant guarantees `index < 258`, so a
372/// lookup driven by it always succeeds.
373pub fn standard_mac_glyph_name(index: u16) -> Option<&'static str> {
374    STANDARD_MAC_GLYPH_NAMES.get(index as usize).copied()
375}
376
377/// Parsed `post` table. The common header is always populated;
378/// `format` carries the version-dependent trailing data.
379#[derive(Debug, Clone)]
380// internal — exposed for tests/fuzz; not part of the stable API
381#[doc(hidden)]
382pub struct PostTable {
383    /// Raw `Version16Dot16` from the header. Preserved verbatim so
384    /// callers that want to introspect the exact published version
385    /// (e.g. to distinguish v1.0 from v2.0 explicitly) can do so;
386    /// typed `format` covers the structural reading.
387    pub version_raw: u32,
388    /// Italic angle in counter-clockwise degrees from the vertical.
389    /// `0.0` for upright; negative for forward-slanted (the common
390    /// case).
391    pub italic_angle: f32,
392    /// Suggested y-coordinate of the top of the underline.
393    pub underline_position: i16,
394    /// Suggested underline thickness.
395    pub underline_thickness: i16,
396    /// `true` when the font is monospaced (header `isFixedPitch`
397    /// `!= 0`), `false` for proportionally-spaced fonts.
398    pub is_fixed_pitch: bool,
399    /// PostScript memory-management hints. Set to `0` when the font
400    /// foundry did not measure them.
401    pub min_mem_type42: u32,
402    /// See [`PostTable::min_mem_type42`].
403    pub max_mem_type42: u32,
404    /// See [`PostTable::min_mem_type42`].
405    pub min_mem_type1: u32,
406    /// See [`PostTable::min_mem_type42`].
407    pub max_mem_type1: u32,
408    /// Version-specific tail.
409    pub format: PostFormat,
410}
411
412/// Version-dependent trailing data.
413#[derive(Debug, Clone)]
414pub enum PostFormat {
415    /// v1.0: no trailing data. The font claims to be the standard
416    /// Macintosh 258-glyph layout.
417    Version10,
418    /// v2.0: per-glyph `(name_index, optional Pascal string)` table.
419    Version20(PostV20),
420    /// v2.5 (deprecated): per-glyph signed offset into the standard
421    /// Macintosh order.
422    Version25(PostV25),
423    /// v3.0: no glyph names at all.
424    Version30,
425}
426
427/// Version 2.0 trailing data — index array + Pascal-string pool.
428#[derive(Debug, Clone)]
429pub struct PostV20 {
430    /// `numGlyphs` — must match `maxp.numGlyphs`. Preserved verbatim
431    /// so callers can sanity-check against `maxp`.
432    pub num_glyphs: u16,
433    /// `glyphNameIndex[numGlyphs]` — per-glyph index into either the
434    /// standard Macintosh 258-name set or the Pascal-string pool.
435    pub glyph_name_indices: Vec<u16>,
436    /// Pascal strings extracted from `stringData`, in publication
437    /// order. Index `k` here corresponds to the v2.0 lookup rule
438    /// "subtract 258 from `nameIndex` and use that as the array
439    /// index"; that is, `pascal_strings[k]` is the name a
440    /// `glyphNameIndex` value of `258 + k` selects.
441    pub pascal_strings: Vec<String>,
442    /// `true` when at least one Pascal string exceeds the §5.2.10.2
443    /// recommended 63-byte cap. Names are kept verbatim regardless;
444    /// the flag exists so callers that need strict conformance can
445    /// detect it without re-scanning.
446    pub has_oversize_glyph_name: bool,
447    /// `true` when at least one Pascal string contains a byte outside
448    /// the §5.2.10.2 allow-set (`A..Z`, `a..z`, `0..9`, `.`, `_`).
449    /// Such names still decode (they are interpreted as ASCII bytes
450    /// because the §5.2.10.2 wording requires ASCII) but flagged so
451    /// strict consumers can reject the font.
452    pub has_non_conformant_glyph_name: bool,
453}
454
455/// Version 2.5 trailing data — per-glyph signed offset into the
456/// standard Macintosh order.
457#[derive(Debug, Clone)]
458pub struct PostV25 {
459    /// `numGlyphs` — must match `maxp.numGlyphs`.
460    pub num_glyphs: u16,
461    /// `offset[numGlyphs]` — signed delta from this font's glyph id
462    /// to the standard Macintosh order. Per §5.2.10.3 the standard
463    /// glyph index is `glyph_id + offset[glyph_id]`.
464    pub offsets: Vec<i8>,
465}
466
467/// Resolved name of a single glyph as carried by `post`.
468///
469/// Callers consume this via [`PostTable::glyph_name_ref`]. The
470/// `StandardMac { index }` variant carries a 0-based index into the
471/// 258-name standard Macintosh glyph order; resolve it through
472/// [`standard_mac_glyph_name`] (or use [`PostTable::resolved_glyph_name`]
473/// to resolve both branches at once). The raw index is surfaced so
474/// tooling can introspect the reference without name resolution.
475#[derive(Debug, Clone, Copy, PartialEq, Eq)]
476pub enum GlyphNameRef<'a> {
477    /// The glyph's name is the `index`th entry of the 258-name
478    /// standard Macintosh glyph order. `index < 258` is guaranteed.
479    StandardMac { index: u16 },
480    /// The glyph's name is the font-supplied Pascal string. Already
481    /// trimmed of its length byte.
482    Custom(&'a str),
483}
484
485impl PostTable {
486    /// Parse the `post` table from its slice. Returns `BadStructure`
487    /// for unrecognised versions and `UnexpectedEof` for truncated
488    /// arrays.
489    pub fn parse(bytes: &[u8]) -> Result<Self, Error> {
490        if bytes.len() < POST_HEADER_LEN {
491            return Err(Error::UnexpectedEof);
492        }
493        let version_raw = read_u32(bytes, 0)?;
494        let italic_raw = read_i32(bytes, 4)?;
495        let italic_angle = italic_raw as f32 / 65536.0;
496        let underline_position = read_i16(bytes, 8)?;
497        let underline_thickness = read_i16(bytes, 10)?;
498        let is_fixed_pitch = read_u32(bytes, 12)? != 0;
499        let min_mem_type42 = read_u32(bytes, 16)?;
500        let max_mem_type42 = read_u32(bytes, 20)?;
501        let min_mem_type1 = read_u32(bytes, 24)?;
502        let max_mem_type1 = read_u32(bytes, 28)?;
503
504        let tail = &bytes[POST_HEADER_LEN..];
505        let format = match version_raw {
506            POST_VERSION_10 => PostFormat::Version10,
507            POST_VERSION_20 => PostFormat::Version20(parse_v20(tail)?),
508            POST_VERSION_25 => PostFormat::Version25(parse_v25(tail)?),
509            POST_VERSION_30 => PostFormat::Version30,
510            _ => return Err(Error::BadStructure("post: unsupported version")),
511        };
512
513        Ok(Self {
514            version_raw,
515            italic_angle,
516            underline_position,
517            underline_thickness,
518            is_fixed_pitch,
519            min_mem_type42,
520            max_mem_type42,
521            min_mem_type1,
522            max_mem_type1,
523            format,
524        })
525    }
526
527    /// `true` when the table carries any glyph-name information
528    /// (v1.0, v2.0, or v2.5). v3.0 returns `false`.
529    pub fn has_glyph_names(&self) -> bool {
530        !matches!(self.format, PostFormat::Version30)
531    }
532
533    /// `true` when at least one v2.0 Pascal string exceeds the
534    /// §5.2.10.2 recommended 63-byte limit. `false` for every other
535    /// version.
536    pub fn has_oversize_glyph_name(&self) -> bool {
537        match &self.format {
538            PostFormat::Version20(v) => v.has_oversize_glyph_name,
539            _ => false,
540        }
541    }
542
543    /// `true` when at least one v2.0 Pascal string contains a byte
544    /// outside the §5.2.10.2 allow-set. `false` for every other
545    /// version.
546    pub fn has_non_conformant_glyph_name(&self) -> bool {
547        match &self.format {
548            PostFormat::Version20(v) => v.has_non_conformant_glyph_name,
549            _ => false,
550        }
551    }
552
553    /// Number of distinct Pascal strings in the v2.0 string pool, or
554    /// `0` for every other version.
555    pub fn pascal_string_count(&self) -> usize {
556        match &self.format {
557            PostFormat::Version20(v) => v.pascal_strings.len(),
558            _ => 0,
559        }
560    }
561
562    /// Look up the `gid`th glyph's name reference.
563    ///
564    /// Returns `None` when:
565    /// - the table is v3.0 (no names at all);
566    /// - `gid` is out of range for the v2.0 / v2.5 array;
567    /// - the v2.0 `glyphNameIndex[gid]` selects a Pascal string the
568    ///   pool does not actually contain (malformed font; preserved
569    ///   as `None` rather than treated as a parse error so a single
570    ///   bad glyph does not poison the whole table);
571    /// - the v2.5 offset overflows `u16` (likewise malformed).
572    ///
573    /// For v1.0 every `gid < 258` yields
574    /// `Some(GlyphNameRef::StandardMac { index: gid })`; for v1.0 the
575    /// numGlyphs upper bound is not encoded inside `post` so the
576    /// caller is responsible for keeping `gid` below `maxp.numGlyphs`.
577    pub fn glyph_name_ref(&self, gid: u16) -> Option<GlyphNameRef<'_>> {
578        match &self.format {
579            PostFormat::Version10 => {
580                if gid < STANDARD_MAC_GLYPH_COUNT {
581                    Some(GlyphNameRef::StandardMac { index: gid })
582                } else {
583                    None
584                }
585            }
586            PostFormat::Version20(v) => {
587                let idx = *v.glyph_name_indices.get(gid as usize)?;
588                if idx < STANDARD_MAC_GLYPH_COUNT {
589                    Some(GlyphNameRef::StandardMac { index: idx })
590                } else {
591                    let pi = (idx - STANDARD_MAC_GLYPH_COUNT) as usize;
592                    v.pascal_strings
593                        .get(pi)
594                        .map(|s| GlyphNameRef::Custom(s.as_str()))
595                }
596            }
597            PostFormat::Version25(v) => {
598                let off = *v.offsets.get(gid as usize)?;
599                // Standard glyph index = gid + offset; reject if it
600                // falls outside `[0, 258)`.
601                let std = i32::from(gid) + i32::from(off);
602                if (0..i32::from(STANDARD_MAC_GLYPH_COUNT)).contains(&std) {
603                    Some(GlyphNameRef::StandardMac { index: std as u16 })
604                } else {
605                    None
606                }
607            }
608            PostFormat::Version30 => None,
609        }
610    }
611
612    /// Convenience: the v2.0 Pascal string for a glyph, if the font
613    /// names it through a custom string. Returns `None` for the
614    /// `StandardMac` indices, for missing glyph ids, and for every
615    /// non-v2.0 version.
616    pub fn custom_glyph_name(&self, gid: u16) -> Option<&str> {
617        match self.glyph_name_ref(gid)? {
618            GlyphNameRef::Custom(s) => Some(s),
619            GlyphNameRef::StandardMac { .. } => None,
620        }
621    }
622
623    /// Fully resolve a glyph's PostScript name, covering **both**
624    /// branches: a font-supplied v2.0 Pascal string is returned
625    /// directly, and a `StandardMac { index }` reference is resolved
626    /// through [`STANDARD_MAC_GLYPH_NAMES`] into the canonical standard
627    /// Macintosh name. Returns `None` only when the table publishes no
628    /// name for `gid` (no `post`, v3.0, out-of-range glyph id, or an
629    /// unsatisfiable Pascal pool reference).
630    pub fn resolved_glyph_name(&self, gid: u16) -> Option<&str> {
631        match self.glyph_name_ref(gid)? {
632            GlyphNameRef::Custom(s) => Some(s),
633            GlyphNameRef::StandardMac { index } => standard_mac_glyph_name(index),
634        }
635    }
636
637    /// The number of glyphs whose names this table spans, when the count
638    /// is encoded inside `post` itself.
639    ///
640    /// - v2.0 / v2.5 carry an explicit `numGlyphs`, returned here.
641    /// - v1.0 does **not** encode a count: it asserts the font is the
642    ///   258-glyph standard Macintosh layout, so [`STANDARD_MAC_GLYPH_COUNT`]
643    ///   (258) is returned as the implied span.
644    /// - v3.0 publishes no names, so `0` is returned.
645    pub fn named_glyph_count(&self) -> u16 {
646        match &self.format {
647            PostFormat::Version10 => STANDARD_MAC_GLYPH_COUNT,
648            PostFormat::Version20(v) => v.num_glyphs,
649            PostFormat::Version25(v) => v.num_glyphs,
650            PostFormat::Version30 => 0,
651        }
652    }
653
654    /// Reverse lookup: the **first** glyph id whose resolved PostScript
655    /// name equals `name`, scanning glyph ids in ascending order.
656    ///
657    /// This inverts [`PostTable::resolved_glyph_name`] over every named
658    /// glyph the table publishes: a v2.0 custom Pascal string, a
659    /// standard-Macintosh name (from v1.0, v2.0 `glyphNameIndex < 258`,
660    /// or v2.5), are all matched. `name` is compared by exact byte
661    /// equality (PostScript glyph names are ASCII per §5.2.10.2).
662    ///
663    /// The search bound is [`PostTable::named_glyph_count`]: for v1.0
664    /// the whole 258-name standard set is searched; for v2.0 / v2.5 the
665    /// table's own `numGlyphs`. v3.0 always returns `None`.
666    ///
667    /// Returns the lowest matching glyph id, or `None` when no glyph in
668    /// range carries that name. When several glyphs share a name (legal
669    /// but unusual), the lowest id wins — matching the convention that a
670    /// font's first occurrence of a name is its canonical owner.
671    pub fn gid_for_name(&self, name: &str) -> Option<u16> {
672        if matches!(self.format, PostFormat::Version30) {
673            return None;
674        }
675        let count = self.named_glyph_count();
676        // A fast path for v2.0 standard-name queries: resolve the target
677        // name to a standard-Mac index once, then the per-glyph compare
678        // is an integer match rather than a string compare. Custom names
679        // still fall through to the string path below.
680        let std_target = STANDARD_MAC_GLYPH_NAMES
681            .iter()
682            .position(|n| *n == name)
683            .map(|i| i as u16);
684        for gid in 0..count {
685            match self.glyph_name_ref(gid) {
686                Some(GlyphNameRef::StandardMac { index }) if Some(index) == std_target => {
687                    return Some(gid);
688                }
689                Some(GlyphNameRef::Custom(s)) if s == name => {
690                    return Some(gid);
691                }
692                _ => {}
693            }
694        }
695        None
696    }
697
698    /// Iterate every `(glyph_id, resolved_name)` pair the table
699    /// publishes, in ascending glyph-id order.
700    ///
701    /// Glyph ids whose entry resolves to no name (e.g. a v2.0
702    /// `glyphNameIndex` pointing past the Pascal pool, or a v2.5 offset
703    /// landing outside the standard set) are skipped. v3.0 yields an
704    /// empty iterator. The bound is [`PostTable::named_glyph_count`].
705    pub fn iter_glyph_names(&self) -> impl Iterator<Item = (u16, &str)> + '_ {
706        let count = self.named_glyph_count();
707        (0..count).filter_map(move |gid| self.resolved_glyph_name(gid).map(|n| (gid, n)))
708    }
709}
710
711fn parse_v20(tail: &[u8]) -> Result<PostV20, Error> {
712    if tail.len() < 2 {
713        return Err(Error::UnexpectedEof);
714    }
715    let num_glyphs = read_u16(tail, 0)?;
716    let idx_bytes_len = 2usize
717        .checked_mul(num_glyphs as usize)
718        .ok_or(Error::BadStructure("post v2.0: numGlyphs overflow"))?;
719    let idx_end = 2usize
720        .checked_add(idx_bytes_len)
721        .ok_or(Error::BadStructure("post v2.0: numGlyphs overflow"))?;
722    if tail.len() < idx_end {
723        return Err(Error::UnexpectedEof);
724    }
725    let mut glyph_name_indices = Vec::with_capacity(num_glyphs as usize);
726    let mut max_pascal_referenced: i32 = -1;
727    for i in 0..num_glyphs as usize {
728        let v = read_u16(tail, 2 + i * 2)?;
729        if v >= STANDARD_MAC_GLYPH_COUNT {
730            let pi = (v - STANDARD_MAC_GLYPH_COUNT) as i32;
731            if pi > max_pascal_referenced {
732                max_pascal_referenced = pi;
733            }
734        }
735        glyph_name_indices.push(v);
736    }
737
738    // Pascal strings extend from `idx_end` to the end of the table.
739    let pool = &tail[idx_end..];
740    let mut pascal_strings: Vec<String> = Vec::new();
741    let mut has_oversize_glyph_name = false;
742    let mut has_non_conformant_glyph_name = false;
743    let mut p = 0usize;
744    while p < pool.len() {
745        let len = read_u8(pool, p)? as usize;
746        p += 1;
747        if p + len > pool.len() {
748            return Err(Error::UnexpectedEof);
749        }
750        let raw = &pool[p..p + len];
751        if len > RECOMMENDED_GLYPH_NAME_MAX_LEN {
752            has_oversize_glyph_name = true;
753        }
754        if !raw.iter().all(|b| is_conformant_glyph_name_byte(*b)) {
755            has_non_conformant_glyph_name = true;
756        }
757        // Per §5.2.10.2 names are ASCII; for non-conformant bytes we
758        // still keep the byte values (clamped into a `String` via
759        // `from_utf8_lossy`) so caller diagnostics can see them.
760        let s = match std::str::from_utf8(raw) {
761            Ok(s) => s.to_string(),
762            Err(_) => String::from_utf8_lossy(raw).into_owned(),
763        };
764        pascal_strings.push(s);
765        p += len;
766    }
767
768    // §5.2.10.2 worked example: glyphNameIndex[408] == 262 selects
769    // pascal_strings[4]. A font that references a Pascal index its
770    // pool cannot satisfy is malformed; `glyph_name_ref` returns
771    // `None` for those gids, but we accept the parse so the
772    // well-formed glyphs still decode.
773    let _ = max_pascal_referenced;
774
775    Ok(PostV20 {
776        num_glyphs,
777        glyph_name_indices,
778        pascal_strings,
779        has_oversize_glyph_name,
780        has_non_conformant_glyph_name,
781    })
782}
783
784fn parse_v25(tail: &[u8]) -> Result<PostV25, Error> {
785    if tail.len() < 2 {
786        return Err(Error::UnexpectedEof);
787    }
788    let num_glyphs = read_u16(tail, 0)?;
789    let needed = 2usize
790        .checked_add(num_glyphs as usize)
791        .ok_or(Error::BadStructure("post v2.5: numGlyphs overflow"))?;
792    if tail.len() < needed {
793        return Err(Error::UnexpectedEof);
794    }
795    let mut offsets = Vec::with_capacity(num_glyphs as usize);
796    for i in 0..num_glyphs as usize {
797        offsets.push(tail[2 + i] as i8);
798    }
799    Ok(PostV25 {
800        num_glyphs,
801        offsets,
802    })
803}
804
805fn is_conformant_glyph_name_byte(b: u8) -> bool {
806    // §5.2.10.2 glyph-name allow-set: A..Z, a..z, 0..9, '.' (0x2E),
807    // '_' (0x5F).
808    b.is_ascii_uppercase() || b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'.' || b == b'_'
809}
810
811#[cfg(test)]
812mod tests {
813    use super::*;
814
815    fn header(version: u32) -> Vec<u8> {
816        let mut b = vec![0u8; POST_HEADER_LEN];
817        b[0..4].copy_from_slice(&version.to_be_bytes());
818        // italicAngle = -10.0 (Fixed: -10 * 65536)
819        b[4..8].copy_from_slice(&((-10i32) << 16).to_be_bytes());
820        b[8..10].copy_from_slice(&(-100i16).to_be_bytes());
821        b[10..12].copy_from_slice(&50i16.to_be_bytes());
822        b[12..16].copy_from_slice(&1u32.to_be_bytes());
823        b[16..20].copy_from_slice(&0u32.to_be_bytes());
824        b[20..24].copy_from_slice(&0u32.to_be_bytes());
825        b[24..28].copy_from_slice(&0u32.to_be_bytes());
826        b[28..32].copy_from_slice(&0u32.to_be_bytes());
827        b
828    }
829
830    #[test]
831    fn parses_minimal_v3_header() {
832        let b = header(POST_VERSION_30);
833        let p = PostTable::parse(&b).unwrap();
834        assert_eq!(p.version_raw, POST_VERSION_30);
835        assert!((p.italic_angle - (-10.0)).abs() < 0.001);
836        assert_eq!(p.underline_position, -100);
837        assert_eq!(p.underline_thickness, 50);
838        assert!(p.is_fixed_pitch);
839        assert!(matches!(p.format, PostFormat::Version30));
840        assert!(!p.has_glyph_names());
841        assert!(p.glyph_name_ref(0).is_none());
842    }
843
844    #[test]
845    fn parses_v10_returns_standard_mac_indices() {
846        let b = header(POST_VERSION_10);
847        let p = PostTable::parse(&b).unwrap();
848        assert!(matches!(p.format, PostFormat::Version10));
849        assert!(p.has_glyph_names());
850        assert_eq!(
851            p.glyph_name_ref(0),
852            Some(GlyphNameRef::StandardMac { index: 0 })
853        );
854        assert_eq!(
855            p.glyph_name_ref(217),
856            Some(GlyphNameRef::StandardMac { index: 217 })
857        );
858        // gid == 258 is out of the standard set; v1.0 cannot name it.
859        assert!(p.glyph_name_ref(258).is_none());
860        // Resolution into the canonical standard Macintosh names.
861        assert_eq!(p.resolved_glyph_name(0), Some(".notdef"));
862        assert_eq!(p.resolved_glyph_name(217), Some("tilde"));
863        assert!(p.resolved_glyph_name(258).is_none());
864    }
865
866    /// §5.2.10.2 worked example: glyphNameIndex[302] is 217 → standard
867    /// Macintosh entry 217; glyphNameIndex[408] is 262 → fifth Pascal
868    /// string (index 4).
869    #[test]
870    fn v20_resolves_spec_worked_example() {
871        let num_glyphs: u16 = 409;
872        let mut tail = Vec::new();
873        tail.extend_from_slice(&num_glyphs.to_be_bytes());
874        for gid in 0..num_glyphs {
875            let idx: u16 = match gid {
876                302 => 217,
877                408 => 262, // 258 + 4 → pascal_strings[4]
878                _ => 0,     // .notdef placeholder
879            };
880            tail.extend_from_slice(&idx.to_be_bytes());
881        }
882        // Pascal pool: 5 strings.
883        for name in ["one", "two", "three", "four", "weird"] {
884            tail.push(name.len() as u8);
885            tail.extend_from_slice(name.as_bytes());
886        }
887
888        let mut bytes = header(POST_VERSION_20);
889        bytes.extend_from_slice(&tail);
890        let p = PostTable::parse(&bytes).unwrap();
891        assert!(p.has_glyph_names());
892        assert_eq!(p.pascal_string_count(), 5);
893        assert_eq!(
894            p.glyph_name_ref(302),
895            Some(GlyphNameRef::StandardMac { index: 217 })
896        );
897        assert_eq!(p.glyph_name_ref(408), Some(GlyphNameRef::Custom("weird")));
898        assert_eq!(p.custom_glyph_name(408), Some("weird"));
899        assert!(p.custom_glyph_name(302).is_none());
900        // resolved_glyph_name covers both branches: the standard-Mac
901        // reference resolves to "tilde", the custom one to "weird".
902        assert_eq!(p.resolved_glyph_name(302), Some("tilde"));
903        assert_eq!(p.resolved_glyph_name(408), Some("weird"));
904    }
905
906    #[test]
907    fn v20_pascal_pool_indices_are_zero_based() {
908        // First custom Pascal string is `nameIndex == 258`.
909        let num_glyphs: u16 = 2;
910        let mut tail = Vec::new();
911        tail.extend_from_slice(&num_glyphs.to_be_bytes());
912        tail.extend_from_slice(&258u16.to_be_bytes()); // gid 0 -> "Alpha"
913        tail.extend_from_slice(&259u16.to_be_bytes()); // gid 1 -> "Beta"
914        for name in ["Alpha", "Beta"] {
915            tail.push(name.len() as u8);
916            tail.extend_from_slice(name.as_bytes());
917        }
918        let mut bytes = header(POST_VERSION_20);
919        bytes.extend_from_slice(&tail);
920        let p = PostTable::parse(&bytes).unwrap();
921        assert_eq!(p.glyph_name_ref(0), Some(GlyphNameRef::Custom("Alpha")));
922        assert_eq!(p.glyph_name_ref(1), Some(GlyphNameRef::Custom("Beta")));
923    }
924
925    #[test]
926    fn v20_rejects_truncated_pascal_string() {
927        // Length byte claims 5 chars, only 3 follow.
928        let num_glyphs: u16 = 1;
929        let mut tail = Vec::new();
930        tail.extend_from_slice(&num_glyphs.to_be_bytes());
931        tail.extend_from_slice(&258u16.to_be_bytes());
932        tail.push(5); // claim 5 bytes
933        tail.extend_from_slice(b"abc");
934        let mut bytes = header(POST_VERSION_20);
935        bytes.extend_from_slice(&tail);
936        assert!(matches!(
937            PostTable::parse(&bytes),
938            Err(Error::UnexpectedEof)
939        ));
940    }
941
942    #[test]
943    fn v20_flags_oversize_and_non_conformant_names() {
944        // 1 glyph naming index, 1 oversize name (64 chars of 'a') and
945        // 1 non-conformant name (contains '/').
946        let num_glyphs: u16 = 2;
947        let mut tail = Vec::new();
948        tail.extend_from_slice(&num_glyphs.to_be_bytes());
949        tail.extend_from_slice(&258u16.to_be_bytes()); // -> pool[0] (oversize)
950        tail.extend_from_slice(&259u16.to_be_bytes()); // -> pool[1] (non-conformant)
951        let oversize = "a".repeat(64);
952        tail.push(oversize.len() as u8);
953        tail.extend_from_slice(oversize.as_bytes());
954        let bad = "weird/name";
955        tail.push(bad.len() as u8);
956        tail.extend_from_slice(bad.as_bytes());
957        let mut bytes = header(POST_VERSION_20);
958        bytes.extend_from_slice(&tail);
959        let p = PostTable::parse(&bytes).unwrap();
960        assert!(p.has_oversize_glyph_name());
961        assert!(p.has_non_conformant_glyph_name());
962    }
963
964    #[test]
965    fn v25_resolves_signed_offset_into_standard_set() {
966        // §5.2.10.3 worked example: 3 glyphs (font ids 0, 1, 2) =
967        // standard ids 36, 37, 38 (A, B, C in standard order). Each
968        // offset is +36.
969        let num_glyphs: u16 = 3;
970        let mut tail = Vec::new();
971        tail.extend_from_slice(&num_glyphs.to_be_bytes());
972        tail.push(36i8 as u8);
973        tail.push(36i8 as u8);
974        tail.push(36i8 as u8);
975        let mut bytes = header(POST_VERSION_25);
976        bytes.extend_from_slice(&tail);
977        let p = PostTable::parse(&bytes).unwrap();
978        assert!(p.has_glyph_names());
979        assert_eq!(
980            p.glyph_name_ref(0),
981            Some(GlyphNameRef::StandardMac { index: 36 })
982        );
983        assert_eq!(
984            p.glyph_name_ref(1),
985            Some(GlyphNameRef::StandardMac { index: 37 })
986        );
987        assert_eq!(
988            p.glyph_name_ref(2),
989            Some(GlyphNameRef::StandardMac { index: 38 })
990        );
991        // Standard indices 36/37/38 are A/B/C.
992        assert_eq!(p.resolved_glyph_name(0), Some("A"));
993        assert_eq!(p.resolved_glyph_name(1), Some("B"));
994        assert_eq!(p.resolved_glyph_name(2), Some("C"));
995    }
996
997    #[test]
998    fn v25_negative_offset_below_zero_yields_none() {
999        // gid 0 with offset -1 would map to standard index -1; reject.
1000        let num_glyphs: u16 = 1;
1001        let mut tail = Vec::new();
1002        tail.extend_from_slice(&num_glyphs.to_be_bytes());
1003        tail.push((-1i8) as u8);
1004        let mut bytes = header(POST_VERSION_25);
1005        bytes.extend_from_slice(&tail);
1006        let p = PostTable::parse(&bytes).unwrap();
1007        assert!(p.glyph_name_ref(0).is_none());
1008    }
1009
1010    #[test]
1011    fn v25_offset_past_standard_set_yields_none() {
1012        // gid 0 with offset 257 maps to standard index 257 → still
1013        // legal. gid 0 with offset 127 + gid 250 with offset 127 maps
1014        // to 377 → out of range.
1015        let num_glyphs: u16 = 251;
1016        let mut tail = Vec::new();
1017        tail.extend_from_slice(&num_glyphs.to_be_bytes());
1018        for _ in 0..num_glyphs {
1019            tail.push(127i8 as u8);
1020        }
1021        let mut bytes = header(POST_VERSION_25);
1022        bytes.extend_from_slice(&tail);
1023        let p = PostTable::parse(&bytes).unwrap();
1024        // gid 0 + 127 = 127 (in range).
1025        assert_eq!(
1026            p.glyph_name_ref(0),
1027            Some(GlyphNameRef::StandardMac { index: 127 })
1028        );
1029        // gid 250 + 127 = 377 (out of [0,258)).
1030        assert!(p.glyph_name_ref(250).is_none());
1031    }
1032
1033    #[test]
1034    fn standard_mac_glyph_names_table_is_well_formed() {
1035        // Exactly 258 entries, all distinct (the count is keyed off the
1036        // shared STANDARD_MAC_GLYPH_COUNT constant).
1037        assert_eq!(STANDARD_MAC_GLYPH_NAMES.len(), 258);
1038        assert_eq!(
1039            STANDARD_MAC_GLYPH_NAMES.len(),
1040            STANDARD_MAC_GLYPH_COUNT as usize
1041        );
1042        let mut sorted: Vec<&str> = STANDARD_MAC_GLYPH_NAMES.to_vec();
1043        sorted.sort_unstable();
1044        sorted.dedup();
1045        assert_eq!(sorted.len(), 258, "all 258 names must be distinct");
1046    }
1047
1048    #[test]
1049    fn standard_mac_glyph_name_spot_checks() {
1050        // Anchors from the staged ordering doc.
1051        assert_eq!(standard_mac_glyph_name(0), Some(".notdef"));
1052        assert_eq!(standard_mac_glyph_name(1), Some(".null"));
1053        assert_eq!(standard_mac_glyph_name(2), Some("nonmarkingreturn"));
1054        assert_eq!(standard_mac_glyph_name(3), Some("space"));
1055        assert_eq!(standard_mac_glyph_name(36), Some("A"));
1056        assert_eq!(standard_mac_glyph_name(192), Some("fi"));
1057        assert_eq!(standard_mac_glyph_name(193), Some("fl"));
1058        assert_eq!(standard_mac_glyph_name(217), Some("tilde"));
1059        assert_eq!(standard_mac_glyph_name(257), Some("dcroat"));
1060        // Out of the standard set.
1061        assert_eq!(standard_mac_glyph_name(258), None);
1062        assert_eq!(standard_mac_glyph_name(u16::MAX), None);
1063    }
1064
1065    #[test]
1066    fn rejects_unknown_version() {
1067        // Apple v4.0 is "not supported in OpenType" per §5.2.10.
1068        let b = header(0x0004_0000);
1069        assert!(matches!(PostTable::parse(&b), Err(Error::BadStructure(_))));
1070    }
1071
1072    #[test]
1073    fn rejects_short_header() {
1074        let b = vec![0u8; 31];
1075        assert!(matches!(PostTable::parse(&b), Err(Error::UnexpectedEof)));
1076    }
1077
1078    #[test]
1079    fn v20_truncated_index_array_rejected() {
1080        // numGlyphs=2 but only 2 bytes of index data (need 4).
1081        let mut tail = Vec::new();
1082        tail.extend_from_slice(&2u16.to_be_bytes());
1083        tail.extend_from_slice(&0u16.to_be_bytes()); // only one entry
1084        let mut bytes = header(POST_VERSION_20);
1085        bytes.extend_from_slice(&tail);
1086        assert!(matches!(
1087            PostTable::parse(&bytes),
1088            Err(Error::UnexpectedEof)
1089        ));
1090    }
1091
1092    #[test]
1093    fn v20_pascal_index_out_of_pool_decodes_glyph_as_none() {
1094        // glyphNameIndex == 258 but no Pascal strings present.
1095        let num_glyphs: u16 = 1;
1096        let mut tail = Vec::new();
1097        tail.extend_from_slice(&num_glyphs.to_be_bytes());
1098        tail.extend_from_slice(&258u16.to_be_bytes());
1099        // No string-data bytes.
1100        let mut bytes = header(POST_VERSION_20);
1101        bytes.extend_from_slice(&tail);
1102        let p = PostTable::parse(&bytes).unwrap();
1103        assert!(p.glyph_name_ref(0).is_none());
1104    }
1105
1106    #[test]
1107    fn v10_reverse_lookup_spans_full_standard_set() {
1108        let b = header(POST_VERSION_10);
1109        let p = PostTable::parse(&b).unwrap();
1110        assert_eq!(p.named_glyph_count(), 258);
1111        // Every standard name resolves back to its index for v1.0.
1112        assert_eq!(p.gid_for_name(".notdef"), Some(0));
1113        assert_eq!(p.gid_for_name("A"), Some(36));
1114        assert_eq!(p.gid_for_name("tilde"), Some(217));
1115        assert_eq!(p.gid_for_name("dcroat"), Some(257));
1116        // A name not in the standard set has no glyph.
1117        assert_eq!(p.gid_for_name("Alpha"), None);
1118        assert_eq!(p.gid_for_name(""), None);
1119    }
1120
1121    #[test]
1122    fn v20_reverse_lookup_covers_custom_and_standard() {
1123        let num_glyphs: u16 = 4;
1124        let mut tail = Vec::new();
1125        tail.extend_from_slice(&num_glyphs.to_be_bytes());
1126        // gid 0 -> standard 0 (.notdef), gid 1 -> standard 36 (A),
1127        // gid 2 -> custom "Alpha", gid 3 -> custom "Beta".
1128        for idx in [0u16, 36, 258, 259] {
1129            tail.extend_from_slice(&idx.to_be_bytes());
1130        }
1131        for name in ["Alpha", "Beta"] {
1132            tail.push(name.len() as u8);
1133            tail.extend_from_slice(name.as_bytes());
1134        }
1135        let mut bytes = header(POST_VERSION_20);
1136        bytes.extend_from_slice(&tail);
1137        let p = PostTable::parse(&bytes).unwrap();
1138        assert_eq!(p.named_glyph_count(), 4);
1139        assert_eq!(p.gid_for_name(".notdef"), Some(0));
1140        assert_eq!(p.gid_for_name("A"), Some(1));
1141        assert_eq!(p.gid_for_name("Alpha"), Some(2));
1142        assert_eq!(p.gid_for_name("Beta"), Some(3));
1143        assert_eq!(p.gid_for_name("missing"), None);
1144    }
1145
1146    #[test]
1147    fn reverse_lookup_returns_lowest_gid_on_duplicate() {
1148        // Two glyphs both named "A" (standard index 36).
1149        let num_glyphs: u16 = 3;
1150        let mut tail = Vec::new();
1151        tail.extend_from_slice(&num_glyphs.to_be_bytes());
1152        for idx in [0u16, 36, 36] {
1153            tail.extend_from_slice(&idx.to_be_bytes());
1154        }
1155        let mut bytes = header(POST_VERSION_20);
1156        bytes.extend_from_slice(&tail);
1157        let p = PostTable::parse(&bytes).unwrap();
1158        // Lowest matching gid (1) wins over gid 2.
1159        assert_eq!(p.gid_for_name("A"), Some(1));
1160    }
1161
1162    #[test]
1163    fn v25_reverse_lookup_inverts_offset() {
1164        // §5.2.10.3 worked example: gids 0,1,2 -> standard 36,37,38.
1165        let num_glyphs: u16 = 3;
1166        let mut tail = Vec::new();
1167        tail.extend_from_slice(&num_glyphs.to_be_bytes());
1168        for _ in 0..3 {
1169            tail.push(36i8 as u8);
1170        }
1171        let mut bytes = header(POST_VERSION_25);
1172        bytes.extend_from_slice(&tail);
1173        let p = PostTable::parse(&bytes).unwrap();
1174        assert_eq!(p.named_glyph_count(), 3);
1175        assert_eq!(p.gid_for_name("A"), Some(0));
1176        assert_eq!(p.gid_for_name("B"), Some(1));
1177        assert_eq!(p.gid_for_name("C"), Some(2));
1178        // "D" is standard index 39 but no glyph maps to it here.
1179        assert_eq!(p.gid_for_name("D"), None);
1180    }
1181
1182    #[test]
1183    fn v30_reverse_lookup_and_iter_are_empty() {
1184        let b = header(POST_VERSION_30);
1185        let p = PostTable::parse(&b).unwrap();
1186        assert_eq!(p.named_glyph_count(), 0);
1187        assert_eq!(p.gid_for_name(".notdef"), None);
1188        assert_eq!(p.iter_glyph_names().count(), 0);
1189    }
1190
1191    #[test]
1192    fn iter_glyph_names_round_trips_through_reverse_lookup() {
1193        let num_glyphs: u16 = 4;
1194        let mut tail = Vec::new();
1195        tail.extend_from_slice(&num_glyphs.to_be_bytes());
1196        for idx in [0u16, 36, 258, 259] {
1197            tail.extend_from_slice(&idx.to_be_bytes());
1198        }
1199        for name in ["Alpha", "Beta"] {
1200            tail.push(name.len() as u8);
1201            tail.extend_from_slice(name.as_bytes());
1202        }
1203        let mut bytes = header(POST_VERSION_20);
1204        bytes.extend_from_slice(&tail);
1205        let p = PostTable::parse(&bytes).unwrap();
1206        let pairs: Vec<(u16, &str)> = p.iter_glyph_names().collect();
1207        assert_eq!(
1208            pairs,
1209            vec![(0, ".notdef"), (1, "A"), (2, "Alpha"), (3, "Beta")]
1210        );
1211        // Each iterated name reverse-resolves to its own gid (these are
1212        // all unique names, so the lowest-gid rule is exact here).
1213        for (gid, name) in pairs {
1214            assert_eq!(p.gid_for_name(name), Some(gid));
1215        }
1216    }
1217
1218    #[test]
1219    fn v20_iter_skips_unsatisfiable_pascal_reference() {
1220        // gid 0 names .notdef; gid 1 references a Pascal string that the
1221        // empty pool cannot satisfy -> skipped by iter, no reverse hit.
1222        let num_glyphs: u16 = 2;
1223        let mut tail = Vec::new();
1224        tail.extend_from_slice(&num_glyphs.to_be_bytes());
1225        tail.extend_from_slice(&0u16.to_be_bytes());
1226        tail.extend_from_slice(&258u16.to_be_bytes());
1227        let mut bytes = header(POST_VERSION_20);
1228        bytes.extend_from_slice(&tail);
1229        let p = PostTable::parse(&bytes).unwrap();
1230        let pairs: Vec<(u16, &str)> = p.iter_glyph_names().collect();
1231        assert_eq!(pairs, vec![(0, ".notdef")]);
1232    }
1233
1234    #[test]
1235    fn version_constants_sanity() {
1236        assert_eq!(POST_VERSION_10, 0x0001_0000);
1237        assert_eq!(POST_VERSION_20, 0x0002_0000);
1238        assert_eq!(POST_VERSION_25, 0x0002_5000);
1239        assert_eq!(POST_VERSION_30, 0x0003_0000);
1240        assert_eq!(STANDARD_MAC_GLYPH_COUNT, 258);
1241        assert_eq!(POST_HEADER_LEN, 32);
1242    }
1243}