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