1use super::namespaces::COMMAND_STATE_DONE;
4use crate::error::SoapError;
5
6pub struct ReceiveOutput {
12 pub stdout: Vec<u8>,
14 pub stderr: Vec<u8>,
16 pub exit_code: Option<i32>,
18 pub done: bool,
21}
22
23#[allow(unreachable_pub)] pub fn parse_shell_id(xml: &str) -> Result<String, SoapError> {
29 extract_element_text(xml, "ShellId").ok_or_else(|| SoapError::MissingElement("ShellId".into()))
30}
31
32#[allow(unreachable_pub)] pub fn parse_command_id(xml: &str) -> Result<String, SoapError> {
38 extract_element_text(xml, "CommandId")
39 .ok_or_else(|| SoapError::MissingElement("CommandId".into()))
40}
41
42#[allow(unreachable_pub)] pub fn parse_receive_output(xml: &str) -> Result<ReceiveOutput, SoapError> {
50 use base64::Engine;
51 use base64::engine::general_purpose::STANDARD as B64;
52
53 let mut stdout = Vec::new();
54 let mut stderr = Vec::new();
55 let mut exit_code: Option<i32> = None;
56 let mut done = false;
57
58 for (name, data) in extract_streams(xml) {
61 let decoded = B64
62 .decode(data.trim_ascii())
63 .map_err(|e| SoapError::ParseError(format!("base64 decode: {e}")))?;
64 match name.as_str() {
65 "stdout" => stdout.extend_from_slice(&decoded),
66 "stderr" => {
67 if decoded.starts_with(b"#< CLIXML") {
68 stderr.extend_from_slice(&parse_clixml(&decoded));
69 } else {
70 stderr.extend_from_slice(&decoded);
71 }
72 }
73 _ => {}
74 }
75 }
76
77 if xml.contains(COMMAND_STATE_DONE) {
79 done = true;
80 }
81
82 if let Some(code_str) = extract_element_text(xml, "ExitCode") {
84 exit_code = code_str.parse().ok();
85 }
86
87 if let Some(fault) = extract_soap_fault(xml) {
89 return Err(fault);
90 }
91
92 Ok(ReceiveOutput {
93 stdout,
94 stderr,
95 exit_code,
96 done,
97 })
98}
99
100pub(crate) fn parse_enumerate_response(xml: &str) -> Result<(String, Option<String>), SoapError> {
105 check_soap_fault(xml)?;
106
107 let items = extract_element_text(xml, "Items").unwrap_or_default();
109
110 let context = extract_element_text(xml, "EnumerationContext");
112
113 let end_of_seq = xml.contains("EndOfSequence");
115
116 let context = if end_of_seq { None } else { context };
117
118 Ok((items, context))
119}
120
121#[allow(unreachable_pub)] pub fn check_soap_fault(xml: &str) -> Result<(), SoapError> {
127 if let Some(fault) = extract_soap_fault(xml) {
128 return Err(fault);
129 }
130 Ok(())
131}
132
133fn parse_clixml(data: &[u8]) -> Vec<u8> {
139 let text = String::from_utf8_lossy(data);
140 let mut result = String::new();
141
142 let error_tag = "<S S=\"Error\">";
144 let close_tag = "</S>";
145 let mut search_from = 0;
146
147 while let Some(start) = text[search_from..].find(error_tag) {
148 let content_start = search_from + start + error_tag.len();
149 if let Some(end) = text[content_start..].find(close_tag) {
150 let fragment = &text[content_start..content_start + end];
151 result.push_str(fragment);
152 search_from = content_start + end + close_tag.len();
153 } else {
154 break;
155 }
156 }
157
158 let result = result
160 .replace("_x000D_", "\r")
161 .replace("_x000A_", "\n")
162 .replace("_x0009_", "\t")
163 .replace("_x001B_", "\x1b");
164
165 result.into_bytes()
166}
167
168fn extract_element_text(xml: &str, element: &str) -> Option<String> {
175 let suffixed = format!(":{element}>");
177 let bare_open = format!("<{element}>");
178
179 let mut search_from = 0;
180 while search_from < xml.len() {
181 let region = &xml[search_from..];
182
183 let (tag_content_start, ns_prefix) = if let Some(pos) = region.find(&suffixed) {
185 let abs_pos = search_from + pos;
187 let before = &xml[..abs_pos];
188 let lt = before.rfind('<')?;
189 if xml[lt..].starts_with("</") {
191 search_from = abs_pos + suffixed.len();
192 continue;
193 }
194 let prefix = &xml[lt + 1..abs_pos];
195 (abs_pos + suffixed.len(), Some(prefix.to_string()))
196 } else if let Some(pos) = region.find(&bare_open) {
197 (search_from + pos + bare_open.len(), None)
198 } else {
199 return None;
200 };
201
202 let close_tag = match &ns_prefix {
204 Some(p) => format!("</{p}:{element}>"),
205 None => format!("</{element}>"),
206 };
207
208 if let Some(end) = xml[tag_content_start..].find(&close_tag) {
209 let text = xml[tag_content_start..tag_content_start + end].trim();
210 if !text.is_empty() {
211 return Some(text.to_string());
212 }
213 }
214
215 search_from = tag_content_start;
216 }
217 None
218}
219
220fn extract_streams(xml: &str) -> Vec<(String, String)> {
222 let mut streams = Vec::new();
223 let mut search_from = 0;
224
225 let stream_tags = ["<rsp:Stream ", "<Stream "];
226
227 while search_from < xml.len() {
228 let found = stream_tags
229 .iter()
230 .filter_map(|tag| {
231 xml[search_from..]
232 .find(tag)
233 .map(|pos| (search_from + pos, *tag))
234 })
235 .min_by_key(|(pos, _)| *pos);
236
237 let Some((tag_start, _)) = found else {
238 break;
239 };
240
241 let tag_region = &xml[tag_start..];
242
243 let Some(tag_end) = tag_region.find('>') else {
245 break;
246 };
247 let opening_tag = &tag_region[..tag_end];
248
249 let name = extract_attribute(opening_tag, "Name").unwrap_or_default();
251
252 let content_start = tag_start + tag_end + 1;
254
255 let close_tags = ["</rsp:Stream>", "</Stream>"];
256 let close_pos = close_tags
257 .iter()
258 .filter_map(|close| xml[content_start..].find(close))
259 .min();
260
261 if let Some(end_offset) = close_pos {
262 let content = &xml[content_start..content_start + end_offset];
263 if !content.trim_ascii().is_empty() {
264 streams.push((name, content.to_string()));
265 }
266 search_from = content_start + end_offset + 1;
267 } else {
268 break;
269 }
270 }
271
272 streams
273}
274
275fn extract_attribute(tag: &str, attr_name: &str) -> Option<String> {
277 let pattern = format!("{attr_name}=\"");
278 let start = tag.find(&pattern)? + pattern.len();
279 let end = tag[start..].find('"')? + start;
280 Some(tag[start..end].to_string())
281}
282
283fn extract_soap_fault(xml: &str) -> Option<SoapError> {
285 let has_fault = xml.contains(":Fault>") || xml.contains("<Fault>");
287 if !has_fault {
288 return None;
289 }
290
291 let code = extract_subcode_value(xml)
295 .or_else(|| extract_element_text(xml, "Value"))
296 .or_else(|| extract_element_text(xml, "faultcode"))
297 .unwrap_or_else(|| "unknown".into());
298 let reason = extract_element_text(xml, "Text")
299 .or_else(|| extract_element_text(xml, "Message"))
300 .or_else(|| extract_element_text(xml, "faultstring"))
301 .unwrap_or_else(|| "SOAP fault".into());
302
303 Some(SoapError::Fault { code, reason })
304}
305
306fn extract_subcode_value(xml: &str) -> Option<String> {
309 let sub_start = xml.find("Subcode>")?;
311 let rest = &xml[sub_start..];
312 let inner = extract_element_text(rest, "Value")?;
313 if inner.is_empty() {
314 return None;
315 }
316 Some(inner)
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn parse_shell_id_from_response() {
325 let xml = r"<s:Envelope><s:Body><rsp:Shell>
326 <rsp:ShellId>ABC-DEF-123</rsp:ShellId>
327 </rsp:Shell></s:Body></s:Envelope>";
328 let id = parse_shell_id(xml).unwrap();
329 assert_eq!(id, "ABC-DEF-123");
330 }
331
332 #[test]
333 fn parse_command_id_from_response() {
334 let xml = r"<s:Envelope><s:Body><rsp:CommandResponse>
335 <rsp:CommandId>CMD-456</rsp:CommandId>
336 </rsp:CommandResponse></s:Body></s:Envelope>";
337 let id = parse_command_id(xml).unwrap();
338 assert_eq!(id, "CMD-456");
339 }
340
341 #[test]
342 fn parse_receive_output_with_streams() {
343 let xml = r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
346 <rsp:Stream Name="stdout" CommandId="C1">aGVsbG8=</rsp:Stream>
347 <rsp:Stream Name="stderr" CommandId="C1">ZXJy</rsp:Stream>
348 <rsp:CommandState CommandId="C1" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
349 <rsp:ExitCode>0</rsp:ExitCode>
350 </rsp:CommandState>
351 </rsp:ReceiveResponse></s:Body></s:Envelope>"#;
352
353 let output = parse_receive_output(xml).unwrap();
354 assert_eq!(output.stdout, b"hello");
355 assert_eq!(output.stderr, b"err");
356 assert_eq!(output.exit_code, Some(0));
357 assert!(output.done);
358 }
359
360 #[test]
361 fn parse_receive_output_not_done() {
362 let xml = r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
363 <rsp:Stream Name="stdout" CommandId="C1">dGVzdA==</rsp:Stream>
364 <rsp:CommandState CommandId="C1" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Running"/>
365 </rsp:ReceiveResponse></s:Body></s:Envelope>"#;
366
367 let output = parse_receive_output(xml).unwrap();
368 assert_eq!(output.stdout, b"test");
369 assert!(!output.done);
370 assert!(output.exit_code.is_none());
371 }
372
373 #[test]
374 fn detect_soap_fault() {
375 let xml = r"<s:Envelope><s:Body><s:Fault>
376 <s:Code><s:Value>s:Receiver</s:Value></s:Code>
377 <s:Reason><s:Text>Access denied</s:Text></s:Reason>
378 </s:Fault></s:Body></s:Envelope>";
379
380 let result = check_soap_fault(xml);
381 assert!(result.is_err());
382 let err = result.unwrap_err();
383 match err {
384 SoapError::Fault { code, reason } => {
385 assert_eq!(code, "s:Receiver");
386 assert_eq!(reason, "Access denied");
387 }
388 _ => panic!("expected SoapFault"),
389 }
390 }
391
392 #[test]
393 fn extract_attribute_works() {
394 let tag = r#"<rsp:Stream Name="stdout" CommandId="C1""#;
395 assert_eq!(extract_attribute(tag, "Name"), Some("stdout".into()));
396 assert_eq!(extract_attribute(tag, "CommandId"), Some("C1".into()));
397 assert_eq!(extract_attribute(tag, "Missing"), None);
398 }
399
400 #[test]
401 fn parse_receive_output_with_soap_fault() {
402 let xml = r"<s:Envelope><s:Body>
403 <s:Fault>
404 <s:Code><s:Value>s:Sender</s:Value></s:Code>
405 <s:Reason><s:Text>Invalid input</s:Text></s:Reason>
406 </s:Fault>
407 </s:Body></s:Envelope>";
408 let result = parse_receive_output(xml);
409 assert!(result.is_err());
410 }
411
412 #[test]
413 fn check_soap_fault_no_fault() {
414 let xml = r"<s:Envelope><s:Body><Data>ok</Data></s:Body></s:Envelope>";
415 assert!(check_soap_fault(xml).is_ok());
416 }
417
418 #[test]
419 fn extract_element_text_closing_tag_first() {
420 let xml = r"</rsp:ShellId><rsp:ShellId>ABC</rsp:ShellId>";
422 let result = parse_shell_id(xml).unwrap();
423 assert_eq!(result, "ABC");
424 }
425
426 #[test]
427 fn extract_element_text_empty_content() {
428 let xml = r"<rsp:ShellId></rsp:ShellId>";
429 assert!(parse_shell_id(xml).is_err());
430 }
431
432 #[test]
433 fn extract_streams_empty_stream() {
434 let xml = r#"<rsp:Stream Name="stdout" CommandId="C1"></rsp:Stream>"#;
436 let streams = extract_streams(xml);
437 assert!(streams.is_empty());
438 }
439
440 #[test]
441 fn parse_receive_output_non_base64_stream() {
442 let xml = r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
443 <rsp:Stream Name="stdout" CommandId="C1">!!!not-base64!!!</rsp:Stream>
444 </rsp:ReceiveResponse></s:Body></s:Envelope>"#;
445 let result = parse_receive_output(xml);
446 assert!(result.is_err());
447 }
448
449 #[test]
450 fn soap_error_display() {
451 let e = SoapError::MissingElement("ShellId".into());
452 assert_eq!(format!("{e}"), "missing element: ShellId");
453
454 let e = SoapError::ParseError("bad data".into());
455 assert_eq!(format!("{e}"), "parse error: bad data");
456
457 let e = SoapError::Fault {
458 code: "s:Sender".into(),
459 reason: "bad".into(),
460 };
461 assert_eq!(format!("{e}"), "SOAP fault [s:Sender]: bad");
462 }
463
464 #[test]
465 fn extract_streams_bare_tag() {
466 let xml = r#"<Stream Name="stdout">aGVsbG8=</Stream>"#;
468 let streams = extract_streams(xml);
469 assert_eq!(streams.len(), 1);
470 assert_eq!(streams[0].0, "stdout");
471 }
472
473 #[test]
474 fn detect_soap_fault_with_legacy_tags() {
475 let xml = r"<Fault><faultcode>s:Client</faultcode><faultstring>oops</faultstring></Fault>";
476 let result = check_soap_fault(xml);
477 assert!(result.is_err());
478 }
479
480 #[test]
481 fn extract_streams_bare_before_namespaced() {
482 let xml =
484 r#"<Stream Name="stdout">aGVsbG8=</Stream><rsp:Stream Name="stderr">ZXJy</rsp:Stream>"#;
485 let streams = extract_streams(xml);
486 assert_eq!(streams.len(), 2);
487 assert_eq!(streams[0].0, "stdout");
488 assert_eq!(streams[1].0, "stderr");
489 }
490
491 #[test]
492 fn extract_streams_namespaced_before_bare_close() {
493 let xml = r#"<rsp:Stream Name="stdout">dGVzdA==</Stream></rsp:Stream>"#;
495 let streams = extract_streams(xml);
496 assert_eq!(streams.len(), 1);
497 assert_eq!(streams[0].0, "stdout");
498 }
499
500 #[test]
501 fn parse_receive_output_no_exit_code() {
502 let xml = r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
503 <rsp:CommandState CommandId="C1" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done"/>
504 </rsp:ReceiveResponse></s:Body></s:Envelope>"#;
505 let output = parse_receive_output(xml).unwrap();
506 assert!(output.done);
507 assert!(output.exit_code.is_none());
508 assert!(output.stdout.is_empty());
509 }
510
511 #[test]
514 fn extract_element_text_skips_empty_finds_second() {
515 let xml = r"<rsp:ShellId></rsp:ShellId><rsp:ShellId>FOUND</rsp:ShellId>";
516 assert_eq!(parse_shell_id(xml).unwrap(), "FOUND");
517 }
518
519 #[test]
520 fn extract_element_bare_element() {
521 let xml = r"<ShellId>BARE-ID</ShellId>";
522 assert_eq!(parse_shell_id(xml).unwrap(), "BARE-ID");
523 }
524
525 #[test]
526 fn parse_receive_output_multiple_stdout_chunks() {
527 let xml = r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
529 <rsp:Stream Name="stdout" CommandId="C1">aGVs</rsp:Stream>
530 <rsp:Stream Name="stdout" CommandId="C1">bG8=</rsp:Stream>
531 <rsp:CommandState CommandId="C1" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
532 <rsp:ExitCode>0</rsp:ExitCode>
533 </rsp:CommandState>
534 </rsp:ReceiveResponse></s:Body></s:Envelope>"#;
535 let output = parse_receive_output(xml).unwrap();
536 assert_eq!(output.stdout, b"hello");
537 }
538
539 #[test]
540 fn parse_receive_output_interleaved_streams() {
541 let xml = r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
543 <rsp:Stream Name="stdout" CommandId="C1">QUI=</rsp:Stream>
544 <rsp:Stream Name="stderr" CommandId="C1">ZXJy</rsp:Stream>
545 <rsp:Stream Name="stdout" CommandId="C1">Q0Q=</rsp:Stream>
546 <rsp:CommandState CommandId="C1" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
547 <rsp:ExitCode>1</rsp:ExitCode>
548 </rsp:CommandState>
549 </rsp:ReceiveResponse></s:Body></s:Envelope>"#;
550 let output = parse_receive_output(xml).unwrap();
551 assert_eq!(output.stdout, b"ABCD");
552 assert_eq!(output.stderr, b"err");
553 assert_eq!(output.exit_code, Some(1));
554 }
555
556 #[test]
557 fn extract_element_text_multiple_closing_before_opening() {
558 let xml = r"</rsp:CommandId></rsp:CommandId><rsp:CommandId>REAL-CMD</rsp:CommandId>";
559 assert_eq!(parse_command_id(xml).unwrap(), "REAL-CMD");
560 }
561
562 #[test]
563 fn extract_streams_three_sequential() {
564 let xml = r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
566 <rsp:Stream Name="stdout" CommandId="C1">QQ==</rsp:Stream>
567 <rsp:Stream Name="stderr" CommandId="C1">Qg==</rsp:Stream>
568 <rsp:Stream Name="stdout" CommandId="C1">Qw==</rsp:Stream>
569 <rsp:CommandState CommandId="C1" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
570 <rsp:ExitCode>0</rsp:ExitCode>
571 </rsp:CommandState>
572 </rsp:ReceiveResponse></s:Body></s:Envelope>"#;
573 let output = parse_receive_output(xml).unwrap();
574 assert_eq!(output.stdout, b"AC");
575 assert_eq!(output.stderr, b"B");
576 }
577
578 #[test]
579 fn extract_element_text_trims_whitespace() {
580 let xml = r"<rsp:ShellId> TRIMMED </rsp:ShellId>";
581 assert_eq!(parse_shell_id(xml).unwrap(), "TRIMMED");
582 }
583
584 #[test]
585 fn parse_receive_output_negative_exit_code() {
586 let xml = r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
587 <rsp:CommandState CommandId="C1" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
588 <rsp:ExitCode>-1</rsp:ExitCode>
589 </rsp:CommandState>
590 </rsp:ReceiveResponse></s:Body></s:Envelope>"#;
591 let output = parse_receive_output(xml).unwrap();
592 assert!(output.done);
593 assert_eq!(output.exit_code, Some(-1));
594 }
595
596 #[test]
597 fn parse_receive_output_non_numeric_exit_code() {
598 let xml = r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
599 <rsp:CommandState CommandId="C1" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
600 <rsp:ExitCode>notanumber</rsp:ExitCode>
601 </rsp:CommandState>
602 </rsp:ReceiveResponse></s:Body></s:Envelope>"#;
603 let output = parse_receive_output(xml).unwrap();
604 assert!(output.done);
605 assert!(output.exit_code.is_none());
606 }
607
608 #[test]
609 fn extract_element_text_at_end_of_string() {
610 let xml = "<rsp:CommandId>VAL</rsp:CommandId>";
611 assert_eq!(parse_command_id(xml).unwrap(), "VAL");
612 }
613
614 #[test]
615 fn extract_element_bare_at_start_of_string() {
616 let xml = "<CommandId>START-ID</CommandId>";
617 assert_eq!(parse_command_id(xml).unwrap(), "START-ID");
618 }
619
620 #[test]
621 fn extract_element_bare_with_prefix_text() {
622 let xml = "X<ShellId>OFFSET-ID</ShellId>";
623 assert_eq!(parse_shell_id(xml).unwrap(), "OFFSET-ID");
624 }
625
626 #[test]
627 fn extract_streams_at_end_of_string() {
628 let xml = r#"<rsp:Stream Name="stdout" CommandId="C1">b2s=</rsp:Stream>"#;
630 let streams = extract_streams(xml);
631 assert_eq!(streams.len(), 1);
632 assert_eq!(streams[0].0, "stdout");
633 }
634
635 #[test]
636 fn extract_streams_rsp_before_bare_picks_first() {
637 let xml =
639 r#"<rsp:Stream Name="stdout">QQ==</rsp:Stream> <Stream Name="stderr">Qg==</Stream>"#;
640 let streams = extract_streams(xml);
641 assert_eq!(streams.len(), 2);
642 assert_eq!(streams[0].0, "stdout");
643 assert_eq!(streams[0].1, "QQ==");
644 assert_eq!(streams[1].0, "stderr");
645 assert_eq!(streams[1].1, "Qg==");
646 }
647
648 #[test]
649 fn extract_streams_close_tag_ordering() {
650 let xml = r#"<rsp:Stream Name="stdout">WA==</Stream>extra</rsp:Stream>"#;
652 let streams = extract_streams(xml);
653 assert_eq!(streams.len(), 1);
654 assert_eq!(streams[0].1, "WA==");
655 }
656
657 #[test]
658 fn extract_streams_adjacent_streams_search_from_advance() {
659 let xml = r#"<rsp:Stream Name="stdout">WA==</rsp:Stream><rsp:Stream Name="stderr">WQ==</rsp:Stream>"#;
661 let streams = extract_streams(xml);
662 assert_eq!(streams.len(), 2, "both adjacent streams must be found");
663 assert_eq!(streams[0].0, "stdout");
664 assert_eq!(streams[0].1, "WA==");
665 assert_eq!(streams[1].0, "stderr");
666 assert_eq!(streams[1].1, "WQ==");
667 }
668
669 #[test]
670 fn extract_streams_three_adjacent_with_content() {
671 let xml = r#"<rsp:Stream Name="stdout">QQ==</rsp:Stream><rsp:Stream Name="stderr">Qg==</rsp:Stream><rsp:Stream Name="stdout">Qw==</rsp:Stream>"#;
673 let streams = extract_streams(xml);
674 assert_eq!(streams.len(), 3, "all three adjacent streams must be found");
675 assert_eq!(streams[0].1, "QQ==");
676 assert_eq!(streams[1].1, "Qg==");
677 assert_eq!(streams[2].1, "Qw==");
678 }
679
680 #[test]
681 fn extract_element_text_empty_then_filled_tight() {
682 let xml = "<rsp:ShellId></rsp:ShellId><rsp:ShellId>OK</rsp:ShellId>";
683 assert_eq!(parse_shell_id(xml).unwrap(), "OK");
684 }
685
686 #[test]
687 fn parse_receive_output_stream_at_xml_end() {
688 let xml = r#"<s:Envelope><s:Body><rsp:ReceiveResponse><rsp:Stream Name="stdout" CommandId="C1">Wg==</rsp:Stream><rsp:CommandState CommandId="C1" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done"><rsp:ExitCode>0</rsp:ExitCode></rsp:CommandState></rsp:ReceiveResponse></s:Body></s:Envelope>"#;
690 let output = parse_receive_output(xml).unwrap();
691 assert_eq!(output.stdout, b"Z");
692 assert_eq!(output.exit_code, Some(0));
693 assert!(output.done);
694 }
695
696 #[test]
699 fn extract_streams_long_content_with_second_stream() {
700 let long_b64 = "QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB"; let xml = format!(
702 r#"<rsp:Stream Name="stdout">{long_b64}</rsp:Stream><rsp:Stream Name="stderr">ZXJy</rsp:Stream>"#
703 );
704 let streams = extract_streams(&xml);
705 assert_eq!(streams.len(), 2, "must find both streams with long content");
706 assert_eq!(streams[0].0, "stdout");
707 assert_eq!(streams[0].1, long_b64);
708 assert_eq!(streams[1].0, "stderr");
709 assert_eq!(streams[1].1, "ZXJy");
710 }
711
712 #[test]
713 fn parse_receive_output_long_stdout_with_stderr() {
714 let long_b64 = "QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB";
715 let xml = format!(
716 r#"<s:Envelope><s:Body><rsp:ReceiveResponse>
717 <rsp:Stream Name="stdout" CommandId="C1">{long_b64}</rsp:Stream>
718 <rsp:Stream Name="stderr" CommandId="C1">ZXJy</rsp:Stream>
719 <rsp:CommandState CommandId="C1" State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done">
720 <rsp:ExitCode>0</rsp:ExitCode>
721 </rsp:CommandState>
722 </rsp:ReceiveResponse></s:Body></s:Envelope>"#
723 );
724 let output = parse_receive_output(&xml).unwrap();
725 assert_eq!(output.stdout.len(), 30, "should decode 30 bytes of stdout");
726 assert_eq!(output.stderr, b"err", "should decode stderr correctly");
727 assert_eq!(output.exit_code, Some(0));
728 }
729
730 #[test]
731 fn parse_clixml_basic_error() {
732 let input = b"#< CLIXML\r\n<Objs Version=\"1.1.0.1\" xmlns=\"http://schemas.microsoft.com/powershell/2004/04\"><S S=\"Error\">Something went wrong</S></Objs>";
733 let result = parse_clixml(input);
734 assert_eq!(String::from_utf8_lossy(&result), "Something went wrong");
735 }
736
737 #[test]
738 fn parse_clixml_escaped_newlines() {
739 let input = b"#< CLIXML\r\n<Objs><S S=\"Error\">line1_x000D__x000A_line2</S></Objs>";
740 let result = parse_clixml(input);
741 assert_eq!(String::from_utf8_lossy(&result), "line1\r\nline2");
742 }
743
744 #[test]
745 fn parse_clixml_multiple_errors() {
746 let input = b"#< CLIXML\r\n<Objs><S S=\"Error\">err1</S><S S=\"Error\">err2</S></Objs>";
747 let result = parse_clixml(input);
748 assert_eq!(String::from_utf8_lossy(&result), "err1err2");
749 }
750
751 #[test]
752 fn parse_clixml_not_clixml_passthrough() {
753 let input = b"plain error text without CLIXML";
755 let result = parse_clixml(input);
756 assert!(result.is_empty());
757 }
758
759 #[test]
760 fn parse_receive_output_clixml_stderr() {
761 use base64::Engine;
762 use base64::engine::general_purpose::STANDARD as B64;
763
764 let clixml = b"#< CLIXML\r\n<Objs><S S=\"Error\">PowerShell error_x000D__x000A_</S></Objs>";
765 let encoded = B64.encode(clixml);
766 let xml = format!(
767 r#"<rsp:ReceiveResponse><rsp:Stream Name="stderr">{encoded}</rsp:Stream><rsp:CommandState State="http://schemas.microsoft.com/wbem/wsman/1/windows/shell/CommandState/Done"><rsp:ExitCode>1</rsp:ExitCode></rsp:CommandState></rsp:ReceiveResponse>"#
768 );
769 let output = parse_receive_output(&xml).unwrap();
770 assert_eq!(
771 String::from_utf8_lossy(&output.stderr),
772 "PowerShell error\r\n"
773 );
774 assert_eq!(output.exit_code, Some(1));
775 }
776
777 #[test]
779 fn extract_subcode_from_soap_fault() {
780 let xml = r"<s:Fault>
781 <s:Code><s:Value>s:Sender</s:Value>
782 <s:Subcode><s:Value>wsa:DestinationUnreachable</s:Value></s:Subcode></s:Code>
783 <s:Reason><s:Text>no route</s:Text></s:Reason></s:Fault>";
784 let val = extract_subcode_value(xml);
785 assert_eq!(val.as_deref(), Some("wsa:DestinationUnreachable"));
786 }
787
788 #[test]
790 fn extract_element_text_exact_boundary() {
791 let xml = "<Root><Name>val</Name></Root>";
793 assert_eq!(extract_element_text(xml, "Name").as_deref(), Some("val"));
794 }
795
796 #[test]
798 fn extract_streams_at_string_boundary() {
799 let xml = r#"<rsp:Stream Name="stdout">YWJD</rsp:Stream>"#;
801 let streams = extract_streams(xml);
802 assert_eq!(streams.len(), 1);
803 assert_eq!(streams[0].0, "stdout");
804 assert_eq!(streams[0].1, "YWJD");
805 }
806
807 #[test]
809 fn parse_enumerate_response_with_items_and_end() {
810 let xml = r#"<s:Envelope xmlns:wsen="http://schemas.xmlsoap.org/ws/2004/09/enumeration">
811 <s:Body><wsen:EnumerateResponse>
812 <wsen:Items><data>hello</data></wsen:Items>
813 <wsen:EndOfSequence/>
814 </wsen:EnumerateResponse></s:Body></s:Envelope>"#;
815 let (items, context) = parse_enumerate_response(xml).unwrap();
816 assert!(items.contains("hello"));
817 assert!(context.is_none(), "EndOfSequence means no continuation");
818 }
819
820 #[test]
821 fn parse_enumerate_response_with_continuation() {
822 let xml = r#"<s:Envelope xmlns:wsen="http://schemas.xmlsoap.org/ws/2004/09/enumeration">
823 <s:Body><wsen:EnumerateResponse>
824 <wsen:Items><data>page1</data></wsen:Items>
825 <wsen:EnumerationContext>ctx-123</wsen:EnumerationContext>
826 </wsen:EnumerateResponse></s:Body></s:Envelope>"#;
827 let (items, context) = parse_enumerate_response(xml).unwrap();
828 assert!(items.contains("page1"));
829 assert_eq!(context.as_deref(), Some("ctx-123"));
830 }
831
832 #[test]
834 fn parse_clixml_consecutive_error_fragments() {
835 let clixml = b"#< CLIXML\r\n<Objs><S S=\"Error\">aaa</S><S S=\"Error\">bbb</S></Objs>";
836 let result = parse_clixml(clixml);
837 assert_eq!(String::from_utf8_lossy(&result), "aaabbb");
838 }
839}