hermes-tdata 0.1.1

Pure Rust parser for Telegram Desktop's tdata storage format. Decrypts local storage and extracts auth keys without Qt/C++ dependencies.
Documentation
//! `QDataStream` reader implementation

use byteorder::{BigEndian, ReadBytesExt};
use std::io::{Cursor, Read, Seek, SeekFrom};

use crate::{Error, Result};

use super::constants::{EXTENDED_LENGTH_MARKER, NULL_MARKER, QT_VERSION_5_1};

/// `QDataStream` reader for parsing Qt binary serialization format
pub struct QDataStream<'a> {
    pub(super) cursor: Cursor<&'a [u8]>,
    pub(super) version: u32,
}

impl std::fmt::Debug for QDataStream<'_> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("QDataStream")
            .field("position", &self.cursor.position())
            .field("version", &self.version)
            .finish()
    }
}

impl<'a> QDataStream<'a> {
    /// Create a new `QDataStream` reader with Qt 5.1 version
    #[must_use]
    pub const fn new(data: &'a [u8]) -> Self {
        Self { cursor: Cursor::new(data), version: QT_VERSION_5_1 }
    }

    /// Create a new `QDataStream` reader with specified version
    #[must_use]
    pub const fn with_version(data: &'a [u8], version: u32) -> Self {
        Self { cursor: Cursor::new(data), version }
    }

    /// Get the Qt version
    #[must_use]
    pub const fn version(&self) -> u32 {
        self.version
    }

    /// Get current position in the stream
    #[must_use]
    pub const fn position(&self) -> u64 {
        self.cursor.position()
    }

    /// Check if we've reached the end of the stream
    #[must_use]
    pub const fn at_end(&self) -> bool {
        self.cursor.position() >= self.cursor.get_ref().len() as u64
    }

    /// Get remaining bytes count
    #[must_use]
    pub const fn remaining(&self) -> usize {
        let pos = self.cursor.position() as usize;
        let len = self.cursor.get_ref().len();
        len.saturating_sub(pos)
    }

    /// Skip n bytes
    pub fn skip(&mut self, n: usize) -> Result<()> {
        let _ = self
            .cursor
            .seek(SeekFrom::Current(n as i64))
            .map_err(|_| Error::UnexpectedEof { offset: self.position() })?;
        Ok(())
    }

    /// Read a single byte (quint8)
    pub fn read_u8(&mut self) -> Result<u8> {
        self.cursor.read_u8().map_err(|_| Error::UnexpectedEof { offset: self.position() })
    }

    /// Read a signed 8-bit integer (qint8)
    pub fn read_i8(&mut self) -> Result<i8> {
        self.cursor.read_i8().map_err(|_| Error::UnexpectedEof { offset: self.position() })
    }

    /// Read an unsigned 16-bit integer (quint16) - Big Endian
    pub fn read_u16(&mut self) -> Result<u16> {
        self.cursor
            .read_u16::<BigEndian>()
            .map_err(|_| Error::UnexpectedEof { offset: self.position() })
    }

    /// Read a signed 16-bit integer (qint16) - Big Endian
    pub fn read_i16(&mut self) -> Result<i16> {
        self.cursor
            .read_i16::<BigEndian>()
            .map_err(|_| Error::UnexpectedEof { offset: self.position() })
    }

    /// Read an unsigned 32-bit integer (quint32) - Big Endian
    pub fn read_u32(&mut self) -> Result<u32> {
        self.cursor
            .read_u32::<BigEndian>()
            .map_err(|_| Error::UnexpectedEof { offset: self.position() })
    }

    /// Read a signed 32-bit integer (qint32) - Big Endian
    pub fn read_i32(&mut self) -> Result<i32> {
        self.cursor
            .read_i32::<BigEndian>()
            .map_err(|_| Error::UnexpectedEof { offset: self.position() })
    }

    /// Read an unsigned 64-bit integer (quint64) - Big Endian
    pub fn read_u64(&mut self) -> Result<u64> {
        self.cursor
            .read_u64::<BigEndian>()
            .map_err(|_| Error::UnexpectedEof { offset: self.position() })
    }

    /// Read a signed 64-bit integer (qint64) - Big Endian
    pub fn read_i64(&mut self) -> Result<i64> {
        self.cursor
            .read_i64::<BigEndian>()
            .map_err(|_| Error::UnexpectedEof { offset: self.position() })
    }

    /// Read a boolean value
    pub fn read_bool(&mut self) -> Result<bool> {
        Ok(self.read_u8()? != 0)
    }

    /// Read a 32-bit float - Big Endian
    pub fn read_f32(&mut self) -> Result<f32> {
        self.cursor
            .read_f32::<BigEndian>()
            .map_err(|_| Error::UnexpectedEof { offset: self.position() })
    }

    /// Read a 64-bit double - Big Endian
    pub fn read_f64(&mut self) -> Result<f64> {
        self.cursor
            .read_f64::<BigEndian>()
            .map_err(|_| Error::UnexpectedEof { offset: self.position() })
    }

    /// Read raw bytes of specified length
    pub fn read_raw(&mut self, len: usize) -> Result<Vec<u8>> {
        if self.remaining() < len {
            return Err(Error::UnexpectedEof { offset: self.position() });
        }

        let mut buf = vec![0u8; len];
        self.cursor
            .read_exact(&mut buf)
            .map_err(|_| Error::UnexpectedEof { offset: self.position() })?;
        Ok(buf)
    }

    /// Read a `QByteArray`
    ///
    /// Wire format:
    /// - 4 bytes: length (quint32 BE)
    ///   - 0xFFFFFFFF = null `QByteArray` (returns empty vec)
    ///   - 0xFFFFFFFE = extended 64-bit length (followed by quint64)
    /// - N bytes: raw data
    pub fn read_qbytearray(&mut self) -> Result<Vec<u8>> {
        let len = self.read_u32()?;

        match len {
            NULL_MARKER => Ok(Vec::new()),
            EXTENDED_LENGTH_MARKER => {
                // Extended 64-bit length (Qt 6.7+)
                let real_len = self.read_u64()? as usize;
                self.read_raw(real_len)
            },
            _ => self.read_raw(len as usize),
        }
    }

    /// Read a `QString`
    ///
    /// Wire format:
    /// - 4 bytes: length in BYTES (not chars!) of UTF-16 data
    ///   - 0xFFFFFFFF = null `QString` (returns empty string)
    /// - N bytes: UTF-16 Big Endian encoded characters
    pub fn read_qstring(&mut self) -> Result<String> {
        let byte_len = self.read_u32()?;

        if byte_len == NULL_MARKER {
            return Ok(String::new());
        }

        if byte_len % 2 != 0 {
            return Err(Error::qdatastream("QString byte length is not even"));
        }

        let char_count = (byte_len / 2) as usize;
        let mut utf16: Vec<u16> = Vec::with_capacity(char_count);

        for _ in 0..char_count {
            utf16.push(self.read_u16()?);
        }

        String::from_utf16(&utf16).map_err(|_| Error::InvalidUtf16)
    }

    /// Read a length-prefixed C string (writeBytes format)
    ///
    /// Wire format:
    /// - 4 bytes: length including null terminator
    /// - N bytes: string data including null terminator
    pub fn read_cstring(&mut self) -> Result<String> {
        let data = self.read_qbytearray()?;

        // Remove null terminator if present
        let data = if data.last() == Some(&0) { &data[..data.len() - 1] } else { &*data };

        String::from_utf8(data.to_vec())
            .map_err(|_| Error::qdatastream("invalid UTF-8 in C string"))
    }
}