sdocx 1.1.0

Reverse-engineered tooling and SDK for converting Samsung Notes (.sdocx) files
Documentation
use serde::Deserialize;
use sha2::{Digest, Sha256};

use super::*;

const CAPTURE: &str = include_str!("../../../../../../conformance/table-text-wrap-numeric.json");

#[derive(Deserialize)]
struct Capture {
    apk_sha256: String,
    text_library_sha256: String,
    base_library_sha256: String,
    memory_fills: [u8; 3],
    repeat_zero_fill: bool,
    get_block_info: String,
    set_layout: String,
    cases: Vec<Case>,
}

#[derive(Deserialize)]
struct Case {
    name: String,
    supplied_entries: Vec<Entry>,
    requested_range_utf16: [usize; 2],
    supplied_available_rect_bits: [u32; 4],
    supplied_size_bits: u32,
    supplied_layout_options: u32,
    visual_to_logical_utf16: Vec<usize>,
    budget_bits: u32,
    alignment: u32,
    selected_range_utf16_inclusive: [usize; 2],
    block_layout_rect_bits: [u32; 4],
    block_metric_bits: [u32; 4],
    space_count: u32,
    candidate_operations: Vec<Candidate>,
    commit_operations: Vec<Commit>,
    entries: Vec<PositionedEntry>,
    retained_line: RetainedLine,
}

#[derive(Deserialize)]
struct Entry {
    advance_bits: u32,
    kind: u32,
    break_end_utf16: usize,
    object_type: u32,
}

#[derive(Deserialize)]
struct Candidate {
    entry_utf16: usize,
    committed_bits: u32,
    pending_bits: u32,
    advance_bits: u32,
    base_bits: u32,
    candidate_bits: u32,
    available_bits: u32,
}

#[derive(Deserialize)]
struct Commit {
    entry_utf16: usize,
    reason: String,
    committed_before_bits: u32,
    pending_bits: u32,
    committed_after_bits: u32,
}

#[derive(Deserialize)]
struct PositionedEntry {
    position_bits: [u32; 2],
    layout_rect_bits: [u32; 4],
}

#[derive(Deserialize)]
struct RetainedLine {
    layout_rect_bits: [u32; 4],
}

fn capture() -> Capture {
    assert_eq!(
        format!("{:x}", Sha256::digest(CAPTURE.as_bytes())),
        "b3380614708e4a16712f2bdfa9d9a80ed3610b733f4ff274d50d09da962e09b8"
    );
    let capture: Capture = serde_json::from_str(CAPTURE).unwrap();
    assert_eq!(
        capture.apk_sha256,
        "daed1eff8c8ee9dfb8afe2771e39e893a8808f3230d6d522a8aa647db09b8667"
    );
    assert_eq!(
        capture.text_library_sha256,
        "5483711673a499743625eb3275e34b37a006919af346212b46b8d8857834308b"
    );
    assert_eq!(
        capture.base_library_sha256,
        "e10da0116946691cf68302437ef261282e1dfe0eec15bf2dfa66093286985deb"
    );
    assert_eq!(capture.memory_fills, [0, 0xa5, 0xff]);
    assert!(capture.repeat_zero_fill);
    assert_eq!(capture.get_block_info, "0x6ab9c");
    assert_eq!(capture.set_layout, "0x6b4a4");
    assert_eq!(capture.cases.len(), 17);
    capture
}

fn kind(value: u32) -> NativeWrapKind {
    match value {
        0 => NativeWrapKind::Ordinary,
        1 => NativeWrapKind::Space,
        2 => NativeWrapKind::Tab,
        _ => panic!("unsupported capture entry kind {value}"),
    }
}

fn entries(case: &Case) -> Vec<NativeWrapEntry> {
    case.supplied_entries
        .iter()
        .map(|entry| {
            assert_eq!(entry.object_type, 0);
            NativeWrapEntry {
                advance: f32::from_bits(entry.advance_bits),
                kind: kind(entry.kind),
                break_end_utf16: Some(entry.break_end_utf16),
                metrics: NativeWrapMetrics {
                    font_size: f32::from_bits(case.supplied_size_bits),
                    height: f32::from_bits(case.supplied_size_bits),
                },
            }
        })
        .collect()
}

#[test]
fn native_wrap_replays_actual_candidates_commits_and_selected_blocks() {
    let capture = capture();
    let mut candidate_count = 0;
    let mut commit_count = 0;
    let mut supplied_count = 0;
    let mut selected_count = 0;
    for case in &capture.cases {
        let entries = entries(case);
        let mut operations = Vec::new();
        let [start, end] = case.requested_range_utf16;
        assert_eq!(
            case.supplied_available_rect_bits,
            [0, 0, case.budget_bits, 100_f32.to_bits()]
        );
        let block = select_with_operations(
            &entries,
            start..end,
            NativeWrapWidths {
                available: f32::from_bits(case.budget_bits),
                full: f32::from_bits(case.budget_bits),
            },
            |operation| operations.push(operation),
        )
        .unwrap()
        .unwrap();
        assert_eq!(
            [
                *block.range_utf16_inclusive.start(),
                *block.range_utf16_inclusive.end()
            ],
            case.selected_range_utf16_inclusive,
            "{} range",
            case.name
        );
        assert_eq!(
            block.width.to_bits(),
            case.block_layout_rect_bits[2],
            "{} width",
            case.name
        );
        assert_eq!(
            [
                block.metrics.font_size.to_bits(),
                block.metrics.height.to_bits(),
                0,
                0
            ],
            case.block_metric_bits,
            "{} metrics",
            case.name
        );
        assert_eq!(block.space_weight, case.space_count, "{} spaces", case.name);
        let mut candidates = case.candidate_operations.iter();
        let mut commits = case.commit_operations.iter();
        for operation in operations {
            match operation {
                NativeWrapOperation::Candidate {
                    entry_utf16,
                    committed,
                    pending,
                    advance,
                    base,
                    candidate,
                    available,
                } => {
                    let expected = candidates.next().unwrap();
                    assert_eq!(
                        entry_utf16, expected.entry_utf16,
                        "{} candidate source",
                        case.name
                    );
                    assert_eq!(
                        [
                            committed.to_bits(),
                            pending.to_bits(),
                            advance.to_bits(),
                            base.to_bits(),
                            candidate.to_bits(),
                            available.to_bits()
                        ],
                        [
                            expected.committed_bits,
                            expected.pending_bits,
                            expected.advance_bits,
                            expected.base_bits,
                            expected.candidate_bits,
                            expected.available_bits
                        ],
                        "{} candidate {entry_utf16}",
                        case.name
                    );
                    candidate_count += 1;
                }
                NativeWrapOperation::Commit {
                    entry_utf16,
                    reason,
                    committed_before,
                    pending,
                    committed_after,
                } => {
                    let expected = commits.next().unwrap();
                    assert_eq!(
                        entry_utf16, expected.entry_utf16,
                        "{} commit source",
                        case.name
                    );
                    assert_eq!(
                        reason,
                        match expected.reason.as_str() {
                            "break_end" => NativeCommitReason::BreakEnd,
                            "space" => NativeCommitReason::Space,
                            "tab" => NativeCommitReason::Tab,
                            _ => panic!("unknown native commit"),
                        }
                    );
                    assert_eq!(
                        [
                            committed_before.to_bits(),
                            pending.to_bits(),
                            committed_after.to_bits()
                        ],
                        [
                            expected.committed_before_bits,
                            expected.pending_bits,
                            expected.committed_after_bits
                        ],
                        "{} commit {entry_utf16}",
                        case.name
                    );
                    commit_count += 1;
                }
            }
        }
        assert!(
            candidates.next().is_none() && commits.next().is_none(),
            "{} missing operations",
            case.name
        );
        supplied_count += entries.len();
        selected_count += block.range_utf16_inclusive.count();
    }
    assert_eq!(
        (
            supplied_count,
            candidate_count,
            commit_count,
            selected_count
        ),
        (59, 55, 16, 45)
    );
}

#[test]
fn native_entry_cursor_replays_actual_set_layout_in_visual_slot_order() {
    let capture = capture();
    for case in &capture.cases {
        let entries = entries(case);
        let [start, end] = case.requested_range_utf16;
        let available = f32::from_bits(case.budget_bits);
        let block = select_native_block(
            &entries,
            start..end,
            NativeWrapWidths {
                available,
                full: available,
            },
        )
        .unwrap()
        .unwrap();
        assert_eq!(case.supplied_layout_options, 0);
        let share = if case.alignment == 3 {
            block.justification_share(available).unwrap()
        } else {
            0.0
        };
        let mut cursor =
            NativeEntryCursor::new(f32::from_bits(case.block_layout_rect_bits[0])).unwrap();
        for visual in block.range_utf16_inclusive.clone() {
            let logical = case.visual_to_logical_utf16[visual];
            let entry = entries[logical];
            let placed = cursor
                .advance_justified(entry.advance, entry.kind, share)
                .unwrap();
            let expected = &case.entries[logical];
            assert_eq!(
                placed.left.to_bits(),
                expected.position_bits[0],
                "{} position slot {logical}",
                case.name
            );
            assert_eq!(
                [placed.left.to_bits(), placed.right.to_bits()],
                [expected.layout_rect_bits[0], expected.layout_rect_bits[2]],
                "{} rectangle slot {logical}",
                case.name
            );
        }
        assert_eq!(
            cursor.x.to_bits(),
            case.retained_line.layout_rect_bits[2],
            "{} final X",
            case.name
        );
    }
}

#[test]
fn native_wrap_rejects_invalid_source_and_numeric_domains_without_mutating_cursor() {
    let entry = NativeWrapEntry {
        advance: 1.0,
        kind: NativeWrapKind::Ordinary,
        break_end_utf16: None,
        metrics: NativeWrapMetrics {
            font_size: 1.0,
            height: 1.0,
        },
    };
    let widths = NativeWrapWidths {
        available: 10.0,
        full: 10.0,
    };
    assert_eq!(
        select_native_block(&[entry], 0..0, widths),
        Err(NativeWrapError::InvalidRange)
    );
    assert_eq!(
        select_native_block(&[entry], 0..2, widths),
        Err(NativeWrapError::InvalidRange)
    );
    assert_eq!(
        select_native_block(
            &[NativeWrapEntry {
                advance: f32::NAN,
                ..entry
            }],
            0..1,
            widths
        ),
        Err(NativeWrapError::InvalidAdvance)
    );
    assert_eq!(
        select_native_block(
            &[NativeWrapEntry {
                break_end_utf16: Some(0),
                ..entry
            }],
            0..1,
            widths
        ),
        Err(NativeWrapError::InvalidBreakEnd)
    );
    assert_eq!(
        select_native_block(
            &[entry],
            0..1,
            NativeWrapWidths {
                available: f32::INFINITY,
                ..widths
            }
        ),
        Err(NativeWrapError::InvalidGeometry)
    );
    let mut cursor = NativeEntryCursor::new(f32::MAX).unwrap();
    assert_eq!(
        cursor.advance(f32::MAX),
        Err(NativeWrapError::NumericOverflow)
    );
    assert_eq!(cursor.x.to_bits(), f32::MAX.to_bits());
}