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