Skip to main content

wacore_binary/
encoder.rs

1use std::io::Write;
2
3#[cfg(feature = "simd")]
4use core::simd::Select;
5#[cfg(feature = "simd")]
6use core::simd::prelude::*;
7#[cfg(feature = "simd")]
8use core::simd::{Simd, u8x16};
9
10use crate::error::{BinaryError, Result};
11use crate::jid::{self, Jid, JidRef};
12use crate::node::{Node, NodeContent, NodeContentRef, NodeRef, NodeValue, ValueRef};
13use crate::token;
14
15pub trait ByteWriter {
16    fn write_u8(&mut self, value: u8) -> Result<()>;
17    fn write_bytes(&mut self, bytes: &[u8]) -> Result<()>;
18}
19
20pub(crate) struct IoByteWriter<W: Write> {
21    writer: W,
22}
23
24impl<W: Write> IoByteWriter<W> {
25    fn new(writer: W) -> Self {
26        Self { writer }
27    }
28}
29
30impl<W: Write> ByteWriter for IoByteWriter<W> {
31    #[inline]
32    fn write_u8(&mut self, value: u8) -> Result<()> {
33        self.writer.write_all(&[value])?;
34        Ok(())
35    }
36
37    #[inline]
38    fn write_bytes(&mut self, bytes: &[u8]) -> Result<()> {
39        self.writer.write_all(bytes)?;
40        Ok(())
41    }
42}
43
44pub struct VecByteWriter<'a> {
45    buffer: &'a mut Vec<u8>,
46}
47
48impl<'a> VecByteWriter<'a> {
49    fn new(buffer: &'a mut Vec<u8>) -> Self {
50        Self { buffer }
51    }
52}
53
54impl ByteWriter for VecByteWriter<'_> {
55    #[inline]
56    fn write_u8(&mut self, value: u8) -> Result<()> {
57        self.buffer.push(value);
58        Ok(())
59    }
60
61    #[inline]
62    fn write_bytes(&mut self, bytes: &[u8]) -> Result<()> {
63        self.buffer.extend_from_slice(bytes);
64        Ok(())
65    }
66}
67
68pub(crate) struct SliceByteWriter<'a> {
69    buffer: &'a mut [u8],
70    position: usize,
71}
72
73impl<'a> SliceByteWriter<'a> {
74    fn new(buffer: &'a mut [u8]) -> Self {
75        Self {
76            buffer,
77            position: 0,
78        }
79    }
80
81    #[inline]
82    fn bytes_written(&self) -> usize {
83        self.position
84    }
85}
86
87impl ByteWriter for SliceByteWriter<'_> {
88    #[inline]
89    fn write_u8(&mut self, value: u8) -> Result<()> {
90        if self.position >= self.buffer.len() {
91            return Err(BinaryError::UnexpectedEof);
92        }
93        self.buffer[self.position] = value;
94        self.position += 1;
95        Ok(())
96    }
97
98    #[inline]
99    fn write_bytes(&mut self, bytes: &[u8]) -> Result<()> {
100        let end = self.position + bytes.len();
101        if end > self.buffer.len() {
102            return Err(BinaryError::UnexpectedEof);
103        }
104        self.buffer[self.position..end].copy_from_slice(bytes);
105        self.position = end;
106        Ok(())
107    }
108}
109
110/// Trait for encoding node structures (both owned Node and borrowed NodeRef).
111/// All encoding logic lives in the trait implementation, keeping
112/// the Encoder simple and focused on low-level byte writing.
113pub trait EncodeNode {
114    fn tag(&self) -> &str;
115    fn attrs_len(&self) -> usize;
116    fn has_content(&self) -> bool;
117
118    /// Encode all attributes to the encoder
119    fn encode_attrs<'a, W: ByteWriter>(&self, encoder: &mut Encoder<'a, W>) -> Result<()>;
120
121    /// Encode content (string, bytes, or child nodes) to the encoder
122    fn encode_content<'a, W: ByteWriter>(&self, encoder: &mut Encoder<'a, W>) -> Result<()>;
123}
124
125impl EncodeNode for Node {
126    fn tag(&self) -> &str {
127        &self.tag
128    }
129
130    fn attrs_len(&self) -> usize {
131        self.attrs.len()
132    }
133
134    fn has_content(&self) -> bool {
135        self.content.is_some()
136    }
137
138    fn encode_attrs<'a, W: ByteWriter>(&self, encoder: &mut Encoder<'a, W>) -> Result<()> {
139        for (k, v) in &self.attrs {
140            encoder.write_string(k)?;
141            match v {
142                NodeValue::String(s) => encoder.write_string(s)?,
143                NodeValue::Jid(jid) => encoder.write_jid_owned(jid)?,
144            }
145        }
146        Ok(())
147    }
148
149    fn encode_content<'a, W: ByteWriter>(&self, encoder: &mut Encoder<'a, W>) -> Result<()> {
150        if let Some(content) = &self.content {
151            match content {
152                NodeContent::String(s) => encoder.write_string(s)?,
153                NodeContent::Bytes(b) => encoder.write_bytes_with_len(b)?,
154                NodeContent::Nodes(nodes) => {
155                    encoder.write_list_start(nodes.len())?;
156                    for node in nodes {
157                        encoder.write_node(node)?;
158                    }
159                }
160            }
161        }
162        Ok(())
163    }
164}
165
166impl EncodeNode for NodeRef<'_> {
167    fn tag(&self) -> &str {
168        &self.tag
169    }
170
171    fn attrs_len(&self) -> usize {
172        self.attrs.len()
173    }
174
175    fn has_content(&self) -> bool {
176        self.content.is_some()
177    }
178
179    fn encode_attrs<'a, W: ByteWriter>(&self, encoder: &mut Encoder<'a, W>) -> Result<()> {
180        for (k, v) in self.attrs.iter() {
181            encoder.write_string(k)?;
182            match v {
183                ValueRef::String(s) => encoder.write_string(s)?,
184                ValueRef::Jid(jid) => encoder.write_jid_ref(jid)?,
185            }
186        }
187        Ok(())
188    }
189
190    fn encode_content<'a, W: ByteWriter>(&self, encoder: &mut Encoder<'a, W>) -> Result<()> {
191        if let Some(content) = self.content.as_ref() {
192            match content {
193                NodeContentRef::String(s) => encoder.write_string(s)?,
194                NodeContentRef::Bytes(b) => encoder.write_bytes_with_len(b)?,
195                NodeContentRef::Nodes(nodes) => {
196                    encoder.write_list_start(nodes.len())?;
197                    for node in nodes.iter() {
198                        encoder.write_node(node)?;
199                    }
200                }
201            }
202        }
203        Ok(())
204    }
205}
206
207// u8 offsets keep StringHint small — the hint tape stores one per string —
208// and always fit: classify_string_hint only treats strings <= 48 bytes as
209// JID candidates.
210#[derive(Debug, Clone, Copy, PartialEq, Eq)]
211struct ParsedJidMeta {
212    user_end: u8,
213    server_start: u8,
214    domain_type: u8,
215    device: Option<u8>,
216}
217
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219enum StringHint {
220    Empty,
221    SingleToken(u8),
222    DoubleToken { dict: u8, token: u8 },
223    PackedNibble,
224    PackedHex,
225    Jid(ParsedJidMeta),
226    RawBytes,
227}
228
229/// Replay tape: sound only while plan and encode visit strings in the same
230/// (`write_node`) order — `write_string` debug-asserts each replayed hint to
231/// catch divergence. 32 inline keeps a typical stanza off the heap.
232#[derive(Debug, Default)]
233pub(crate) struct StringHintCache {
234    hints: smallvec::SmallVec<[StringHint; 32]>,
235    cursor: std::cell::Cell<usize>,
236}
237
238impl StringHintCache {
239    /// Plan side: classify once and append to the tape.
240    #[inline]
241    fn record(&mut self, s: &str) -> StringHint {
242        // Strings longer than PACKED_MAX (127) can't be protocol tokens
243        // (max 48), packed nibble/hex, or JIDs — skip classification.
244        let hint = if s.len() > token::PACKED_MAX as usize {
245            StringHint::RawBytes
246        } else {
247            classify_string_hint(s)
248        };
249        self.hints.push(hint);
250        hint
251    }
252
253    /// Encode side: consume the next recorded hint.
254    #[inline]
255    fn next(&self) -> Option<StringHint> {
256        let i = self.cursor.get();
257        let hint = self.hints.get(i).copied();
258        if hint.is_some() {
259            self.cursor.set(i + 1);
260        }
261        hint
262    }
263
264    /// The exact-marshal fns require this after encoding: a leftover hint
265    /// means plan and encode diverged, and the output can't be trusted.
266    /// (Reusing an exhausted tape for a second encode is merely slow, not
267    /// wrong — `next` returns None and `write_string` classifies inline.)
268    #[inline]
269    pub(crate) fn fully_consumed(&self) -> bool {
270        self.cursor.get() == self.hints.len()
271    }
272}
273
274#[derive(Debug)]
275pub(crate) struct MarshaledSizePlan {
276    pub(crate) size: usize,
277    pub(crate) hints: StringHintCache,
278}
279
280fn parse_jid_meta(input: &str) -> Option<ParsedJidMeta> {
281    let sep_idx = input.find('@')?;
282    let server_start = sep_idx + 1;
283    let server = &input[server_start..];
284    let user_combined = &input[..sep_idx];
285
286    let (user_agent, device) = if let Some(colon_idx) = user_combined.find(':')
287        && let Ok(parsed_device) = user_combined[colon_idx + 1..].parse::<u8>()
288    {
289        (&user_combined[..colon_idx], Some(parsed_device))
290    } else {
291        (user_combined, None)
292    };
293
294    let user_end = if let Some(underscore_idx) = user_agent.find('_')
295        && user_agent[underscore_idx + 1..].parse::<u8>().is_ok()
296    {
297        underscore_idx
298    } else {
299        user_agent.len()
300    };
301
302    let server_kind = jid::Server::parse_known(server);
303    let domain_type = match server_kind {
304        Some(jid::Server::Pn) => 0,
305        Some(jid::Server::Lid) => 1,
306        Some(jid::Server::Hosted) => 128,
307        Some(jid::Server::HostedLid) => 129,
308        _ => 0,
309    };
310
311    // Single source of truth: only servers whose `domain_type` the decoder
312    // round-trips back can use AD_JID. For everyone else drop the device
313    // and fall through to JID_PAIR (which preserves the server name).
314    let device = server_kind
315        .filter(|s| server_supports_ad_jid(*s))
316        .and(device);
317
318    Some(ParsedJidMeta {
319        user_end: u8::try_from(user_end).ok()?,
320        server_start: u8::try_from(server_start).ok()?,
321        domain_type,
322        device,
323    })
324}
325
326#[inline]
327fn split_jid_from_meta(input: &str, meta: ParsedJidMeta) -> (&str, &str) {
328    (
329        &input[..meta.user_end as usize],
330        &input[meta.server_start as usize..],
331    )
332}
333
334/// Map a JID server string to the AD_JID domain_type byte.
335///
336/// The AD_JID binary encoding uses a single byte to identify the server:
337///   0 = s.whatsapp.net (default)
338///   1 = lid
339///   128 = hosted
340///   129 = hosted.lid
341///
342/// WARNING: This must stay in sync with the string-path mapping in
343/// `classify_string_hint` / `parse_jid_meta` and the inverse mapping in
344/// `decoder.rs read_ad_jid`. Writing `jid.agent` unconditionally here
345/// (instead of only as a fallback) was the root cause of a regression
346/// where LID group messages were silently rejected by the server (error 421).
347#[inline]
348fn server_to_domain_type(server: jid::Server) -> u8 {
349    match server {
350        jid::Server::Pn => 0,
351        jid::Server::Lid => 1,
352        jid::Server::Hosted => 128,
353        jid::Server::HostedLid => 129,
354        _ => 0,
355    }
356}
357
358/// Whether this JID needs the dedicated `INTEROP_JID` token to survive the
359/// round-trip.
360///
361/// An interop JID carries an `integrator` that no other wire form has a field
362/// for: `JID_PAIR` writes only user and server, so encoding one that way silently
363/// drops it. WA Web has a matching branch (`WA/Wap.js`, the `JID_INTEROP` arm of
364/// its JID writer) and its decoder reads the field back, as does ours.
365///
366/// Restricted to a non-zero integrator on purpose. An interop JID without one
367/// loses nothing through `JID_PAIR`, and that is the form we have always sent —
368/// this fixes the case that was lossy without changing the bytes for the case
369/// that was not.
370#[inline]
371fn needs_interop_jid(server: jid::Server, integrator: u16) -> bool {
372    server == jid::Server::Interop && integrator != 0
373}
374
375/// AD_JID round-trips back to a server via `domain_type` only for the four
376/// servers the decoder explicitly maps. For everything else (bot, group,
377/// broadcast, newsletter, call, interop, msgr, legacy) no valid AD_JID domain
378/// type exists. Writers must check this and emit JID_PAIR for non-AD-capable
379/// servers even when `device > 0`.
380/// Matches whatsmeow `writeJID` and WA Web `WAWap.De`.
381#[inline]
382fn server_supports_ad_jid(server: jid::Server) -> bool {
383    matches!(
384        server,
385        jid::Server::Pn | jid::Server::Lid | jid::Server::Hosted | jid::Server::HostedLid
386    )
387}
388
389#[inline]
390fn classify_string_hint(s: &str) -> StringHint {
391    if s.is_empty() {
392        return StringHint::Empty;
393    }
394
395    let is_likely_jid = s.len() <= 48;
396
397    if let Some(kind) = token::index_of_token(s) {
398        return match kind {
399            token::TokenKind::Single(token) => StringHint::SingleToken(token),
400            token::TokenKind::Double(dict, token) => StringHint::DoubleToken { dict, token },
401        };
402    }
403
404    if validate_nibble(s) {
405        StringHint::PackedNibble
406    } else if validate_hex(s) {
407        StringHint::PackedHex
408    } else if is_likely_jid {
409        parse_jid_meta(s).map_or(StringHint::RawBytes, StringHint::Jid)
410    } else {
411        StringHint::RawBytes
412    }
413}
414
415pub(crate) fn build_marshaled_node_plan(node: &Node) -> MarshaledSizePlan {
416    let mut hints = StringHintCache::default();
417    let size = 1 + node_encoded_size_with_cache(node, &mut hints);
418    MarshaledSizePlan { size, hints }
419}
420
421pub(crate) fn build_marshaled_node_ref_plan(node: &NodeRef<'_>) -> MarshaledSizePlan {
422    let mut hints = StringHintCache::default();
423    let size = 1 + node_ref_encoded_size_with_cache(node, &mut hints);
424    MarshaledSizePlan { size, hints }
425}
426
427#[inline]
428fn list_start_encoded_size(len: usize) -> usize {
429    if len == 0 {
430        1
431    } else if len < 256 {
432        2
433    } else {
434        3
435    }
436}
437
438#[inline]
439fn binary_len_prefix_size(len: usize) -> usize {
440    if len < 256 {
441        2
442    } else if len < (1 << 20) {
443        4
444    } else {
445        5
446    }
447}
448
449#[inline]
450fn bytes_with_len_encoded_size(len: usize) -> usize {
451    binary_len_prefix_size(len) + len
452}
453
454#[inline]
455fn packed_encoded_size(value_len: usize) -> usize {
456    2 + value_len.div_ceil(2)
457}
458
459// Statement order below matters: hints are recorded for replay, so strings
460// must be visited exactly as write_node emits them — tag, then each attr's
461// key then value, then content.
462fn node_encoded_size_with_cache(node: &Node, hints: &mut StringHintCache) -> usize {
463    let content_len = usize::from(node.content.is_some());
464    let list_len = 1 + (node.attrs.len() * 2) + content_len;
465
466    let mut size =
467        list_start_encoded_size(list_len) + string_encoded_size_with_cache(&node.tag, hints);
468
469    for (k, v) in &node.attrs {
470        size += string_encoded_size_with_cache(k, hints);
471        size += match v {
472            NodeValue::String(s) => string_encoded_size_with_cache(s, hints),
473            NodeValue::Jid(jid) => owned_jid_encoded_size_with_cache(jid, hints),
474        };
475    }
476
477    size += match &node.content {
478        Some(NodeContent::String(s)) => string_encoded_size_with_cache(s, hints),
479        Some(NodeContent::Bytes(b)) => bytes_with_len_encoded_size(b.len()),
480        Some(NodeContent::Nodes(nodes)) => {
481            list_start_encoded_size(nodes.len())
482                + nodes
483                    .iter()
484                    .map(|child| node_encoded_size_with_cache(child, hints))
485                    .sum::<usize>()
486        }
487        None => 0,
488    };
489    size
490}
491
492// Same statement-order constraint as node_encoded_size_with_cache.
493fn node_ref_encoded_size_with_cache(node: &NodeRef<'_>, hints: &mut StringHintCache) -> usize {
494    let content_len = usize::from(node.content.is_some());
495    let list_len = 1 + (node.attrs.len() * 2) + content_len;
496
497    let mut size = list_start_encoded_size(list_len)
498        + string_encoded_size_with_cache(node.tag.as_ref(), hints);
499
500    for (k, v) in node.attrs.iter() {
501        size += string_encoded_size_with_cache(k, hints);
502        size += match v {
503            ValueRef::String(s) => string_encoded_size_with_cache(s, hints),
504            ValueRef::Jid(jid) => jid_ref_encoded_size_with_cache(jid, hints),
505        };
506    }
507
508    size += match node.content.as_ref() {
509        Some(NodeContentRef::String(s)) => string_encoded_size_with_cache(s, hints),
510        Some(NodeContentRef::Bytes(b)) => bytes_with_len_encoded_size(b.len()),
511        Some(NodeContentRef::Nodes(nodes)) => {
512            list_start_encoded_size(nodes.len())
513                + nodes
514                    .iter()
515                    .map(|child| node_ref_encoded_size_with_cache(child, hints))
516                    .sum::<usize>()
517        }
518        None => 0,
519    };
520    size
521}
522
523#[inline]
524fn string_encoded_size_with_cache(s: &str, hints: &mut StringHintCache) -> usize {
525    let hint = hints.record(s);
526    string_encoded_size_from_hint_with_cache(s, hint, hints)
527}
528
529#[inline]
530fn string_encoded_size_from_hint_with_cache(
531    s: &str,
532    hint: StringHint,
533    hints: &mut StringHintCache,
534) -> usize {
535    match hint {
536        StringHint::Empty => 2,
537        StringHint::SingleToken(_) => 1,
538        StringHint::DoubleToken { .. } => 2,
539        StringHint::PackedNibble | StringHint::PackedHex => packed_encoded_size(s.len()),
540        StringHint::RawBytes => bytes_with_len_encoded_size(s.len()),
541        StringHint::Jid(meta) => parsed_jid_encoded_size_with_cache(s, meta, hints),
542    }
543}
544
545#[inline]
546fn parsed_jid_encoded_size_with_cache(
547    jid: &str,
548    meta: ParsedJidMeta,
549    hints: &mut StringHintCache,
550) -> usize {
551    let (user, server) = split_jid_from_meta(jid, meta);
552    if meta.device.is_some() {
553        3 + string_encoded_size_with_cache(user, hints)
554    } else {
555        let user_size = if user.is_empty() {
556            1
557        } else {
558            string_encoded_size_with_cache(user, hints)
559        };
560        1 + user_size + string_encoded_size_with_cache(server, hints)
561    }
562}
563
564/// Byte count for one encoded JID.
565///
566/// Must mirror `write_jid_ref`/`write_jid_owned` branch for branch: the exact
567/// marshal sizes its output slice from this and then writes into it, so a plan
568/// that disagrees with the writer is not a bad estimate — it is an
569/// `UnexpectedEof` on a send. Both JID flavours route through here so the two
570/// cannot drift apart.
571#[inline]
572fn jid_encoded_size_with_cache(
573    user: &str,
574    server: jid::Server,
575    device: u16,
576    integrator: u16,
577    hints: &mut StringHintCache,
578) -> usize {
579    if needs_interop_jid(server, integrator) {
580        // token + user + u16 device + u16 integrator; no server, see
581        // `write_interop_jid`.
582        return 1 + string_encoded_size_with_cache(user, hints) + 2 + 2;
583    }
584    if device > 0 && server_supports_ad_jid(server) {
585        return 3 + string_encoded_size_with_cache(user, hints);
586    }
587    let user_size = if user.is_empty() {
588        1
589    } else {
590        string_encoded_size_with_cache(user, hints)
591    };
592    1 + user_size + string_encoded_size_with_cache(server.as_str(), hints)
593}
594
595#[inline]
596fn owned_jid_encoded_size_with_cache(jid: &Jid, hints: &mut StringHintCache) -> usize {
597    jid_encoded_size_with_cache(&jid.user, jid.server, jid.device, jid.integrator, hints)
598}
599
600#[inline]
601fn jid_ref_encoded_size_with_cache(jid: &JidRef<'_>, hints: &mut StringHintCache) -> usize {
602    jid_encoded_size_with_cache(&jid.user, jid.server, jid.device, jid.integrator, hints)
603}
604
605#[inline]
606fn validate_nibble(value: &str) -> bool {
607    if value.len() > token::PACKED_MAX as usize {
608        return false;
609    }
610    value
611        .as_bytes()
612        .iter()
613        .all(|&b| b.is_ascii_digit() || b == b'-' || b == b'.')
614}
615
616#[inline]
617fn validate_hex(value: &str) -> bool {
618    if value.len() > token::PACKED_MAX as usize {
619        return false;
620    }
621    value
622        .as_bytes()
623        .iter()
624        .all(|&b| b.is_ascii_digit() || (b'A'..=b'F').contains(&b))
625}
626
627pub struct Encoder<'a, W: ByteWriter> {
628    writer: W,
629    string_hints: Option<&'a StringHintCache>,
630}
631
632impl<W: Write> Encoder<'static, IoByteWriter<W>> {
633    pub fn new(writer: W) -> Result<Self> {
634        let mut enc = Self {
635            writer: IoByteWriter::new(writer),
636            string_hints: None,
637        };
638        enc.write_u8(0)?;
639        Ok(enc)
640    }
641}
642
643impl<'v> Encoder<'static, VecByteWriter<'v>> {
644    pub fn new_vec(buffer: &'v mut Vec<u8>) -> Result<Self> {
645        buffer.clear();
646        let mut enc = Self {
647            writer: VecByteWriter::new(buffer),
648            string_hints: None,
649        };
650        enc.write_u8(0)?;
651        Ok(enc)
652    }
653}
654
655impl<'a> Encoder<'a, SliceByteWriter<'a>> {
656    pub(crate) fn new_slice(
657        buffer: &'a mut [u8],
658        string_hints: Option<&'a StringHintCache>,
659    ) -> Result<Self> {
660        let mut enc = Self {
661            writer: SliceByteWriter::new(buffer),
662            string_hints,
663        };
664        enc.write_u8(0)?;
665        Ok(enc)
666    }
667
668    #[inline]
669    pub(crate) fn bytes_written(&self) -> usize {
670        self.writer.bytes_written()
671    }
672}
673
674impl<'a, W: ByteWriter> Encoder<'a, W> {
675    #[inline(always)]
676    fn write_u8(&mut self, val: u8) -> Result<()> {
677        self.writer.write_u8(val)
678    }
679
680    #[inline(always)]
681    fn write_u16_be(&mut self, val: u16) -> Result<()> {
682        self.writer.write_bytes(&val.to_be_bytes())
683    }
684
685    #[inline(always)]
686    fn write_u32_be(&mut self, val: u32) -> Result<()> {
687        self.writer.write_bytes(&val.to_be_bytes())
688    }
689
690    #[inline(always)]
691    fn write_u20_be(&mut self, value: u32) -> Result<()> {
692        let bytes = [
693            ((value >> 16) & 0x0F) as u8,
694            ((value >> 8) & 0xFF) as u8,
695            (value & 0xFF) as u8,
696        ];
697        self.writer.write_bytes(&bytes)
698    }
699
700    #[inline(always)]
701    fn write_raw_bytes(&mut self, bytes: &[u8]) -> Result<()> {
702        self.writer.write_bytes(bytes)
703    }
704
705    #[inline(always)]
706    pub fn write_bytes_with_len(&mut self, bytes: &[u8]) -> Result<()> {
707        let len = bytes.len();
708        if len < 256 {
709            self.write_u8(token::BINARY_8)?;
710            self.write_u8(len as u8)?;
711        } else if len < (1 << 20) {
712            self.write_u8(token::BINARY_20)?;
713            self.write_u20_be(len as u32)?;
714        } else {
715            self.write_u8(token::BINARY_32)?;
716            self.write_u32_be(len as u32)?;
717        }
718        self.write_raw_bytes(bytes)
719    }
720
721    #[inline(always)]
722    pub fn write_string(&mut self, s: &str) -> Result<()> {
723        if let Some(string_hints) = self.string_hints
724            && let Some(hint) = string_hints.next()
725        {
726            debug_assert_eq!(
727                hint,
728                if s.len() > token::PACKED_MAX as usize {
729                    StringHint::RawBytes
730                } else {
731                    classify_string_hint(s)
732                },
733                "hint tape misaligned at {s:?}: plan and encode no longer \
734                 traverse strings in the same order"
735            );
736            return self.write_string_with_hint(s, hint);
737        }
738        self.write_string_uncached(s)
739    }
740
741    #[inline(always)]
742    fn write_string_uncached(&mut self, s: &str) -> Result<()> {
743        // Strings longer than PACKED_MAX (127) can't be protocol tokens (max 48),
744        // packed nibble/hex, or JIDs — emit as raw bytes without classification.
745        if s.len() > token::PACKED_MAX as usize {
746            return self.write_bytes_with_len(s.as_bytes());
747        }
748        self.write_string_with_hint(s, classify_string_hint(s))
749    }
750
751    #[inline(always)]
752    fn write_string_with_hint(&mut self, s: &str, hint: StringHint) -> Result<()> {
753        match hint {
754            StringHint::Empty => {
755                self.write_u8(token::BINARY_8)?;
756                self.write_u8(0)?;
757            }
758            StringHint::SingleToken(token) => self.write_u8(token)?,
759            StringHint::DoubleToken { dict, token } => {
760                self.write_u8(token::DICTIONARY_0 + dict)?;
761                self.write_u8(token)?;
762            }
763            StringHint::PackedNibble => self.write_packed_bytes(s, token::NIBBLE_8)?,
764            StringHint::PackedHex => self.write_packed_bytes(s, token::HEX_8)?,
765            StringHint::Jid(meta) => self.write_jid_from_meta(s, meta)?,
766            StringHint::RawBytes => self.write_bytes_with_len(s.as_bytes())?,
767        }
768        Ok(())
769    }
770
771    #[inline(always)]
772    fn write_jid_from_meta(&mut self, jid: &str, meta: ParsedJidMeta) -> Result<()> {
773        let (user, server) = split_jid_from_meta(jid, meta);
774        if let Some(device) = meta.device {
775            self.write_u8(token::AD_JID)?;
776            self.write_u8(meta.domain_type)?;
777            self.write_u8(device)?;
778            self.write_string(user)?;
779        } else {
780            self.write_u8(token::JID_PAIR)?;
781            if user.is_empty() {
782                self.write_u8(token::LIST_EMPTY)?;
783            } else {
784                self.write_string(user)?;
785            }
786            self.write_string(server)?;
787        }
788        Ok(())
789    }
790
791    /// Write a JidRef directly without converting to string first.
792    /// This avoids the allocation that would occur with `jid.to_string()`.
793    /// `INTEROP_JID`: token, user, `u16` device, `u16` integrator — and no server.
794    ///
795    /// Mirrors WA Web's outbound writer (`WA/Wap.js`, the `JID_INTEROP` arm):
796    /// `writeUint8(S), te(user), writeUint16(device), writeUint16(integrator)`.
797    ///
798    /// Its inbound decoder reads a fourth value after those — the server — and so
799    /// does ours. That asymmetry is deliberate here rather than a bug being
800    /// copied: the fourth read describes what the *server sends us*, not what it
801    /// accepts from us, and the writer above is what actually runs against the
802    /// real server. Emitting a server the server does not expect would not merely
803    /// mis-parse the JID: the extra token would be read as the next value in the
804    /// stanza, desynchronising everything after it.
805    ///
806    /// The consequence is that this output does not round-trip through our own
807    /// `read_interop_jid`. That is a property of the protocol's two directions,
808    /// not a defect to fix by making both ends agree locally.
809    fn write_interop_jid(&mut self, user: &str, device: u16, integrator: u16) -> Result<()> {
810        self.write_u8(token::INTEROP_JID)?;
811        self.write_string(user)?;
812        self.write_u16_be(device)?;
813        self.write_u16_be(integrator)
814    }
815
816    pub fn write_jid_ref(&mut self, jid: &JidRef<'_>) -> Result<()> {
817        if needs_interop_jid(jid.server, jid.integrator) {
818            return self.write_interop_jid(&jid.user, jid.device, jid.integrator);
819        }
820        if jid.device > 0 && server_supports_ad_jid(jid.server) {
821            // AD_JID format: domain_type, device, user
822            let device = u8::try_from(jid.device).map_err(|_| {
823                BinaryError::AttrParse(format!("AD_JID device id out of range: {}", jid.device))
824            })?;
825            self.write_u8(token::AD_JID)?;
826            self.write_u8(server_to_domain_type(jid.server))?;
827            self.write_u8(device)?;
828            self.write_string(&jid.user)?;
829        } else {
830            // JID_PAIR format: user, server
831            self.write_u8(token::JID_PAIR)?;
832            if jid.user.is_empty() {
833                self.write_u8(token::LIST_EMPTY)?;
834            } else {
835                self.write_string(&jid.user)?;
836            }
837            self.write_string(jid.server.as_str())?;
838        }
839        Ok(())
840    }
841
842    /// Write an owned Jid directly without converting to string first.
843    /// This avoids the allocation that would occur with `jid.to_string()`.
844    pub fn write_jid_owned(&mut self, jid: &Jid) -> Result<()> {
845        if needs_interop_jid(jid.server, jid.integrator) {
846            return self.write_interop_jid(&jid.user, jid.device, jid.integrator);
847        }
848        if jid.device > 0 && server_supports_ad_jid(jid.server) {
849            // AD_JID format: domain_type, device, user
850            let device = u8::try_from(jid.device).map_err(|_| {
851                BinaryError::AttrParse(format!("AD_JID device id out of range: {}", jid.device))
852            })?;
853            self.write_u8(token::AD_JID)?;
854            self.write_u8(server_to_domain_type(jid.server))?;
855            self.write_u8(device)?;
856            self.write_string(&jid.user)?;
857        } else {
858            // JID_PAIR format: user, server
859            self.write_u8(token::JID_PAIR)?;
860            if jid.user.is_empty() {
861                self.write_u8(token::LIST_EMPTY)?;
862            } else {
863                self.write_string(&jid.user)?;
864            }
865            self.write_string(jid.server.as_str())?;
866        }
867        Ok(())
868    }
869
870    #[inline(always)]
871    fn pack_nibble(value: u8) -> u8 {
872        match value {
873            b'-' => 10,
874            b'.' => 11,
875            0 => 15,
876            c if c.is_ascii_digit() => c - b'0',
877            _ => panic!("Invalid char for nibble packing: {value}"),
878        }
879    }
880
881    #[inline(always)]
882    fn pack_hex(value: u8) -> u8 {
883        match value {
884            c if c.is_ascii_digit() => c - b'0',
885            c if (b'A'..=b'F').contains(&c) => 10 + (c - b'A'),
886            0 => 15,
887            _ => panic!("Invalid char for hex packing: {value}"),
888        }
889    }
890
891    #[inline(always)]
892    fn pack_byte_pair(packer: fn(u8) -> u8, part1: u8, part2: u8) -> u8 {
893        (packer(part1) << 4) | packer(part2)
894    }
895
896    fn write_packed_bytes(&mut self, value: &str, data_type: u8) -> Result<()> {
897        if value.len() > token::PACKED_MAX as usize {
898            panic!("String too long to be packed: {}", value.len());
899        }
900
901        self.write_u8(data_type)?;
902
903        let mut rounded_len = value.len().div_ceil(2) as u8;
904        if !value.len().is_multiple_of(2) {
905            rounded_len |= 0x80;
906        }
907        self.write_u8(rounded_len)?;
908
909        #[allow(unused_mut)]
910        let mut input_bytes = value.as_bytes();
911
912        if data_type == token::NIBBLE_8 {
913            #[cfg(feature = "simd")]
914            {
915                const NIBBLE_LOOKUP: [u8; 16] =
916                    [10, 11, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255];
917                let lookup = Simd::from_array(NIBBLE_LOOKUP);
918                let nibble_base = Simd::splat(b'-');
919
920                while input_bytes.len() >= 16 {
921                    let (chunk, rest) = input_bytes.split_at(16);
922                    let input = u8x16::from_slice(chunk);
923                    let indices = input.saturating_sub(nibble_base);
924                    let nibbles = lookup.swizzle_dyn(indices);
925
926                    let (evens, odds) = nibbles.deinterleave(nibbles.rotate_elements_left::<1>());
927                    let packed: Simd<u8, 16> = (evens << Simd::splat(4)) | odds;
928                    let packed_bytes = packed.to_array();
929                    self.write_raw_bytes(&packed_bytes[..8])?;
930
931                    input_bytes = rest;
932                }
933            }
934
935            let mut bytes_iter = input_bytes.iter().copied();
936            while let Some(part1) = bytes_iter.next() {
937                let part2 = bytes_iter.next().unwrap_or(0);
938                self.write_u8(Self::pack_byte_pair(Self::pack_nibble, part1, part2))?;
939            }
940        } else {
941            #[cfg(feature = "simd")]
942            {
943                let ascii_0 = Simd::splat(b'0');
944                let ascii_a = Simd::splat(b'A');
945                let ten = Simd::splat(10);
946
947                while input_bytes.len() >= 16 {
948                    let (chunk, rest) = input_bytes.split_at(16);
949                    let input = u8x16::from_slice(chunk);
950
951                    let digit_vals = input - ascii_0;
952                    let letter_vals = input - ascii_a + ten;
953                    let is_letter = input.simd_ge(ascii_a);
954                    let nibbles = is_letter.select(letter_vals, digit_vals);
955
956                    let (evens, odds) = nibbles.deinterleave(nibbles.rotate_elements_left::<1>());
957                    let packed: Simd<u8, 16> = (evens << Simd::splat(4)) | odds;
958                    let packed_bytes = packed.to_array();
959                    self.write_raw_bytes(&packed_bytes[..8])?;
960
961                    input_bytes = rest;
962                }
963            }
964
965            let mut bytes_iter = input_bytes.iter().copied();
966            while let Some(part1) = bytes_iter.next() {
967                let part2 = bytes_iter.next().unwrap_or(0);
968                self.write_u8(Self::pack_byte_pair(Self::pack_hex, part1, part2))?;
969            }
970        }
971        Ok(())
972    }
973
974    pub fn write_list_start(&mut self, len: usize) -> Result<()> {
975        if len == 0 {
976            self.write_u8(token::LIST_EMPTY)?;
977        } else if len < 256 {
978            self.write_u8(248)?;
979            self.write_u8(len as u8)?;
980        } else if len <= u16::MAX as usize {
981            self.write_u8(249)?;
982            self.write_u16_be(len as u16)?;
983        } else {
984            return Err(BinaryError::InvalidNode);
985        }
986        Ok(())
987    }
988
989    /// Write any node type (owned or borrowed) using the EncodeNode trait.
990    pub fn write_node<N: EncodeNode>(&mut self, node: &N) -> Result<()> {
991        let content_len = if node.has_content() { 1 } else { 0 };
992        let list_len = 1 + (node.attrs_len() * 2) + content_len;
993
994        self.write_list_start(list_len)?;
995        self.write_string(node.tag())?;
996        node.encode_attrs(self)?;
997        node.encode_content(self)?;
998        Ok(())
999    }
1000}
1001
1002#[cfg(test)]
1003mod tests {
1004    use super::*;
1005    use crate::builder::NodeBuilder;
1006    use crate::node::Attrs;
1007    use std::io::Cursor;
1008
1009    type TestResult = Result<()>;
1010
1011    #[test]
1012    fn test_encode_node() -> TestResult {
1013        let node = Node::new(
1014            "message",
1015            Attrs::new(),
1016            Some(NodeContent::String("receipt".into())),
1017        );
1018
1019        let mut buffer = Vec::new();
1020        let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1021        encoder.write_node(&node)?;
1022
1023        let expected = vec![0, 248, 2, 19, 7];
1024        assert_eq!(buffer, expected);
1025        assert_eq!(buffer.len(), 5);
1026        Ok(())
1027    }
1028
1029    #[test]
1030    fn test_nibble_packing() -> TestResult {
1031        // Test string with nibble characters: '-', '.', '0'-'9'
1032        let test_str = "-.0123456789";
1033        let node = Node::new(
1034            "test",
1035            Attrs::new(),
1036            Some(NodeContent::String(test_str.into())),
1037        );
1038
1039        let mut buffer = Vec::new();
1040        let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1041        encoder.write_node(&node)?;
1042
1043        let expected = vec![
1044            0, 248, 2, 252, 4, 116, 101, 115, 116, 255, 6, 171, 1, 35, 69, 103, 137,
1045        ];
1046        assert_eq!(buffer, expected);
1047        assert_eq!(buffer.len(), 17);
1048        Ok(())
1049    }
1050
1051    /// Test LIST_8 boundary (length 255)
1052    #[test]
1053    fn test_list_size_list8_boundary() -> TestResult {
1054        let mut buffer = Vec::new();
1055        let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1056
1057        // LIST_8 should be used for lengths 1-255
1058        encoder.write_list_start(255)?;
1059
1060        // Expected: LIST_8 (248), then length 255
1061        assert_eq!(buffer[1], token::LIST_8);
1062        assert_eq!(buffer[2], 255);
1063        Ok(())
1064    }
1065
1066    /// Test LIST_16 boundary (length 256)
1067    #[test]
1068    fn test_list_size_list16_boundary() -> TestResult {
1069        let mut buffer = Vec::new();
1070        let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1071
1072        // LIST_16 should be used for lengths 256+
1073        encoder.write_list_start(256)?;
1074
1075        // Expected: LIST_16 (249), then length as u16 big-endian
1076        assert_eq!(buffer[1], token::LIST_16);
1077        assert_eq!(buffer[2], 0x01); // 256 >> 8
1078        assert_eq!(buffer[3], 0x00); // 256 & 0xFF
1079        Ok(())
1080    }
1081
1082    /// Test empty list encoding
1083    #[test]
1084    fn test_list_size_empty() -> TestResult {
1085        let mut buffer = Vec::new();
1086        let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1087
1088        encoder.write_list_start(0)?;
1089
1090        // Empty list uses LIST_EMPTY token
1091        assert_eq!(buffer[1], token::LIST_EMPTY);
1092        Ok(())
1093    }
1094
1095    /// Test hex packing validation
1096    #[test]
1097    fn test_hex_validation() {
1098        // Valid hex strings (uppercase A-F, digits 0-9)
1099        assert!(validate_hex("0123456789ABCDEF"));
1100        assert!(validate_hex("DEADBEEF"));
1101        assert!(validate_hex("1234"));
1102
1103        // Invalid: lowercase letters
1104        assert!(!validate_hex("abcdef"));
1105        assert!(!validate_hex("DeadBeef"));
1106
1107        // Invalid: special characters
1108        assert!(!validate_hex("-"));
1109        assert!(!validate_hex("."));
1110        assert!(!validate_hex(" "));
1111
1112        // Empty string is valid (but will be encoded as regular string)
1113        assert!(validate_hex(""));
1114    }
1115
1116    /// Test nibble packing validation
1117    #[test]
1118    fn test_nibble_validation() {
1119        // Valid nibble strings: digits, dash, dot
1120        assert!(validate_nibble("0123456789"));
1121        assert!(validate_nibble("-"));
1122        assert!(validate_nibble("."));
1123        assert!(validate_nibble("123-456.789"));
1124
1125        // Invalid: letters
1126        assert!(!validate_nibble("abc"));
1127        assert!(!validate_nibble("123abc"));
1128
1129        // Invalid: uppercase letters
1130        assert!(!validate_nibble("ABC"));
1131
1132        // Invalid: special characters other than - and .
1133        assert!(!validate_nibble("123!456"));
1134        assert!(!validate_nibble("@"));
1135    }
1136
1137    /// Test BINARY_8, BINARY_20, BINARY_32 boundary transitions
1138    #[test]
1139    fn test_binary_length_boundaries() -> TestResult {
1140        // BINARY_8: length < 256
1141        let short_data = vec![0x42; 255];
1142        let mut buffer = Vec::new();
1143        let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1144        encoder.write_bytes_with_len(&short_data)?;
1145        assert_eq!(buffer[1], token::BINARY_8);
1146        assert_eq!(buffer[2], 255);
1147
1148        // BINARY_20: 256 <= length < 2^20
1149        let medium_data = vec![0x42; 256];
1150        let mut buffer = Vec::new();
1151        let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1152        encoder.write_bytes_with_len(&medium_data)?;
1153        assert_eq!(buffer[1], token::BINARY_20);
1154        // 256 in u20 big-endian: 0x00, 0x01, 0x00
1155        assert_eq!(buffer[2], 0x00);
1156        assert_eq!(buffer[3], 0x01);
1157        assert_eq!(buffer[4], 0x00);
1158
1159        Ok(())
1160    }
1161
1162    /// Test node with many children uses correct list encoding
1163    #[test]
1164    fn test_node_with_255_children() -> TestResult {
1165        let children: Vec<Node> = (0..255)
1166            .map(|_| Node::new("child", Attrs::new(), None))
1167            .collect();
1168
1169        let parent = Node::new("parent", Attrs::new(), Some(NodeContent::Nodes(children)));
1170
1171        let mut buffer = Vec::new();
1172        let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1173        encoder.write_node(&parent)?;
1174
1175        // Should encode successfully with LIST_8 for children
1176        assert!(!buffer.is_empty());
1177        Ok(())
1178    }
1179
1180    /// Test node with 256 children uses LIST_16
1181    #[test]
1182    fn test_node_with_256_children() -> TestResult {
1183        let children: Vec<Node> = (0..256)
1184            .map(|_| Node::new("x", Attrs::new(), None))
1185            .collect();
1186
1187        let parent = Node::new("parent", Attrs::new(), Some(NodeContent::Nodes(children)));
1188
1189        let mut buffer = Vec::new();
1190        let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1191        encoder.write_node(&parent)?;
1192
1193        // Should encode successfully with LIST_16 for children
1194        assert!(!buffer.is_empty());
1195        Ok(())
1196    }
1197
1198    /// Test string at PACKED_MAX boundary (127 chars)
1199    #[test]
1200    fn test_packed_max_boundary() {
1201        // Exactly PACKED_MAX characters should be valid for packing
1202        let max_nibble = "0".repeat(token::PACKED_MAX as usize);
1203        assert!(validate_nibble(&max_nibble));
1204
1205        // One more than PACKED_MAX should NOT be packed
1206        let over_max = "0".repeat(token::PACKED_MAX as usize + 1);
1207        assert!(!validate_nibble(&over_max));
1208    }
1209
1210    /// Test empty string encoding - should be BINARY_8 + 0, not just 0
1211    #[test]
1212    fn test_empty_string_encoding() -> TestResult {
1213        let mut buffer = Vec::new();
1214        let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1215        encoder.write_string("")?;
1216
1217        // According to WhatsApp web protocol:
1218        // Empty string should be encoded as BINARY_8 (252) + 0
1219        // NOT as token 0 (LIST_EMPTY)
1220        println!("Empty string encoding: {:?}", &buffer[1..]);
1221        assert_eq!(
1222            buffer.len(),
1223            3,
1224            "Empty string should encode to 2 bytes (plus leading 0)"
1225        );
1226        assert_eq!(
1227            buffer[1],
1228            token::BINARY_8,
1229            "First byte should be BINARY_8 (252)"
1230        );
1231        assert_eq!(buffer[2], 0, "Second byte should be 0 (length)");
1232        Ok(())
1233    }
1234
1235    /// Test encode/decode round-trip for empty string in node attributes
1236    #[test]
1237    fn test_empty_string_roundtrip() -> TestResult {
1238        use crate::decoder::Decoder;
1239
1240        let mut attrs = Attrs::new();
1241        attrs.insert("key", ""); // Empty value
1242        attrs.insert("", "value"); // Empty key
1243
1244        let node = Node::new("test", attrs, Some(NodeContent::String("".into())));
1245
1246        let mut buffer = Vec::new();
1247        let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1248        encoder.write_node(&node)?;
1249
1250        let mut decoder = Decoder::new(&buffer[1..]);
1251        let decoded = decoder.read_node_ref()?.to_owned();
1252
1253        assert_eq!(decoded.tag, "test");
1254        assert_eq!(
1255            decoded.attrs.get("key"),
1256            Some(&NodeValue::String("".into()))
1257        );
1258        assert_eq!(
1259            decoded.attrs.get(""),
1260            Some(&NodeValue::String("value".into()))
1261        );
1262
1263        // Empty strings are encoded as BINARY_8 + 0, which decodes as empty bytes
1264        match &decoded.content {
1265            Some(NodeContent::Bytes(b)) => assert!(b.is_empty(), "Content should be empty bytes"),
1266            other => panic!("Expected empty bytes, got {:?}", other),
1267        }
1268        Ok(())
1269    }
1270
1271    /// Test the JID parsing optimization: short JIDs should still be parsed,
1272    /// while long strings should be encoded as raw bytes.
1273    #[test]
1274    fn test_jid_length_heuristic() -> TestResult {
1275        use crate::decoder::Decoder;
1276        use crate::token;
1277
1278        // Short JID: should be encoded as a JID token (48 bytes or less)
1279        let short_jid = "user@s.whatsapp.net";
1280        let mut buffer = Vec::new();
1281        let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1282        encoder.write_string(short_jid)?;
1283
1284        // JID_PAIR token indicates JID encoding was used
1285        assert_eq!(
1286            buffer[1],
1287            token::JID_PAIR,
1288            "Short JID should be encoded as JID_PAIR token"
1289        );
1290
1291        // Long string (> 48 chars): should be encoded as raw bytes, not as JID
1292        let long_text = "x".repeat(300) + "@s.whatsapp.net";
1293        let mut buffer = Vec::new();
1294        let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1295        encoder.write_string(&long_text)?;
1296
1297        // BINARY_20 token indicates raw bytes encoding (length > 255)
1298        assert_eq!(
1299            buffer[1],
1300            token::BINARY_20,
1301            "Long string should be encoded as BINARY_20, not as JID"
1302        );
1303
1304        // Verify round-trip for long string
1305        let node = Node::new(
1306            "msg",
1307            Attrs::new(),
1308            Some(NodeContent::String(long_text.as_str().into())),
1309        );
1310        let mut buffer = Vec::new();
1311        let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1312        encoder.write_node(&node)?;
1313
1314        let mut decoder = Decoder::new(&buffer[1..]);
1315        let decoded = decoder.read_node_ref()?.to_owned();
1316        match &decoded.content {
1317            Some(NodeContent::Bytes(b)) => {
1318                assert_eq!(
1319                    String::from_utf8_lossy(b),
1320                    long_text,
1321                    "Long string should round-trip correctly"
1322                );
1323            }
1324            other => panic!("Expected bytes content, got {:?}", other),
1325        }
1326
1327        Ok(())
1328    }
1329
1330    #[test]
1331    fn test_jid_parser_preserves_non_numeric_device_suffix() -> TestResult {
1332        use crate::decoder::Decoder;
1333
1334        let value = "foo:bar@s.whatsapp.net";
1335        let node = Node::new("msg", Attrs::new(), Some(NodeContent::String(value.into())));
1336
1337        let mut buffer = Vec::new();
1338        let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1339        encoder.write_node(&node)?;
1340
1341        let mut decoder = Decoder::new(&buffer[1..]);
1342        let decoded = decoder.read_node_ref()?.to_owned();
1343        match decoded.content {
1344            Some(NodeContent::String(s)) => assert_eq!(s, value),
1345            other => panic!("Expected string content, got {:?}", other),
1346        }
1347        Ok(())
1348    }
1349
1350    #[test]
1351    fn test_jid_parser_preserves_non_numeric_agent_suffix() -> TestResult {
1352        use crate::decoder::Decoder;
1353
1354        let value = "hello_world@s.whatsapp.net";
1355        let node = Node::new("msg", Attrs::new(), Some(NodeContent::String(value.into())));
1356
1357        let mut buffer = Vec::new();
1358        let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1359        encoder.write_node(&node)?;
1360
1361        let mut decoder = Decoder::new(&buffer[1..]);
1362        let decoded = decoder.read_node_ref()?.to_owned();
1363        match decoded.content {
1364            Some(NodeContent::String(s)) => assert_eq!(s, value),
1365            other => panic!("Expected string content, got {:?}", other),
1366        }
1367        Ok(())
1368    }
1369
1370    /// Regression test: AD_JID domain_type must be derived from the server field,
1371    /// not from jid.agent.
1372    ///
1373    /// The binary AD_JID format is: [0xF7] [domain_type] [device] [user_string]
1374    /// where domain_type encodes the server: 0=s.whatsapp.net, 1=lid, 128=hosted.
1375    ///
1376    /// A previous bug wrote `jid.agent` (always 0) instead of the domain_type,
1377    /// causing LID JIDs to be encoded as s.whatsapp.net JIDs. The real WhatsApp
1378    /// server rejected these with error 421, while our mock server accepted them
1379    /// because it doesn't validate domain_type — hence e2e tests didn't catch it.
1380    #[test]
1381    fn test_ad_jid_domain_type_lid() -> TestResult {
1382        // Encode a LID device JID as a node attribute
1383        let lid_jid = Jid::lid_device("236395184570386", 39);
1384        let node = NodeBuilder::new("to").attr("jid", lid_jid).build();
1385
1386        let mut buffer = Vec::new();
1387        let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1388        encoder.write_node(&node)?;
1389
1390        // Find the AD_JID marker (0xF7 = 247) in the encoded bytes
1391        let ad_jid_pos = buffer
1392            .iter()
1393            .position(|&b| b == token::AD_JID)
1394            .expect("AD_JID token (0xF7) must be present for device JID");
1395
1396        // Byte after AD_JID is domain_type: must be 1 for "lid"
1397        let domain_type = buffer[ad_jid_pos + 1];
1398        assert_eq!(
1399            domain_type, 1,
1400            "LID JID must encode domain_type=1 (lid), got {domain_type} (0=whatsapp, 128=hosted)"
1401        );
1402
1403        // Byte after domain_type is device
1404        let device = buffer[ad_jid_pos + 2];
1405        assert_eq!(device, 39, "Device byte must be 39");
1406
1407        Ok(())
1408    }
1409
1410    #[test]
1411    fn test_ad_jid_domain_type_whatsapp() -> TestResult {
1412        let pn_jid = Jid::pn_device("551199887766", 33);
1413        let node = NodeBuilder::new("to").attr("jid", pn_jid).build();
1414
1415        let mut buffer = Vec::new();
1416        let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1417        encoder.write_node(&node)?;
1418
1419        let ad_jid_pos = buffer
1420            .iter()
1421            .position(|&b| b == token::AD_JID)
1422            .expect("AD_JID token must be present for device JID");
1423
1424        let domain_type = buffer[ad_jid_pos + 1];
1425        assert_eq!(
1426            domain_type, 0,
1427            "s.whatsapp.net JID must encode domain_type=0, got {domain_type}"
1428        );
1429
1430        Ok(())
1431    }
1432
1433    #[test]
1434    fn test_ad_jid_domain_type_whatsapp_ignores_hidden_agent() -> TestResult {
1435        use crate::decoder::Decoder;
1436
1437        let mut pn_jid = Jid::pn_device("551199887766", 33);
1438        pn_jid.agent = 2;
1439        let node = NodeBuilder::new("to").attr("jid", pn_jid.clone()).build();
1440
1441        let mut buffer = Vec::new();
1442        let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1443        encoder.write_node(&node)?;
1444
1445        let ad_jid_pos = buffer
1446            .iter()
1447            .position(|&b| b == token::AD_JID)
1448            .expect("AD_JID token must be present for device JID");
1449
1450        assert_eq!(
1451            buffer[ad_jid_pos + 1],
1452            0,
1453            "PN JID must encode the WA Web domain_type=0 even if a hidden agent is present"
1454        );
1455
1456        let decoded = Decoder::new(&buffer[1..]).read_node_ref()?.to_owned();
1457        let decoded_jid = decoded
1458            .attrs()
1459            .optional_jid("jid")
1460            .expect("jid attr must decode");
1461        assert!(pn_jid.is_same_chat_as(&decoded_jid));
1462        assert_eq!(decoded_jid.agent, 0);
1463
1464        Ok(())
1465    }
1466
1467    /// Verify that string-encoded JIDs and direct Jid-encoded JIDs produce
1468    /// identical bytes AND decode back to the same JID. This catches any
1469    /// divergence between the two encoding paths (root cause of the domain_type
1470    /// bug) and ensures encode→decode round-trip fidelity for all server types.
1471    #[test]
1472    fn test_jid_string_vs_direct_encoding_matches() -> TestResult {
1473        use crate::decoder::Decoder;
1474
1475        let test_cases: Vec<Jid> = vec![
1476            Jid::lid_device("236395184570386", 39),     // LID with device
1477            Jid::pn_device("551199887766", 33),         // PN with device
1478            Jid::lid("236395184570386"),                // LID primary (device 0)
1479            Jid::pn("551199887766"),                    // PN primary (device 0)
1480            "5511999887766:99@hosted".parse().unwrap(), // HOSTED device
1481            "100000012345678:99@hosted.lid".parse().unwrap(), // HOSTED_LID device
1482        ];
1483
1484        for jid in test_cases {
1485            // Path 1: string encoding (known correct — uses parse_jid_meta)
1486            let node_str = NodeBuilder::new("to").attr("jid", jid.to_string()).build();
1487
1488            // Path 2: direct Jid encoding (uses write_jid_owned)
1489            let node_jid = NodeBuilder::new("to").attr("jid", jid.clone()).build();
1490
1491            let mut buf_str = Vec::new();
1492            Encoder::new(Cursor::new(&mut buf_str))?.write_node(&node_str)?;
1493
1494            let mut buf_jid = Vec::new();
1495            Encoder::new(Cursor::new(&mut buf_jid))?.write_node(&node_jid)?;
1496
1497            assert_eq!(
1498                buf_str, buf_jid,
1499                "String vs direct Jid encoding must produce identical bytes for {jid}"
1500            );
1501
1502            // Round-trip: decode the encoded bytes and verify the JID is preserved.
1503            // Skip version byte (first byte) then decode.
1504            let mut decoder = Decoder::new(&buf_jid[1..]);
1505            let decoded_node = decoder.read_node_ref()?.to_owned();
1506            let decoded_jid: Jid = decoded_node
1507                .attrs()
1508                .optional_jid("jid")
1509                .expect("jid attr must round-trip as JID");
1510
1511            assert_eq!(
1512                jid.user, decoded_jid.user,
1513                "Round-trip user mismatch for {jid}"
1514            );
1515            assert_eq!(
1516                jid.device, decoded_jid.device,
1517                "Round-trip device mismatch for {jid}"
1518            );
1519            assert_eq!(
1520                jid.server, decoded_jid.server,
1521                "Round-trip server mismatch for {jid}"
1522            );
1523        }
1524
1525        Ok(())
1526    }
1527
1528    /// Pin domain_type for direct-constructed Hosted/HostedLid JIDs (default
1529    /// `agent=0`); pre-#391 these encoded as `0` instead of `128`/`129`.
1530    #[test]
1531    fn test_direct_constructed_hosted_encodes_correct_domain_type() -> TestResult {
1532        let mut hosted = Jid::new("100000000000001", jid::Server::Hosted);
1533        hosted.device = 99;
1534        assert_eq!(
1535            hosted.agent, 0,
1536            "default agent for direct construction is 0"
1537        );
1538
1539        let mut hosted_lid = Jid::new("100000000000002", jid::Server::HostedLid);
1540        hosted_lid.device = 99;
1541        assert_eq!(hosted_lid.agent, 0);
1542
1543        for (jid, expected) in [(&hosted, 128u8), (&hosted_lid, 129u8)] {
1544            let node = NodeBuilder::new("to").attr("jid", jid.clone()).build();
1545            let mut buf = Vec::new();
1546            Encoder::new(Cursor::new(&mut buf))?.write_node(&node)?;
1547
1548            let pos = buf
1549                .iter()
1550                .position(|&b| b == token::AD_JID)
1551                .expect("AD_JID marker present");
1552            assert_eq!(
1553                buf[pos + 1],
1554                expected,
1555                "direct-constructed {jid} must emit domain_type {expected} \
1556                 (pre-#391 would have emitted agent=0)"
1557            );
1558        }
1559        Ok(())
1560    }
1561
1562    /// Regression test: strings at the PACKED_MAX boundary must be classified
1563    /// normally, while strings above it must be emitted as raw bytes (skipping
1564    /// SipHash/PHF classification entirely).
1565    #[test]
1566    fn test_long_string_skips_classification() -> TestResult {
1567        use crate::decoder::Decoder;
1568        use crate::marshal::marshal;
1569
1570        let at_boundary = "0".repeat(token::PACKED_MAX as usize); // 127 nibble chars
1571        let over_boundary = "0".repeat(token::PACKED_MAX as usize + 1); // 128 chars
1572
1573        // 127-char all-digit string is nibble-packable
1574        let node_at = Node::new(
1575            "test",
1576            Attrs::new(),
1577            Some(NodeContent::String(at_boundary.as_str().into())),
1578        );
1579        let encoded_at = marshal(&node_at)?;
1580
1581        // 128-char string must be emitted as raw bytes (BINARY_8 + length)
1582        let node_over = Node::new(
1583            "test",
1584            Attrs::new(),
1585            Some(NodeContent::String(over_boundary.as_str().into())),
1586        );
1587        let encoded_over = marshal(&node_over)?;
1588
1589        // The 127-char string should be packed (shorter encoding than raw)
1590        assert!(
1591            encoded_at.len() < encoded_over.len(),
1592            "127-char nibble string should pack smaller than 128-char raw: {} vs {}",
1593            encoded_at.len(),
1594            encoded_over.len(),
1595        );
1596
1597        // The 128-char content must be encoded as BINARY_8 + 128 (raw bytes).
1598        // Find the [BINARY_8, 128] pair — the first BINARY_8 is for the tag "test".
1599        let has_raw_128 = encoded_over
1600            .windows(2)
1601            .any(|w| w[0] == token::BINARY_8 && w[1] == 128);
1602        assert!(
1603            has_raw_128,
1604            "128-char string must contain BINARY_8 + length=128 sequence"
1605        );
1606
1607        // Both must round-trip correctly (skip version byte at [0])
1608        let decoded_at = Decoder::new(&encoded_at[1..]).read_node_ref()?.to_owned();
1609        let decoded_over = Decoder::new(&encoded_over[1..]).read_node_ref()?.to_owned();
1610
1611        match &decoded_at.content {
1612            Some(NodeContent::String(s)) => assert_eq!(s.as_str(), at_boundary),
1613            Some(NodeContent::Bytes(b)) => {
1614                assert_eq!(std::str::from_utf8(b).unwrap(), at_boundary)
1615            }
1616            other => panic!("Expected string/bytes content, got {:?}", other),
1617        }
1618        match &decoded_over.content {
1619            Some(NodeContent::Bytes(b)) => {
1620                assert_eq!(std::str::from_utf8(b).unwrap(), over_boundary)
1621            }
1622            other => panic!(
1623                "Expected bytes content for 128-char string, got {:?}",
1624                other
1625            ),
1626        }
1627
1628        Ok(())
1629    }
1630
1631    /// Regression: AD_JID only round-trips for the 4 servers whose domain_type
1632    /// the decoder maps back (Pn/Lid/Hosted/HostedLid). Anything else
1633    /// (bot/group/broadcast/newsletter/...) must go through JID_PAIR so the
1634    /// server string survives. Matches whatsmeow `writeJID` and WA Web
1635    /// `WAWap.De` (`WapJid.create` for non-AD-capable servers).
1636    #[test]
1637    fn test_bot_jid_with_device_round_trips_via_jid_pair() -> TestResult {
1638        use crate::decoder::Decoder;
1639
1640        for value in [
1641            "867051314767696@bot",
1642            "867051314767696:0@bot",
1643            "120363021033254949@g.us",
1644            "12345@broadcast",
1645            "12345@newsletter",
1646        ] {
1647            let node = NodeBuilder::new("msg").attr("from", value).build();
1648
1649            let mut buffer = Vec::new();
1650            let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1651            encoder.write_node(&node)?;
1652
1653            // AD_JID (0xF7) must NOT appear for any of these — they use JID_PAIR
1654            // (0xF8) or raw bytes.
1655            assert!(
1656                !buffer.contains(&token::AD_JID),
1657                "AD_JID must not be emitted for {value} (would lose the server)"
1658            );
1659
1660            let decoded = Decoder::new(&buffer[1..]).read_node_ref()?.to_owned();
1661            let from_attr = decoded
1662                .attrs
1663                .get("from")
1664                .expect("from attr must survive the round-trip");
1665            let got = from_attr.to_string();
1666            // device :0 is equivalent to no device for these servers; either
1667            // form is acceptable as long as the server is preserved.
1668            let expected_user_server = value.split(':').next().unwrap_or(value);
1669            let expected_server = value.split('@').nth(1).unwrap();
1670            assert!(
1671                got.ends_with(&format!("@{expected_server}")),
1672                "round-trip lost the server for {value}: got {got}",
1673            );
1674            assert!(
1675                got.starts_with(expected_user_server.split('@').next().unwrap())
1676                    || got.starts_with(value.split('@').next().unwrap()),
1677                "round-trip lost the user for {value}: got {got}",
1678            );
1679        }
1680        Ok(())
1681    }
1682
1683    /// `@call` must round-trip via JID_PAIR instead of failing the whole node decode.
1684    #[test]
1685    fn test_call_jid_round_trips_via_jid_pair() -> TestResult {
1686        use crate::decoder::Decoder;
1687
1688        let node = NodeBuilder::new("call").attr("from", "12345@call").build();
1689        let mut buffer = Vec::new();
1690        let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1691        encoder.write_node(&node)?;
1692        assert!(
1693            !buffer.contains(&token::AD_JID),
1694            "AD_JID must not be emitted for @call (would lose the server)"
1695        );
1696
1697        let decoded = Decoder::new(&buffer[1..]).read_node_ref()?.to_owned();
1698        let from = decoded
1699            .attrs
1700            .get("from")
1701            .expect("from attr must survive the round-trip");
1702        assert_eq!(from.to_string(), "12345@call");
1703        Ok(())
1704    }
1705
1706    /// Same invariant as above but exercised through the typed
1707    /// `NodeValue::Jid` path (write_jid_owned + size estimators), which
1708    /// previously ignored the server check and emitted AD_JID for any
1709    /// device > 0 — silently mapping the server back to Pn on decode.
1710    #[test]
1711    fn test_typed_non_ad_jid_with_device_round_trips_via_jid_pair() -> TestResult {
1712        use crate::decoder::Decoder;
1713        use std::str::FromStr;
1714
1715        for value in [
1716            // Bot devices, broadcast/newsletter with explicit device — all
1717            // non-AD-capable servers. The decoder cannot recover the server
1718            // from the AD_JID domain_type, so the encoder must avoid AD_JID.
1719            "867051314767696:0@bot",
1720            "12345:5@broadcast",
1721            "67890:9@newsletter",
1722        ] {
1723            let jid = Jid::from_str(value)?;
1724            let node = NodeBuilder::new("msg").attr("from", jid.clone()).build();
1725
1726            let mut buffer = Vec::new();
1727            let mut encoder = Encoder::new(Cursor::new(&mut buffer))?;
1728            encoder.write_node(&node)?;
1729
1730            assert!(
1731                !buffer.contains(&token::AD_JID),
1732                "typed JID {value} must NOT emit AD_JID (decoder would drop the server)"
1733            );
1734
1735            let decoded = Decoder::new(&buffer[1..]).read_node_ref()?.to_owned();
1736            let from = decoded
1737                .attrs
1738                .get("from")
1739                .expect("from attr must survive round-trip")
1740                .to_jid()
1741                .expect("from attr decodes back to a Jid");
1742            assert_eq!(
1743                from.server, jid.server,
1744                "round-trip lost the server for typed {value}"
1745            );
1746            assert_eq!(
1747                from.user, jid.user,
1748                "round-trip lost the user for typed {value}"
1749            );
1750        }
1751        Ok(())
1752    }
1753}