Skip to main content

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

1//! Managing the flow of control.
2//!
3//! Implements 6 instructions.
4//!
5//! See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#managing-the-flow-of-control>
6
7use read_fonts::tables::glyf::bytecode::Opcode;
8
9use super::{Engine, HintErrorKind, OpResult};
10
11impl Engine<'_> {
12    /// If test.
13    ///
14    /// IF[] (0x58)
15    ///
16    /// Pops: e: stack element
17    ///
18    /// Tests the element popped off the stack: if it is zero (FALSE), the
19    /// instruction pointer is jumped to the next ELSE or EIF instruction
20    /// in the instruction stream. If the element at the top of the stack is
21    /// nonzero (TRUE), the next instruction in the instruction stream is
22    /// executed. Execution continues until an ELSE instruction is encountered
23    /// or an EIF instruction ends the IF. If an else statement is found before
24    /// the EIF, the instruction pointer is moved to the EIF statement.
25    ///
26    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#if-test>
27    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3334>
28    pub(super) fn op_if(&mut self) -> OpResult {
29        if self.value_stack.pop()? == 0 {
30            // The condition variable is false so we jump to the next
31            // ELSE or EIF but we have to skip intermediate IF/ELSE/EIF
32            // instructions.
33            let mut nest_depth = 1;
34            let mut out = false;
35            while !out {
36                let opcode = self.decode_next_opcode()?;
37                self.work_budget.skipping_instruction()?;
38                match opcode {
39                    Opcode::IF => nest_depth += 1,
40                    Opcode::ELSE => out = nest_depth == 1,
41                    Opcode::EIF => {
42                        nest_depth -= 1;
43                        out = nest_depth == 0;
44                    }
45                    _ => {}
46                }
47            }
48        }
49        Ok(())
50    }
51
52    /// Else.
53    ///
54    /// ELSE[] (0x1B)
55    ///
56    /// Marks the start of the sequence of instructions that are to be executed
57    /// if an IF instruction encounters a FALSE value on the stack. This
58    /// sequence of instructions is terminated with an EIF instruction.
59    ///
60    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#else>
61    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3378>
62    pub(super) fn op_else(&mut self) -> OpResult {
63        let mut nest_depth = 1;
64        while nest_depth != 0 {
65            let opcode = self.decode_next_opcode()?;
66            self.work_budget.skipping_instruction()?;
67            match opcode {
68                Opcode::IF => nest_depth += 1,
69                Opcode::EIF => nest_depth -= 1,
70                _ => {}
71            }
72        }
73        Ok(())
74    }
75
76    /// End if.
77    ///
78    /// EIF[] (0x59)
79    ///
80    /// Marks the end of an IF[] instruction.
81    ///
82    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#end-if>
83    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3411>
84    pub(super) fn op_eif(&mut self) -> OpResult {
85        // Nothing
86        Ok(())
87    }
88
89    /// Jump relative on true.
90    ///
91    /// JROT[] (0x78)
92    ///
93    /// Pops: e: stack element
94    ///       offset: number of bytes to move the instruction pointer
95    ///
96    /// Pops and tests the element value, and then pops the offset. If the
97    /// element value is non-zero (TRUE), the signed offset will be added
98    /// to the instruction pointer and execution will be resumed at the address
99    /// obtained. Otherwise, the jump is not taken and the next instruction in
100    /// the instruction stream is executed. The jump is relative to the position
101    /// of the instruction itself. That is, the instruction pointer is still
102    /// pointing at the JROT[ ] instruction when offset is added to obtain the
103    /// new address.
104    ///
105    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#jump-relative-on-true>
106    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3459>
107    pub(super) fn op_jrot(&mut self) -> OpResult {
108        let e = self.value_stack.pop()?;
109        self.do_jump(e != 0)
110    }
111
112    /// Jump.
113    ///
114    /// JMPR[] (0x1C)
115    ///
116    /// Pops: offset: number of bytes to move the instruction pointer
117    ///
118    /// The signed offset is added to the instruction pointer and execution
119    /// is resumed at the new location in the instruction steam. The jump is
120    /// relative to the position of the instruction itself. That is, the
121    /// instruction pointer is still pointing at the JROT[] instruction when
122    /// offset is added to obtain the new address.
123    ///
124    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#jump>
125    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3424>
126    pub(super) fn op_jmpr(&mut self) -> OpResult {
127        self.do_jump(true)
128    }
129
130    /// Jump relative on false.
131    ///
132    /// JROF[] (0x78)
133    ///
134    /// Pops: e: stack element
135    ///       offset: number of bytes to move the instruction pointer
136    ///
137    /// Pops and tests the element value, and then pops the offset. If the
138    /// element value is non-zero (TRUE), the signed offset will be added
139    /// to the instruction pointer and execution will be resumed at the address
140    /// obtained. Otherwise, the jump is not taken and the next instruction in
141    /// the instruction stream is executed. The jump is relative to the position
142    /// of the instruction itself. That is, the instruction pointer is still
143    /// pointing at the JROT[ ] instruction when offset is added to obtain the
144    /// new address.
145    ///
146    /// Pops and tests the element value, and then pops the offset. If the
147    /// element value is zero (FALSE), the signed offset will be added to the
148    /// nstruction pointer and execution will be resumed at the address
149    /// obtainted. Otherwise, the jump is not taken and the next instruction
150    /// in the instruction stream is executed. The jump is relative to the
151    /// position of the instruction itself. That is, the instruction pointer is
152    /// still pointing at the JROT[ ] instruction when the offset is added to
153    /// obtain the new address.
154    ///
155    /// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#jump-relative-on-false>
156    /// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3474>
157    pub(super) fn op_jrof(&mut self) -> OpResult {
158        let e = self.value_stack.pop()?;
159        self.do_jump(e == 0)
160    }
161
162    /// Common code for jump instructions.
163    ///
164    /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3424>
165    fn do_jump(&mut self, test: bool) -> OpResult {
166        // Offset is relative to previous jump instruction and decoder is
167        // already pointing to next instruction, so subtract one
168        let jump_offset = self.value_stack.pop()?.wrapping_sub(1);
169        if test {
170            if jump_offset < 0 {
171                if jump_offset == -1 {
172                    // If the offset is -1, we'll just loop in place... forever
173                    return Err(HintErrorKind::InvalidJump);
174                }
175                self.work_budget.doing_backward_jump()?;
176            }
177            self.program.decoder.pc = self
178                .program
179                .decoder
180                .pc
181                .wrapping_add_signed(jump_offset as isize);
182        }
183        Ok(())
184    }
185
186    fn decode_next_opcode(&mut self) -> Result<Opcode, HintErrorKind> {
187        Ok(self
188            .program
189            .decoder
190            .decode()
191            .ok_or(HintErrorKind::UnexpectedEndOfBytecode)??
192            .opcode)
193    }
194}
195
196#[cfg(test)]
197mod tests {
198    use super::{super::MockEngine, super::MAX_RUN_INSTRUCTIONS, HintErrorKind, Opcode};
199
200    #[test]
201    fn if_else() {
202        use Opcode::*;
203        let mut mock = MockEngine::new();
204        let mut engine = mock.engine();
205        // Some code with nested ifs
206        #[rustfmt::skip]
207        let ops = [
208            IF,
209                ADD, // 1
210                SUB,
211                IF,
212                    MUL, // 4
213                    DIV,
214                ELSE, // 8
215                    IUP0, // 7
216                    IUP1,
217                EIF,
218            ELSE, // 10
219                RUTG, // 11
220                IF,
221                EIF,
222            EIF // 14
223        ];
224        let bytecode = ops.map(|op| op as u8);
225        engine.program.decoder.bytecode = bytecode.as_slice();
226        // Outer if
227        {
228            // push a true value to enter the first branch
229            engine.program.decoder.pc = 1;
230            engine.value_stack.push(1).unwrap();
231            engine.op_if().unwrap();
232            assert_eq!(engine.program.decoder.pc, 1);
233            // false enters the else branch
234            engine.program.decoder.pc = 1;
235            engine.value_stack.push(0).unwrap();
236            engine.op_if().unwrap();
237            assert_eq!(engine.program.decoder.pc, 11);
238        }
239        // Inner if
240        {
241            // push a true value to enter the first branch
242            engine.program.decoder.pc = 4;
243            engine.value_stack.push(1).unwrap();
244            engine.op_if().unwrap();
245            assert_eq!(engine.program.decoder.pc, 4);
246            // false enters the else branch
247            engine.program.decoder.pc = 4;
248            engine.value_stack.push(0).unwrap();
249            engine.op_if().unwrap();
250            assert_eq!(engine.program.decoder.pc, 7);
251        }
252        // Else with nested if
253        {
254            // This jumps to the instruction after the next EIF, skipping any
255            // nested conditional blocks
256            engine.program.decoder.pc = 10;
257            engine.op_else().unwrap();
258            assert_eq!(engine.program.decoder.pc, 15);
259            engine.program.decoder.pc = 8;
260            engine.op_else().unwrap();
261            assert_eq!(engine.program.decoder.pc, 10);
262        }
263    }
264
265    #[test]
266    fn jumps() {
267        let mut mock = MockEngine::new();
268        let mut engine = mock.engine();
269        // Unconditional jump
270        {
271            engine.program.decoder.pc = 1000;
272            engine.value_stack.push(100).unwrap();
273            engine.op_jmpr().unwrap();
274            assert_eq!(engine.program.decoder.pc, 1099);
275        }
276        // Jump if true
277        {
278            engine.program.decoder.pc = 1000;
279            // first test false condition, pc shouldn't change
280            engine.value_stack.push(100).unwrap();
281            engine.value_stack.push(0).unwrap();
282            engine.op_jrot().unwrap();
283            assert_eq!(engine.program.decoder.pc, 1000);
284            // then true condition
285            engine.value_stack.push(100).unwrap();
286            engine.value_stack.push(1).unwrap();
287            engine.op_jrot().unwrap();
288            assert_eq!(engine.program.decoder.pc, 1099);
289        }
290        // Jump if false
291        {
292            engine.program.decoder.pc = 1000;
293            // first test true condition, pc shouldn't change
294            engine.value_stack.push(-100).unwrap();
295            engine.value_stack.push(1).unwrap();
296            engine.op_jrof().unwrap();
297            assert_eq!(engine.program.decoder.pc, 1000);
298            // then false condition
299            engine.value_stack.push(-100).unwrap();
300            engine.value_stack.push(0).unwrap();
301            engine.op_jrof().unwrap();
302            assert_eq!(engine.program.decoder.pc, 899);
303        }
304        // Exhaust backward jump loop budget
305        {
306            engine.work_budget.loop_limit = 40;
307            for i in 0..45 {
308                engine.value_stack.push(-5).unwrap();
309                let result = engine.op_jmpr();
310                if i < 39 {
311                    result.unwrap();
312                } else {
313                    assert!(matches!(
314                        result,
315                        Err(HintErrorKind::ExceededExecutionBudget)
316                    ));
317                }
318            }
319        }
320    }
321
322    #[test]
323    fn skipping_budget() {
324        let mut mock = MockEngine::new();
325        let mut engine = mock.engine();
326        let bytecode = [Opcode::IF, Opcode::EIF].map(|op| op as u8);
327        engine.program.decoder.bytecode = bytecode.as_slice();
328        // Simulate the budget being exhausted and trigger one more skipped
329        // instruction while scanning for matching control-flow opcodes.
330        engine.work_budget.skipped = MAX_RUN_INSTRUCTIONS;
331        engine.program.decoder.pc = 1;
332        engine.value_stack.push(0).unwrap();
333        let result = engine.op_if();
334        assert!(matches!(
335            result,
336            Err(HintErrorKind::ExceededExecutionBudget)
337        ));
338    }
339}