sdocx 1.1.0

Reverse-engineered tooling and SDK for converting Samsung Notes (.sdocx) files
Documentation
use read_fonts::tables::gpos::{
    AnchorTable, CursivePosFormat1, ExtensionSubtable, Gpos, MarkArray, MarkBasePosFormat1,
    MarkLigPosFormat1, MarkMarkPosFormat1, PairPos, PositionLookup, SinglePos, ValueRecord,
};
use read_fonts::{ReadError, TableProvider};

use crate::render::harfrust::FontRef;

const MAX_POSITIONING_RECORDS: usize = 100_000;

pub(super) fn requires_device_positioning(font: &FontRef<'_>) -> Result<bool, ReadError> {
    match font.gpos() {
        Ok(table) => Scan::new().gpos(table),
        Err(ReadError::TableIsMissing(_)) => Ok(false),
        Err(error) => Err(error),
    }
}

struct Scan {
    remaining: usize,
}

impl Scan {
    fn new() -> Self {
        Self {
            remaining: MAX_POSITIONING_RECORDS,
        }
    }

    fn any<T>(
        &mut self,
        records: impl IntoIterator<Item = Result<T, ReadError>>,
        mut inspect: impl FnMut(&mut Self, T) -> Result<bool, ReadError>,
    ) -> Result<bool, ReadError> {
        for record in records {
            if self.remaining == 0 {
                return Ok(true);
            }
            self.remaining -= 1;
            if inspect(self, record?)? {
                return Ok(true);
            }
        }
        Ok(false)
    }

    fn gpos(&mut self, table: Gpos<'_>) -> Result<bool, ReadError> {
        self.any(table.lookup_list()?.lookups().iter(), Self::lookup)
    }

    fn lookup(&mut self, lookup: PositionLookup<'_>) -> Result<bool, ReadError> {
        match lookup {
            PositionLookup::Single(inner) => self.any(inner.subtables().iter(), Self::single),
            PositionLookup::Pair(inner) => self.any(inner.subtables().iter(), Self::pair),
            PositionLookup::Cursive(inner) => self.any(inner.subtables().iter(), Self::cursive),
            PositionLookup::MarkToBase(inner) => {
                self.any(inner.subtables().iter(), Self::mark_base)
            }
            PositionLookup::MarkToLig(inner) => {
                self.any(inner.subtables().iter(), Self::mark_ligature)
            }
            PositionLookup::MarkToMark(inner) => {
                self.any(inner.subtables().iter(), Self::mark_mark)
            }
            PositionLookup::Extension(inner) => self.any(inner.subtables().iter(), Self::extension),
            PositionLookup::Contextual(inner) => {
                self.any(inner.subtables().iter(), |_, _| Ok(false))
            }
            PositionLookup::ChainContextual(inner) => {
                self.any(inner.subtables().iter(), |_, _| Ok(false))
            }
        }
    }

    fn extension(&mut self, table: ExtensionSubtable<'_>) -> Result<bool, ReadError> {
        match table {
            ExtensionSubtable::Single(inner) => self.single(inner.extension()?),
            ExtensionSubtable::Pair(inner) => self.pair(inner.extension()?),
            ExtensionSubtable::Cursive(inner) => self.cursive(inner.extension()?),
            ExtensionSubtable::MarkToBase(inner) => self.mark_base(inner.extension()?),
            ExtensionSubtable::MarkToLig(inner) => self.mark_ligature(inner.extension()?),
            ExtensionSubtable::MarkToMark(inner) => self.mark_mark(inner.extension()?),
            ExtensionSubtable::Contextual(inner) => {
                inner.extension()?;
                Ok(false)
            }
            ExtensionSubtable::ChainContextual(inner) => {
                inner.extension()?;
                Ok(false)
            }
        }
    }

    fn value(record: &ValueRecord) -> bool {
        !record.x_placement_device.get().is_null()
            || !record.y_placement_device.get().is_null()
            || !record.x_advance_device.get().is_null()
            || !record.y_advance_device.get().is_null()
    }

    fn single(&mut self, table: SinglePos<'_>) -> Result<bool, ReadError> {
        match table {
            SinglePos::Format1(inner) => Ok(Self::value(&inner.value_record())),
            SinglePos::Format2(inner) => self.any(inner.value_records().iter(), |_, record| {
                Ok(Self::value(&record))
            }),
        }
    }

    fn pair(&mut self, table: PairPos<'_>) -> Result<bool, ReadError> {
        match table {
            PairPos::Format1(inner) => self.any(inner.pair_sets().iter(), |scan, set| {
                scan.any(set.pair_value_records().iter(), |_, record| {
                    Ok(Self::value(record.value_record1()) || Self::value(record.value_record2()))
                })
            }),
            PairPos::Format2(inner) => self.any(inner.class1_records().iter(), |scan, class| {
                scan.any(class.class2_records().iter(), |_, record| {
                    Ok(Self::value(record.value_record1()) || Self::value(record.value_record2()))
                })
            }),
        }
    }

    fn anchor(&mut self, anchor: AnchorTable<'_>) -> Result<bool, ReadError> {
        Ok(match anchor {
            AnchorTable::Format1(_) => false,
            AnchorTable::Format2(_) => true,
            AnchorTable::Format3(inner) => {
                !inner.x_device_offset().is_null() || !inner.y_device_offset().is_null()
            }
        })
    }

    fn nullable_anchor(
        &mut self,
        anchor: Option<Result<AnchorTable<'_>, ReadError>>,
    ) -> Result<bool, ReadError> {
        anchor.map_or(Ok(false), |anchor| self.anchor(anchor?))
    }

    fn cursive(&mut self, table: CursivePosFormat1<'_>) -> Result<bool, ReadError> {
        self.any(table.entry_exit_record().iter().map(Ok), |scan, record| {
            Ok(
                scan.nullable_anchor(record.entry_anchor(table.offset_data()))?
                    || scan.nullable_anchor(record.exit_anchor(table.offset_data()))?,
            )
        })
    }

    fn marks(&mut self, table: MarkArray<'_>) -> Result<bool, ReadError> {
        self.any(table.mark_records().iter().map(Ok), |scan, record| {
            scan.anchor(record.mark_anchor(table.offset_data())?)
        })
    }

    fn mark_base(&mut self, table: MarkBasePosFormat1<'_>) -> Result<bool, ReadError> {
        if self.marks(table.mark_array()?)? {
            return Ok(true);
        }
        let bases = table.base_array()?;
        self.any(bases.base_records().iter(), |scan, record| {
            scan.any(
                record.base_anchors(bases.offset_data()).iter().map(Ok),
                Self::nullable_anchor,
            )
        })
    }

    fn mark_ligature(&mut self, table: MarkLigPosFormat1<'_>) -> Result<bool, ReadError> {
        if self.marks(table.mark_array()?)? {
            return Ok(true);
        }
        self.any(
            table.ligature_array()?.ligature_attaches().iter(),
            |scan, ligature| {
                scan.any(ligature.component_records().iter(), |scan, record| {
                    scan.any(
                        record
                            .ligature_anchors(ligature.offset_data())
                            .iter()
                            .map(Ok),
                        Self::nullable_anchor,
                    )
                })
            },
        )
    }

    fn mark_mark(&mut self, table: MarkMarkPosFormat1<'_>) -> Result<bool, ReadError> {
        if self.marks(table.mark1_array()?)? {
            return Ok(true);
        }
        let marks = table.mark2_array()?;
        self.any(marks.mark2_records().iter(), |scan, record| {
            scan.any(
                record.mark2_anchors(marks.offset_data()).iter().map(Ok),
                Self::nullable_anchor,
            )
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use read_fonts::{FontData, FontRead};

    fn gpos(lookup_type: u16, subtable: &[u16]) -> Vec<u8> {
        [1, 0, 0, 0, 10, 1, 4, lookup_type, 0, 1, 8]
            .into_iter()
            .chain(subtable.iter().copied())
            .flat_map(u16::to_be_bytes)
            .collect()
    }

    fn inspect(lookup_type: u16, subtable: &[u16]) -> Result<bool, ReadError> {
        let bytes = gpos(lookup_type, subtable);
        Scan::new().gpos(Gpos::read(FontData::new(&bytes))?)
    }

    fn cursive(anchor: &[u16]) -> Vec<u16> {
        [1, 10, 1, 16, 0, 1, 1, 3]
            .into_iter()
            .chain(anchor.iter().copied())
            .collect()
    }

    fn mark_attachment(lookup_type: u16, mark: &[u16], attached: &[u16]) -> Vec<u16> {
        let mut table = vec![1, 12, 18, 1, 24, 0, 1, 1, 3, 1, 1, 4, 1, 0, 6];
        table.extend_from_slice(mark);
        table[5] = u16::try_from(table.len() * 2).unwrap();
        table.extend([1, 4]);
        if lookup_type == 5 {
            table.extend([1, 4]);
        }
        table.extend_from_slice(attached);
        table
    }

    #[test]
    fn device_fields_are_checked_in_both_single_formats_and_both_pair_formats() {
        for flag in [0x10, 0x20, 0x40, 0x80] {
            for offset in [0, 14] {
                assert_eq!(
                    inspect(1, &[1, 8, flag, offset, 1, 1, 3]).unwrap(),
                    offset != 0
                );
                assert_eq!(
                    inspect(1, &[2, 10, flag, 1, offset, 1, 1, 3]).unwrap(),
                    offset != 0
                );
                for (first, second) in [(flag, 0), (0, flag)] {
                    let pair1 = [1, 12, first, second, 1, 18, 1, 1, 3, 1, 4, offset];
                    assert_eq!(inspect(2, &pair1).unwrap(), offset != 0);
                    let pair2 = [
                        2, 18, first, second, 24, 30, 1, 1, offset, 1, 1, 3, 1, 0, 0, 1, 0, 0,
                    ];
                    assert_eq!(inspect(2, &pair2).unwrap(), offset != 0);
                }
            }
        }
    }

    #[test]
    fn anchor_devices_and_contour_points_are_rejected_across_attachment_lookups() {
        for (anchor, unsupported) in [
            (&[1, 10, 20][..], false),
            (&[2, 10, 20, 3][..], true),
            (&[3, 10, 20, 0, 0][..], false),
            (&[3, 10, 20, 10, 0][..], true),
            (&[3, 10, 20, 0, 10][..], true),
        ] {
            assert_eq!(inspect(3, &cursive(anchor)).unwrap(), unsupported);
            for lookup in [4, 5, 6] {
                assert_eq!(
                    inspect(lookup, &mark_attachment(lookup, anchor, &[1, 0, 0])).unwrap(),
                    unsupported,
                );
                assert_eq!(
                    inspect(lookup, &mark_attachment(lookup, &[1, 0, 0], anchor)).unwrap(),
                    unsupported,
                );
            }
        }
        assert!(!inspect(3, &[1, 10, 1, 0, 0, 1, 1, 3]).unwrap());
    }

    #[test]
    fn extensions_are_inspected_and_malformed_offsets_are_reported() {
        for offset in [0, 14] {
            let extension = [1, 1, 0, 8, 1, 8, 0x40, offset, 1, 1, 3];
            assert_eq!(inspect(9, &extension).unwrap(), offset != 0);
        }
        assert!(inspect(9, &[1, 9, 0, 8]).is_err());
        assert!(inspect(9, &[1, 1, 0xffff, 0xffff]).is_err());
        assert!(inspect(3, &[1, 10, 1, 0xffff, 0, 1, 1, 3]).is_err());
    }

    #[test]
    fn bounded_scan_rejects_unverified_excess_and_missing_gpos_is_accepted() {
        let bytes = gpos(1, &[1, 8, 0, 1, 1, 3]);
        let mut scan = Scan { remaining: 1 };
        assert!(
            scan.gpos(Gpos::read(FontData::new(&bytes)).unwrap())
                .unwrap()
        );
        let font = FontRef::new(&[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).unwrap();
        assert!(!requires_device_positioning(&font).unwrap());
    }

    #[test]
    fn paint_constructor_reports_unsupported_positioning_before_shaping() {
        use crate::render::fonts::{FontBook, PaintMetricInput, PaintMetricMode, PaintShapeError};

        let face = FontBook::default().resolve("Roboto", false, false).unwrap();
        let mut bytes = face.bytes().to_vec();
        let table = gpos(1, &[1, 8, 0x40, 14, 1, 1, 3, 17, 17, 1, 0]);
        let table_offset = u32::try_from(bytes.len()).unwrap();
        let table_length = u32::try_from(table.len()).unwrap();
        let count = usize::from(u16::from_be_bytes(bytes[4..6].try_into().unwrap()));
        let record = bytes[12..12 + count * 16]
            .as_chunks_mut::<16>()
            .0
            .iter_mut()
            .find(|record| &record[..4] == b"GPOS")
            .unwrap();
        record[8..12].copy_from_slice(&table_offset.to_be_bytes());
        record[12..16].copy_from_slice(&table_length.to_be_bytes());
        bytes.extend(table);
        let input = PaintMetricInput {
            size: 17.0,
            scale_x: 1.0,
            skew_x: 0.0,
            mode: PaintMetricMode::Normal,
        };
        assert!(matches!(
            super::super::PaintShaper::new(&bytes, face.index, input),
            Err(PaintShapeError::UnsupportedPositioningTables),
        ));
    }
}