soblendwa 0.1.0

SoblendWA: a blazing fast WhatsApp Web multi-device client library written in Rust. Crafted by Soblend Development Studio.
Documentation
mod decoder;
mod encoder;
mod node;
#[cfg(test)]
mod robustness;
mod tokens;

use std::collections::HashMap;
use std::sync::LazyLock;

pub use decoder::{decode, decode_frame};
pub use encoder::encode;
pub use node::{Content, Node, Str, Value};

pub(crate) mod tag {
    pub const LIST_EMPTY: u8 = 0;
    pub const DICTIONARY_0: u8 = 236;
    pub const DICTIONARY_3: u8 = 239;
    pub const INTEROP_JID: u8 = 245;
    pub const FB_JID: u8 = 246;
    pub const AD_JID: u8 = 247;
    pub const LIST_8: u8 = 248;
    pub const LIST_16: u8 = 249;
    pub const JID_PAIR: u8 = 250;
    pub const HEX_8: u8 = 251;
    pub const BINARY_8: u8 = 252;
    pub const BINARY_20: u8 = 253;
    pub const BINARY_32: u8 = 254;
    pub const NIBBLE_8: u8 = 255;
    pub const PACKED_MAX: usize = 127;
}

#[derive(Clone, Copy)]
pub(crate) enum Token {
    Single(u8),
    Double(u8, u8),
}

static TOKEN_INDEX: LazyLock<HashMap<&'static str, Token>> = LazyLock::new(|| {
    let mut index = HashMap::with_capacity(tokens::SINGLE_BYTE.len() + 4 * 256);
    for (dict, table) in tokens::DOUBLE_BYTE.iter().enumerate() {
        for (i, token) in table.iter().enumerate() {
            index.insert(*token, Token::Double(dict as u8, i as u8));
        }
    }
    for (i, token) in tokens::SINGLE_BYTE.iter().enumerate().skip(1) {
        index.insert(*token, Token::Single(i as u8));
    }
    index
});

pub(crate) fn lookup_token(value: &str) -> Option<Token> {
    TOKEN_INDEX.get(value).copied()
}

pub(crate) fn single_token(index: u8) -> Option<&'static str> {
    tokens::SINGLE_BYTE.get(index as usize).copied()
}

pub(crate) fn double_token(dict: u8, index: u8) -> Option<&'static str> {
    tokens::DOUBLE_BYTE.get(dict as usize).and_then(|t| t.get(index as usize)).copied()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::jid::Jid;

    #[test]
    fn roundtrip_preserves_everything() {
        let node = Node::new("message")
            .attr("id", "3EB0A1B2C3D4E5F6A7B8C9")
            .attr("to", Jid::phone("5215512345678").with_device(7))
            .attr("type", "text")
            .attr("custom_attribute_value", "hello world, this is raw text")
            .attr("t", 1_700_000_000u64)
            .children([
                Node::new("enc").attr("v", "2").attr("type", "pkmsg").bytes(vec![1u8; 300]),
                Node::new("participants").children([Node::new("to").attr("jid", Jid::group("120363025246125486"))]),
                Node::new("device-identity").bytes(vec![9u8; 5]),
            ]);
        let encoded = encode(&node);
        let decoded = decode_frame(&encoded).unwrap();
        assert_eq!(decoded.tag, "message");
        assert_eq!(decoded.attr_str("id"), Some("3EB0A1B2C3D4E5F6A7B8C9"));
        assert_eq!(decoded.attr_jid("to"), Some(Jid::phone("5215512345678").with_device(7)));
        assert_eq!(decoded.attr_str("custom_attribute_value"), Some("hello world, this is raw text"));
        assert_eq!(decoded.attr_u64("t"), Some(1_700_000_000));
        let enc = decoded.child("enc").unwrap();
        assert_eq!(enc.content_bytes().unwrap(), &[1u8; 300][..]);
        let to = decoded.path(&["participants", "to"]).unwrap();
        assert_eq!(to.attr_jid("jid").unwrap().to_string(), "120363025246125486@g.us");
    }

    #[test]
    fn text_that_merely_looks_like_a_jid_is_kept_verbatim() {
        for text in ["ventas_mx@empresa.com", "soporte@soblend.dev", "a@b@c", "1:300@s.whatsapp.net", "x@c.us"] {
            let node = Node::new("email").text(text.to_owned()).attr("note", text.to_owned());
            let decoded = decode_frame(&encode(&node)).unwrap();
            assert_eq!(decoded.content_string().as_deref(), Some(text));
            assert_eq!(decoded.attr_string("note").as_deref(), Some(text));
        }
        let jid = decode_frame(&encode(&Node::new("x").attr("jid", "5215512345678:3@s.whatsapp.net"))).unwrap();
        assert_eq!(jid.attr_jid("jid"), Some(Jid::phone("5215512345678").with_device(3)));
    }

    #[test]
    fn tokens_are_bijective() {
        for (word, token) in TOKEN_INDEX.iter() {
            let back = match *token {
                Token::Single(i) => single_token(i),
                Token::Double(d, i) => double_token(d, i),
            };
            assert_eq!(back, Some(*word));
        }
    }
}