sdocx 1.0.0

Reverse-engineered tooling and SDK for converting Samsung Notes (.sdocx) files
Documentation
use std::io::{self, Read, Seek, SeekFrom};

use crate::end_tag::END_TAG_SIGNATURE;

const ZIP_END_RECORD_SIZE: usize = 22;
const MAX_TAIL_SIZE: usize = ZIP_END_RECORD_SIZE + u16::MAX as usize + 2 + u16::MAX as usize;

pub(crate) struct ArchiveTail {
    pub archive_length: u64,
    pub end_tag: Option<Vec<u8>>,
}

impl ArchiveTail {
    pub(crate) fn read(reader: &mut (impl Read + Seek)) -> io::Result<Self> {
        let file_length = reader.seek(SeekFrom::End(0))?;
        let tail_length = file_length.min(MAX_TAIL_SIZE as u64) as usize;
        let tail_start = file_length - tail_length as u64;
        reader.seek(SeekFrom::Start(tail_start))?;
        let mut tail = vec![0; tail_length];
        reader.read_exact(&mut tail)?;
        reader.seek(SeekFrom::Start(0))?;

        for offset in 0..tail.len().saturating_sub(ZIP_END_RECORD_SIZE - 1) {
            let footer = &tail[offset..offset + ZIP_END_RECORD_SIZE];
            if &footer[..4] != b"PK\x05\x06"
                || footer[4..8] != [0; 4]
                || footer[8..10] != footer[10..12]
            {
                continue;
            }
            let directory_size = u32::from_le_bytes(footer[12..16].try_into().unwrap());
            let directory_offset = u32::from_le_bytes(footer[16..20].try_into().unwrap());
            if directory_size != u32::MAX
                && directory_offset != u32::MAX
                && u64::from(directory_size) + u64::from(directory_offset)
                    > tail_start + offset as u64
            {
                continue;
            }
            let comment_length = usize::from(u16::from_le_bytes([footer[20], footer[21]]));
            let tag_start = offset + ZIP_END_RECORD_SIZE + comment_length;
            let Some(tag) = tail.get(tag_start..) else {
                continue;
            };
            if tag.is_empty() {
                return Ok(Self {
                    archive_length: file_length,
                    end_tag: None,
                });
            }
            if tag.len() < 2 + END_TAG_SIGNATURE.len() {
                continue;
            }
            let declared_size = usize::from(u16::from_le_bytes([tag[0], tag[1]]));
            if declared_size + 2 != tag.len() && !tag.ends_with(END_TAG_SIGNATURE) {
                continue;
            }
            return Ok(Self {
                archive_length: tail_start + tag_start as u64,
                end_tag: Some(tag.to_vec()),
            });
        }
        Ok(Self {
            archive_length: file_length,
            end_tag: None,
        })
    }
}

pub(crate) struct ArchiveReader<R> {
    reader: R,
    length: u64,
    position: u64,
}

impl<R: Read + Seek> ArchiveReader<R> {
    pub(crate) fn new(mut reader: R, length: u64) -> io::Result<Self> {
        reader.seek(SeekFrom::Start(0))?;
        Ok(Self {
            reader,
            length,
            position: 0,
        })
    }
}

impl<R: Read> Read for ArchiveReader<R> {
    fn read(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
        let length = (self.length.saturating_sub(self.position)).min(buffer.len() as u64) as usize;
        let read = self.reader.read(&mut buffer[..length])?;
        self.position += read as u64;
        Ok(read)
    }
}

impl<R: Seek> Seek for ArchiveReader<R> {
    fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
        let position = match from {
            SeekFrom::Start(position) => Some(position),
            SeekFrom::End(offset) => self.length.checked_add_signed(offset),
            SeekFrom::Current(offset) => self.position.checked_add_signed(offset),
        }
        .ok_or_else(|| {
            io::Error::new(io::ErrorKind::InvalidInput, "archive seek is out of range")
        })?;
        self.position = self.reader.seek(SeekFrom::Start(position))?;
        Ok(self.position)
    }
}