Skip to main content

skrifa/outline/glyf/hint/engine/
mod.rs

1//! TrueType bytecode interpreter.
2
3mod arith;
4mod control_flow;
5mod cvt;
6mod data;
7mod definition;
8mod delta;
9mod dispatch;
10mod graphics;
11mod logical;
12mod misc;
13mod outline;
14mod round;
15mod stack;
16mod storage;
17
18use read_fonts::{
19    tables::glyf::bytecode::Instruction,
20    types::{F26Dot6, F2Dot14, Point},
21};
22
23use super::{
24    super::Outlines,
25    cvt::Cvt,
26    definition::DefinitionState,
27    error::{HintError, HintErrorKind},
28    graphics::{GraphicsState, RetainedGraphicsState},
29    math,
30    program::ProgramState,
31    storage::Storage,
32    value_stack::ValueStack,
33    zone::Zone,
34};
35
36/// Maximum number of instructions we will execute in `Engine::run()`. This
37/// is used to ensure termination of a hinting program.
38/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/include/freetype/config/ftoption.h#L744>
39const MAX_RUN_INSTRUCTIONS: usize = 1_000_000;
40
41pub type OpResult = Result<(), HintErrorKind>;
42
43/// TrueType bytecode interpreter.
44pub struct Engine<'a> {
45    program: ProgramState<'a>,
46    graphics: GraphicsState<'a>,
47    definitions: DefinitionState<'a>,
48    cvt: Cvt<'a>,
49    storage: Storage<'a>,
50    value_stack: ValueStack<'a>,
51    work_budget: WorkBudget,
52    axis_count: u16,
53    coords: &'a [F2Dot14],
54}
55
56impl<'a> Engine<'a> {
57    #[allow(clippy::too_many_arguments)]
58    pub fn new(
59        outlines: &Outlines,
60        program: ProgramState<'a>,
61        graphics: RetainedGraphicsState,
62        definitions: DefinitionState<'a>,
63        cvt: impl Into<Cvt<'a>>,
64        storage: impl Into<Storage<'a>>,
65        value_stack: ValueStack<'a>,
66        twilight: Zone<'a>,
67        glyph: Zone<'a>,
68        axis_count: u16,
69        coords: &'a [F2Dot14],
70        is_composite: bool,
71    ) -> Self {
72        let point_count = if glyph.points.is_empty() {
73            None
74        } else {
75            Some(glyph.points.len())
76        };
77        let graphics = GraphicsState {
78            retained: graphics,
79            zones: [twilight, glyph],
80            is_composite,
81            ..Default::default()
82        };
83        Self {
84            program,
85            graphics,
86            definitions,
87            cvt: cvt.into(),
88            storage: storage.into(),
89            value_stack,
90            work_budget: WorkBudget::new(outlines, point_count),
91            axis_count,
92            coords,
93        }
94    }
95
96    pub fn backward_compatibility(&self) -> bool {
97        self.graphics.backward_compatibility
98    }
99
100    pub fn retained_graphics_state(&self) -> &RetainedGraphicsState {
101        &self.graphics.retained
102    }
103}
104
105/// Tracks budgets for control flow to limit execution time.
106struct WorkBudget {
107    /// Maximum number of times we can do backward jumps or
108    /// loop calls.
109    loop_limit: usize,
110    /// Current number of backward jumps executed.
111    backward_jumps: usize,
112    /// Current number of loop call iterations executed.
113    loop_calls: usize,
114    /// Counts number of instructions skipped when handling if/else conditions.
115    skipped: usize,
116}
117
118impl WorkBudget {
119    fn new(outlines: &Outlines, point_count: Option<usize>) -> Self {
120        let cvt_len = outlines.cvt_len as usize;
121        // Compute limits for loop calls and backward jumps.
122        // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6955>
123        let loop_limit = if let Some(point_count) = point_count {
124            (point_count * 10).max(50) + (cvt_len / 10).max(50)
125        } else {
126            300 + 22 * cvt_len
127        };
128        // FreeType has two variables for neg_jump_counter_max and
129        // loopcall_counter_max but sets them to the same value so
130        // we'll just use a single limit.
131        Self {
132            loop_limit,
133            backward_jumps: 0,
134            loop_calls: 0,
135            skipped: 0,
136        }
137    }
138
139    fn reset(&mut self) {
140        self.backward_jumps = 0;
141        self.loop_calls = 0;
142        self.skipped = 0;
143    }
144
145    fn doing_backward_jump(&mut self) -> Result<(), HintErrorKind> {
146        self.backward_jumps += 1;
147        if self.backward_jumps > self.loop_limit {
148            Err(HintErrorKind::ExceededExecutionBudget)
149        } else {
150            Ok(())
151        }
152    }
153
154    fn doing_loop_call(&mut self, count: usize) -> Result<(), HintErrorKind> {
155        self.loop_calls += count;
156        if self.loop_calls > self.loop_limit {
157            Err(HintErrorKind::ExceededExecutionBudget)
158        } else {
159            Ok(())
160        }
161    }
162
163    fn skipping_instruction(&mut self) -> Result<(), HintErrorKind> {
164        self.skipped += 1;
165        if self.skipped > MAX_RUN_INSTRUCTIONS {
166            Err(HintErrorKind::ExceededExecutionBudget)
167        } else {
168            Ok(())
169        }
170    }
171}
172
173#[cfg(test)]
174use mock::MockEngine;
175
176#[cfg(test)]
177mod mock {
178    use super::{
179        super::{
180            cow_slice::CowSlice,
181            definition::{Definition, DefinitionMap, DefinitionState},
182            program::{Program, ProgramState},
183            zone::Zone,
184            Point, PointFlags,
185        },
186        Engine, F26Dot6, GraphicsState, ValueStack, WorkBudget,
187    };
188
189    /// Mock engine for testing.
190    pub(super) struct MockEngine {
191        cvt_storage: Vec<i32>,
192        value_stack: Vec<i32>,
193        definitions: Vec<Definition>,
194        unscaled: Vec<Point<i32>>,
195        points: Vec<Point<F26Dot6>>,
196        point_flags: Vec<PointFlags>,
197        contours: Vec<u16>,
198        twilight: Vec<Point<F26Dot6>>,
199        twilight_flags: Vec<PointFlags>,
200    }
201
202    impl MockEngine {
203        pub fn new() -> Self {
204            Self {
205                cvt_storage: vec![0; 32],
206                value_stack: vec![0; 32],
207                definitions: vec![Default::default(); 8],
208                unscaled: vec![Default::default(); 32],
209                points: vec![Default::default(); 64],
210                point_flags: vec![Default::default(); 32],
211                contours: vec![31],
212                twilight: vec![Default::default(); 32],
213                twilight_flags: vec![Default::default(); 32],
214            }
215        }
216
217        pub fn engine(&mut self) -> Engine<'_> {
218            let font_code = &[];
219            let cv_code = &[];
220            let glyph_code = &[];
221            let (cvt, storage) = self.cvt_storage.split_at_mut(16);
222            let (function_defs, instruction_defs) = self.definitions.split_at_mut(5);
223            let definition = DefinitionState::new(
224                DefinitionMap::Mut(function_defs),
225                DefinitionMap::Mut(instruction_defs),
226            );
227            for (i, point) in self.unscaled.iter_mut().enumerate() {
228                let i = i as i32;
229                point.x = 57 + i * 2;
230                point.y = -point.x * 3;
231            }
232            let (points, original) = self.points.split_at_mut(32);
233            let glyph_zone = Zone::new(
234                &self.unscaled,
235                original,
236                points,
237                &mut self.point_flags,
238                &self.contours,
239            );
240            let (points, original) = self.twilight.split_at_mut(16);
241            let twilight_zone = Zone::new(&[], original, points, &mut self.twilight_flags, &[]);
242            let mut graphics_state = GraphicsState {
243                zones: [twilight_zone, glyph_zone],
244                ..Default::default()
245            };
246            graphics_state.update_projection_state();
247            Engine {
248                graphics: graphics_state,
249                cvt: CowSlice::new_mut(cvt).into(),
250                storage: CowSlice::new_mut(storage).into(),
251                value_stack: ValueStack::new(&mut self.value_stack, false),
252                program: ProgramState::new(font_code, cv_code, glyph_code, Program::Font),
253                work_budget: WorkBudget {
254                    loop_limit: 10,
255                    backward_jumps: 0,
256                    loop_calls: 0,
257                    skipped: 0,
258                },
259                definitions: definition,
260                axis_count: 0,
261                coords: &[],
262            }
263        }
264    }
265
266    impl Default for MockEngine {
267        fn default() -> Self {
268            Self::new()
269        }
270    }
271
272    impl Engine<'_> {
273        /// Helper to push values to the stack, invoke a callback and check
274        /// the expected result.    
275        pub(super) fn test_exec(
276            &mut self,
277            push: &[i32],
278            expected_result: impl Into<i32>,
279            mut f: impl FnMut(&mut Engine),
280        ) {
281            for &val in push {
282                self.value_stack.push(val).unwrap();
283            }
284            f(self);
285            assert_eq!(self.value_stack.pop().ok(), Some(expected_result.into()));
286        }
287    }
288}