Skip to main content

consortium/
communication.rs

1//! Inter-node messaging protocol.
2//!
3//! Rust implementation of `ClusterShell.Communication`.
4//!
5//! Messages are the fundamental units of communication between nodes in the
6//! propagation tree. They are serialized to/from XML for transmission over
7//! SSH channels. Each message has a type identifier, a unique message ID,
8//! optional attributes, and an optional payload (base64-encoded bytes).
9//!
10//! The [`Channel`] struct manages bidirectional communication using an
11//! [`XmlReader`] for parsing incoming XML and XML generation for outgoing
12//! messages.
13
14use std::collections::{HashMap, VecDeque};
15use std::sync::atomic::{AtomicU64, Ordering};
16
17use thiserror::Error;
18
19// ============================================================================
20// Constants
21// ============================================================================
22
23/// XML character encoding
24pub const ENCODING: &str = "utf-8";
25
26/// Default base64 line length for large payloads
27pub const DEFAULT_B64_LINE_LENGTH: usize = 65536;
28
29/// Pinned pickle protocol version for gateway/tree message payloads.
30///
31/// Mirrors upstream `Communication.GW_PICKLE_PROTOCOL`
32/// (`min(HIGHEST_PROTOCOL, 4)`, commit fee03a3): gateway messages may cross
33/// Python versions within a tree, so the protocol is capped at 4, readable
34/// since Python 3.4 (Python 2 writes protocol 2).
35///
36/// Rust payloads are opaque pre-serialized bytes (no pickle writer on this
37/// side), so the pin is enforced on READ: [`Message::data_decode`] rejects a
38/// payload stamped with a newer protocol, reproducing upstream's
39/// "unsupported pickle protocol: N" negotiation failure instead of silently
40/// mis-decoding. See [`validate_payload_protocol`].
41pub const GW_PICKLE_PROTOCOL: u8 = 4;
42
43// Channel stream name constants
44/// Stream name for the writer side of a channel
45pub const SNAME_WRITER: &str = "ch-writer";
46/// Stream name for the reader side of a channel
47pub const SNAME_READER: &str = "ch-reader";
48/// Stream name for the error side of a channel
49pub const SNAME_ERROR: &str = "ch-error";
50
51// Message type identifier constants
52const IDENT_GEN: &str = "GEN";
53const IDENT_CFG: &str = "CFG";
54const IDENT_CTL: &str = "CTL";
55const IDENT_ACK: &str = "ACK";
56const IDENT_ERR: &str = "ERR";
57const IDENT_OUT: &str = "OUT";
58const IDENT_SER: &str = "SER";
59const IDENT_RET: &str = "RET";
60const IDENT_TIM: &str = "TIM";
61const IDENT_RTR: &str = "RTR";
62const IDENT_CHA: &str = "CHA";
63const IDENT_END: &str = "END";
64
65// ============================================================================
66// Errors
67// ============================================================================
68
69/// Errors raised during message processing.
70#[derive(Error, Debug)]
71pub enum MessageProcessingError {
72    /// An unknown message type was encountered.
73    /// Message text mirrors upstream "Unknown message type %s".
74    #[error("Unknown message type {0}")]
75    UnknownType(String),
76
77    /// A message with no (or an empty) "type" attribute was encountered.
78    /// Message text mirrors upstream "Unknown message with no type".
79    #[error("Unknown message with no type")]
80    NoType,
81
82    /// A required attribute is missing from a message.
83    #[error("Invalid message attributes: missing key \"{0}\"")]
84    MissingAttribute(String),
85
86    /// An unexpected payload was found on a message that doesn't support one.
87    #[error("Got unexpected payload for Message {0}")]
88    UnexpectedPayload(String),
89
90    /// A message was received in a state where it is not expected.
91    /// Message text mirrors upstream "unexpected message: %s".
92    #[error("unexpected message: {0}")]
93    UnexpectedMessage(String),
94
95    /// The message payload is invalid or corrupted.
96    #[error("Message {0} has an invalid payload")]
97    InvalidPayload(String),
98
99    /// The message payload is invalid or corrupted; carries the underlying
100    /// cause like upstream's "Message %s has an invalid payload (%s)"
101    /// (commit fee03a3 — the cause is included since Python 3.14/reporting).
102    #[error("Message {0} has an invalid payload ({1})")]
103    InvalidPayloadDetail(String, String),
104
105    /// XML parse error.
106    #[error("Parse error: {0}")]
107    ParseError(String),
108
109    /// Invalid starting XML tag.
110    #[error("Invalid starting tag {0}")]
111    InvalidTag(String),
112}
113
114// ============================================================================
115// Global message ID counter
116// ============================================================================
117
118static MSG_COUNTER: AtomicU64 = AtomicU64::new(0);
119
120fn next_msg_id() -> u64 {
121    MSG_COUNTER.fetch_add(1, Ordering::Relaxed)
122}
123
124/// Reset the global message counter (for testing only).
125#[cfg(test)]
126fn reset_msg_counter() {
127    MSG_COUNTER.store(0, Ordering::Relaxed);
128}
129
130// ============================================================================
131// Minimal base64
132// ============================================================================
133
134/// Simple base64 alphabet (standard RFC 4648).
135const B64_CHARS: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
136
137/// Encode bytes to base64 string.
138pub fn base64_encode(input: &[u8]) -> String {
139    let mut out = String::with_capacity((input.len() + 2) / 3 * 4);
140    for chunk in input.chunks(3) {
141        let b0 = chunk[0] as u32;
142        let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
143        let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
144        let triple = (b0 << 16) | (b1 << 8) | b2;
145
146        out.push(B64_CHARS[((triple >> 18) & 0x3F) as usize] as char);
147        out.push(B64_CHARS[((triple >> 12) & 0x3F) as usize] as char);
148        if chunk.len() > 1 {
149            out.push(B64_CHARS[((triple >> 6) & 0x3F) as usize] as char);
150        } else {
151            out.push('=');
152        }
153        if chunk.len() > 2 {
154            out.push(B64_CHARS[(triple & 0x3F) as usize] as char);
155        } else {
156            out.push('=');
157        }
158    }
159    out
160}
161
162/// Decode base64 string to bytes. Ignores whitespace/newlines (RFC 4648 relaxed).
163///
164/// On failure returns [`MessageProcessingError::InvalidPayload`] carrying only
165/// the reason (no message ident); use [`Message::data_decode`] for ident-aware
166/// errors that mirror upstream's "Message %s has an invalid payload (%s)".
167pub fn base64_decode(input: &str) -> Result<Vec<u8>, MessageProcessingError> {
168    base64_decode_reason(input).map_err(MessageProcessingError::InvalidPayload)
169}
170
171/// Inner base64 decoder returning a plain reason string on failure, so callers
172/// can attribute the error to a specific message ident (upstream fee03a3 adds
173/// the underlying cause to the raised MessageProcessingError).
174fn base64_decode_reason(input: &str) -> Result<Vec<u8>, String> {
175    fn b64_val(c: u8) -> Result<u8, String> {
176        match c {
177            b'A'..=b'Z' => Ok(c - b'A'),
178            b'a'..=b'z' => Ok(c - b'a' + 26),
179            b'0'..=b'9' => Ok(c - b'0' + 52),
180            b'+' => Ok(62),
181            b'/' => Ok(63),
182            _ => Err("invalid base64 character".to_string()),
183        }
184    }
185
186    // Strip whitespace
187    let clean: Vec<u8> = input.bytes().filter(|b| !b.is_ascii_whitespace()).collect();
188    if clean.is_empty() {
189        return Ok(Vec::new());
190    }
191
192    // Padding rules mirror Python's base64.b64decode (binascii.Error):
193    // length must be a multiple of 4 and '=' may only terminate the data.
194    if !clean.len().is_multiple_of(4) {
195        return Err("Incorrect padding".to_string());
196    }
197    if let Some(pos) = clean.iter().position(|&b| b == b'=') {
198        if clean.len() - pos > 2 || clean[pos..].iter().any(|&b| b != b'=') {
199            return Err("Invalid padding".to_string());
200        }
201    }
202
203    let mut out = Vec::with_capacity(clean.len() * 3 / 4);
204    for chunk in clean.chunks(4) {
205        if chunk.len() < 2 {
206            return Err("truncated base64".to_string());
207        }
208        let a = b64_val(chunk[0])?;
209        let b = b64_val(chunk[1])?;
210        out.push((a << 2) | (b >> 4));
211        if chunk.len() > 2 && chunk[2] != b'=' {
212            let c = b64_val(chunk[2])?;
213            out.push((b << 4) | (c >> 2));
214            if chunk.len() > 3 && chunk[3] != b'=' {
215                let d = b64_val(chunk[3])?;
216                out.push((c << 6) | d);
217            }
218        }
219    }
220    Ok(out)
221}
222
223/// Validate a decoded wire payload against [`GW_PICKLE_PROTOCOL`].
224///
225/// Mirrors the `ValueError` arm of upstream `Message.data_decode()`
226/// (commit fee03a3): `cPickle.loads()` raises `ValueError("unsupported pickle
227/// protocol: N")` when a gateway running a newer Python emits a payload
228/// stamped with a protocol this side cannot read. Pickle protocol 2+ streams
229/// start with the PROTO opcode (`\x80`) followed by the protocol number; a
230/// number greater than the pinned gateway protocol is rejected with the same
231/// cause text so the mismatch is detected and reported instead of silently
232/// mis-decoded. Non-pickle payloads (no PROTO header) pass through unchanged.
233pub fn validate_payload_protocol(ident: &str, raw: &[u8]) -> Result<(), MessageProcessingError> {
234    if raw.len() >= 2 && raw[0] == 0x80 && raw[1] > GW_PICKLE_PROTOCOL {
235        return Err(MessageProcessingError::InvalidPayloadDetail(
236            ident.to_string(),
237            format!("unsupported pickle protocol: {}", raw[1]),
238        ));
239    }
240    Ok(())
241}
242
243// ============================================================================
244// XML escape helpers
245// ============================================================================
246
247fn xml_escape(s: &str) -> String {
248    let mut out = String::with_capacity(s.len());
249    for c in s.chars() {
250        match c {
251            '&' => out.push_str("&amp;"),
252            '<' => out.push_str("&lt;"),
253            '>' => out.push_str("&gt;"),
254            '"' => out.push_str("&quot;"),
255            '\'' => out.push_str("&apos;"),
256            _ => out.push(c),
257        }
258    }
259    out
260}
261
262fn xml_unescape(s: &str) -> String {
263    s.replace("&amp;", "&")
264        .replace("&lt;", "<")
265        .replace("&gt;", ">")
266        .replace("&quot;", "\"")
267        .replace("&apos;", "'")
268}
269
270// ============================================================================
271// Message
272// ============================================================================
273
274/// A communication message in the propagation tree protocol.
275///
276/// Messages are tagged unions representing the different message types
277/// exchanged between ClusterShell nodes. Each variant carries the fields
278/// specific to that message type.
279#[derive(Debug, Clone)]
280pub enum Message {
281    /// Generic base message.
282    General { msgid: u64, data: Option<Vec<u8>> },
283
284    /// Configuration propagation container (ident = "CFG").
285    /// Has payload. Carries serialized topology.
286    Configuration {
287        msgid: u64,
288        gateway: String,
289        data: Option<Vec<u8>>,
290    },
291
292    /// Action request routed through the tree (ident = "CTL").
293    /// Has payload. Carries serialized command data.
294    Control {
295        msgid: u64,
296        srcid: u64,
297        action: String,
298        target: String,
299        data: Option<Vec<u8>>,
300    },
301
302    /// Acknowledgement message (ident = "ACK").
303    Ack { msgid: u64, ack: u64 },
304
305    /// Error message (ident = "ERR").
306    Error { msgid: u64, reason: String },
307
308    /// Standard output container (ident = "OUT").
309    /// Has payload. Carries encoded output data.
310    StdOut {
311        msgid: u64,
312        srcid: u64,
313        nodes: String,
314        data: Option<Vec<u8>>,
315    },
316
317    /// Standard error container (ident = "SER").
318    /// Has payload. Carries encoded stderr data.
319    StdErr {
320        msgid: u64,
321        srcid: u64,
322        nodes: String,
323        data: Option<Vec<u8>>,
324    },
325
326    /// Return code container (ident = "RET").
327    Retcode {
328        msgid: u64,
329        srcid: u64,
330        retcode: i32,
331        nodes: String,
332    },
333
334    /// Timeout notification (ident = "TIM").
335    Timeout {
336        msgid: u64,
337        srcid: u64,
338        nodes: String,
339    },
340
341    /// Routing notification (ident = "RTR").
342    Routing {
343        msgid: u64,
344        srcid: u64,
345        event: String,
346        gateway: String,
347        targets: String,
348    },
349
350    /// Start of channel communication (ident = "CHA").
351    Start { msgid: u64 },
352
353    /// End of channel communication (ident = "END").
354    End { msgid: u64 },
355}
356
357impl Message {
358    // -- Constructors --------------------------------------------------------
359
360    /// Create a new General message.
361    pub fn general() -> Self {
362        Message::General {
363            msgid: next_msg_id(),
364            data: None,
365        }
366    }
367
368    /// Create a new Configuration message.
369    pub fn configuration(gateway: &str) -> Self {
370        Message::Configuration {
371            msgid: next_msg_id(),
372            gateway: gateway.to_string(),
373            data: None,
374        }
375    }
376
377    /// Create a new Control message.
378    pub fn control(srcid: u64) -> Self {
379        Message::Control {
380            msgid: next_msg_id(),
381            srcid,
382            action: String::new(),
383            target: String::new(),
384            data: None,
385        }
386    }
387
388    /// Create a new Ack message.
389    pub fn ack(ackid: u64) -> Self {
390        Message::Ack {
391            msgid: next_msg_id(),
392            ack: ackid,
393        }
394    }
395
396    /// Create a new Error message.
397    pub fn error(reason: &str) -> Self {
398        Message::Error {
399            msgid: next_msg_id(),
400            reason: reason.to_string(),
401        }
402    }
403
404    /// Create a new StdOut message.
405    pub fn stdout(nodes: &str, srcid: u64) -> Self {
406        Message::StdOut {
407            msgid: next_msg_id(),
408            srcid,
409            nodes: nodes.to_string(),
410            data: None,
411        }
412    }
413
414    /// Create a new StdErr message.
415    pub fn stderr(nodes: &str, srcid: u64) -> Self {
416        Message::StdErr {
417            msgid: next_msg_id(),
418            srcid,
419            nodes: nodes.to_string(),
420            data: None,
421        }
422    }
423
424    /// Create a new Retcode message.
425    pub fn retcode(nodes: &str, retcode: i32, srcid: u64) -> Self {
426        Message::Retcode {
427            msgid: next_msg_id(),
428            srcid,
429            retcode,
430            nodes: nodes.to_string(),
431        }
432    }
433
434    /// Create a new Timeout message.
435    pub fn timeout(nodes: &str, srcid: u64) -> Self {
436        Message::Timeout {
437            msgid: next_msg_id(),
438            srcid,
439            nodes: nodes.to_string(),
440        }
441    }
442
443    /// Create a new Routing message.
444    pub fn routing(event: &str, gateway: &str, targets: &str, srcid: u64) -> Self {
445        Message::Routing {
446            msgid: next_msg_id(),
447            srcid,
448            event: event.to_string(),
449            gateway: gateway.to_string(),
450            targets: targets.to_string(),
451        }
452    }
453
454    /// Create a Start message.
455    pub fn start() -> Self {
456        Message::Start {
457            msgid: next_msg_id(),
458        }
459    }
460
461    /// Create an End message.
462    pub fn end() -> Self {
463        Message::End {
464            msgid: next_msg_id(),
465        }
466    }
467
468    // -- Accessors -----------------------------------------------------------
469
470    /// Get the message type identifier string.
471    pub fn ident(&self) -> &str {
472        match self {
473            Message::General { .. } => IDENT_GEN,
474            Message::Configuration { .. } => IDENT_CFG,
475            Message::Control { .. } => IDENT_CTL,
476            Message::Ack { .. } => IDENT_ACK,
477            Message::Error { .. } => IDENT_ERR,
478            Message::StdOut { .. } => IDENT_OUT,
479            Message::StdErr { .. } => IDENT_SER,
480            Message::Retcode { .. } => IDENT_RET,
481            Message::Timeout { .. } => IDENT_TIM,
482            Message::Routing { .. } => IDENT_RTR,
483            Message::Start { .. } => IDENT_CHA,
484            Message::End { .. } => IDENT_END,
485        }
486    }
487
488    /// Get the message ID.
489    pub fn msgid(&self) -> u64 {
490        match self {
491            Message::General { msgid, .. }
492            | Message::Configuration { msgid, .. }
493            | Message::Control { msgid, .. }
494            | Message::Ack { msgid, .. }
495            | Message::Error { msgid, .. }
496            | Message::StdOut { msgid, .. }
497            | Message::StdErr { msgid, .. }
498            | Message::Retcode { msgid, .. }
499            | Message::Timeout { msgid, .. }
500            | Message::Routing { msgid, .. }
501            | Message::Start { msgid, .. }
502            | Message::End { msgid, .. } => *msgid,
503        }
504    }
505
506    /// Whether this message type carries a payload.
507    pub fn has_payload(&self) -> bool {
508        matches!(
509            self,
510            Message::General { .. }
511                | Message::Configuration { .. }
512                | Message::Control { .. }
513                | Message::StdOut { .. }
514                | Message::StdErr { .. }
515        )
516    }
517
518    /// Get a reference to the payload data, if any.
519    pub fn data(&self) -> Option<&[u8]> {
520        match self {
521            Message::General { data, .. }
522            | Message::Configuration { data, .. }
523            | Message::Control { data, .. }
524            | Message::StdOut { data, .. }
525            | Message::StdErr { data, .. } => data.as_deref(),
526            _ => None,
527        }
528    }
529
530    /// Set the payload data (raw bytes, will be base64-encoded on serialization).
531    pub fn set_data(&mut self, payload: Vec<u8>) {
532        match self {
533            Message::General { data, .. }
534            | Message::Configuration { data, .. }
535            | Message::Control { data, .. }
536            | Message::StdOut { data, .. }
537            | Message::StdErr { data, .. } => {
538                *data = Some(payload);
539            }
540            _ => {}
541        }
542    }
543
544    /// Encode arbitrary bytes as base64 and store as payload.
545    pub fn data_encode(&mut self, raw: &[u8]) {
546        let encoded = base64_encode(raw);
547        self.set_data(encoded.into_bytes());
548    }
549
550    /// Decode the base64 payload back to raw bytes.
551    ///
552    /// Mirrors upstream `Message.data_decode()` (commit fee03a3): base64 or
553    /// payload-protocol failures raise an error carrying the message ident
554    /// AND the underlying cause, e.g.
555    /// "Message CFG has an invalid payload (unsupported pickle protocol: 7)".
556    pub fn data_decode(&self) -> Result<Vec<u8>, MessageProcessingError> {
557        match self.data() {
558            Some(d) => {
559                let ident = self.ident().to_string();
560                let s = std::str::from_utf8(d)
561                    .map_err(|_| MessageProcessingError::InvalidPayload(ident.clone()))?;
562                let raw = base64_decode_reason(s).map_err(|reason| {
563                    MessageProcessingError::InvalidPayloadDetail(ident.clone(), reason)
564                })?;
565                validate_payload_protocol(&ident, &raw)?;
566                Ok(raw)
567            }
568            None => Err(MessageProcessingError::InvalidPayload(self.ident().into())),
569        }
570    }
571
572    /// Append data during incremental XML parsing.
573    pub fn data_update(&mut self, raw: &[u8]) -> Result<(), MessageProcessingError> {
574        if !self.has_payload() {
575            return Err(MessageProcessingError::UnexpectedPayload(
576                self.ident().into(),
577            ));
578        }
579        match self {
580            Message::General { data, .. }
581            | Message::Configuration { data, .. }
582            | Message::Control { data, .. }
583            | Message::StdOut { data, .. }
584            | Message::StdErr { data, .. } => {
585                if let Some(ref mut d) = data {
586                    d.extend_from_slice(raw);
587                } else {
588                    *data = Some(raw.to_vec());
589                }
590            }
591            _ => {}
592        }
593        Ok(())
594    }
595
596    /// Get the srcid for routed messages, None for non-routed.
597    pub fn srcid(&self) -> Option<u64> {
598        match self {
599            Message::Control { srcid, .. }
600            | Message::StdOut { srcid, .. }
601            | Message::StdErr { srcid, .. }
602            | Message::Retcode { srcid, .. }
603            | Message::Timeout { srcid, .. }
604            | Message::Routing { srcid, .. } => Some(*srcid),
605            _ => None,
606        }
607    }
608
609    /// Get the nodes field if present.
610    pub fn nodes(&self) -> Option<&str> {
611        match self {
612            Message::StdOut { nodes, .. }
613            | Message::StdErr { nodes, .. }
614            | Message::Retcode { nodes, .. }
615            | Message::Timeout { nodes, .. } => Some(nodes.as_str()),
616            _ => None,
617        }
618    }
619
620    // -- XML serialization ---------------------------------------------------
621
622    /// Serialize this message to XML bytes.
623    ///
624    /// Produces output like:
625    /// `<message type="CFG" msgid="0" gateway="gw1">base64payload</message>`
626    pub fn xml(&self) -> Vec<u8> {
627        let mut attrs = Vec::new();
628
629        // type and msgid are always present
630        attrs.push(format!("type=\"{}\"", xml_escape(self.ident())));
631        attrs.push(format!("msgid=\"{}\"", self.msgid()));
632
633        // variant-specific attributes
634        match self {
635            Message::Configuration { gateway, .. } => {
636                attrs.push(format!("gateway=\"{}\"", xml_escape(gateway)));
637            }
638            Message::Control {
639                srcid,
640                action,
641                target,
642                ..
643            } => {
644                attrs.push(format!("srcid=\"{}\"", srcid));
645                attrs.push(format!("action=\"{}\"", xml_escape(action)));
646                attrs.push(format!("target=\"{}\"", xml_escape(target)));
647            }
648            Message::Ack { ack, .. } => {
649                attrs.push(format!("ack=\"{}\"", ack));
650            }
651            Message::Error { reason, .. } => {
652                attrs.push(format!("reason=\"{}\"", xml_escape(reason)));
653            }
654            Message::StdOut { srcid, nodes, .. } => {
655                attrs.push(format!("srcid=\"{}\"", srcid));
656                attrs.push(format!("nodes=\"{}\"", xml_escape(nodes)));
657            }
658            Message::StdErr { srcid, nodes, .. } => {
659                attrs.push(format!("srcid=\"{}\"", srcid));
660                attrs.push(format!("nodes=\"{}\"", xml_escape(nodes)));
661            }
662            Message::Retcode {
663                srcid,
664                retcode,
665                nodes,
666                ..
667            } => {
668                attrs.push(format!("srcid=\"{}\"", srcid));
669                attrs.push(format!("retcode=\"{}\"", retcode));
670                attrs.push(format!("nodes=\"{}\"", xml_escape(nodes)));
671            }
672            Message::Timeout { srcid, nodes, .. } => {
673                attrs.push(format!("srcid=\"{}\"", srcid));
674                attrs.push(format!("nodes=\"{}\"", xml_escape(nodes)));
675            }
676            Message::Routing {
677                srcid,
678                event,
679                gateway,
680                targets,
681                ..
682            } => {
683                attrs.push(format!("srcid=\"{}\"", srcid));
684                attrs.push(format!("event=\"{}\"", xml_escape(event)));
685                attrs.push(format!("gateway=\"{}\"", xml_escape(gateway)));
686                attrs.push(format!("targets=\"{}\"", xml_escape(targets)));
687            }
688            _ => {}
689        }
690
691        let attr_str = attrs.join(" ");
692        let payload = self.data().unwrap_or(&[]);
693
694        if payload.is_empty() {
695            format!("<message {}/>", attr_str).into_bytes()
696        } else {
697            let payload_str = String::from_utf8_lossy(payload);
698            format!("<message {}>{}</message>", attr_str, payload_str).into_bytes()
699        }
700    }
701
702    /// Build a message from parsed XML attributes.
703    ///
704    /// The message type is selected based on the "type" attribute, and all
705    /// other attributes are read according to the variant's schema.
706    ///
707    /// Mirrors upstream `XMLReader.startElement`: a missing OR empty "type"
708    /// attribute raises "Unknown message with no type"; an unrecognized type
709    /// raises "Unknown message type %s" (commit range message wording).
710    pub fn from_attrs(attrs: &HashMap<String, String>) -> Result<Message, MessageProcessingError> {
711        let msg_type = match attrs.get("type") {
712            Some(t) if !t.is_empty() => t.as_str(),
713            // attributes.get('type') is None or falsy upstream: same error
714            _ => return Err(MessageProcessingError::NoType),
715        };
716
717        let msgid: u64 = attrs
718            .get("msgid")
719            .and_then(|v| v.parse().ok())
720            .unwrap_or_else(next_msg_id);
721
722        match msg_type {
723            IDENT_CFG => {
724                let gateway = attrs.get("gateway").cloned().unwrap_or_default();
725                Ok(Message::Configuration {
726                    msgid,
727                    gateway,
728                    data: None,
729                })
730            }
731            IDENT_CTL => {
732                let srcid = attr_u64(attrs, "srcid")?;
733                let action = attrs.get("action").cloned().unwrap_or_default();
734                let target = attrs.get("target").cloned().unwrap_or_default();
735                Ok(Message::Control {
736                    msgid,
737                    srcid,
738                    action,
739                    target,
740                    data: None,
741                })
742            }
743            IDENT_ACK => {
744                let ack = attr_u64(attrs, "ack")?;
745                Ok(Message::Ack { msgid, ack })
746            }
747            IDENT_ERR => {
748                let reason = attrs.get("reason").cloned().unwrap_or_default();
749                Ok(Message::Error { msgid, reason })
750            }
751            IDENT_OUT => {
752                let srcid = attr_u64(attrs, "srcid")?;
753                let nodes = attrs.get("nodes").cloned().unwrap_or_default();
754                Ok(Message::StdOut {
755                    msgid,
756                    srcid,
757                    nodes,
758                    data: None,
759                })
760            }
761            IDENT_SER => {
762                let srcid = attr_u64(attrs, "srcid")?;
763                let nodes = attrs.get("nodes").cloned().unwrap_or_default();
764                Ok(Message::StdErr {
765                    msgid,
766                    srcid,
767                    nodes,
768                    data: None,
769                })
770            }
771            IDENT_RET => {
772                let srcid = attr_u64(attrs, "srcid")?;
773                let retcode: i32 = attrs
774                    .get("retcode")
775                    .and_then(|v| v.parse().ok())
776                    .ok_or_else(|| MessageProcessingError::MissingAttribute("retcode".into()))?;
777                let nodes = attrs.get("nodes").cloned().unwrap_or_default();
778                Ok(Message::Retcode {
779                    msgid,
780                    srcid,
781                    retcode,
782                    nodes,
783                })
784            }
785            IDENT_TIM => {
786                let srcid = attr_u64(attrs, "srcid")?;
787                let nodes = attrs.get("nodes").cloned().unwrap_or_default();
788                Ok(Message::Timeout {
789                    msgid,
790                    srcid,
791                    nodes,
792                })
793            }
794            IDENT_RTR => {
795                let srcid = attr_u64(attrs, "srcid")?;
796                let event = attrs.get("event").cloned().unwrap_or_default();
797                let gateway = attrs.get("gateway").cloned().unwrap_or_default();
798                let targets = attrs.get("targets").cloned().unwrap_or_default();
799                Ok(Message::Routing {
800                    msgid,
801                    srcid,
802                    event,
803                    gateway,
804                    targets,
805                })
806            }
807            other => Err(MessageProcessingError::UnknownType(other.into())),
808        }
809    }
810}
811
812impl std::fmt::Display for Message {
813    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
814        write!(
815            f,
816            "Message {} (type: {}, msgid: {})",
817            self.ident(),
818            self.ident(),
819            self.msgid()
820        )
821    }
822}
823
824/// Helper: parse a u64 attribute.
825fn attr_u64(attrs: &HashMap<String, String>, key: &str) -> Result<u64, MessageProcessingError> {
826    attrs
827        .get(key)
828        .and_then(|v| v.parse().ok())
829        .ok_or_else(|| MessageProcessingError::MissingAttribute(key.into()))
830}
831
832// ============================================================================
833// XmlReader — incremental SAX-style XML parser
834// ============================================================================
835
836/// Parser states for the simple XML state machine.
837#[derive(Debug, Clone, Copy, PartialEq)]
838enum ParserState {
839    /// Outside any tag, looking for '<'.
840    Text,
841    /// Inside a tag name (after '<').
842    TagName,
843    /// Inside a closing tag (after '</').
844    CloseTagName,
845    /// Reading attributes inside an opening tag.
846    Attributes,
847    /// Inside a self-closing tag (seen '/').
848    SelfClose,
849}
850
851/// Incremental XML reader that parses fragments and produces [`Message`] instances.
852///
853/// Modeled after Python's SAX `ContentHandler`. Feed XML data incrementally
854/// via [`feed()`](XmlReader::feed) and retrieve parsed messages from the queue.
855#[derive(Debug)]
856pub struct XmlReader {
857    msg_queue: VecDeque<Message>,
858    /// Protocol version from `<channel version="...">`.
859    pub version: Option<String>,
860    /// Current message being assembled.
861    draft: Option<Message>,
862    // Parser internals
863    state: ParserState,
864    tag_buf: String,
865    attr_name: String,
866    attr_val: String,
867    attrs: HashMap<String, String>,
868    text_buf: String,
869    in_attr_value: bool,
870    attr_quote: char,
871}
872
873impl XmlReader {
874    /// Create a new empty XmlReader.
875    pub fn new() -> Self {
876        Self {
877            msg_queue: VecDeque::new(),
878            version: None,
879            draft: None,
880            state: ParserState::Text,
881            tag_buf: String::new(),
882            attr_name: String::new(),
883            attr_val: String::new(),
884            attrs: HashMap::new(),
885            text_buf: String::new(),
886            in_attr_value: false,
887            attr_quote: '"',
888        }
889    }
890
891    /// Whether a complete message is available.
892    pub fn msg_available(&self) -> bool {
893        !self.msg_queue.is_empty()
894    }
895
896    /// Pop and return the oldest message from the queue.
897    pub fn pop_msg(&mut self) -> Option<Message> {
898        self.msg_queue.pop_front()
899    }
900
901    /// Feed XML data to the parser.
902    ///
903    /// This is the incremental entry point. Each call may produce zero or more
904    /// messages that can be retrieved with [`pop_msg()`](XmlReader::pop_msg).
905    pub fn feed(&mut self, data: &str) -> Result<(), MessageProcessingError> {
906        for ch in data.chars() {
907            match self.state {
908                ParserState::Text => {
909                    if ch == '<' {
910                        // Flush any accumulated text as character data
911                        if !self.text_buf.is_empty() {
912                            let text = std::mem::take(&mut self.text_buf);
913                            self.characters(&text)?;
914                        }
915                        self.tag_buf.clear();
916                        self.state = ParserState::TagName;
917                    } else {
918                        self.text_buf.push(ch);
919                    }
920                }
921                ParserState::TagName => {
922                    if ch == '<' {
923                        // Stray '<' inside a tag: expat raises
924                        // "not well-formed (invalid token)" here (GH-range
925                        // parse errors must be reported, not mis-parsed).
926                        return Err(MessageProcessingError::ParseError(
927                            "not well-formed (invalid token)".into(),
928                        ));
929                    } else if ch == '/' && self.tag_buf.is_empty() {
930                        // Closing tag: </...>
931                        self.tag_buf.clear();
932                        self.state = ParserState::CloseTagName;
933                    } else if ch == ' ' || ch == '\t' {
934                        // End of tag name, start reading attributes
935                        self.attrs.clear();
936                        self.attr_name.clear();
937                        self.state = ParserState::Attributes;
938                    } else if ch == '>' {
939                        // End of opening tag with no attributes
940                        self.attrs.clear();
941                        self.start_element(&self.tag_buf.clone(), &self.attrs.clone())?;
942                        self.state = ParserState::Text;
943                    } else if ch == '/' {
944                        self.attrs.clear();
945                        self.state = ParserState::SelfClose;
946                    } else {
947                        self.tag_buf.push(ch);
948                    }
949                }
950                ParserState::CloseTagName => {
951                    if ch == '>' {
952                        self.end_element(&self.tag_buf.clone())?;
953                        self.state = ParserState::Text;
954                    } else if ch == '<' {
955                        return Err(MessageProcessingError::ParseError(
956                            "not well-formed (invalid token)".into(),
957                        ));
958                    } else {
959                        self.tag_buf.push(ch);
960                    }
961                }
962                ParserState::Attributes => {
963                    if self.in_attr_value {
964                        if ch == self.attr_quote {
965                            // End of attribute value
966                            let name = std::mem::take(&mut self.attr_name);
967                            let val = xml_unescape(&std::mem::take(&mut self.attr_val));
968                            self.attrs.insert(name, val);
969                            self.in_attr_value = false;
970                        } else {
971                            self.attr_val.push(ch);
972                        }
973                    } else if ch == '<' {
974                        // e.g. `<message type="ABC"</message>` — expat raises
975                        // "not well-formed (invalid token)".
976                        return Err(MessageProcessingError::ParseError(
977                            "not well-formed (invalid token)".into(),
978                        ));
979                    } else if ch == '=' {
980                        // About to read attribute value
981                    } else if ch == '"' || ch == '\'' {
982                        self.attr_quote = ch;
983                        self.attr_val.clear();
984                        self.in_attr_value = true;
985                    } else if ch == '>' {
986                        // End of opening tag
987                        self.start_element(&self.tag_buf.clone(), &self.attrs.clone())?;
988                        self.state = ParserState::Text;
989                    } else if ch == '/' {
990                        self.state = ParserState::SelfClose;
991                    } else if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' {
992                        // whitespace between attributes
993                    } else {
994                        self.attr_name.push(ch);
995                    }
996                }
997                ParserState::SelfClose => {
998                    if ch == '>' {
999                        // Self-closing tag: treat as open + close
1000                        let tag = self.tag_buf.clone();
1001                        let attrs = self.attrs.clone();
1002                        self.start_element(&tag, &attrs)?;
1003                        self.end_element(&tag)?;
1004                        self.state = ParserState::Text;
1005                    } else {
1006                        // Anything but '>' after '/' is malformed XML.
1007                        return Err(MessageProcessingError::ParseError(
1008                            "not well-formed (invalid token)".into(),
1009                        ));
1010                    }
1011                }
1012            }
1013        }
1014        Ok(())
1015    }
1016
1017    fn start_element(
1018        &mut self,
1019        name: &str,
1020        attrs: &HashMap<String, String>,
1021    ) -> Result<(), MessageProcessingError> {
1022        match name {
1023            "channel" => {
1024                self.version = attrs.get("version").cloned();
1025                self.msg_queue.push_back(Message::start());
1026            }
1027            "message" => {
1028                let msg = Message::from_attrs(attrs)?;
1029                self.draft = Some(msg);
1030            }
1031            _ => {
1032                return Err(MessageProcessingError::InvalidTag(name.into()));
1033            }
1034        }
1035        Ok(())
1036    }
1037
1038    fn end_element(&mut self, name: &str) -> Result<(), MessageProcessingError> {
1039        match name {
1040            "message" => {
1041                if let Some(msg) = self.draft.take() {
1042                    self.msg_queue.push_back(msg);
1043                }
1044            }
1045            "channel" => {
1046                self.msg_queue.push_back(Message::end());
1047            }
1048            _ => {}
1049        }
1050        Ok(())
1051    }
1052
1053    fn characters(&mut self, content: &str) -> Result<(), MessageProcessingError> {
1054        if let Some(ref mut draft) = self.draft {
1055            let trimmed = content.trim();
1056            if !trimmed.is_empty() {
1057                draft.data_update(trimmed.as_bytes())?;
1058            }
1059        }
1060        Ok(())
1061    }
1062}
1063
1064impl Default for XmlReader {
1065    fn default() -> Self {
1066        Self::new()
1067    }
1068}
1069
1070// ============================================================================
1071// Channel
1072// ============================================================================
1073
1074/// Trait for channel event handling (start/recv).
1075///
1076/// Implementors define the logic for initializing a channel and processing
1077/// incoming messages.
1078pub trait ChannelHandler: Send {
1079    /// Called when the channel is connected and ready.
1080    fn start(&mut self, channel: &mut Channel);
1081
1082    /// Called when a complete message has been received.
1083    fn recv(&mut self, channel: &mut Channel, msg: Message);
1084}
1085
1086/// Communication channel between nodes in the propagation tree.
1087///
1088/// A Channel manages XML-based message exchange over a transport (typically
1089/// an SSH pipe). It uses an [`XmlReader`] for incoming messages and XML
1090/// generation for outgoing messages.
1091#[derive(Debug)]
1092pub struct Channel {
1093    /// Whether the channel has been opened (received StartMessage).
1094    pub opened: bool,
1095    /// Whether the channel setup is complete (received ACK to config).
1096    pub setup: bool,
1097    /// Whether this end initiated the channel.
1098    pub initiator: bool,
1099    /// Whether the channel has been closed (fatal error or EndMessage).
1100    pub closed: bool,
1101    /// The XML reader for parsing incoming data.
1102    pub xml_reader: XmlReader,
1103    /// Outgoing message buffer.
1104    pub outbox: VecDeque<Vec<u8>>,
1105}
1106
1107impl Channel {
1108    /// Create a new Channel.
1109    ///
1110    /// # Arguments
1111    /// * `initiator` - true if this end is initiating the channel
1112    pub fn new(initiator: bool) -> Self {
1113        Self {
1114            opened: false,
1115            setup: false,
1116            initiator,
1117            closed: false,
1118            xml_reader: XmlReader::new(),
1119            outbox: VecDeque::new(),
1120        }
1121    }
1122
1123    /// Generate the XML document header.
1124    pub fn init_xml(&self) -> Vec<u8> {
1125        b"<?xml version='1.0' encoding='UTF-8'?>".to_vec()
1126    }
1127
1128    /// Generate the opening `<channel>` tag.
1129    pub fn open_xml(&self, version: &str) -> Vec<u8> {
1130        format!("<channel version=\"{}\">", xml_escape(version)).into_bytes()
1131    }
1132
1133    /// Generate the closing `</channel>` tag.
1134    pub fn close_xml(&self) -> Vec<u8> {
1135        b"</channel>".to_vec()
1136    }
1137
1138    /// Serialize a message and enqueue it for sending.
1139    pub fn send(&mut self, msg: &Message) {
1140        let mut xml = msg.xml();
1141        xml.push(b'\n');
1142        self.outbox.push_back(xml);
1143    }
1144
1145    /// Feed incoming data to the XML reader and return any parsed messages.
1146    pub fn feed(&mut self, data: &str) -> Result<Vec<Message>, MessageProcessingError> {
1147        self.xml_reader.feed(data)?;
1148        let mut messages = Vec::new();
1149        while let Some(msg) = self.xml_reader.pop_msg() {
1150            messages.push(msg);
1151        }
1152        Ok(messages)
1153    }
1154
1155    /// Take the next outgoing message from the outbox.
1156    pub fn take_outgoing(&mut self) -> Option<Vec<u8>> {
1157        self.outbox.pop_front()
1158    }
1159
1160    /// Mark the channel as closed (mirrors upstream `Channel._close`).
1161    pub fn close(&mut self) {
1162        self.closed = true;
1163        self.opened = false;
1164        self.setup = false;
1165    }
1166
1167    /// Report a channel-level error, mirroring upstream `Channel.ev_read`'s
1168    /// SAXParseException / MessageProcessingError handlers (commit 6f45bf5).
1169    ///
1170    /// - **initiator side**: returns a synthetic stderr [`Message`] (ident
1171    ///   `SER`, srcid 0, nodes = `node`) whose payload is the error text
1172    ///   encoded with [`ENCODING`]. Upstream passes *bytes* here; passing the
1173    ///   error text through unchanged (instead of a str/bytes mix) is what
1174    ///   fixed the TypeError that hid the original error. The caller delivers
1175    ///   the returned message to its own `recv()` path, exactly like
1176    ///   upstream's `self.recv(StdErrMessage(node, text.encode(ENCODING)))`.
1177    ///   For a parse error the bare parser message is used (upstream sends
1178    ///   `ex.getMessage()`, e.g. "not well-formed (invalid token)").
1179    /// - **target side**: enqueues an Error message back to the peer
1180    ///   ("Parse error: %s" for parse errors, the plain error text otherwise)
1181    ///   and returns `None`.
1182    ///
1183    /// In both cases the channel is closed: upstream treats these as fatal
1184    /// channel errors. This function is infallible — the error-reporting
1185    /// path must never itself fail (6f45bf5).
1186    pub fn handle_channel_error(
1187        &mut self,
1188        node: &str,
1189        err: &MessageProcessingError,
1190    ) -> Option<Message> {
1191        let report = if self.initiator {
1192            // Bare error text for the local stderr report.
1193            let text = match err {
1194                MessageProcessingError::ParseError(inner) => inner.clone(),
1195                other => other.to_string(),
1196            };
1197            let mut msg = Message::stderr(node, 0);
1198            msg.data_encode(text.as_bytes());
1199            Some(msg)
1200        } else {
1201            // Send an error message back to the initiator.
1202            let reason = match err {
1203                MessageProcessingError::ParseError(inner) => format!("Parse error: {}", inner),
1204                other => other.to_string(),
1205            };
1206            self.send(&Message::error(&reason));
1207            None
1208        };
1209        // Fatal channel error: close the channel now.
1210        self.close();
1211        report
1212    }
1213}
1214
1215// ============================================================================
1216// Tests
1217// ============================================================================
1218
1219#[cfg(test)]
1220mod tests {
1221    use super::*;
1222
1223    // -- Base64 tests --------------------------------------------------------
1224
1225    #[test]
1226    fn test_base64_roundtrip() {
1227        let data = b"Hello, ClusterShell!";
1228        let encoded = base64_encode(data);
1229        let decoded = base64_decode(&encoded).unwrap();
1230        assert_eq!(decoded, data);
1231    }
1232
1233    #[test]
1234    fn test_base64_empty() {
1235        assert_eq!(base64_encode(b""), "");
1236        assert_eq!(base64_decode("").unwrap(), Vec::<u8>::new());
1237    }
1238
1239    #[test]
1240    fn test_base64_padding() {
1241        // 1 byte -> 4 chars with ==
1242        let e1 = base64_encode(b"A");
1243        assert!(e1.ends_with("=="));
1244        assert_eq!(base64_decode(&e1).unwrap(), b"A");
1245
1246        // 2 bytes -> 4 chars with =
1247        let e2 = base64_encode(b"AB");
1248        assert!(e2.ends_with('=') && !e2.ends_with("=="));
1249        assert_eq!(base64_decode(&e2).unwrap(), b"AB");
1250
1251        // 3 bytes -> 4 chars, no padding
1252        let e3 = base64_encode(b"ABC");
1253        assert!(!e3.ends_with('='));
1254        assert_eq!(base64_decode(&e3).unwrap(), b"ABC");
1255    }
1256
1257    #[test]
1258    fn test_base64_with_newlines() {
1259        // base64 decoder should ignore whitespace
1260        let encoded = "SGVs\nbG8=";
1261        let decoded = base64_decode(encoded).unwrap();
1262        assert_eq!(decoded, b"Hello");
1263    }
1264
1265    // -- XML escape tests ----------------------------------------------------
1266
1267    #[test]
1268    fn test_xml_escape() {
1269        assert_eq!(xml_escape("hello"), "hello");
1270        assert_eq!(xml_escape("a&b"), "a&amp;b");
1271        assert_eq!(xml_escape("<tag>"), "&lt;tag&gt;");
1272        assert_eq!(xml_escape("x=\"y\""), "x=&quot;y&quot;");
1273    }
1274
1275    #[test]
1276    fn test_xml_unescape() {
1277        assert_eq!(xml_unescape("a&amp;b"), "a&b");
1278        assert_eq!(xml_unescape("&lt;tag&gt;"), "<tag>");
1279    }
1280
1281    // -- Message construction tests ------------------------------------------
1282
1283    #[test]
1284    fn test_message_idents() {
1285        assert_eq!(Message::general().ident(), "GEN");
1286        assert_eq!(Message::configuration("gw1").ident(), "CFG");
1287        assert_eq!(Message::control(0).ident(), "CTL");
1288        assert_eq!(Message::ack(0).ident(), "ACK");
1289        assert_eq!(Message::error("oops").ident(), "ERR");
1290        assert_eq!(Message::stdout("node1", 0).ident(), "OUT");
1291        assert_eq!(Message::stderr("node1", 0).ident(), "SER");
1292        assert_eq!(Message::retcode("node1", 0, 0).ident(), "RET");
1293        assert_eq!(Message::timeout("node1", 0).ident(), "TIM");
1294        assert_eq!(Message::routing("ev", "gw", "tgt", 0).ident(), "RTR");
1295        assert_eq!(Message::start().ident(), "CHA");
1296        assert_eq!(Message::end().ident(), "END");
1297    }
1298
1299    #[test]
1300    fn test_message_has_payload() {
1301        assert!(Message::general().has_payload());
1302        assert!(Message::configuration("gw1").has_payload());
1303        assert!(Message::control(0).has_payload());
1304        assert!(Message::stdout("n1", 0).has_payload());
1305        assert!(Message::stderr("n1", 0).has_payload());
1306        assert!(!Message::ack(0).has_payload());
1307        assert!(!Message::error("x").has_payload());
1308        assert!(!Message::retcode("n1", 0, 0).has_payload());
1309        assert!(!Message::timeout("n1", 0).has_payload());
1310        assert!(!Message::start().has_payload());
1311        assert!(!Message::end().has_payload());
1312    }
1313
1314    #[test]
1315    fn test_message_srcid() {
1316        assert_eq!(Message::control(42).srcid(), Some(42));
1317        assert_eq!(Message::stdout("n", 7).srcid(), Some(7));
1318        assert_eq!(Message::stderr("n", 8).srcid(), Some(8));
1319        assert_eq!(Message::retcode("n", 0, 9).srcid(), Some(9));
1320        assert_eq!(Message::timeout("n", 10).srcid(), Some(10));
1321        assert_eq!(Message::routing("e", "g", "t", 11).srcid(), Some(11));
1322        assert_eq!(Message::general().srcid(), None);
1323        assert_eq!(Message::ack(0).srcid(), None);
1324        assert_eq!(Message::configuration("gw").srcid(), None);
1325    }
1326
1327    #[test]
1328    fn test_message_nodes() {
1329        assert_eq!(Message::stdout("node[1-3]", 0).nodes(), Some("node[1-3]"));
1330        assert_eq!(Message::stderr("node5", 0).nodes(), Some("node5"));
1331        assert_eq!(Message::retcode("node1", 0, 0).nodes(), Some("node1"));
1332        assert_eq!(Message::timeout("node2", 0).nodes(), Some("node2"));
1333        assert_eq!(Message::general().nodes(), None);
1334        assert_eq!(Message::ack(0).nodes(), None);
1335    }
1336
1337    // -- Data encode/decode tests -------------------------------------------
1338
1339    #[test]
1340    fn test_data_encode_decode() {
1341        let mut msg = Message::configuration("gw1");
1342        let payload = b"some topology data here";
1343        msg.data_encode(payload);
1344        assert!(msg.data().is_some());
1345        let decoded = msg.data_decode().unwrap();
1346        assert_eq!(decoded, payload);
1347    }
1348
1349    #[test]
1350    fn test_data_update() {
1351        let mut msg = Message::configuration("gw");
1352        msg.data_update(b"SGVs").unwrap();
1353        msg.data_update(b"bG8=").unwrap();
1354        assert_eq!(msg.data().unwrap(), b"SGVsbG8=");
1355        let decoded = msg.data_decode().unwrap();
1356        assert_eq!(decoded, b"Hello");
1357    }
1358
1359    #[test]
1360    fn test_data_update_unexpected() {
1361        let mut msg = Message::ack(0);
1362        let result = msg.data_update(b"data");
1363        assert!(result.is_err());
1364        assert!(matches!(
1365            result.unwrap_err(),
1366            MessageProcessingError::UnexpectedPayload(_)
1367        ));
1368    }
1369
1370    // -- XML serialization tests --------------------------------------------
1371
1372    #[test]
1373    fn test_xml_ack() {
1374        let msg = Message::Ack { msgid: 5, ack: 3 };
1375        let xml = String::from_utf8(msg.xml()).unwrap();
1376        assert!(xml.contains("type=\"ACK\""));
1377        assert!(xml.contains("msgid=\"5\""));
1378        assert!(xml.contains("ack=\"3\""));
1379        assert!(xml.contains("/>") || xml.contains("</message>"));
1380    }
1381
1382    #[test]
1383    fn test_xml_configuration_with_payload() {
1384        let mut msg = Message::Configuration {
1385            msgid: 0,
1386            gateway: "gateway1".into(),
1387            data: None,
1388        };
1389        msg.data_encode(b"topology");
1390        let xml = String::from_utf8(msg.xml()).unwrap();
1391        assert!(xml.contains("type=\"CFG\""));
1392        assert!(xml.contains("gateway=\"gateway1\""));
1393        assert!(xml.contains("</message>"));
1394        // Should have base64 content between tags
1395        assert!(xml.contains(&base64_encode(b"topology")));
1396    }
1397
1398    #[test]
1399    fn test_xml_error() {
1400        let msg = Message::Error {
1401            msgid: 1,
1402            reason: "something broke".into(),
1403        };
1404        let xml = String::from_utf8(msg.xml()).unwrap();
1405        assert!(xml.contains("type=\"ERR\""));
1406        assert!(xml.contains("reason=\"something broke\""));
1407    }
1408
1409    #[test]
1410    fn test_xml_retcode() {
1411        let msg = Message::Retcode {
1412            msgid: 2,
1413            srcid: 10,
1414            retcode: -1,
1415            nodes: "node[1-5]".into(),
1416        };
1417        let xml = String::from_utf8(msg.xml()).unwrap();
1418        assert!(xml.contains("type=\"RET\""));
1419        assert!(xml.contains("retcode=\"-1\""));
1420        assert!(xml.contains("nodes=\"node[1-5]\""));
1421    }
1422
1423    #[test]
1424    fn test_xml_control() {
1425        let msg = Message::Control {
1426            msgid: 3,
1427            srcid: 1,
1428            action: "shell".into(),
1429            target: "node[1-10]".into(),
1430            data: None,
1431        };
1432        let xml = String::from_utf8(msg.xml()).unwrap();
1433        assert!(xml.contains("type=\"CTL\""));
1434        assert!(xml.contains("action=\"shell\""));
1435        assert!(xml.contains("target=\"node[1-10]\""));
1436    }
1437
1438    #[test]
1439    fn test_xml_routing() {
1440        let msg = Message::Routing {
1441            msgid: 4,
1442            srcid: 2,
1443            event: "connect".into(),
1444            gateway: "gw2".into(),
1445            targets: "node[20-30]".into(),
1446        };
1447        let xml = String::from_utf8(msg.xml()).unwrap();
1448        assert!(xml.contains("type=\"RTR\""));
1449        assert!(xml.contains("event=\"connect\""));
1450        assert!(xml.contains("gateway=\"gw2\""));
1451        assert!(xml.contains("targets=\"node[20-30]\""));
1452    }
1453
1454    // -- XmlReader tests ----------------------------------------------------
1455
1456    #[test]
1457    fn test_xmlreader_channel_start_end() {
1458        let mut reader = XmlReader::new();
1459        reader.feed("<channel version=\"1.0\"></channel>").unwrap();
1460        assert!(reader.msg_available());
1461        let m1 = reader.pop_msg().unwrap();
1462        assert_eq!(m1.ident(), "CHA");
1463        assert_eq!(reader.version.as_deref(), Some("1.0"));
1464
1465        let m2 = reader.pop_msg().unwrap();
1466        assert_eq!(m2.ident(), "END");
1467        assert!(!reader.msg_available());
1468    }
1469
1470    #[test]
1471    fn test_xmlreader_ack_message() {
1472        let mut reader = XmlReader::new();
1473        reader
1474            .feed(
1475                "<channel version=\"1.0\"><message type=\"ACK\" msgid=\"0\" ack=\"5\"/></channel>",
1476            )
1477            .unwrap();
1478
1479        let start = reader.pop_msg().unwrap();
1480        assert_eq!(start.ident(), "CHA");
1481
1482        let ack = reader.pop_msg().unwrap();
1483        assert_eq!(ack.ident(), "ACK");
1484        if let Message::Ack { ack, msgid, .. } = ack {
1485            assert_eq!(ack, 5);
1486            assert_eq!(msgid, 0);
1487        } else {
1488            panic!("expected Ack variant");
1489        }
1490
1491        let end = reader.pop_msg().unwrap();
1492        assert_eq!(end.ident(), "END");
1493    }
1494
1495    #[test]
1496    fn test_xmlreader_message_with_payload() {
1497        let payload_b64 = base64_encode(b"hello world");
1498        let xml = format!(
1499            "<channel version=\"2.0\"><message type=\"CFG\" msgid=\"1\" gateway=\"gw1\">{}</message></channel>",
1500            payload_b64
1501        );
1502
1503        let mut reader = XmlReader::new();
1504        reader.feed(&xml).unwrap();
1505
1506        let _start = reader.pop_msg().unwrap();
1507        let cfg = reader.pop_msg().unwrap();
1508        assert_eq!(cfg.ident(), "CFG");
1509        let decoded = cfg.data_decode().unwrap();
1510        assert_eq!(decoded, b"hello world");
1511
1512        if let Message::Configuration { gateway, .. } = &cfg {
1513            assert_eq!(gateway, "gw1");
1514        } else {
1515            panic!("expected Configuration variant");
1516        }
1517    }
1518
1519    #[test]
1520    fn test_xmlreader_incremental_feed() {
1521        let mut reader = XmlReader::new();
1522
1523        // Feed in chunks
1524        reader.feed("<chan").unwrap();
1525        assert!(!reader.msg_available());
1526        reader.feed("nel version=\"1.0\">").unwrap();
1527        assert!(reader.msg_available());
1528        let start = reader.pop_msg().unwrap();
1529        assert_eq!(start.ident(), "CHA");
1530
1531        reader
1532            .feed("<message type=\"ACK\" msgid=\"0\" ack=\"1\"/>")
1533            .unwrap();
1534        let ack = reader.pop_msg().unwrap();
1535        assert_eq!(ack.ident(), "ACK");
1536
1537        reader.feed("</channel>").unwrap();
1538        let end = reader.pop_msg().unwrap();
1539        assert_eq!(end.ident(), "END");
1540    }
1541
1542    #[test]
1543    fn test_xmlreader_unknown_type() {
1544        let mut reader = XmlReader::new();
1545        let result =
1546            reader.feed("<channel version=\"1\"><message type=\"BOGUS\" msgid=\"0\"/></channel>");
1547        assert!(result.is_err());
1548    }
1549
1550    #[test]
1551    fn test_xmlreader_error_message() {
1552        let mut reader = XmlReader::new();
1553        reader
1554            .feed("<channel version=\"1\"><message type=\"ERR\" msgid=\"0\" reason=\"parse failed\"/></channel>")
1555            .unwrap();
1556
1557        let _start = reader.pop_msg().unwrap();
1558        let err = reader.pop_msg().unwrap();
1559        assert_eq!(err.ident(), "ERR");
1560        if let Message::Error { reason, .. } = &err {
1561            assert_eq!(reason, "parse failed");
1562        } else {
1563            panic!("expected Error variant");
1564        }
1565    }
1566
1567    #[test]
1568    fn test_xmlreader_retcode() {
1569        let mut reader = XmlReader::new();
1570        reader
1571            .feed(
1572                "<message type=\"RET\" msgid=\"7\" srcid=\"3\" retcode=\"0\" nodes=\"node[1-5]\"/>",
1573            )
1574            .unwrap();
1575
1576        let msg = reader.pop_msg().unwrap();
1577        if let Message::Retcode {
1578            srcid,
1579            retcode,
1580            nodes,
1581            ..
1582        } = &msg
1583        {
1584            assert_eq!(*srcid, 3);
1585            assert_eq!(*retcode, 0);
1586            assert_eq!(nodes, "node[1-5]");
1587        } else {
1588            panic!("expected Retcode variant");
1589        }
1590    }
1591
1592    #[test]
1593    fn test_xmlreader_routing() {
1594        let mut reader = XmlReader::new();
1595        reader
1596            .feed("<message type=\"RTR\" msgid=\"0\" srcid=\"1\" event=\"connect\" gateway=\"gw1\" targets=\"node[1-10]\"/>")
1597            .unwrap();
1598
1599        let msg = reader.pop_msg().unwrap();
1600        if let Message::Routing {
1601            event,
1602            gateway,
1603            targets,
1604            ..
1605        } = &msg
1606        {
1607            assert_eq!(event, "connect");
1608            assert_eq!(gateway, "gw1");
1609            assert_eq!(targets, "node[1-10]");
1610        } else {
1611            panic!("expected Routing variant");
1612        }
1613    }
1614
1615    #[test]
1616    fn test_xmlreader_stdout_stderr() {
1617        let payload = base64_encode(b"output line");
1618        let xml = format!(
1619            "<message type=\"OUT\" msgid=\"0\" srcid=\"5\" nodes=\"node1\">{}</message>",
1620            payload
1621        );
1622        let mut reader = XmlReader::new();
1623        reader.feed(&xml).unwrap();
1624
1625        let msg = reader.pop_msg().unwrap();
1626        assert_eq!(msg.ident(), "OUT");
1627        assert_eq!(msg.srcid(), Some(5));
1628        assert_eq!(msg.nodes(), Some("node1"));
1629        assert_eq!(msg.data_decode().unwrap(), b"output line");
1630
1631        // StdErr
1632        let xml2 = format!(
1633            "<message type=\"SER\" msgid=\"1\" srcid=\"6\" nodes=\"node2\">{}</message>",
1634            base64_encode(b"error line")
1635        );
1636        reader.feed(&xml2).unwrap();
1637        let msg2 = reader.pop_msg().unwrap();
1638        assert_eq!(msg2.ident(), "SER");
1639        assert_eq!(msg2.data_decode().unwrap(), b"error line");
1640    }
1641
1642    #[test]
1643    fn test_xmlreader_timeout() {
1644        let mut reader = XmlReader::new();
1645        reader
1646            .feed("<message type=\"TIM\" msgid=\"0\" srcid=\"1\" nodes=\"node[5-8]\"/>")
1647            .unwrap();
1648        let msg = reader.pop_msg().unwrap();
1649        assert_eq!(msg.ident(), "TIM");
1650        assert_eq!(msg.nodes(), Some("node[5-8]"));
1651    }
1652
1653    #[test]
1654    fn test_xmlreader_control() {
1655        let payload = base64_encode(b"command data");
1656        let xml = format!(
1657            "<message type=\"CTL\" msgid=\"0\" srcid=\"1\" action=\"shell\" target=\"node[1-3]\">{}</message>",
1658            payload
1659        );
1660        let mut reader = XmlReader::new();
1661        reader.feed(&xml).unwrap();
1662
1663        let msg = reader.pop_msg().unwrap();
1664        if let Message::Control { action, target, .. } = &msg {
1665            assert_eq!(action, "shell");
1666            assert_eq!(target, "node[1-3]");
1667        } else {
1668            panic!("expected Control variant");
1669        }
1670        assert_eq!(msg.data_decode().unwrap(), b"command data");
1671    }
1672
1673    // -- XML roundtrip tests ------------------------------------------------
1674
1675    #[test]
1676    fn test_xml_roundtrip_ack() {
1677        let original = Message::Ack { msgid: 10, ack: 7 };
1678        let xml = String::from_utf8(original.xml()).unwrap();
1679        let mut reader = XmlReader::new();
1680        reader.feed(&xml).unwrap();
1681        let parsed = reader.pop_msg().unwrap();
1682        assert_eq!(parsed.ident(), "ACK");
1683        if let Message::Ack { msgid, ack, .. } = parsed {
1684            assert_eq!(msgid, 10);
1685            assert_eq!(ack, 7);
1686        }
1687    }
1688
1689    #[test]
1690    fn test_xml_roundtrip_cfg_with_data() {
1691        let mut original = Message::Configuration {
1692            msgid: 0,
1693            gateway: "my-gateway".into(),
1694            data: None,
1695        };
1696        original.data_encode(b"topology bytes here");
1697        let xml = String::from_utf8(original.xml()).unwrap();
1698
1699        let mut reader = XmlReader::new();
1700        reader.feed(&xml).unwrap();
1701        let parsed = reader.pop_msg().unwrap();
1702        assert_eq!(parsed.ident(), "CFG");
1703        if let Message::Configuration { gateway, .. } = &parsed {
1704            assert_eq!(gateway, "my-gateway");
1705        }
1706        assert_eq!(parsed.data_decode().unwrap(), b"topology bytes here");
1707    }
1708
1709    #[test]
1710    fn test_xml_roundtrip_error() {
1711        let original = Message::Error {
1712            msgid: 3,
1713            reason: "test <error> & reason".into(),
1714        };
1715        let xml = String::from_utf8(original.xml()).unwrap();
1716        let mut reader = XmlReader::new();
1717        reader.feed(&xml).unwrap();
1718        let parsed = reader.pop_msg().unwrap();
1719        if let Message::Error { reason, .. } = &parsed {
1720            assert_eq!(reason, "test <error> & reason");
1721        } else {
1722            panic!("expected Error variant");
1723        }
1724    }
1725
1726    #[test]
1727    fn test_xml_roundtrip_retcode() {
1728        let original = Message::Retcode {
1729            msgid: 5,
1730            srcid: 2,
1731            retcode: 127,
1732            nodes: "node[1-100]".into(),
1733        };
1734        let xml = String::from_utf8(original.xml()).unwrap();
1735        let mut reader = XmlReader::new();
1736        reader.feed(&xml).unwrap();
1737        let parsed = reader.pop_msg().unwrap();
1738        if let Message::Retcode { retcode, nodes, .. } = &parsed {
1739            assert_eq!(*retcode, 127);
1740            assert_eq!(nodes, "node[1-100]");
1741        } else {
1742            panic!("expected Retcode variant");
1743        }
1744    }
1745
1746    // -- Channel tests ------------------------------------------------------
1747
1748    #[test]
1749    fn test_channel_creation() {
1750        let ch = Channel::new(true);
1751        assert!(!ch.opened);
1752        assert!(!ch.setup);
1753        assert!(ch.initiator);
1754    }
1755
1756    #[test]
1757    fn test_channel_init_xml() {
1758        let ch = Channel::new(false);
1759        let xml = ch.init_xml();
1760        assert_eq!(
1761            String::from_utf8(xml).unwrap(),
1762            "<?xml version='1.0' encoding='UTF-8'?>"
1763        );
1764    }
1765
1766    #[test]
1767    fn test_channel_open_close_xml() {
1768        let ch = Channel::new(true);
1769        let open = String::from_utf8(ch.open_xml("3.0")).unwrap();
1770        assert_eq!(open, "<channel version=\"3.0\">");
1771        let close = String::from_utf8(ch.close_xml()).unwrap();
1772        assert_eq!(close, "</channel>");
1773    }
1774
1775    #[test]
1776    fn test_channel_send() {
1777        let mut ch = Channel::new(true);
1778        let msg = Message::ack(42);
1779        ch.send(&msg);
1780        let outgoing = ch.take_outgoing().unwrap();
1781        let xml = String::from_utf8(outgoing).unwrap();
1782        assert!(xml.contains("type=\"ACK\""));
1783        assert!(xml.ends_with('\n'));
1784    }
1785
1786    #[test]
1787    fn test_channel_feed_and_receive() {
1788        let mut ch = Channel::new(false);
1789        let messages = ch
1790            .feed(
1791                "<channel version=\"1.0\"><message type=\"ACK\" msgid=\"0\" ack=\"1\"/></channel>",
1792            )
1793            .unwrap();
1794        assert_eq!(messages.len(), 3); // Start, ACK, End
1795        assert_eq!(messages[0].ident(), "CHA");
1796        assert_eq!(messages[1].ident(), "ACK");
1797        assert_eq!(messages[2].ident(), "END");
1798    }
1799
1800    #[test]
1801    fn test_channel_outbox_ordering() {
1802        let mut ch = Channel::new(true);
1803        ch.send(&Message::ack(1));
1804        ch.send(&Message::ack(2));
1805        ch.send(&Message::ack(3));
1806
1807        let m1 = ch.take_outgoing().unwrap();
1808        let m2 = ch.take_outgoing().unwrap();
1809        let m3 = ch.take_outgoing().unwrap();
1810        assert!(ch.take_outgoing().is_none());
1811
1812        // They should come out in order
1813        assert!(String::from_utf8(m1).unwrap().contains("ACK"));
1814        assert!(String::from_utf8(m2).unwrap().contains("ACK"));
1815        assert!(String::from_utf8(m3).unwrap().contains("ACK"));
1816    }
1817
1818    // -- Message Display test -----------------------------------------------
1819
1820    #[test]
1821    fn test_message_display() {
1822        let msg = Message::ack(0);
1823        let display = format!("{}", msg);
1824        assert!(display.contains("ACK"));
1825    }
1826
1827    // -- Edge case tests ----------------------------------------------------
1828
1829    #[test]
1830    fn test_from_attrs_missing_type() {
1831        let attrs = HashMap::new();
1832        assert!(Message::from_attrs(&attrs).is_err());
1833    }
1834
1835    #[test]
1836    fn test_from_attrs_missing_srcid() {
1837        let mut attrs = HashMap::new();
1838        attrs.insert("type".into(), "CTL".into());
1839        attrs.insert("msgid".into(), "0".into());
1840        // missing srcid
1841        assert!(Message::from_attrs(&attrs).is_err());
1842    }
1843
1844    #[test]
1845    fn test_data_decode_no_data() {
1846        let msg = Message::configuration("gw");
1847        let result = msg.data_decode();
1848        assert!(result.is_err());
1849    }
1850
1851    #[test]
1852    fn test_xml_special_characters_roundtrip() {
1853        let msg = Message::Error {
1854            msgid: 0,
1855            reason: "node<1>&\"2\"".into(),
1856        };
1857        let xml = String::from_utf8(msg.xml()).unwrap();
1858        // Should be escaped in XML
1859        assert!(xml.contains("&lt;"));
1860        assert!(xml.contains("&amp;"));
1861
1862        // Parse it back
1863        let mut reader = XmlReader::new();
1864        reader.feed(&xml).unwrap();
1865        let parsed = reader.pop_msg().unwrap();
1866        if let Message::Error { reason, .. } = &parsed {
1867            assert_eq!(reason, "node<1>&\"2\"");
1868        }
1869    }
1870
1871    // -- Upstream 1.10.1 ports (fee03a3, 6f45bf5, TreeGatewayTest mirrors) --
1872
1873    /// Mirror of TreeMessageTest::test_msg_pickle_protocol.
1874    ///
1875    /// The pinned gateway pickle protocol is exposed as a constant and
1876    /// enforced on decode: payloads stamped with a protocol <= 4 roundtrip,
1877    /// anything newer is rejected with upstream's cause text.
1878    #[test]
1879    fn test_msg_pickle_protocol_pin() {
1880        // pinned at 4, readable since Python 3.4 (upstream: min(HIGHEST_PROTOCOL, 4))
1881        assert_eq!(GW_PICKLE_PROTOCOL, 4);
1882
1883        // A protocol-4-stamped payload (pickle PROTO opcode + version 4) is accepted
1884        let mut msg = Message::configuration("n1");
1885        let raw = b"\x80\x04some-payload-bytes";
1886        msg.data_encode(raw);
1887        // protocol 2+ streams start with the PROTO opcode then the version
1888        let encoded = msg.data().unwrap();
1889        let decoded_b64 = base64_decode(std::str::from_utf8(encoded).unwrap()).unwrap();
1890        assert_eq!(decoded_b64[0], 0x80);
1891        assert!(decoded_b64[1] <= GW_PICKLE_PROTOCOL);
1892        assert_eq!(msg.data_decode().unwrap(), raw);
1893    }
1894
1895    /// Mirror of TreeGatewayTest::test_channel_err_pickle_proto_pl
1896    /// (logic-level substitution: message decode instead of live gateway).
1897    ///
1898    /// A CFG payload with an unknown pickle protocol (7) is rejected with
1899    /// "Message CFG has an invalid payload (unsupported pickle protocol: 7)".
1900    #[test]
1901    fn test_channel_err_pickle_proto_pl() {
1902        // Same wire vector as upstream: base64(b'\x80\x07spam')
1903        let payload = base64_encode(b"\x80\x07spam");
1904        let xml = format!(
1905            "<channel version=\"1.10.1\"><message msgid=\"14\" type=\"CFG\" gateway=\"n1\">{}</message></channel>",
1906            payload
1907        );
1908        let mut reader = XmlReader::new();
1909        reader.feed(&xml).unwrap();
1910        let _start = reader.pop_msg().unwrap();
1911        let cfg = reader.pop_msg().unwrap();
1912        assert_eq!(cfg.ident(), "CFG");
1913        let err = cfg.data_decode().unwrap_err();
1914        assert_eq!(
1915            err.to_string(),
1916            "Message CFG has an invalid payload (unsupported pickle protocol: 7)"
1917        );
1918    }
1919
1920    /// Mirror of TreeGatewayTest::test_channel_err_no_type_msg.
1921    #[test]
1922    fn test_channel_err_no_type_msg() {
1923        let mut reader = XmlReader::new();
1924        let err = reader
1925            .feed("<channel version=\"1\"><message msgid=\"24\"></message></channel>")
1926            .unwrap_err();
1927        assert_eq!(err.to_string(), "Unknown message with no type");
1928    }
1929
1930    /// Mirror of TreeGatewayTest::test_channel_err_empty_type_msg.
1931    #[test]
1932    fn test_channel_err_empty_type_msg() {
1933        let mut reader = XmlReader::new();
1934        let err = reader
1935            .feed("<channel version=\"1\"><message msgid=\"24\" type=\"\"></message></channel>")
1936            .unwrap_err();
1937        assert_eq!(err.to_string(), "Unknown message with no type");
1938    }
1939
1940    /// Mirror of updated TreeGatewayTest::test_err_unknown_msg /
1941    /// test_channel_err_unknown_msg: the offending type is named.
1942    #[test]
1943    fn test_channel_err_unknown_msg_names_type() {
1944        let mut reader = XmlReader::new();
1945        let err = reader
1946            .feed("<channel version=\"1\"><message msgid=\"24\" type=\"ABC\"></message></channel>")
1947            .unwrap_err();
1948        assert_eq!(err.to_string(), "Unknown message type ABC");
1949    }
1950
1951    /// Badly encoded payloads report the message ident AND the cause
1952    /// (upstream fee03a3 wording; mirrors test_channel_err_badenc_pickle_pl
1953    /// and test_channel_err_missing_pl at logic level).
1954    #[test]
1955    fn test_channel_err_invalid_payload_includes_cause() {
1956        // invalid base64 content (same vector as upstream's "bar" payload:
1957        // binascii.Error / Incorrect padding)
1958        let mut msg = Message::configuration("n1");
1959        msg.set_data(b"bar".to_vec());
1960        let err = msg.data_decode().unwrap_err();
1961        assert_eq!(
1962            err.to_string(),
1963            "Message CFG has an invalid payload (Incorrect padding)"
1964        );
1965
1966        // valid base64 but not a valid payload stream: "barm" decodes to
1967        // bytes whose first byte is not a pickle PROTO opcode, so it passes
1968        // the protocol check and is returned raw (Rust payloads are opaque;
1969        // upstream rejects it at unpickle time with the same prefix).
1970        let mut msg2 = Message::configuration("n1");
1971        msg2.set_data(b"barm".to_vec());
1972        assert!(msg2.data_decode().is_ok());
1973
1974        // missing payload keeps the ident-only form
1975        let msg3 = Message::configuration("n1");
1976        let err3 = msg3.data_decode().unwrap_err();
1977        assert_eq!(err3.to_string(), "Message CFG has an invalid payload");
1978    }
1979
1980    /// Malformed XML inside a tag raises an expat-style parse error
1981    /// (mirrors the input of TreeHeadChannelErrorTest::
1982    /// test_head_channel_err_parse_error).
1983    #[test]
1984    fn test_xmlreader_parse_error_not_well_formed() {
1985        let mut reader = XmlReader::new();
1986        let err = reader.feed("<message type=\"ABC\"</message>").unwrap_err();
1987        assert!(matches!(err, MessageProcessingError::ParseError(_)));
1988        assert!(err.to_string().contains("not well-formed"));
1989    }
1990
1991    /// Mirror of TreeHeadChannelErrorTest::test_head_channel_err_invalid_tag
1992    /// (initiator side): the error is reported as a synthetic SER message
1993    /// with srcid 0 and the bare error text as bytes payload; the channel is
1994    /// closed (fatal). Reporting must not itself fail (6f45bf5).
1995    #[test]
1996    fn test_handle_channel_error_initiator_reports_stderr() {
1997        let mut chan = Channel::new(true); // initiator (head side)
1998        chan.opened = true;
1999
2000        let err = MessageProcessingError::InvalidTag("foo".into());
2001        let msg = chan.handle_channel_error("gw1", &err).unwrap();
2002
2003        // synthetic stderr message: srcid 0, nodes = gateway node
2004        assert_eq!(msg.ident(), "SER");
2005        assert_eq!(msg.srcid(), Some(0));
2006        assert_eq!(msg.nodes(), Some("gw1"));
2007        // payload is the bare error text encoded as bytes (ENCODING)
2008        assert_eq!(msg.data_decode().unwrap(), b"Invalid starting tag foo");
2009
2010        // fatal channel error: channel must be closed
2011        assert!(chan.closed);
2012        assert!(!chan.opened);
2013    }
2014
2015    /// Initiator-side parse errors report the bare parser message
2016    /// (upstream sends ex.getMessage(), e.g. "not well-formed ...").
2017    #[test]
2018    fn test_handle_channel_error_initiator_parse_error_bare_text() {
2019        let mut chan = Channel::new(true);
2020        let err = MessageProcessingError::ParseError("not well-formed (invalid token)".into());
2021        let msg = chan.handle_channel_error("gw1", &err).unwrap();
2022        assert_eq!(
2023            msg.data_decode().unwrap(),
2024            b"not well-formed (invalid token)"
2025        );
2026        assert!(chan.closed);
2027    }
2028
2029    /// Target (gateway) side: an Error message is sent back to the peer and
2030    /// the channel is closed; parse errors gain the "Parse error: " prefix.
2031    #[test]
2032    fn test_handle_channel_error_target_sends_error_message() {
2033        let mut chan = Channel::new(false); // target side
2034        chan.opened = true;
2035
2036        let err = MessageProcessingError::NoType;
2037        let report = chan.handle_channel_error("n1", &err);
2038        assert!(report.is_none());
2039
2040        let out = String::from_utf8(chan.take_outgoing().unwrap()).unwrap();
2041        assert!(out.contains("type=\"ERR\""));
2042        assert!(out.contains("reason=\"Unknown message with no type\""));
2043        assert!(chan.closed);
2044
2045        // parse error: prefixed reason
2046        let mut chan2 = Channel::new(false);
2047        let err2 = MessageProcessingError::ParseError("not well-formed (invalid token)".into());
2048        chan2.handle_channel_error("n1", &err2);
2049        let out2 = String::from_utf8(chan2.take_outgoing().unwrap()).unwrap();
2050        assert!(out2.contains("reason=\"Parse error: not well-formed (invalid token)\""));
2051    }
2052}