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}