sdocx 0.7.0

Reverse-engineered tooling and SDK for converting Samsung Notes (.sdocx) files
Documentation
use crate::ParseLimits;
use crate::binary::Reader;
use crate::error::{Error, Result};

pub(crate) const END_TAG_SIGNATURE: &[u8] = b"Document for S-Pen SDK";

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EndTagSource {
    ArchiveEntry,
    Appended,
}

#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct StoredEndTag {
    pub format_version: u32,
    pub note_id: Option<String>,
    pub modified_time: i64,
    pub property_flags: u32,
    pub cover_image: Option<String>,
    pub note_width: u32,
    pub note_height: f32,
    pub application_name: Option<String>,
    pub application_major_version: i32,
    pub application_minor_version: i32,
    pub application_patch_name: Option<String>,
    pub minimum_format_version: u32,
    pub created_time: i64,
    pub last_viewed_page_index: i32,
    pub page_mode: u16,
    pub document_type: Option<u16>,
    pub owner_id: Option<String>,
    pub reserved_data: Option<Vec<u8>>,
    pub encryption_data: Option<Vec<u8>>,
    pub display_timestamps: Option<EndTagDisplayTimestamps>,
    pub last_recognized_data_modified_time: Option<i64>,
    pub fixed_style: Option<EndTagFixedStyle>,
    pub server_checkpoint: Option<i64>,
    pub new_orientation: Option<i32>,
    pub minimum_unknown_version: Option<i32>,
    pub application_custom_data: Option<String>,
    pub trailing_data: Vec<u8>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EndTagDisplayTimestamps {
    pub created_time: i64,
    pub modified_time: i64,
}

#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EndTagFixedStyle {
    pub font: Option<String>,
    pub text_direction: i32,
    pub background_theme: i32,
}

#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[non_exhaustive]
pub struct EndTagEncryption {
    pub original_plaintext_size: u32,
    pub salt: Vec<u8>,
    pub initialization_vector: Vec<u8>,
    pub wrapped_key: Vec<u8>,
    pub trailing_data: Vec<u8>,
}

impl StoredEndTag {
    pub fn encryption_info(&self) -> Result<Option<EndTagEncryption>> {
        let Some(data) = self
            .encryption_data
            .as_deref()
            .filter(|bytes| !bytes.is_empty())
        else {
            return Ok(None);
        };
        let mut reader = Reader::new(data, "end tag encryption");
        Ok(Some(EndTagEncryption {
            original_plaintext_size: reader.read_u32("original plaintext size")?,
            salt: read_blob(&mut reader, "salt")?,
            initialization_vector: read_blob(&mut reader, "initialization vector")?,
            wrapped_key: read_blob(&mut reader, "wrapped key")?,
            trailing_data: reader
                .read_bytes(reader.remaining(), "trailing data")?
                .to_vec(),
        }))
    }
}

pub fn parse_end_tag_bytes(data: &[u8]) -> Result<StoredEndTag> {
    parse_end_tag_bytes_with_limits(data, &ParseLimits::default())
}

pub fn parse_end_tag_bytes_with_limits(data: &[u8], limits: &ParseLimits) -> Result<StoredEndTag> {
    if data.len() as u64 > limits.max_entry_size {
        return Err(Error::LimitExceeded {
            resource: "end tag size",
            limit: limits.max_entry_size,
            actual: data.len() as u64,
        });
    }
    let mut record = Reader::new(data, "end tag");
    let declared_size = usize::from(record.read_u16("record size")?);
    if declared_size != record.remaining() {
        return Err(Error::Format(format!(
            "end tag: declared size {declared_size} differs from payload size {}",
            record.remaining()
        )));
    }
    let payload = record.read_bytes(declared_size, "payload")?;
    let fields = payload
        .strip_suffix(END_TAG_SIGNATURE)
        .ok_or_else(|| Error::Format("end tag: missing Document for S-Pen SDK signature".into()))?;
    let mut reader = Reader::new(fields, "end tag");
    let format_version = reader.read_u32("format version")?;
    if format_version < 2034 {
        return Err(Error::Format(format!(
            "end tag: unsupported WDoc format version {format_version}"
        )));
    }
    let mut tag = StoredEndTag {
        format_version,
        note_id: read_string(&mut reader, "note ID", limits)?,
        modified_time: reader.read_i64("modified time")?,
        property_flags: reader.read_u32("property flags")?,
        cover_image: read_string(&mut reader, "cover image", limits)?,
        note_width: reader.read_u32("note width")?,
        note_height: reader.read_f32("note height")?,
        application_name: read_string(&mut reader, "application name", limits)?,
        application_major_version: reader.read_i32("application major version")?,
        application_minor_version: reader.read_i32("application minor version")?,
        application_patch_name: read_string(&mut reader, "application patch name", limits)?,
        minimum_format_version: reader.read_u32("minimum format version")?,
        created_time: reader.read_i64("created time")?,
        last_viewed_page_index: reader.read_i32("last viewed page index")?,
        page_mode: reader.read_u16("page mode")?,
        ..StoredEndTag::default()
    };
    tag.document_type = read_extension(&mut reader, |r| r.read_u16("document type"))?;
    tag.owner_id = read_extension(&mut reader, |r| read_string(r, "owner ID", limits))?.flatten();
    tag.reserved_data = read_extension(&mut reader, |r| read_blob(r, "reserved data"))?;
    tag.encryption_data = read_extension(&mut reader, |r| read_blob(r, "encryption data"))?;
    tag.display_timestamps = read_extension(&mut reader, |r| {
        Ok(EndTagDisplayTimestamps {
            created_time: r.read_i64("display created time")?,
            modified_time: r.read_i64("display modified time")?,
        })
    })?;
    tag.last_recognized_data_modified_time = read_extension(&mut reader, |r| {
        r.read_i64("last recognized data modified time")
    })?;
    tag.fixed_style = read_extension(&mut reader, |r| {
        Ok(EndTagFixedStyle {
            font: read_string(r, "fixed font", limits)?,
            text_direction: r.read_i32("fixed text direction")?,
            background_theme: r.read_i32("fixed background theme")?,
        })
    })?;
    tag.server_checkpoint = read_extension(&mut reader, |r| r.read_i64("server checkpoint"))?;
    tag.new_orientation = read_extension(&mut reader, |r| r.read_i32("new orientation"))?;
    tag.minimum_unknown_version =
        read_extension(&mut reader, |r| r.read_i32("minimum unknown version"))?;
    tag.application_custom_data = read_extension(&mut reader, |r| {
        let units = r.read_u32("application custom data length")?;
        read_string_units(r, units, u32::MAX, "application custom data", limits)
    })?
    .flatten();
    tag.trailing_data = reader
        .read_bytes(reader.remaining(), "trailing data")?
        .to_vec();
    Ok(tag)
}

fn read_extension<T>(
    reader: &mut Reader<'_>,
    read: impl FnOnce(&mut Reader<'_>) -> Result<T>,
) -> Result<Option<T>> {
    if reader.remaining() == 0 {
        Ok(None)
    } else {
        read(reader).map(Some)
    }
}

fn read_blob(reader: &mut Reader<'_>, field: &'static str) -> Result<Vec<u8>> {
    let length = reader.read_u32(field)? as usize;
    Ok(reader.read_bytes(length, field)?.to_vec())
}

fn read_string(
    reader: &mut Reader<'_>,
    field: &'static str,
    limits: &ParseLimits,
) -> Result<Option<String>> {
    let units = u32::from(reader.read_u16(field)?);
    read_string_units(reader, units, u32::from(u16::MAX), field, limits)
}

fn read_string_units(
    reader: &mut Reader<'_>,
    units: u32,
    null_sentinel: u32,
    field: &'static str,
    limits: &ParseLimits,
) -> Result<Option<String>> {
    if units == null_sentinel {
        return Ok(None);
    }
    if u64::from(units) > limits.max_text_characters as u64 {
        return Err(Error::LimitExceeded {
            resource: "text characters",
            limit: limits.max_text_characters as u64,
            actual: u64::from(units),
        });
    }
    let byte_count = (units as usize)
        .checked_mul(2)
        .ok_or_else(|| Error::Format(format!("end tag: {field} length overflows")))?;
    let bytes = reader.read_bytes(byte_count, field)?;
    char::decode_utf16(
        bytes
            .as_chunks::<2>()
            .0
            .iter()
            .map(|b| u16::from_le_bytes(*b)),
    )
    .collect::<std::result::Result<String, _>>()
    .map(Some)
    .map_err(|_| Error::Format(format!("end tag: invalid UTF-16 in {field}")))
}