1include!("../../generated/generated_aat.rs");
6
7pub mod class {
11 pub const END_OF_TEXT: u8 = 0;
12 pub const OUT_OF_BOUNDS: u8 = 1;
13 pub const DELETED_GLYPH: u8 = 2;
14}
15
16impl Lookup0<'_> {
17 pub fn values<T: LookupValue>(&self) -> Result<&[BigEndian<T>], ReadError> {
18 let data = self.values_data();
19 let data_len = data.len();
20 let n_elems = data_len / T::RAW_BYTE_LEN;
21 let len_in_bytes = n_elems * T::RAW_BYTE_LEN;
22 FontData::new(&data[..len_in_bytes])
23 .cursor()
24 .read_array::<BigEndian<T>>(n_elems)
25 }
26 pub fn value<T: LookupValue>(&self, index: u16) -> Result<T, ReadError> {
27 self.values::<T>()?
28 .get(index as usize)
29 .map(|val| val.get())
30 .ok_or(ReadError::OutOfBounds)
31 }
32}
33
34#[derive(Copy, Clone, bytemuck::AnyBitPattern)]
36#[repr(C, packed)]
37pub struct LookupSegment2<T>
38where
39 T: LookupValue,
40{
41 pub last_glyph: BigEndian<u16>,
43 pub first_glyph: BigEndian<u16>,
45 pub value: BigEndian<T>,
47}
48
49impl<T: LookupValue> FixedSize for LookupSegment2<T> {
51 const RAW_BYTE_LEN: usize = std::mem::size_of::<Self>();
52}
53
54impl Lookup2<'_> {
55 pub fn value<T: LookupValue>(&self, index: u16) -> Result<T, ReadError> {
56 let segments = self.segments::<T>()?;
57 let ix = match segments.binary_search_by(|segment| segment.first_glyph.get().cmp(&index)) {
58 Ok(ix) => ix,
59 Err(ix) => ix.saturating_sub(1),
60 };
61 let segment = segments.get(ix).ok_or(ReadError::OutOfBounds)?;
62 if (segment.first_glyph.get()..=segment.last_glyph.get()).contains(&index) {
63 let value = segment.value;
64 return Ok(value.get());
65 }
66 Err(ReadError::OutOfBounds)
67 }
68
69 pub fn segments<T: LookupValue>(&self) -> Result<&[LookupSegment2<T>], ReadError> {
70 FontData::new(self.segments_data())
71 .cursor()
72 .read_array(self.n_units() as usize)
73 }
74}
75
76impl Lookup4<'_> {
77 pub fn value<T: LookupValue>(&self, index: u16) -> Result<T, ReadError> {
78 let segments = self.segments();
79 let ix = match segments.binary_search_by(|segment| segment.first_glyph.get().cmp(&index)) {
80 Ok(ix) => ix,
81 Err(ix) => ix.saturating_sub(1),
82 };
83 let segment = segments.get(ix).ok_or(ReadError::OutOfBounds)?;
84 if (segment.first_glyph.get()..=segment.last_glyph.get()).contains(&index) {
85 let base_offset = segment.value_offset() as usize;
86 let offset = base_offset
87 + index
88 .checked_sub(segment.first_glyph())
89 .ok_or(ReadError::OutOfBounds)? as usize
90 * T::RAW_BYTE_LEN;
91 return self.offset_data().read_at(offset);
92 }
93 Err(ReadError::OutOfBounds)
94 }
95 pub fn segment_values<T: LookupValue>(
96 &self,
97 segment: usize,
98 ) -> Result<&[BigEndian<T>], ReadError> {
99 let segment = self.segments().get(segment).ok_or(ReadError::OutOfBounds)?;
100 let base_offset = segment.value_offset() as usize;
101 let n_elems = segment
102 .last_glyph
103 .get()
104 .checked_sub(segment.first_glyph.get())
105 .ok_or(ReadError::MalformedData(
106 "invalid segment in format 4 AAT lookup table",
107 ))? as usize
108 + 1;
109 self.offset_data()
110 .read_array::<BigEndian<T>>(base_offset..base_offset + n_elems * T::RAW_BYTE_LEN)
111 }
112}
113
114#[derive(Copy, Clone, bytemuck::AnyBitPattern)]
116#[repr(C, packed)]
117pub struct LookupSingle<T>
118where
119 T: LookupValue,
120{
121 pub glyph: BigEndian<u16>,
123 pub value: BigEndian<T>,
125}
126
127impl<T: LookupValue> FixedSize for LookupSingle<T> {
129 const RAW_BYTE_LEN: usize = std::mem::size_of::<Self>();
130}
131
132impl Lookup6<'_> {
133 pub fn value<T: LookupValue>(&self, index: u16) -> Result<T, ReadError> {
134 let entries = self.entries::<T>()?;
135 if let Ok(ix) = entries.binary_search_by_key(&index, |entry| entry.glyph.get()) {
136 let entry = &entries[ix];
137 let value = entry.value;
138 return Ok(value.get());
139 }
140 Err(ReadError::OutOfBounds)
141 }
142
143 pub fn entries<T: LookupValue>(&self) -> Result<&[LookupSingle<T>], ReadError> {
144 FontData::new(self.entries_data())
145 .cursor()
146 .read_array(self.n_units() as usize)
147 }
148}
149
150impl Lookup8<'_> {
151 pub fn value<T: LookupValue>(&self, index: u16) -> Result<T, ReadError> {
152 index
153 .checked_sub(self.first_glyph())
154 .and_then(|ix| {
155 self.value_array()
156 .get(ix as usize)
157 .map(|val| T::from_u16(val.get()))
158 })
159 .ok_or(ReadError::OutOfBounds)
160 }
161}
162
163impl Lookup10<'_> {
164 pub fn value<T: LookupValue>(&self, index: u16) -> Result<T, ReadError> {
165 let ix = index
166 .checked_sub(self.first_glyph())
167 .ok_or(ReadError::OutOfBounds)? as usize;
168 let unit_size = self.unit_size() as usize;
169 let offset = ix * unit_size;
170 let mut cursor = FontData::new(self.values_data()).cursor();
171 cursor.advance_by(offset);
172 let val = match unit_size {
173 1 => cursor.read::<u8>()? as u32,
174 2 => cursor.read::<u16>()? as u32,
175 4 => cursor.read::<u32>()?,
176 _ => {
177 return Err(ReadError::MalformedData(
178 "invalid unit_size in format 10 AAT lookup table",
179 ))
180 }
181 };
182 Ok(T::from_u32(val))
183 }
184}
185
186impl Lookup<'_> {
187 pub fn value<T: LookupValue>(&self, index: u16) -> Result<T, ReadError> {
188 match self {
189 Lookup::Format0(lookup) => lookup.value::<T>(index),
190 Lookup::Format2(lookup) => lookup.value::<T>(index),
191 Lookup::Format4(lookup) => lookup.value::<T>(index),
192 Lookup::Format6(lookup) => lookup.value::<T>(index),
193 Lookup::Format8(lookup) => lookup.value::<T>(index),
194 Lookup::Format10(lookup) => lookup.value::<T>(index),
195 }
196 }
197}
198
199#[derive(Clone)]
200pub struct TypedLookup<'a, T> {
201 pub lookup: Lookup<'a>,
202 _marker: std::marker::PhantomData<fn() -> T>,
203}
204
205impl<T: LookupValue> TypedLookup<'_, T> {
206 pub fn value(&self, index: u16) -> Result<T, ReadError> {
208 self.lookup.value::<T>(index)
209 }
210}
211
212impl<'a, T> FontRead<'a> for TypedLookup<'a, T> {
213 fn read(data: FontData<'a>) -> Result<Self, ReadError> {
214 Ok(Self {
215 lookup: Lookup::read(data)?,
216 _marker: std::marker::PhantomData,
217 })
218 }
219}
220
221#[cfg(feature = "experimental_traverse")]
222impl<'a, T> SomeTable<'a> for TypedLookup<'a, T> {
223 fn type_name(&self) -> &str {
224 "TypedLookup"
225 }
226
227 fn get_field(&self, idx: usize) -> Option<Field<'a>> {
228 self.lookup.get_field(idx)
229 }
230}
231
232pub trait LookupValue: Copy + Scalar + bytemuck::AnyBitPattern {
234 fn from_u16(v: u16) -> Self;
235 fn from_u32(v: u32) -> Self;
236}
237
238impl LookupValue for u16 {
239 fn from_u16(v: u16) -> Self {
240 v
241 }
242
243 fn from_u32(v: u32) -> Self {
244 v as _
246 }
247}
248
249impl LookupValue for u32 {
250 fn from_u16(v: u16) -> Self {
251 v as _
252 }
253
254 fn from_u32(v: u32) -> Self {
255 v
256 }
257}
258
259impl LookupValue for GlyphId16 {
260 fn from_u16(v: u16) -> Self {
261 GlyphId16::from(v)
262 }
263
264 fn from_u32(v: u32) -> Self {
265 GlyphId16::from(v as u16)
267 }
268}
269
270pub type LookupU16<'a> = TypedLookup<'a, u16>;
271pub type LookupU32<'a> = TypedLookup<'a, u32>;
272pub type LookupGlyphId<'a> = TypedLookup<'a, GlyphId16>;
273
274#[derive(Copy, Clone, bytemuck::AnyBitPattern, Debug)]
280pub struct NoPayload(());
281
282impl FixedSize for NoPayload {
283 const RAW_BYTE_LEN: usize = 0;
284}
285
286#[derive(Clone, Debug)]
288pub struct StateEntry<T = NoPayload> {
289 pub new_state: u16,
291 pub flags: u16,
293 pub payload: T,
295}
296
297impl<'a, T: bytemuck::AnyBitPattern + FixedSize> FontRead<'a> for StateEntry<T> {
298 fn read(data: FontData<'a>) -> Result<Self, ReadError> {
299 let mut cursor = data.cursor();
300 let new_state = cursor.read()?;
301 let flags = cursor.read()?;
302 let remaining = cursor.remaining().ok_or(ReadError::OutOfBounds)?;
303 let payload = *remaining.read_ref_at(0)?;
304 Ok(Self {
305 new_state,
306 flags,
307 payload,
308 })
309 }
310}
311
312impl<T> FixedSize for StateEntry<T>
313where
314 T: FixedSize,
315{
316 const RAW_BYTE_LEN: usize = u16::RAW_BYTE_LEN + u16::RAW_BYTE_LEN + T::RAW_BYTE_LEN;
318}
319
320#[derive(Clone)]
330pub struct StateTable<'a> {
331 pub header: StateHeader<'a>,
332 n_classes: usize,
333 class_first_glyph: u16,
334 class_array: &'a [u8],
335 state_array: &'a [u8],
336 entry_table: &'a [u8],
337}
338
339impl StateTable<'_> {
340 pub const HEADER_LEN: usize = u16::RAW_BYTE_LEN * 4;
341
342 pub fn class(&self, glyph_id: GlyphId16) -> Result<u8, ReadError> {
344 let glyph_id = glyph_id.to_u16();
345 if glyph_id == 0xFFFF {
346 return Ok(class::DELETED_GLYPH);
347 }
348 glyph_id
349 .checked_sub(self.class_first_glyph)
350 .and_then(|ix| self.class_array.get(ix as usize).copied())
351 .ok_or(ReadError::OutOfBounds)
352 }
353
354 #[inline(always)]
356 pub fn entry(&self, state: u16, class: u8) -> Result<StateEntry, ReadError> {
357 let mut class = class as usize;
358 if class >= self.n_classes {
359 class = class::OUT_OF_BOUNDS as usize;
360 }
361 let entry_ix = self
362 .state_array
363 .get(state as usize * self.n_classes + class)
364 .copied()
365 .ok_or(ReadError::OutOfBounds)? as usize;
366 let entry_offset = entry_ix * 4;
367 let entry_data = self
368 .entry_table
369 .get(entry_offset..)
370 .ok_or(ReadError::OutOfBounds)?;
371 let mut entry = StateEntry::read(FontData::new(entry_data))?;
372 let new_state = (entry.new_state as i32)
375 .checked_sub(self.header.state_array_offset().to_u32() as i32)
376 .ok_or(ReadError::OutOfBounds)?
377 / self.n_classes as i32;
378 entry.new_state = new_state.try_into().map_err(|_| ReadError::OutOfBounds)?;
379 Ok(entry)
380 }
381
382 pub fn read_value<T: Scalar>(&self, offset: usize) -> Result<T, ReadError> {
384 self.header.offset_data().read_at::<T>(offset)
385 }
386}
387
388impl<'a> FontRead<'a> for StateTable<'a> {
389 fn read(data: FontData<'a>) -> Result<Self, ReadError> {
390 let header = StateHeader::read(data)?;
391 let n_classes = header.state_size() as usize;
393 if n_classes == 0 {
394 return Err(ReadError::MalformedData("empty AAT state table"));
396 }
397 let class_table = header.class_table()?;
398 let class_first_glyph = class_table.first_glyph();
399 let class_array = class_table.class_array();
400 let state_array = header.state_array()?.data();
401 let entry_table = header.entry_table()?.data();
402 Ok(Self {
403 header: StateHeader::read(data)?,
404 n_classes,
405 class_first_glyph,
406 class_array,
407 state_array,
408 entry_table,
409 })
410 }
411}
412
413#[cfg(feature = "experimental_traverse")]
414impl<'a> SomeTable<'a> for StateTable<'a> {
415 fn type_name(&self) -> &str {
416 "StateTable"
417 }
418
419 fn get_field(&self, idx: usize) -> Option<Field<'a>> {
420 self.header.get_field(idx)
421 }
422}
423
424#[derive(Clone)]
425pub struct ExtendedStateTable<'a, T = NoPayload> {
426 pub n_classes: usize,
427 pub class_table: LookupU16<'a>,
428 state_array: &'a [BigEndian<u16>],
429 entry_table: &'a [u8],
430 _marker: std::marker::PhantomData<fn() -> T>,
431}
432
433impl<T> ExtendedStateTable<'_, T> {
434 pub const HEADER_LEN: usize = u32::RAW_BYTE_LEN * 4;
435}
436
437impl<T> ExtendedStateTable<'_, T>
447where
448 T: FixedSize + bytemuck::AnyBitPattern,
449{
450 pub fn class(&self, glyph_id: GlyphId) -> Result<u16, ReadError> {
452 let glyph_id: u16 = glyph_id
453 .to_u32()
454 .try_into()
455 .map_err(|_| ReadError::OutOfBounds)?;
456 if glyph_id == 0xFFFF {
457 return Ok(class::DELETED_GLYPH as u16);
458 }
459 self.class_table.value(glyph_id)
460 }
461
462 pub fn entry(&self, state: u16, class: u16) -> Result<StateEntry<T>, ReadError> {
464 let mut class = class as usize;
465 if class >= self.n_classes {
466 class = class::OUT_OF_BOUNDS as usize;
467 }
468 let state_ix = state as usize * self.n_classes + class;
469 let entry_ix = self
470 .state_array
471 .get(state_ix)
472 .copied()
473 .ok_or(ReadError::OutOfBounds)?
474 .get() as usize;
475 let entry_offset = entry_ix * StateEntry::<T>::RAW_BYTE_LEN;
476 let entry_data = self
477 .entry_table
478 .get(entry_offset..)
479 .ok_or(ReadError::OutOfBounds)?;
480 StateEntry::read(FontData::new(entry_data))
481 }
482}
483
484impl<'a, T> FontRead<'a> for ExtendedStateTable<'a, T> {
485 fn read(data: FontData<'a>) -> Result<Self, ReadError> {
486 let header = StxHeader::read(data)?;
487 let n_classes = header.n_classes() as usize;
488 let class_table = header.class_table()?;
489 let state_array = header.state_array()?.data();
490 let entry_table = header.entry_table()?.data();
491 Ok(Self {
492 n_classes,
493 class_table,
494 state_array,
495 entry_table,
496 _marker: std::marker::PhantomData,
497 })
498 }
499}
500
501#[cfg(feature = "experimental_traverse")]
502impl<'a, T> SomeTable<'a> for ExtendedStateTable<'a, T> {
503 fn type_name(&self) -> &str {
504 "ExtendedStateTable"
505 }
506
507 fn get_field(&self, _idx: usize) -> Option<Field<'a>> {
508 None
509 }
510}
511
512pub(crate) fn safe_read_array_to_end<'a, T: bytemuck::AnyBitPattern + FixedSize>(
520 data: &FontData<'a>,
521 offset: usize,
522) -> Result<&'a [T], ReadError> {
523 let len = data
524 .len()
525 .checked_sub(offset)
526 .ok_or(ReadError::OutOfBounds)?;
527 let end = offset + len / T::RAW_BYTE_LEN * T::RAW_BYTE_LEN;
528 data.read_array(offset..end)
529}
530
531#[cfg(test)]
532mod tests {
533 use font_test_data::bebuffer::BeBuffer;
534
535 use super::*;
536
537 #[test]
538 fn lookup_format_0() {
539 #[rustfmt::skip]
540 let words = [
541 0_u16, 0, 2, 4, 6, 8, 10, 12, 14, 16, ];
544 let mut buf = BeBuffer::new();
545 buf = buf.extend(words);
546 let lookup = LookupU16::read(buf.data().into()).unwrap();
547 for gid in 0..=8 {
548 assert_eq!(lookup.value(gid).unwrap(), gid * 2);
549 }
550 assert!(lookup.value(9).is_err());
551 }
552
553 #[test]
555 fn lookup_format_2() {
556 #[rustfmt::skip]
557 let words = [
558 2_u16, 6, 3, 12, 1, 6, 22, 20, 4, 24, 23, 5, 28, 25, 6, ];
568 let mut buf = BeBuffer::new();
569 buf = buf.extend(words);
570 let lookup = LookupU16::read(buf.data().into()).unwrap();
571 let expected = [(20..=22, 4), (23..=24, 5), (25..=28, 6)];
572 for (range, class) in expected {
573 for gid in range {
574 assert_eq!(lookup.value(gid).unwrap(), class);
575 }
576 }
577 for fail in [0, 10, 19, 29, 0xFFFF] {
578 assert!(lookup.value(fail).is_err());
579 }
580 }
581
582 #[test]
583 fn lookup_format_4() {
584 #[rustfmt::skip]
585 let words = [
586 4_u16, 6, 3, 12, 1, 6, 22, 20, 30, 24, 23, 36, 28, 25, 40, 3, 2, 1,
597 100, 150,
598 8, 6, 7, 9
599 ];
600 let mut buf = BeBuffer::new();
601 buf = buf.extend(words);
602 let lookup = LookupU16::read(buf.data().into()).unwrap();
603 let expected = [
604 (20, 3),
605 (21, 2),
606 (22, 1),
607 (23, 100),
608 (24, 150),
609 (25, 8),
610 (26, 6),
611 (27, 7),
612 (28, 9),
613 ];
614 for (in_glyph, out_glyph) in expected {
615 assert_eq!(lookup.value(in_glyph).unwrap(), out_glyph);
616 }
617 for fail in [0, 10, 19, 29, 0xFFFF] {
618 assert!(lookup.value(fail).is_err());
619 }
620 }
621
622 #[test]
624 fn lookup_format_6() {
625 #[rustfmt::skip]
626 let words = [
627 6_u16, 4, 4, 16, 2, 0, 50, 600, 51, 601, 201, 602, 202, 900, ];
638 let mut buf = BeBuffer::new();
639 buf = buf.extend(words);
640 let lookup = LookupU16::read(buf.data().into()).unwrap();
641 let expected = [(50, 600), (51, 601), (201, 602), (202, 900)];
642 for (in_glyph, out_glyph) in expected {
643 assert_eq!(lookup.value(in_glyph).unwrap(), out_glyph);
644 }
645 for fail in [0, 10, 49, 52, 203, 0xFFFF] {
646 assert!(lookup.value(fail).is_err());
647 }
648 }
649
650 #[test]
651 fn lookup_format_8() {
652 #[rustfmt::skip]
653 let words = [
654 8_u16, 201, 7, 3, 8, 2, 9, 1, 200, 60, ];
659 let mut buf = BeBuffer::new();
660 buf = buf.extend(words);
661 let lookup = LookupU16::read(buf.data().into()).unwrap();
662 let expected = &words[3..];
663 for (gid, expected) in (201..209).zip(expected) {
664 assert_eq!(lookup.value(gid).unwrap(), *expected);
665 }
666 for fail in [0, 10, 200, 210, 0xFFFF] {
667 assert!(lookup.value(fail).is_err());
668 }
669 }
670
671 #[test]
672 fn lookup_format_10() {
673 #[rustfmt::skip]
674 let words = [
675 10_u16, 4, 201, 7, ];
680 let mapped = [3_u32, 8, 2902384, 9, 1, u32::MAX, 60];
682 let mut buf = BeBuffer::new();
683 buf = buf.extend(words).extend(mapped);
684 let lookup = LookupU32::read(buf.data().into()).unwrap();
685 for (gid, expected) in (201..209).zip(mapped) {
686 assert_eq!(lookup.value(gid).unwrap(), expected);
687 }
688 for fail in [0, 10, 200, 210, 0xFFFF] {
689 assert!(lookup.value(fail).is_err());
690 }
691 }
692
693 #[test]
694 fn extended_state_table() {
695 #[rustfmt::skip]
696 let header = [
697 6_u32, 20, 56, 92, 0, ];
703 #[rustfmt::skip]
704 let class_table = [
705 6_u16, 4, 5, 16, 2, 0, 50, 4, 51, 4, 80, 5, 201, 4, 202, 4, !0, !0
717 ];
718 #[rustfmt::skip]
719 let state_array: [u16; 18] = [
720 0, 0, 0, 0, 0, 1,
721 0, 0, 0, 0, 0, 1,
722 0, 0, 0, 0, 2, 1,
723 ];
724 #[rustfmt::skip]
725 let entry_table: [u16; 12] = [
726 0, 0, u16::MAX, u16::MAX,
727 2, 0, u16::MAX, u16::MAX,
728 0, 0, u16::MAX, 0,
729 ];
730 let buf = BeBuffer::new()
731 .extend(header)
732 .extend(class_table)
733 .extend(state_array)
734 .extend(entry_table);
735 let table = ExtendedStateTable::<ContextualData>::read(buf.data().into()).unwrap();
736 let [class_50, class_80, class_201] =
738 [50, 80, 201].map(|gid| table.class(GlyphId::new(gid)).unwrap());
739 assert_eq!(class_50, 4);
740 assert_eq!(class_80, 5);
741 assert_eq!(class_201, 4);
742 let entry = table.entry(0, 4).unwrap();
744 assert_eq!(entry.new_state, 0);
745 assert_eq!(entry.payload.current_index, !0);
746 let entry = table.entry(0, 5).unwrap();
748 assert_eq!(entry.new_state, 2);
749 let entry = table.entry(2, 4).unwrap();
752 assert_eq!(entry.new_state, 0);
753 assert_eq!(entry.payload.current_index, 0);
754 }
755
756 #[derive(Copy, Clone, Debug, bytemuck::AnyBitPattern)]
757 #[repr(C, packed)]
758 struct ContextualData {
759 _mark_index: BigEndian<u16>,
760 current_index: BigEndian<u16>,
761 }
762
763 impl FixedSize for ContextualData {
764 const RAW_BYTE_LEN: usize = 4;
765 }
766
767 #[test]
770 fn state_table() {
771 #[rustfmt::skip]
772 let header = [
773 7_u16, 10, 18, 40, 64, ];
779 #[rustfmt::skip]
780 let class_table = [
781 3_u16, 4, ];
784 let classes = [1u8, 2, 3, 4];
785 #[rustfmt::skip]
786 let state_array: [u8; 22] = [
787 2, 0, 0, 2, 1, 0, 0,
788 2, 0, 0, 2, 1, 0, 0,
789 2, 3, 3, 2, 3, 4, 5,
790 0, ];
792 #[rustfmt::skip]
793 let entry_table: [u16; 10] = [
794 18, 0x8112,
797 32, 0x8112,
798 18, 0x0000,
799 32, 0x8114,
800 18, 0x8116,
801 ];
802 let buf = BeBuffer::new()
803 .extend(header)
804 .extend(class_table)
805 .extend(classes)
806 .extend(state_array)
807 .extend(entry_table);
808 let table = StateTable::read(buf.data().into()).unwrap();
809 for i in 0..4u8 {
811 assert_eq!(table.class(GlyphId16::from(i as u16 + 3)).unwrap(), i + 1);
812 }
813 let cases = [
815 ((0, 4), (2, 0x8112)),
816 ((2, 1), (2, 0x8114)),
817 ((1, 3), (0, 0x0000)),
818 ((2, 5), (0, 0x8116)),
819 ];
820 for ((state, class), (new_state, flags)) in cases {
821 let entry = table.entry(state, class).unwrap();
822 assert_eq!(
823 entry.new_state, new_state,
824 "state {state}, class {class} should map to new state {new_state} (got {})",
825 entry.new_state
826 );
827 assert_eq!(
828 entry.flags, flags,
829 "state {state}, class {class} should map to flags 0x{flags:X} (got 0x{:X})",
830 entry.flags
831 );
832 }
833 }
834}