diff --git a/crates/ironposh-client-core/src/runspace/win_rs.rs b/crates/ironposh-client-core/src/runspace/win_rs.rs index b0ef38e..7a839f4 100644 --- a/crates/ironposh-client-core/src/runspace/win_rs.rs +++ b/crates/ironposh-client-core/src/runspace/win_rs.rs @@ -1,10 +1,10 @@ use base64::Engine; use ironposh_winrm::{ - cores::{Attribute, DesiredStream, Receive, Shell, Tag, Text, Time, tag_name}, + cores::{Attribute, DesiredStreamTag, StreamTag, Tag, Text, Time}, rsp::{ commandline::CommandLineValue, - receive::{CommandStateValue, ReceiveValue}, - shell_value::ShellValue, + receive::{CommandStateTag, CommandStateValue, ReceiveTag, ReceiveValue}, + shell_value::{ShellTag, ShellValue}, }, soap::{SoapEnvelope, body::SoapBody}, ws_management::{OptionSetValue, SelectorSetValue, WsAction, WsMan}, @@ -67,7 +67,7 @@ impl WinRunspace { option_set: Option, open_content: &'a str, ) -> impl Into> { - let shell = Tag::from_name(Shell) + let shell = Tag::from_name(ShellTag) .with_attribute(ironposh_winrm::cores::Attribute::ShellId( self.id.to_string().into(), )) @@ -130,7 +130,7 @@ impl WinRunspace { // Join stream names with spaces as required by Windows Shell schema let combined_streams = stream_names.join(" "); let mut tag = - Tag::from_name(DesiredStream).with_value(Text::from(combined_streams)); + Tag::from_name(DesiredStreamTag).with_value(Text::from(combined_streams)); if let Some(command_id) = command_id { tag = tag.with_attribute(Attribute::CommandId(command_id)); @@ -144,7 +144,7 @@ impl WinRunspace { .desired_streams(desired_stream_tags) .build(); - let receive_tag = Tag::from_name(Receive) + let receive_tag = Tag::from_name(ReceiveTag) .with_value(receive) .with_declaration(ironposh_winrm::cores::Namespace::WsmanShell); @@ -172,9 +172,12 @@ impl WinRunspace { /// Build a Disconnect request targeting this shell (MS-WSMV 3.1.4.13). pub(crate) fn fire_disconnect<'a>(&'a self, ws_man: &'a WsMan) -> impl Into> { - use ironposh_winrm::{cores::Namespace, rsp::disconnect::DisconnectValue}; + use ironposh_winrm::{ + cores::Namespace, + rsp::disconnect::{DisconnectTag, DisconnectValue}, + }; - let disconnect_tag = Tag::from_name(tag_name::Disconnect) + let disconnect_tag = Tag::from_name(DisconnectTag) .with_declaration(Namespace::WsmanShell) .with_value(DisconnectValue::builder().build()); @@ -197,13 +200,16 @@ impl WinRunspace { option_set: Option, connect_payload: &'a str, ) -> impl Into> { - use ironposh_winrm::{cores::Namespace, rsp::connect::ConnectValue}; + use ironposh_winrm::{ + cores::Namespace, + rsp::connect::{ConnectTag, ConnectValue}, + }; let connect_value = ConnectValue { connect_xml: Tag::new(connect_payload).with_declaration(Namespace::PowerShellRemoting), }; - let connect_tag = Tag::from_name(tag_name::Connect) + let connect_tag = Tag::from_name(ConnectTag) .with_declaration(Namespace::WsmanShell) .with_value(connect_value); @@ -218,9 +224,9 @@ impl WinRunspace { /// Build a Reconnect request targeting this shell (MS-WSMV 3.1.4.14). pub(crate) fn fire_reconnect<'a>(&'a self, ws_man: &'a WsMan) -> impl Into> { - use ironposh_winrm::cores::{Empty, Namespace}; + use ironposh_winrm::cores::{Empty, Namespace, ReconnectTag}; - let reconnect_tag = Tag::from_name(tag_name::Reconnect) + let reconnect_tag = Tag::from_name(ReconnectTag) .with_declaration(Namespace::WsmanShell) .with_value(Empty); @@ -390,20 +396,17 @@ impl WinRunspace { data: &'a [String], ) -> Result>, crate::PwshCoreError> { use ironposh_winrm::{ - cores::{ - Namespace, Tag, - tag_name::{Send, Stream}, - }, - rsp::send::SendValue, + cores::{Namespace, StreamTag, Tag}, + rsp::send::{SendTag, SendValue}, soap::body::SoapBody, }; // Create a Stream tag for each fragment // Each fragment is a base64-encoded PSRP fragment that goes in its own element - let streams: Vec> = data + let streams: Vec> = data .iter() .map(|fragment| { - Tag::from_name(Stream) + Tag::from_name(StreamTag) .with_value(Text::from(fragment.as_str())) .with_attribute(Attribute::Name("stdin".into())) }) @@ -413,12 +416,12 @@ impl WinRunspace { // Add send tag with SendValue containing multiple streams let send_tag = if let Some(cmd_id) = command_id { - Tag::from_name(Send) + Tag::from_name(SendTag) .with_value(send_value) .with_attribute(Attribute::CommandId(cmd_id)) .with_declaration(Namespace::WsmanShell) } else { - Tag::from_name(Send) + Tag::from_name(SendTag) .with_value(send_value) .with_declaration(Namespace::WsmanShell) }; @@ -460,18 +463,15 @@ impl WinRunspace { connection: &'a WsMan, id: Uuid, ) -> Result>, crate::PwshCoreError> { - use ironposh_winrm::cores::{ - Namespace, - tag_name::{Signal, SignalCode}, - }; + use ironposh_winrm::cores::{Namespace, SignalCodeTag, SignalTag}; // Build http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/ctrl_c - let code = Tag::from_name(SignalCode).with_value(Text::from( + let code = Tag::from_name(SignalCodeTag).with_value(Text::from( "http://schemas.microsoft.com/wbem/wsman/1/windows/shell/signal/terminate", )); // Build ... - let signal = Tag::from_name(Signal) + let signal = Tag::from_name(SignalTag) .with_attribute(Attribute::CommandId(id)) .with_value(code) .with_declaration(Namespace::WsmanShell); @@ -535,10 +535,10 @@ impl Stream { } } -impl<'a> TryFrom<&Tag<'a, Text<'a>, tag_name::Stream>> for Stream { +impl<'a> TryFrom<&Tag<'a, Text<'a>, StreamTag>> for Stream { type Error = crate::PwshCoreError; - fn try_from(value: &Tag<'a, Text<'a>, tag_name::Stream>) -> Result { + fn try_from(value: &Tag<'a, Text<'a>, StreamTag>) -> Result { let attributes = &value.attributes; let name = attributes .iter() @@ -577,11 +577,11 @@ pub struct CommandState { pub exit_code: Option, } -impl<'a> TryFrom<&Tag<'a, CommandStateValue<'a>, tag_name::CommandState>> for CommandState { +impl<'a> TryFrom<&Tag<'a, CommandStateValue<'a>, CommandStateTag>> for CommandState { type Error = crate::PwshCoreError; fn try_from( - value: &Tag<'a, CommandStateValue<'a>, tag_name::CommandState>, + value: &Tag<'a, CommandStateValue<'a>, CommandStateTag>, ) -> Result { let command_id = value .attributes diff --git a/crates/ironposh-client-core/tests/test_send_roundtrip.rs b/crates/ironposh-client-core/tests/test_send_roundtrip.rs index 33e3776..2fe718a 100644 --- a/crates/ironposh-client-core/tests/test_send_roundtrip.rs +++ b/crates/ironposh-client-core/tests/test_send_roundtrip.rs @@ -7,8 +7,11 @@ use ironposh_psrp::{ fragmentation::{DefragmentResult, Defragmenter, Fragmenter}, }; use ironposh_winrm::{ - cores::{Attribute, ReceiveResponse, Send, Tag, Text, tag_name::Stream}, - rsp::{receive::ReceiveResponseValue, send::SendValue}, + cores::{Attribute, StreamTag, Tag, Text}, + rsp::{ + receive::{ReceiveResponseTag, ReceiveResponseValue}, + send::{SendTag, SendValue}, + }, soap::{SoapEnvelope, body::SoapBody}, }; use ironposh_xml::builder::Element; @@ -42,10 +45,10 @@ fn test_send_receive_roundtrip_with_fragmentation() { .collect(); // 4. Create SendValue with multiple Stream elements (NEW CODE PATH) - let streams: Vec> = base64_fragments + let streams: Vec> = base64_fragments .iter() .map(|fragment| { - Tag::from_name(Stream) + Tag::from_name(StreamTag) .with_value(Text::from(fragment.as_str())) .with_attribute(Attribute::Name("stdin".into())) }) @@ -53,7 +56,7 @@ fn test_send_receive_roundtrip_with_fragmentation() { let send_value = SendValue::builder().streams(streams).build(); - let send_tag = Tag::from_name(Send) + let send_tag = Tag::from_name(SendTag) .with_value(send_value) .with_attribute(Attribute::CommandId(command_id)) .with_declaration(ironposh_winrm::cores::Namespace::WsmanShell); @@ -80,10 +83,10 @@ fn test_send_receive_roundtrip_with_fragmentation() { // 6. Simulate receiving the response - create ReceiveResponse with same streams // (In real flow, server would echo back or client would receive similar structure) - let receive_streams: Vec> = base64_fragments + let receive_streams: Vec> = base64_fragments .iter() .map(|fragment| { - Tag::from_name(Stream) + Tag::from_name(StreamTag) .with_value(Text::from(fragment.as_str())) .with_attribute(Attribute::Name("stdout".into())) .with_attribute(Attribute::CommandId(command_id)) @@ -95,7 +98,7 @@ fn test_send_receive_roundtrip_with_fragmentation() { .command_state(None) .build(); - let receive_response_tag = Tag::from_name(ReceiveResponse) + let receive_response_tag = Tag::from_name(ReceiveResponseTag) .with_value(receive_response_value) .with_declaration(ironposh_winrm::cores::Namespace::WsmanShell); @@ -183,7 +186,7 @@ fn create_large_session_capability() -> SessionCapability { fn test_send_with_no_fragments() { let send_value = SendValue::builder().streams(vec![]).build(); - let send_tag = Tag::from_name(Send) + let send_tag = Tag::from_name(SendTag) .with_value(send_value) .with_declaration(ironposh_winrm::cores::Namespace::WsmanShell); @@ -222,13 +225,13 @@ fn test_send_with_single_fragment() { let base64_fragment = base64::engine::general_purpose::STANDARD.encode(&fragments[0]); - let stream = Tag::from_name(Stream) + let stream = Tag::from_name(StreamTag) .with_value(Text::from(base64_fragment.as_str())) .with_attribute(Attribute::Name("stdin".into())); let send_value = SendValue::builder().streams(vec![stream]).build(); - let send_tag = Tag::from_name(Send) + let send_tag = Tag::from_name(SendTag) .with_value(send_value) .with_attribute(Attribute::CommandId(command_id)) .with_declaration(ironposh_winrm::cores::Namespace::WsmanShell); diff --git a/crates/ironposh-macros/src/lib.rs b/crates/ironposh-macros/src/lib.rs index 232f71d..d58f532 100644 --- a/crates/ironposh-macros/src/lib.rs +++ b/crates/ironposh-macros/src/lib.rs @@ -1260,30 +1260,47 @@ pub fn derive_simple_tag_value(input: TokenStream) -> TokenStream { } /// Derives [`ironposh_xml::mapping::FromXml`] for a WinRM struct whose fields -/// are `Tag<'a, V, N>` / `Option>`. +/// are tag types (`Tag<'a, V, N>`, or a `tag!` alias for one, optionally wrapped +/// in `Option`). /// -/// Generates a direct, namespace-correct `from_xml(node)` — no visitor struct, -/// no `finish()`. Each child element is matched by its `(namespace-URI, -/// local-name)` pair, sourced from the field's `N: TagName` (`NAMESPACE` + -/// `TAG_NAME`); the prefix is never compared. `Option<_>` fields stay `None` -/// when absent; required fields error. This is the deserialize-side replacement -/// for the visitor that `SimpleXmlDeserialize` generates. +/// Generates a direct, namespace-correct `from_xml(node)` — no visitor. Each +/// child is matched by its `(namespace-URI, local-name)` pair, read from the +/// field type via `NamedTag` (so it works through type aliases); the prefix is +/// never compared. `Option<_>` fields stay `None` when absent; required fields +/// error. +/// +/// Requirements: the deriving struct must carry a single lifetime parameter `'a`, +/// and the consumer crate must expose `cores::{NamedTag, TagValue}` — this derive +/// is winrm-specific. #[proc_macro_derive(FromXml)] pub fn derive_from_xml(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - TokenStream::from(impl_from_xml(&input)) + match impl_from_xml(&input) { + Ok(ts) => ts.into(), + Err(e) => e.to_compile_error().into(), + } } -fn impl_from_xml(input: &DeriveInput) -> TokenStream2 { +fn impl_from_xml(input: &DeriveInput) -> Result { let name = &input.ident; let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); let fields = match &input.data { Data::Struct(data) => match &data.fields { Fields::Named(fields) => &fields.named, - _ => panic!("FromXml can only be derived for structs with named fields"), + _ => { + return Err(syn::Error::new_spanned( + input, + "FromXml can only be derived for structs with named fields", + )); + } }, - _ => panic!("FromXml can only be derived for structs"), + _ => { + return Err(syn::Error::new_spanned( + input, + "FromXml can only be derived for structs", + )); + } }; let entries: Vec = fields @@ -1307,6 +1324,7 @@ fn impl_from_xml(input: &DeriveInput) -> TokenStream2 { // One namespace-correct match per field: identity is (URI, local-name), // read from the field's tag type via `NamedTag` so it works through aliases. + // Emitted as an `if … else if …` chain so each child binds at most one field. let matchers = entries.iter().map(|e| { let f = &e.field_name; let ty = &e.value_type; @@ -1315,6 +1333,13 @@ fn impl_from_xml(input: &DeriveInput) -> TokenStream2 { <#ty as crate::cores::NamedTag>::NAMESPACE, <#ty as crate::cores::NamedTag>::TAG_NAME, ) { + if #f.is_some() { + return Err(ironposh_xml::XmlError::InvalidXml(format!( + "duplicate <{}> in {}", + <#ty as crate::cores::NamedTag>::TAG_NAME, + stringify!(#name), + ))); + } #f = Some(ironposh_xml::mapping::FromXml::from_xml(child)?); } } @@ -1333,7 +1358,7 @@ fn impl_from_xml(input: &DeriveInput) -> TokenStream2 { } }); - quote! { + Ok(quote! { impl #impl_generics ironposh_xml::mapping::FromXml<'a> for #name #ty_generics #where_clause { fn from_xml( node: ironposh_xml::parser::Node<'a, 'a>, @@ -1344,12 +1369,12 @@ fn impl_from_xml(input: &DeriveInput) -> TokenStream2 { if !child.is_element() { continue; } - #(#matchers)* + #(#matchers)else* } Ok(#name { #(#construct),* }) } } - } + }) } fn impl_simple_tag_value(input: &DeriveInput) -> TokenStream2 { diff --git a/crates/ironposh-test-support/src/fake_server.rs b/crates/ironposh-test-support/src/fake_server.rs index f296232..955ece9 100644 --- a/crates/ironposh-test-support/src/fake_server.rs +++ b/crates/ironposh-test-support/src/fake_server.rs @@ -15,12 +15,9 @@ use ironposh_psrp::{ PowerShellRemotingMessage, Size, }; use ironposh_winrm::{ - cores::{ - tag_name::{Envelope, Stream}, - Attribute, Namespace, ReceiveResponse, Tag, Text, - }, - rsp::receive::ReceiveResponseValue, - soap::{body::SoapBody, SoapEnvelope}, + cores::{Attribute, Namespace, StreamTag, Tag, Text}, + rsp::receive::{ReceiveResponseTag, ReceiveResponseValue}, + soap::{body::SoapBody, Envelope, SoapEnvelope}, }; use ironposh_xml::builder::Element; use uuid::Uuid; @@ -156,10 +153,10 @@ pub fn receive_response_xml(rpid: Uuid, messages: &[&dyn PsObjectWithType]) -> S }) .collect(); - let streams: Vec, Stream>> = base64_fragments + let streams: Vec, StreamTag>> = base64_fragments .iter() .map(|fragment| { - Tag::from_name(Stream) + Tag::from_name(StreamTag) .with_value(Text::from(fragment.as_str())) .with_attribute(Attribute::Name("stdout".into())) }) @@ -170,7 +167,7 @@ pub fn receive_response_xml(rpid: Uuid, messages: &[&dyn PsObjectWithType]) -> S .command_state(None) .build(); - let receive_response_tag = Tag::from_name(ReceiveResponse) + let receive_response_tag = Tag::from_name(ReceiveResponseTag) .with_value(receive_response_value) .with_declaration(Namespace::WsmanShell); @@ -180,7 +177,7 @@ pub fn receive_response_xml(rpid: Uuid, messages: &[&dyn PsObjectWithType]) -> S let envelope = SoapEnvelope::builder().body(body).build(); - let soap = Tag::, Envelope>::new(envelope) + let soap = Envelope::new(envelope) .with_declaration(Namespace::SoapEnvelope2003) .with_declaration(Namespace::WsAddressing2004) .with_declaration(Namespace::DmtfWsmanSchema) diff --git a/crates/ironposh-winrm/src/cores/tag.rs b/crates/ironposh-winrm/src/cores/tag.rs index b579f58..e9efbba 100644 --- a/crates/ironposh-winrm/src/cores/tag.rs +++ b/crates/ironposh-winrm/src/cores/tag.rs @@ -173,12 +173,31 @@ where let element = if node.is_element_named(N::NAMESPACE, N::TAG_NAME) { node } else { - node.children() - .find(|child| child.is_element_named(N::NAMESPACE, N::TAG_NAME)) + // Wrapper case (`Tag, _>`): exactly one child element, which + // must be N. Reject zero (wrong wrapper), >1 (malformed), or a single + // child of the wrong name rather than silently picking one. + let mut elements = node + .children() + .filter(ironposh_xml::parser::Node::is_element); + let only = elements + .next() .ok_or_else(|| ironposh_xml::XmlError::XmlInvalidTag { expected: N::TAG_NAME.to_string(), found: node.tag_name().name().to_string(), - })? + })?; + if elements.next().is_some() { + return Err(ironposh_xml::XmlError::InvalidXml(format!( + "expected exactly one child element in <{}>", + node.tag_name().name() + ))); + } + if !only.is_element_named(N::NAMESPACE, N::TAG_NAME) { + return Err(ironposh_xml::XmlError::XmlInvalidTag { + expected: N::TAG_NAME.to_string(), + found: only.tag_name().name().to_string(), + }); + } + only }; let value = V::from_xml(element)?; @@ -260,7 +279,7 @@ impl_tag_from!(uuid::Uuid => Tag<'a, WsUuid, N>); #[cfg(test)] mod tests { use super::*; - use crate::cores::{CommandId, CommandResponse}; + use crate::cores::CommandResponse; use ironposh_xml::parser::parse; const RSP: &str = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell"; @@ -275,9 +294,8 @@ mod tests { r#"{uuid}"# ); let doc = parse(&xml).unwrap(); - let tag: Tag<'_, Tag<'_, WsUuid, CommandId>, CommandResponse> = - Tag::from_xml(doc.root_element()) - .expect("nested CommandResponse/CommandId should parse"); + let tag = CommandResponse::from_xml(doc.root_element()) + .expect("nested CommandResponse/CommandId should parse"); assert_eq!(tag.value.value.0.to_string().to_uppercase(), uuid); } } diff --git a/crates/ironposh-winrm/src/cores/tag_name.rs b/crates/ironposh-winrm/src/cores/tag_name.rs index 0504c6e..69d9f73 100644 --- a/crates/ironposh-winrm/src/cores/tag_name.rs +++ b/crates/ironposh-winrm/src/cores/tag_name.rs @@ -1,5 +1,5 @@ -use crate::cores::namespace::*; -use crate::{define_custom_tagname, define_tagname}; +use crate::cores::tag_value::{Empty, I32, ReadOnlyUnParsed, Text, Time, U32, WsUuid}; +use crate::tag; pub trait TagName { const TAG_NAME: &'static str; @@ -14,131 +14,91 @@ pub trait TagName { } } -// ========================== -// PowerShell Remoting Shell (rsp namespace) -// ========================== -define_tagname!(ShellId, Some(Namespace::WsmanShell.uri())); -define_tagname!(Name, Some(Namespace::WsmanShell.uri())); -define_tagname!(ResourceUri, Some(Namespace::WsmanShell.uri())); -define_tagname!(Owner, Some(Namespace::WsmanShell.uri())); -define_tagname!(ClientIP, Some(Namespace::WsmanShell.uri())); -define_tagname!(ProcessId, Some(Namespace::WsmanShell.uri())); -define_tagname!(IdleTimeOut, Some(Namespace::WsmanShell.uri())); -define_tagname!(InputStreams, Some(Namespace::WsmanShell.uri())); -define_tagname!(OutputStreams, Some(Namespace::WsmanShell.uri())); -define_tagname!(MaxIdleTimeOut, Some(Namespace::WsmanShell.uri())); -define_tagname!(CompressionMode, Some(Namespace::WsmanShell.uri())); -define_tagname!(ProfileLoaded, Some(Namespace::WsmanShell.uri())); -define_tagname!(Encoding, Some(Namespace::WsmanShell.uri())); -define_tagname!(BufferMode, Some(Namespace::WsmanShell.uri())); -define_tagname!(State, Some(Namespace::WsmanShell.uri())); -define_tagname!(ShellRunTime, Some(Namespace::WsmanShell.uri())); -define_tagname!(ShellInactivity, Some(Namespace::WsmanShell.uri())); -define_tagname!(CompressionType, Some(Namespace::WsmanShell.uri())); -define_tagname!(DesiredStream, Some(Namespace::WsmanShell.uri())); +// Leaf-valued tags live here (their value types are all in `cores`). Tags whose +// value is a domain struct (Shell = ShellValue, Body = SoapBody, …) are defined +// with `tag!` next to that struct instead — see the rsp/soap/ws_* modules. -define_custom_tagname!( - CreationXml, - "creationXml", - Some(Namespace::PowerShellRemoting.uri()) -); +// ============================================================ +// PowerShell Remoting Shell (rsp namespace) +// ============================================================ +tag!(ShellId = Text<'a> => WsmanShell); +tag!(Name = Text<'a> => WsmanShell); +tag!(ResourceUri = Text<'a> => WsmanShell); +tag!(Owner = Text<'a> => WsmanShell); +tag!(ClientIP = Text<'a> => WsmanShell); +tag!(ProcessId = Text<'a> => WsmanShell); +tag!(IdleTimeOut = Time => WsmanShell); +tag!(InputStreams = Text<'a> => WsmanShell); +tag!(OutputStreams = Text<'a> => WsmanShell); +tag!(MaxIdleTimeOut = Text<'a> => WsmanShell); +tag!(CompressionMode = Text<'a> => WsmanShell); +tag!(ProfileLoaded = Text<'a> => WsmanShell); +tag!(Encoding = Text<'a> => WsmanShell); +tag!(BufferMode = Text<'a> => WsmanShell); +tag!(State = Text<'a> => WsmanShell); +tag!(ShellRunTime = Text<'a> => WsmanShell); +tag!(ShellInactivity = Text<'a> => WsmanShell); +tag!(CompressionType = Text<'a> => WsmanShell); +tag!(DesiredStream = Text<'a> => WsmanShell); +tag!(Stream = Text<'a> => WsmanShell); +tag!(ExitCode = I32 => WsmanShell); +tag!(CommandId = WsUuid => WsmanShell); +tag!(CommandResponse = CommandId<'a> => WsmanShell); // wraps a single CommandId child +tag!(Command = Text<'a> => WsmanShell); +tag!(Arguments = Text<'a> => WsmanShell); +tag!(DisconnectResponse = Empty => WsmanShell); +tag!(Reconnect = Empty => WsmanShell); +tag!(ReconnectResponse = Empty => WsmanShell); +tag!(SignalCode = "Code": Text<'a> => WsmanShell); +tag!(Signal = SignalCode<'a> => WsmanShell); // wraps a single Code child +tag!(SignalResponse = Empty => WsmanShell); -define_tagname!(CommandLine, Some(Namespace::WsmanShell.uri())); -define_tagname!(Shell, Some(Namespace::WsmanShell.uri())); -define_tagname!(Command, Some(Namespace::WsmanShell.uri())); -define_tagname!(Receive, Some(Namespace::WsmanShell.uri())); -define_tagname!(ReceiveResponse, Some(Namespace::WsmanShell.uri())); -define_tagname!(CommandResponse, Some(Namespace::WsmanShell.uri())); -define_tagname!(CommandId, Some(Namespace::WsmanShell.uri())); -define_tagname!(Stream, Some(Namespace::WsmanShell.uri())); -define_tagname!(CommandState, Some(Namespace::WsmanShell.uri())); -define_tagname!(ExitCode, Some(Namespace::WsmanShell.uri())); -define_tagname!(Send, Some(Namespace::WsmanShell.uri())); -define_tagname!(Disconnect, Some(Namespace::WsmanShell.uri())); -define_tagname!(DisconnectResponse, Some(Namespace::WsmanShell.uri())); -define_tagname!(Reconnect, Some(Namespace::WsmanShell.uri())); -define_tagname!(ReconnectResponse, Some(Namespace::WsmanShell.uri())); -define_tagname!(Connect, Some(Namespace::WsmanShell.uri())); -define_tagname!(ConnectResponse, Some(Namespace::WsmanShell.uri())); -define_custom_tagname!( - ConnectXml, - "connectXml", - Some(Namespace::PowerShellRemoting.uri()) -); -define_custom_tagname!( - ConnectResponseXml, - "connectResponseXml", - Some(Namespace::PowerShellRemoting.uri()) -); -define_tagname!(Signal, Some(Namespace::WsmanShell.uri())); -define_tagname!(SignalResponse, Some(Namespace::WsmanShell.uri())); -define_custom_tagname!(SignalCode, "Code", Some(Namespace::WsmanShell.uri())); -define_tagname!(Arguments, Some(Namespace::WsmanShell.uri())); +tag!(CreationXml = "creationXml": Text<'a> => PowerShellRemoting); +tag!(ConnectXml = "connectXml": Text<'a> => PowerShellRemoting); +tag!(ConnectResponseXml = "connectResponseXml": Text<'a> => PowerShellRemoting); -// ==================== +// ============================================================ // WS-Addressing (a namespace) -// ==================== -define_tagname!(Action, Some(Namespace::WsAddressing2004.uri())); -define_tagname!(To, Some(Namespace::WsAddressing2004.uri())); -define_tagname!(MessageID, Some(Namespace::WsAddressing2004.uri())); -define_tagname!(RelatesTo, Some(Namespace::WsAddressing2004.uri())); -define_tagname!(ReplyTo, Some(Namespace::WsAddressing2004.uri())); -define_tagname!(Address, Some(Namespace::WsAddressing2004.uri())); -define_tagname!(ReferenceParameters, Some(Namespace::WsAddressing2004.uri())); +// ============================================================ +tag!(Action = Text<'a> => WsAddressing2004); +tag!(To = Text<'a> => WsAddressing2004); +tag!(MessageID = WsUuid => WsAddressing2004); +tag!(RelatesTo = WsUuid => WsAddressing2004); +tag!(Address = Text<'a> => WsAddressing2004); -// ============= +// ============================================================ // SOAP (s namespace) -// ============= -define_tagname!(Envelope, Some(Namespace::SoapEnvelope2003.uri())); -define_tagname!(Header, Some(Namespace::SoapEnvelope2003.uri())); -define_tagname!(Body, Some(Namespace::SoapEnvelope2003.uri())); - -// SOAP Fault elements -define_tagname!(Fault, Some(Namespace::SoapEnvelope2003.uri())); -define_tagname!(Code, Some(Namespace::SoapEnvelope2003.uri())); -define_tagname!(Reason, Some(Namespace::SoapEnvelope2003.uri())); -define_tagname!(Detail, Some(Namespace::SoapEnvelope2003.uri())); -define_tagname!(Subcode, Some(Namespace::SoapEnvelope2003.uri())); -define_custom_tagname!(SoapValue, "Value", Some(Namespace::SoapEnvelope2003.uri())); -define_custom_tagname!(SoapText, "Text", Some(Namespace::SoapEnvelope2003.uri())); +// ============================================================ +tag!(Detail = ReadOnlyUnParsed<'a> => SoapEnvelope2003); +tag!(SoapValue = "Value": Text<'a> => SoapEnvelope2003); +tag!(SoapText = "Text": Text<'a> => SoapEnvelope2003); -// =============================== +// ============================================================ // WS-Management DMTF (w namespace) -// =============================== -define_tagname!(Identify, Some(Namespace::DmtfWsmanSchema.uri())); -define_tagname!(Get, Some(Namespace::DmtfWsmanSchema.uri())); -define_tagname!(Put, Some(Namespace::DmtfWsmanSchema.uri())); -define_tagname!(Delete, Some(Namespace::DmtfWsmanSchema.uri())); -define_tagname!(Enumerate, Some(Namespace::DmtfWsmanSchema.uri())); -define_tagname!(Pull, Some(Namespace::DmtfWsmanSchema.uri())); -define_tagname!(Release, Some(Namespace::DmtfWsmanSchema.uri())); -define_tagname!(GetStatus, Some(Namespace::DmtfWsmanSchema.uri())); +// ============================================================ +tag!(Identify = Empty => DmtfWsmanSchema); +tag!(Get = Text<'a> => DmtfWsmanSchema); +tag!(Put = Text<'a> => DmtfWsmanSchema); +tag!(Delete = Text<'a> => DmtfWsmanSchema); +tag!(Enumerate = ReadOnlyUnParsed<'a> => DmtfWsmanSchema); +tag!(ResourceURI = Text<'a> => DmtfWsmanSchema); +tag!(OperationTimeout = Time => DmtfWsmanSchema); +tag!(MaxEnvelopeSize = U32 => DmtfWsmanSchema); +tag!(Selector = Text<'a> => DmtfWsmanSchema); +tag!(OptionTagName = "Option": Empty => DmtfWsmanSchema); +tag!(LocaleEmpty = "Locale": Empty => DmtfWsmanSchema); +tag!(LocaleText = "Locale": Text<'a> => DmtfWsmanSchema); -// WS-Management DMTF Headers (w namespace) -define_tagname!(ResourceURI, Some(Namespace::DmtfWsmanSchema.uri())); -define_tagname!(OperationTimeout, Some(Namespace::DmtfWsmanSchema.uri())); -define_tagname!(MaxEnvelopeSize, Some(Namespace::DmtfWsmanSchema.uri())); -define_tagname!(SelectorSet, Some(Namespace::DmtfWsmanSchema.uri())); -define_tagname!(OptionSet, Some(Namespace::DmtfWsmanSchema.uri())); -define_tagname!(Locale, Some(Namespace::DmtfWsmanSchema.uri())); -define_tagname!(Selector, Some(Namespace::DmtfWsmanSchema.uri())); -define_custom_tagname!( - OptionTagName, - "Option", - Some(Namespace::DmtfWsmanSchema.uri()) -); - -// =================================== +// ============================================================ // WS-Transfer (x namespace) -// =================================== -define_tagname!(Create, Some(Namespace::WsTransfer2004.uri())); - -define_tagname!(ResourceCreated, Some(Namespace::WsTransfer2004.uri())); +// ============================================================ +tag!(Create = Text<'a> => WsTransfer2004); -// ==================================== +// ============================================================ // Microsoft WS-Management (p namespace) -// ==================================== -define_tagname!(SequenceId, Some(Namespace::MsWsmanSchema.uri())); -define_tagname!(OperationID, Some(Namespace::MsWsmanSchema.uri())); -define_tagname!(SessionId, Some(Namespace::MsWsmanSchema.uri())); -define_tagname!(DataLocale, Some(Namespace::MsWsmanSchema.uri())); +// ============================================================ +tag!(SequenceId = Text<'a> => MsWsmanSchema); +tag!(OperationID = WsUuid => MsWsmanSchema); +tag!(SessionId = WsUuid => MsWsmanSchema); +tag!(DataLocaleEmpty = "DataLocale": Empty => MsWsmanSchema); +tag!(DataLocaleText = "DataLocale": Text<'a> => MsWsmanSchema); diff --git a/crates/ironposh-winrm/src/cores/tag_value.rs b/crates/ironposh-winrm/src/cores/tag_value.rs index 60df28d..39ea8ce 100644 --- a/crates/ironposh-winrm/src/cores/tag_value.rs +++ b/crates/ironposh-winrm/src/cores/tag_value.rs @@ -8,6 +8,22 @@ pub trait TagValue<'a> { fn append_to_element(self, element: Element<'a>) -> Element<'a>; } +/// The text content of a leaf element, rejecting mixed content. A text-valued +/// element (`Text`, `WsUuid`, `Time`, numerics) must not contain child elements; +/// silently truncating such malformed input would let it slip through. +pub(crate) fn leaf_text<'a>(node: Node<'a, 'a>) -> Result<&'a str, XmlError> { + // Only text children are allowed. Any element (mixed content), comment, or PI + // is rejected — otherwise `node.text()` (which yields the first child only when + // it is a text node) could silently shadow or drop the real value. + if node.children().any(|child| !child.is_text()) { + return Err(XmlError::InvalidXml(format!( + "<{}> is a text leaf but contains non-text content", + node.tag_name().name() + ))); + } + Ok(node.text().unwrap_or("").trim()) +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Text<'a>(Cow<'a, str>); @@ -49,7 +65,7 @@ impl<'a> TagValue<'a> for Text<'a> { impl<'a> FromXml<'a> for Text<'a> { fn from_xml(node: Node<'a, 'a>) -> Result { - Ok(Self(node.text().unwrap_or("").trim().into())) + Ok(Self(leaf_text(node)?.into())) } } @@ -69,7 +85,15 @@ impl<'a> TagValue<'a> for Empty { } impl<'a> FromXml<'a> for Empty { - fn from_xml(_node: Node<'a, 'a>) -> Result { + fn from_xml(node: Node<'a, 'a>) -> Result { + // `leaf_text` rejects any non-text child (element/comment/PI); an empty + // tag additionally must have no non-whitespace text. + if !leaf_text(node)?.is_empty() { + return Err(XmlError::InvalidXml(format!( + "<{}> must be empty but has text content", + node.tag_name().name() + ))); + } Ok(Self) } } @@ -96,7 +120,7 @@ impl<'a> TagValue<'a> for WsUuid { impl<'a> FromXml<'a> for WsUuid { fn from_xml(node: Node<'a, 'a>) -> Result { - let text = node.text().unwrap_or("").trim(); + let text = leaf_text(node)?; // WS-Management prefixes UUIDs with "uuid:" — strip it if present. let raw = text.strip_prefix("uuid:").unwrap_or(text); uuid::Uuid::parse_str(raw) @@ -135,7 +159,7 @@ impl<'a> TagValue<'a> for Time { impl<'a> FromXml<'a> for Time { fn from_xml(node: Node<'a, 'a>) -> Result { // WS-Management timeout format: "PT180.000S". - let text = node.text().unwrap_or("").trim(); + let text = leaf_text(node)?; let seconds = text .strip_prefix("PT") .and_then(|s| s.strip_suffix('S')) diff --git a/crates/ironposh-winrm/src/macros.rs b/crates/ironposh-winrm/src/macros.rs index ff0ce87..fa495c5 100644 --- a/crates/ironposh-winrm/src/macros.rs +++ b/crates/ironposh-winrm/src/macros.rs @@ -1,40 +1,38 @@ +/// Defines a WinRM/SOAP tag. +/// +/// Generates a zero-sized `TagName` marker (`Tag`) that pins the element's +/// wire name + namespace at the type level, plus an ergonomic type alias +/// `Alias<'a> = Tag<'a, Value, AliasTag>` used in struct fields and construction. +/// The marker stays internal; everything else writes the alias. +/// +/// Forms: +/// - `tag!(Get = Text<'a> => DmtfWsmanSchema)` — wire name is the alias ident. +/// - `tag!(SoapValue = "Value": Text<'a> => SoapEnvelope2003)` — custom wire name +/// (and lets several aliases share one wire name, e.g. `LocaleEmpty`/`LocaleText`). #[macro_export] -macro_rules! define_custom_tagname { - ($name:ident, $tagName:expr, $namespace:expr) => { - #[derive(Debug, Clone, PartialEq, Eq)] - pub struct $name; - - impl $crate::cores::TagName for $name { - const TAG_NAME: &'static str = $tagName; - const NAMESPACE: Option<&'static str> = $namespace; - - fn tag_name(&self) -> &'static str { - Self::TAG_NAME - } +macro_rules! tag { + ($alias:ident = $value:ty => $ns:ident) => { + $crate::tag!(@build $alias, stringify!($alias), $value, $ns); + }; + ($alias:ident = $wire:literal : $value:ty => $ns:ident) => { + $crate::tag!(@build $alias, $wire, $value, $ns); + }; + (@build $alias:ident, $wire:expr, $value:ty, $ns:ident) => { + paste::paste! { + #[derive(Debug, Clone, PartialEq, Eq)] + pub struct [<$alias Tag>]; - fn namespace(&self) -> Option<&'static str> { - Self::NAMESPACE + impl $crate::cores::TagName for [<$alias Tag>] { + const TAG_NAME: &'static str = $wire; + const NAMESPACE: Option<&'static str> = + ::core::option::Option::Some($crate::cores::Namespace::$ns.uri()); } - } - impl<'a> $name { - pub fn new_tag(value: V) -> $crate::cores::tag::Tag<'a, V, Self> - where - V: $crate::cores::TagValue<'a>, - { - $crate::cores::tag::Tag::new(value) - } + pub type $alias<'a> = $crate::cores::Tag<'a, $value, [<$alias Tag>]>; } }; } -#[macro_export] -macro_rules! define_tagname { - ($name:ident, $namespace:expr) => { - $crate::define_custom_tagname!($name, stringify!($name), $namespace); - }; -} - #[macro_export] macro_rules! impl_tag_from { ($src:ty => $taggen:ty) => { @@ -64,7 +62,7 @@ macro_rules! xml_num_value { // ------------ Deserialize ------------- impl<'a> ironposh_xml::mapping::FromXml<'a> for $name { fn from_xml(node: ironposh_xml::parser::Node<'a, 'a>) -> Result { - let text = node.text().unwrap_or("").trim(); + let text = $crate::cores::tag_value::leaf_text(node)?; Ok($name(text.parse::<$inner>().map_err(|_| { ironposh_xml::XmlError::InvalidXml(format!("invalid {} value: {}", stringify!($name), text)) })?)) diff --git a/crates/ironposh-winrm/src/rsp/commandline.rs b/crates/ironposh-winrm/src/rsp/commandline.rs index 9f556ed..edb2894 100644 --- a/crates/ironposh-winrm/src/rsp/commandline.rs +++ b/crates/ironposh-winrm/src/rsp/commandline.rs @@ -1,9 +1,10 @@ use ironposh_xml::mapping::{FromXml, NodeExt}; -use crate::cores::{ - Tag, TagName, TagValue, Text, - tag_name::{Arguments, Command}, -}; +use crate::cores::tag_value::leaf_text; +use crate::cores::{ArgumentsTag, CommandTag, Tag, TagName, TagValue, Text}; +use crate::tag; + +tag!(CommandLine = CommandLineValue => WsmanShell); #[derive(Debug, Clone)] pub struct CommandLineValue { @@ -16,10 +17,12 @@ impl TagValue<'_> for CommandLineValue { self, mut element: ironposh_xml::builder::Element, ) -> ironposh_xml::builder::Element { + // `Command` carries either nothing (``) or text, so it's built + // explicitly from the marker rather than a single-value alias. let command_element = self.command.map_or_else( - || Tag::from_name(Command).with_value(()).into_element(), + || Tag::from_name(CommandTag).with_value(()).into_element(), |cmd| { - Tag::from_name(Command) + Tag::from_name(CommandTag) .with_value(Text::from(cmd)) .into_element() }, @@ -28,7 +31,7 @@ impl TagValue<'_> for CommandLineValue { element = element.add_child(command_element); for arg in self.arguments { - let arg_element = Tag::from_name(Arguments) + let arg_element = Tag::from_name(ArgumentsTag) .with_value(Text::from(arg)) .into_element(); element = element.add_child(arg_element); @@ -41,14 +44,22 @@ impl TagValue<'_> for CommandLineValue { impl<'a> FromXml<'a> for CommandLineValue { fn from_xml(node: ironposh_xml::parser::Node<'a, 'a>) -> Result { let mut command = None; + let mut seen_command = false; let mut arguments = Vec::new(); for child in node.children() { - if child.is_element_named(Command::NAMESPACE, Command::TAG_NAME) { - command = child.text().map(ToString::to_string); - } else if child.is_element_named(Arguments::NAMESPACE, Arguments::TAG_NAME) - && let Some(text) = child.text() - { - arguments.push(text.to_string()); + if child.is_element_named(CommandTag::NAMESPACE, CommandTag::TAG_NAME) { + if seen_command { + return Err(ironposh_xml::XmlError::InvalidXml( + "duplicate in CommandLine".into(), + )); + } + seen_command = true; + // An empty `` is "no command", matching how the + // serializer's `None` path emits it. + let text = leaf_text(child)?; + command = (!text.is_empty()).then(|| text.to_string()); + } else if child.is_element_named(ArgumentsTag::NAMESPACE, ArgumentsTag::TAG_NAME) { + arguments.push(leaf_text(child)?.to_string()); } } Ok(Self { command, arguments }) diff --git a/crates/ironposh-winrm/src/rsp/connect.rs b/crates/ironposh-winrm/src/rsp/connect.rs index 8018c35..0c46d84 100644 --- a/crates/ironposh-winrm/src/rsp/connect.rs +++ b/crates/ironposh-winrm/src/rsp/connect.rs @@ -1,16 +1,17 @@ -use crate::cores::{ - Tag, Text, - tag_name::{ConnectResponseXml, ConnectXml}, -}; +use crate::cores::{ConnectResponseXml, ConnectXml}; +use crate::tag; use ironposh_macros::{FromXml, SimpleTagValue}; +tag!(Connect = ConnectValue<'a> => WsmanShell); +tag!(ConnectResponse = ConnectResponseValue<'a> => WsmanShell); + /// Value for the Connect element (MS-WSMV 3.1.4.15). /// /// Carries the base64 PSRP payload (SESSION_CAPABILITY + CONNECT_RUNSPACEPOOL) /// in a `connectXml` child element, analogous to `creationXml` on shell create. #[derive(Debug, Clone, SimpleTagValue, FromXml)] pub struct ConnectValue<'a> { - pub connect_xml: Tag<'a, Text<'a>, ConnectXml>, + pub connect_xml: ConnectXml<'a>, } /// Value for the ConnectResponse element (MS-WSMV 3.1.4.15). @@ -20,5 +21,5 @@ pub struct ConnectValue<'a> { /// can surface a descriptive error instead of failing the whole envelope parse. #[derive(Debug, Clone, SimpleTagValue, FromXml)] pub struct ConnectResponseValue<'a> { - pub connect_response_xml: Option, ConnectResponseXml>>, + pub connect_response_xml: Option>, } diff --git a/crates/ironposh-winrm/src/rsp/disconnect.rs b/crates/ironposh-winrm/src/rsp/disconnect.rs index 4082e44..43f2c9a 100644 --- a/crates/ironposh-winrm/src/rsp/disconnect.rs +++ b/crates/ironposh-winrm/src/rsp/disconnect.rs @@ -1,12 +1,15 @@ -use crate::cores::{Tag, Time, tag_name::IdleTimeOut}; +use crate::cores::IdleTimeOut; +use crate::tag; use ironposh_macros::{FromXml, SimpleTagValue}; +tag!(Disconnect = DisconnectValue<'a> => WsmanShell); + /// Value for the Disconnect element (MS-WSMV 3.1.4.13). /// Optionally carries an IdleTimeOut serialized as `PT{seconds}S`. #[derive(Debug, Clone, typed_builder::TypedBuilder, SimpleTagValue, FromXml)] pub struct DisconnectValue<'a> { #[builder(default, setter(strip_option, into))] - pub idle_time_out: Option>, + pub idle_time_out: Option>, } #[cfg(test)] diff --git a/crates/ironposh-winrm/src/rsp/receive.rs b/crates/ironposh-winrm/src/rsp/receive.rs index 02ee4f9..aa39cd8 100644 --- a/crates/ironposh-winrm/src/rsp/receive.rs +++ b/crates/ironposh-winrm/src/rsp/receive.rs @@ -1,17 +1,22 @@ use ironposh_macros::{FromXml, SimpleTagValue}; use crate::cores::{ - CommandState, DesiredStream, ExitCode, Stream, Tag, TagName, TagValue, Text, tag_value, + DesiredStream, DesiredStreamTag, ExitCode, Stream, StreamTag, TagName, TagValue, }; +use crate::tag; use ironposh_xml::{ XmlError, builder::Element, mapping::{FromXml, NodeExt}, }; +tag!(Receive = ReceiveValue<'a> => WsmanShell); +tag!(ReceiveResponse = ReceiveResponseValue<'a> => WsmanShell); +tag!(CommandState = CommandStateValue<'a> => WsmanShell); + #[derive(Debug, Clone, typed_builder::TypedBuilder)] pub struct ReceiveValue<'a> { - pub desired_streams: Vec, DesiredStream>>, + pub desired_streams: Vec>, } impl<'a> TagValue<'a> for ReceiveValue<'a> { @@ -27,8 +32,8 @@ impl<'a> FromXml<'a> for ReceiveValue<'a> { fn from_xml(node: ironposh_xml::parser::Node<'a, 'a>) -> Result { let mut desired_streams = Vec::new(); for child in node.children() { - if child.is_element_named(DesiredStream::NAMESPACE, DesiredStream::TAG_NAME) { - desired_streams.push(Tag::from_xml(child)?); + if child.is_element_named(DesiredStreamTag::NAMESPACE, DesiredStreamTag::TAG_NAME) { + desired_streams.push(DesiredStream::from_xml(child)?); } } Ok(ReceiveValue { desired_streams }) @@ -81,14 +86,14 @@ impl TryFrom<&str> for CommandStateValueState { #[derive(Debug, Clone, SimpleTagValue, FromXml)] pub struct CommandStateValue<'a> { - pub exit_code: Option>, + pub exit_code: Option>, } // ReceiveResponse main structure #[derive(Debug, Clone, typed_builder::TypedBuilder)] pub struct ReceiveResponseValue<'a> { - pub streams: Vec, Stream>>, - pub command_state: Option, CommandState>>, + pub streams: Vec>, + pub command_state: Option>, } impl<'a> TagValue<'a> for ReceiveResponseValue<'a> { @@ -106,10 +111,16 @@ impl<'a> FromXml<'a> for ReceiveResponseValue<'a> { let mut streams = Vec::new(); let mut command_state = None; for child in node.children() { - if child.is_element_named(Stream::NAMESPACE, Stream::TAG_NAME) { - streams.push(Tag::from_xml(child)?); - } else if child.is_element_named(CommandState::NAMESPACE, CommandState::TAG_NAME) { - command_state = Some(Tag::from_xml(child)?); + if child.is_element_named(StreamTag::NAMESPACE, StreamTag::TAG_NAME) { + streams.push(Stream::from_xml(child)?); + } else if child.is_element_named(CommandStateTag::NAMESPACE, CommandStateTag::TAG_NAME) + { + if command_state.is_some() { + return Err(XmlError::InvalidXml( + "duplicate in ReceiveResponse".into(), + )); + } + command_state = Some(CommandState::from_xml(child)?); } } Ok(ReceiveResponseValue { diff --git a/crates/ironposh-winrm/src/rsp/send.rs b/crates/ironposh-winrm/src/rsp/send.rs index b607234..3606e63 100644 --- a/crates/ironposh-winrm/src/rsp/send.rs +++ b/crates/ironposh-winrm/src/rsp/send.rs @@ -1,18 +1,22 @@ -use crate::cores::{ - Tag, TagValue, Text, - tag_name::{Stream, TagName}, -}; +use crate::cores::{Stream, StreamTag, TagName, TagValue}; +use crate::tag; use ironposh_xml::{ XmlError, builder::Element, mapping::{FromXml, NodeExt}, }; +// NOTE: this `Send` type alias shares its spelling with `std::marker::Send`. +// They live in different namespaces (type alias vs trait), so they don't +// collide here — but avoid writing a bare `Send` trait bound in modules that +// import this alias. +tag!(Send = SendValue<'a> => WsmanShell); + /// Value for Send element containing multiple Stream elements /// Each Stream contains a base64-encoded PSRP fragment #[derive(Debug, Clone, typed_builder::TypedBuilder)] pub struct SendValue<'a> { - pub streams: Vec, Stream>>, + pub streams: Vec>, } impl<'a> TagValue<'a> for SendValue<'a> { @@ -29,8 +33,8 @@ impl<'a> FromXml<'a> for SendValue<'a> { fn from_xml(node: ironposh_xml::parser::Node<'a, 'a>) -> Result { let mut streams = Vec::new(); for child in node.children() { - if child.is_element_named(Stream::NAMESPACE, Stream::TAG_NAME) { - streams.push(Tag::from_xml(child)?); + if child.is_element_named(StreamTag::NAMESPACE, StreamTag::TAG_NAME) { + streams.push(Stream::from_xml(child)?); } } Ok(SendValue { streams }) diff --git a/crates/ironposh-winrm/src/rsp/shell_value.rs b/crates/ironposh-winrm/src/rsp/shell_value.rs index 77a5acd..8227451 100644 --- a/crates/ironposh-winrm/src/rsp/shell_value.rs +++ b/crates/ironposh-winrm/src/rsp/shell_value.rs @@ -1,58 +1,53 @@ use crate::cores::{ - Tag, Text, Time, - tag_name::{ - BufferMode, ClientIP, CompressionMode, CreationXml, DataLocale, Encoding, IdleTimeOut, - InputStreams, Locale, MaxIdleTimeOut, Name, OutputStreams, Owner, ProcessId, ProfileLoaded, - ResourceUri, ShellId, ShellInactivity, ShellRunTime, State, - }, + BufferMode, ClientIP, CompressionMode, CreationXml, DataLocaleText, Encoding, IdleTimeOut, + InputStreams, LocaleText, MaxIdleTimeOut, Name, OutputStreams, Owner, ProcessId, ProfileLoaded, + ResourceUri, ShellId, ShellInactivity, ShellRunTime, State, }; +use crate::tag; use ironposh_macros::{FromXml, SimpleTagValue}; -// The XmlTagContainer derive macro generates: -// - TagValue implementation -// - ShellValueVisitor struct -// - XmlVisitor implementation for ShellValueVisitor -// - XmlDeserialize implementation +tag!(Shell = ShellValue<'a> => WsmanShell); + #[derive(Debug, Clone, typed_builder::TypedBuilder, SimpleTagValue, FromXml)] pub struct ShellValue<'a> { #[builder(default, setter(strip_option, into))] - pub shell_id: Option, ShellId>>, + pub shell_id: Option>, #[builder(default, setter(strip_option, into))] - pub name: Option, Name>>, + pub name: Option>, #[builder(default, setter(strip_option, into))] - pub resource_uri: Option, ResourceUri>>, + pub resource_uri: Option>, #[builder(default, setter(strip_option, into))] - pub owner: Option, Owner>>, + pub owner: Option>, #[builder(default, setter(strip_option, into))] - pub client_ip: Option, ClientIP>>, + pub client_ip: Option>, #[builder(default, setter(strip_option, into))] - pub process_id: Option, ProcessId>>, + pub process_id: Option>, #[builder(default, setter(strip_option(fallback_suffix = "_opt"), into))] - pub idle_time_out: Option>, + pub idle_time_out: Option>, #[builder(default, setter(strip_option, into))] - pub input_streams: Option, InputStreams>>, + pub input_streams: Option>, #[builder(default, setter(strip_option, into))] - pub output_streams: Option, OutputStreams>>, + pub output_streams: Option>, #[builder(default, setter(strip_option, into))] - pub max_idle_time_out: Option, MaxIdleTimeOut>>, + pub max_idle_time_out: Option>, #[builder(default, setter(strip_option, into))] - pub locale: Option, Locale>>, + pub locale: Option>, #[builder(default, setter(strip_option, into))] - pub data_locale: Option, DataLocale>>, + pub data_locale: Option>, #[builder(default, setter(strip_option, into))] - pub compression_mode: Option, CompressionMode>>, + pub compression_mode: Option>, #[builder(default, setter(strip_option, into))] - pub profile_loaded: Option, ProfileLoaded>>, + pub profile_loaded: Option>, #[builder(default, setter(strip_option, into))] - pub encoding: Option, Encoding>>, + pub encoding: Option>, #[builder(default, setter(strip_option, into))] - pub buffer_mode: Option, BufferMode>>, + pub buffer_mode: Option>, #[builder(default, setter(strip_option, into))] - pub state: Option, State>>, + pub state: Option>, #[builder(default, setter(strip_option, into))] - pub shell_run_time: Option, ShellRunTime>>, + pub shell_run_time: Option>, #[builder(default, setter(strip_option, into))] - pub shell_inactivity: Option, ShellInactivity>>, + pub shell_inactivity: Option>, #[builder(default, setter(strip_option, into))] - pub creation_xml: Option, CreationXml>>, + pub creation_xml: Option>, } diff --git a/crates/ironposh-winrm/src/soap/body.rs b/crates/ironposh-winrm/src/soap/body.rs index c3da76e..5be75c6 100644 --- a/crates/ironposh-winrm/src/soap/body.rs +++ b/crates/ironposh-winrm/src/soap/body.rs @@ -1,70 +1,76 @@ use ironposh_macros::{FromXml, SimpleTagValue}; +use crate::tag; use crate::{ - cores::*, + cores::{ + CommandResponse, Create, Delete, DisconnectResponse, Enumerate, Get, Identify, Put, + Reconnect, ReconnectResponse, Signal, SignalResponse, + }, rsp::{ - commandline::CommandLineValue, - connect::{ConnectResponseValue, ConnectValue}, - disconnect::DisconnectValue, - receive::{ReceiveResponseValue, ReceiveValue}, - send::SendValue, - shell_value::ShellValue, + commandline::CommandLine, + connect::{Connect, ConnectResponse}, + disconnect::Disconnect, + receive::{Receive, ReceiveResponse}, + send::Send, + shell_value::Shell, }, - soap::fault::SoapFaultValue, - ws_management::body::ResourceCreatedValue, + soap::fault::Fault, + ws_management::body::ResourceCreated, }; +tag!(Body = SoapBody<'a> => SoapEnvelope2003); + #[derive(Debug, Clone, typed_builder::TypedBuilder, SimpleTagValue, FromXml)] pub struct SoapBody<'a> { /// WS-Management operations #[builder(default, setter(into, strip_option))] - pub identify: Option>, + pub identify: Option>, #[builder(default, setter(into, strip_option))] - pub get: Option, Get>>, + pub get: Option>, #[builder(default, setter(into, strip_option))] - pub put: Option, Put>>, + pub put: Option>, #[builder(default, setter(into, strip_option))] - pub create: Option, Create>>, + pub create: Option>, #[builder(default, setter(into, strip_option))] - pub delete: Option, Delete>>, + pub delete: Option>, #[builder(default, setter(into, strip_option))] - pub enumerate: Option, Enumerate>>, + pub enumerate: Option>, /// WS-Transfer operations #[builder(default, setter(into, strip_option))] - pub resource_created: Option, ResourceCreated>>, + pub resource_created: Option>, /// PowerShell Remoting operations #[builder(default, setter(into, strip_option))] - pub shell: Option, Shell>>, + pub shell: Option>, #[builder(default, setter(into, strip_option))] - pub command_line: Option>, + pub command_line: Option>, #[builder(default, setter(into, strip_option))] - pub receive: Option, Receive>>, + pub receive: Option>, #[builder(default, setter(into, strip_option))] - pub receive_response: Option, ReceiveResponse>>, + pub receive_response: Option>, #[builder(default, setter(into, strip_option))] - pub command_response: Option, CommandResponse>>, + pub command_response: Option>, #[builder(default, setter(into, strip_option))] - pub send: Option, Send>>, + pub send: Option>, #[builder(default, setter(into, strip_option))] - pub signal: Option, SignalCode>, Signal>>, + pub signal: Option>, #[builder(default, setter(into, strip_option))] - pub signal_response: Option>, + pub signal_response: Option>, #[builder(default, setter(into, strip_option))] - pub disconnect: Option, Disconnect>>, + pub disconnect: Option>, #[builder(default, setter(into, strip_option))] - pub disconnect_response: Option>, + pub disconnect_response: Option>, #[builder(default, setter(into, strip_option))] - pub reconnect: Option>, + pub reconnect: Option>, #[builder(default, setter(into, strip_option))] - pub reconnect_response: Option>, + pub reconnect_response: Option>, #[builder(default, setter(into, strip_option))] - pub connect: Option, Connect>>, + pub connect: Option>, #[builder(default, setter(into, strip_option))] - pub connect_response: Option, ConnectResponse>>, + pub connect_response: Option>, /// SOAP fault handling #[builder(default, setter(into, strip_option))] - pub fault: Option, Fault>>, + pub fault: Option>, } diff --git a/crates/ironposh-winrm/src/soap/fault.rs b/crates/ironposh-winrm/src/soap/fault.rs index e4dce81..f2527d8 100644 --- a/crates/ironposh-winrm/src/soap/fault.rs +++ b/crates/ironposh-winrm/src/soap/fault.rs @@ -1,36 +1,42 @@ -use crate::cores::*; +use crate::cores::{Detail, SoapText, SoapValue}; +use crate::tag; use ironposh_macros::{FromXml, SimpleTagValue}; // SOAP Fault structures for handling SOAP error responses +tag!(Fault = SoapFaultValue<'a> => SoapEnvelope2003); +tag!(Code = SoapFaultCodeValue<'a> => SoapEnvelope2003); +tag!(Subcode = SoapFaultSubcodeValue<'a> => SoapEnvelope2003); +tag!(Reason = SoapFaultReasonValue<'a> => SoapEnvelope2003); + #[derive(Debug, Clone, typed_builder::TypedBuilder, SimpleTagValue, FromXml)] pub struct SoapFaultValue<'a> { #[builder(default, setter(into, strip_option))] - pub code: Option, Code>>, + pub code: Option>, #[builder(default, setter(into, strip_option))] - pub reason: Option, Reason>>, + pub reason: Option>, #[builder(default, setter(into, strip_option))] - pub detail: Option, Detail>>, + pub detail: Option>, } #[derive(Debug, Clone, typed_builder::TypedBuilder, SimpleTagValue, FromXml)] pub struct SoapFaultCodeValue<'a> { #[builder(default, setter(into, strip_option))] - pub value: Option, SoapValue>>, + pub value: Option>, #[builder(default, setter(into, strip_option))] - pub subcode: Option, Subcode>>, + pub subcode: Option>, } #[derive(Debug, Clone, typed_builder::TypedBuilder, SimpleTagValue, FromXml)] pub struct SoapFaultSubcodeValue<'a> { #[builder(default, setter(into, strip_option))] - pub value: Option, SoapValue>>, + pub value: Option>, } #[derive(Debug, Clone, typed_builder::TypedBuilder, SimpleTagValue, FromXml)] pub struct SoapFaultReasonValue<'a> { #[builder(default, setter(into, strip_option))] - pub text: Option, SoapText>>, + pub text: Option>, } impl SoapFaultValue<'_> { diff --git a/crates/ironposh-winrm/src/soap/header.rs b/crates/ironposh-winrm/src/soap/header.rs index efec08d..5133ed0 100644 --- a/crates/ironposh-winrm/src/soap/header.rs +++ b/crates/ironposh-winrm/src/soap/header.rs @@ -1,8 +1,12 @@ -use crate::{ - cores::*, - ws_addressing::AddressValue, - ws_management::{OptionSetValue, SelectorSetValue}, +use crate::cores::{ + Action, CompressionType, DataLocaleEmpty, LocaleEmpty, MaxEnvelopeSize, MessageID, OperationID, + OperationTimeout, RelatesTo, ResourceURI, SequenceId, SessionId, To, }; +use crate::tag; +use crate::ws_addressing::ReplyTo; +use crate::ws_management::{OptionSet, SelectorSet}; + +tag!(Header = SoapHeaders<'a> => SoapEnvelope2003); #[derive( Debug, @@ -14,37 +18,41 @@ use crate::{ pub struct SoapHeaders<'a> { /// WS-Addressing headers #[builder(default, setter(into, strip_option))] - pub to: Option, To>>, + pub to: Option>, #[builder(default, setter(into, strip_option))] - pub action: Option, Action>>, + pub action: Option>, #[builder(default, setter(into, strip_option))] - pub reply_to: Option, ReplyTo>>, + pub reply_to: Option>, #[builder(default, setter(into, strip_option))] - pub message_id: Option>, + pub message_id: Option>, #[builder(default, setter(into, strip_option))] - pub relates_to: Option>, + pub relates_to: Option>, /// WS-Management headers #[builder(default, setter(into, strip_option))] - pub resource_uri: Option, ResourceURI>>, + pub resource_uri: Option>, #[builder(default, setter(into, strip_option))] - pub max_envelope_size: Option>, + pub max_envelope_size: Option>, + // `Locale`/`DataLocale` are empty-bodied in headers, hence the `*Empty` + // aliases. They share a wire name with the text-bodied `*Text` variants used + // in ShellValue; since both resolve to the same `(URI, "Locale")`, the field + // alias chosen here is what determines how an inbound element is parsed. #[builder(default, setter(into, strip_option))] - pub locale: Option>, + pub locale: Option>, #[builder(default, setter(into, strip_option))] - pub data_locale: Option>, + pub data_locale: Option>, #[builder(default, setter(into, strip_option))] - pub session_id: Option>, + pub session_id: Option>, #[builder(default, setter(into, strip_option))] - pub operation_id: Option>, + pub operation_id: Option>, #[builder(default, setter(into, strip_option))] - pub sequence_id: Option, SequenceId>>, + pub sequence_id: Option>, #[builder(default, setter(into, strip_option(fallback_suffix = "_opt")))] - pub option_set: Option>, + pub option_set: Option>, #[builder(default, setter(into, strip_option(fallback_suffix = "_opt")))] - pub selector_set: Option>, + pub selector_set: Option>, #[builder(default, setter(into, strip_option))] - pub operation_timeout: Option>, + pub operation_timeout: Option>, #[builder(default, setter(into, strip_option))] - pub compression_type: Option, CompressionType>>, + pub compression_type: Option>, } diff --git a/crates/ironposh-winrm/src/soap/mod.rs b/crates/ironposh-winrm/src/soap/mod.rs index 1c50247..1d2c261 100644 --- a/crates/ironposh-winrm/src/soap/mod.rs +++ b/crates/ironposh-winrm/src/soap/mod.rs @@ -5,17 +5,18 @@ pub mod parsing; use ironposh_macros::FromXml; -use crate::{ - cores::{Tag, TagValue, tag_name::*}, - soap::{body::SoapBody, header::SoapHeaders}, -}; +use crate::cores::TagValue; +use crate::tag; +use crate::{soap::body::Body, soap::header::Header}; + +tag!(Envelope = SoapEnvelope<'a> => SoapEnvelope2003); #[derive(Debug, Clone, typed_builder::TypedBuilder, FromXml)] pub struct SoapEnvelope<'a> { #[builder(default, setter(into, strip_option))] - pub header: Option, Header>>, + pub header: Option>, #[builder(setter(into))] - pub body: Tag<'a, SoapBody<'a>, Body>, + pub body: Body<'a>, } impl<'a> TagValue<'a> for SoapEnvelope<'a> { diff --git a/crates/ironposh-winrm/src/test_macro.rs b/crates/ironposh-winrm/src/test_macro.rs index b0a0fd7..048ccb6 100644 --- a/crates/ironposh-winrm/src/test_macro.rs +++ b/crates/ironposh-winrm/src/test_macro.rs @@ -1,132 +1,60 @@ -use crate::cores::tag_name::*; -use crate::cores::{Tag, Text}; +use crate::cores::Text; +use crate::tag; use ironposh_macros::{FromXml, SimpleTagValue}; -// Example struct with mixed required and optional fields using new derive macros +// Self-contained tags for exercising the SimpleTagValue + FromXml derives. +tag!(ReqA = Text<'a> => WsAddressing2004); +tag!(ReqB = Text<'a> => WsAddressing2004); +tag!(OptA = Text<'a> => WsAddressing2004); +tag!(OptB = Text<'a> => WsAddressing2004); + +// Example struct with mixed required and optional fields using the derives. #[derive(Debug, Clone, SimpleTagValue, FromXml)] pub struct TestStruct<'a> { - pub action: Tag<'a, Text<'a>, Action>, // Required - pub message_id: Tag<'a, Text<'a>, MessageID>, // Required - pub to: Option, To>>, // Optional - pub relates_to: Option, RelatesTo>>, // Optional + pub req_a: ReqA<'a>, // Required + pub req_b: ReqB<'a>, // Required + pub opt_a: Option>, // Optional + pub opt_b: Option>, // Optional } #[cfg(test)] mod tests { use super::*; - use crate::cores::TagValue; + use crate::cores::{Tag, TagValue}; use ironposh_xml::mapping::FromXml; + const A: &str = "http://schemas.xmlsoap.org/ws/2004/08/addressing"; + #[test] fn test_serialization_and_deserialization_roundtrip() { - // Create a test struct with both required and optional fields let original = TestStruct { - action: Tag::new(Text::from("test-action")), - message_id: Tag::new(Text::from("msg-123")), - to: Some(Tag::new(Text::from("destination"))), - relates_to: None, // This optional field is not set + req_a: Tag::new(Text::from("a-value")), + req_b: Tag::new(Text::from("b-value")), + opt_a: Some(Tag::new(Text::from("opt-a-value"))), + opt_b: None, }; - // Test that the TagValue implementation works (serialize to XML) + // Serialize via the TagValue derive (must not panic). let element = ironposh_xml::builder::Element::new("test"); - let _serialized_element = original.append_to_element(element); - - // The TagValue implementation worked if we got here without panicking - - // Test deserialization. Action/MessageID/To live in the WS-Addressing - // namespace, so the document must declare it — matching is by URI. - let test_xml = r#" - test-action - msg-123 - destination - "#; - - // Parse the XML back - let doc = ironposh_xml::parser::parse(test_xml).expect("Failed to parse XML"); - let root = doc.root_element(); - - // Deserialize back to struct - let deserialized = TestStruct::from_xml(root).expect("Failed to deserialize"); - - println!("Deserialized struct: {deserialized:#?}"); - - // Verify deserialization matches original - assert_eq!(deserialized.action.value.as_ref(), "test-action"); - assert_eq!(deserialized.message_id.value.as_ref(), "msg-123"); - assert!(deserialized.to.is_some()); - assert_eq!(deserialized.to.unwrap().value.as_ref(), "destination"); - assert!(deserialized.relates_to.is_none()); - } - - #[test] - fn test_deserialize_with_all_fields() { - let xml = r#" - - test-action - msg-123 - destination - relation-123 - - "#; - - let doc = ironposh_xml::parser::parse(xml).expect("Failed to parse XML"); - let root = doc.root_element(); + let _serialized = original.append_to_element(element); - let result = TestStruct::from_xml(root).expect("Failed to deserialize"); - println!("Deserialized with all fields: {result:#?}"); - - // Verify required fields - assert_eq!(result.action.value.as_ref(), "test-action"); - assert_eq!(result.message_id.value.as_ref(), "msg-123"); - - // Verify optional fields - assert!(result.to.is_some()); - assert_eq!(result.to.unwrap().value.as_ref(), "destination"); - assert!(result.relates_to.is_some()); - assert_eq!(result.relates_to.unwrap().value.as_ref(), "relation-123"); - } - - #[test] - fn test_deserialize_with_only_required_fields() { - let xml = r#" - - test-action - msg-123 - - "#; - - let doc = ironposh_xml::parser::parse(xml).expect("Failed to parse XML"); - let root = doc.root_element(); - - let result = TestStruct::from_xml(root).expect("Failed to deserialize"); - println!("Deserialized with required fields only: {result:#?}"); - - // Verify required fields - assert_eq!(result.action.value.as_ref(), "test-action"); - assert_eq!(result.message_id.value.as_ref(), "msg-123"); + // Deserialize a namespaced document — matching is by URI. + let xml = format!( + r#"a-valueb-valueopt-a-value"# + ); + let doc = ironposh_xml::parser::parse(&xml).expect("parse"); + let parsed = TestStruct::from_xml(doc.root_element()).expect("deserialize"); - // Verify optional fields are None - assert!(result.to.is_none()); - assert!(result.relates_to.is_none()); + assert_eq!(parsed.req_a.value.as_ref(), "a-value"); + assert_eq!(parsed.req_b.value.as_ref(), "b-value"); + assert_eq!(parsed.opt_a.unwrap().value.as_ref(), "opt-a-value"); + assert!(parsed.opt_b.is_none()); } #[test] fn test_deserialize_missing_required_field() { - let xml = r#" - - test-action - - - "#; - - let doc = ironposh_xml::parser::parse(xml).expect("Failed to parse XML"); - let root = doc.root_element(); - - let result = TestStruct::from_xml(root); - assert!(result.is_err()); - println!( - "Expected error for missing required field: {:?}", - result.err() - ); + let xml = format!(r#"only-a"#); + let doc = ironposh_xml::parser::parse(&xml).expect("parse"); + assert!(TestStruct::from_xml(doc.root_element()).is_err()); } } diff --git a/crates/ironposh-winrm/src/ws_addressing/mod.rs b/crates/ironposh-winrm/src/ws_addressing/mod.rs index cc33e4a..4ab1396 100644 --- a/crates/ironposh-winrm/src/ws_addressing/mod.rs +++ b/crates/ironposh-winrm/src/ws_addressing/mod.rs @@ -1,10 +1,13 @@ use ironposh_macros::{FromXml, SimpleTagValue}; -use crate::cores::{Tag, tag_name::*, tag_value::Text}; +use crate::cores::Address; +use crate::tag; + +tag!(ReplyTo = AddressValue<'a> => WsAddressing2004); #[derive(Debug, Clone, SimpleTagValue, FromXml)] pub struct AddressValue<'a> { - pub url: Tag<'a, Text<'a>, Address>, + pub url: Address<'a>, } // impl<'a> TagValue<'a> for AddressValue<'a> { diff --git a/crates/ironposh-winrm/src/ws_management/body.rs b/crates/ironposh-winrm/src/ws_management/body.rs index 4bcddd3..bbfd51b 100644 --- a/crates/ironposh-winrm/src/ws_management/body.rs +++ b/crates/ironposh-winrm/src/ws_management/body.rs @@ -1,10 +1,12 @@ use ironposh_macros::{FromXml, SimpleTagValue}; use ironposh_xml::builder::Element; -use crate::{ - cores::{ResourceURI, SelectorSet, Tag, TagValue, tag_name::*, tag_value::Text}, - ws_management::SelectorSetValue, -}; +use crate::cores::{Address, ResourceURI, TagValue, tag_value::Text}; +use crate::tag; +use crate::ws_management::SelectorSet; + +tag!(ReferenceParameters = ReferenceParametersValue<'a> => WsAddressing2004); +tag!(ResourceCreated = ResourceCreatedValue<'a> => WsTransfer2004); // Enumeration operations #[derive(Debug, Clone, Default)] @@ -127,14 +129,14 @@ impl<'a> TagValue<'a> for GetStatusValue<'a> { #[derive(Debug, Clone, SimpleTagValue, FromXml)] pub struct ReferenceParametersValue<'a> { - pub resource_uri: Tag<'a, Text<'a>, ResourceURI>, - pub selector_set: Tag<'a, SelectorSetValue, SelectorSet>, + pub resource_uri: ResourceURI<'a>, + pub selector_set: SelectorSet<'a>, } #[derive(Debug, Clone, SimpleTagValue, FromXml)] pub struct ResourceCreatedValue<'a> { - pub address: Tag<'a, Text<'a>, Address>, - pub reference_parameters: Tag<'a, ReferenceParametersValue<'a>, ReferenceParameters>, + pub address: Address<'a>, + pub reference_parameters: ReferenceParameters<'a>, } #[cfg(test)] @@ -169,7 +171,7 @@ mod tests { let element = ironposh_xml::parser::parse(xml).unwrap(); let root = element.root_element(); - let tag: Tag<'_, ResourceCreatedValue, ResourceCreated> = Tag::from_xml(root).unwrap(); + let tag = ResourceCreated::from_xml(root).unwrap(); let value = tag.value; assert_eq!( diff --git a/crates/ironposh-winrm/src/ws_management/header.rs b/crates/ironposh-winrm/src/ws_management/header.rs index e31bc60..738cd50 100644 --- a/crates/ironposh-winrm/src/ws_management/header.rs +++ b/crates/ironposh-winrm/src/ws_management/header.rs @@ -5,7 +5,12 @@ use ironposh_xml::{ mapping::{FromXml, NodeExt}, }; -use crate::cores::{self, OptionTagName, Selector, Tag, TagName, TagValue, Text}; +use crate::cores::tag_value::leaf_text; +use crate::cores::{self, OptionTagNameTag, Selector, SelectorTag, TagName, TagValue, Text}; +use crate::tag; + +tag!(SelectorSet = SelectorSetValue => DmtfWsmanSchema); +tag!(OptionSet = OptionSetValue => DmtfWsmanSchema); #[derive(Debug, Clone, Default)] pub struct SelectorSetValue { @@ -39,8 +44,7 @@ impl SelectorSetValue { impl<'a> TagValue<'a> for SelectorSetValue { fn append_to_element(self, mut element: Element<'a>) -> Element<'a> { for (name, value) in self.selectors { - let selector = Tag::from_name(Selector) - .with_value(Text::from(value)) + let selector = Selector::new(Text::from(value)) .with_attribute(crate::cores::Attribute::Name(name.into())); let selector = selector.into_element(); @@ -56,13 +60,18 @@ impl<'a> FromXml<'a> for SelectorSetValue { fn from_xml(node: ironposh_xml::parser::Node<'a, 'a>) -> Result { let mut selectors = HashMap::new(); for child in node.children() { - if child.is_element_named(Selector::NAMESPACE, Selector::TAG_NAME) + if child.is_element_named(SelectorTag::NAMESPACE, SelectorTag::TAG_NAME) && let Some(name) = child .attributes() .find(|attr| attr.name() == "Name") .map(|attr| attr.value().to_string()) { - selectors.insert(name, child.text().unwrap_or_default().to_string()); + if selectors.contains_key(&name) { + return Err(ironposh_xml::XmlError::InvalidXml(format!( + "duplicate selector {name:?}" + ))); + } + selectors.insert(name, leaf_text(child)?.to_string()); } } Ok(Self { selectors }) @@ -99,7 +108,7 @@ impl<'a> TagValue<'a> for OptionSetValue { for (name, value) in self.options { let option_element = Element::new("Option") .set_namespace(ironposh_xml::builder::Namespace::from( - OptionTagName::NAMESPACE.expect("OptionTagName definately has a namespace"), + OptionTagNameTag::NAMESPACE.expect("OptionTagName definately has a namespace"), )) .set_text(value) .add_attribute(cores::Attribute::Name(name.into()).into()) @@ -115,13 +124,18 @@ impl<'a> FromXml<'a> for OptionSetValue { fn from_xml(node: ironposh_xml::parser::Node<'a, 'a>) -> Result { let mut options = HashMap::new(); for child in node.children() { - if child.is_element_named(OptionTagName::NAMESPACE, OptionTagName::TAG_NAME) + if child.is_element_named(OptionTagNameTag::NAMESPACE, OptionTagNameTag::TAG_NAME) && let Some(name) = child .attributes() .find(|attr| attr.name() == "Name") .map(|attr| attr.value().to_string()) { - options.insert(name, child.text().unwrap_or_default().to_string()); + if options.contains_key(&name) { + return Err(ironposh_xml::XmlError::InvalidXml(format!( + "duplicate option {name:?}" + ))); + } + options.insert(name, leaf_text(child)?.to_string()); } } Ok(Self { options }) diff --git a/crates/ironposh-winrm/src/ws_management/mod.rs b/crates/ironposh-winrm/src/ws_management/mod.rs index c3f932b..15a7d16 100644 --- a/crates/ironposh-winrm/src/ws_management/mod.rs +++ b/crates/ironposh-winrm/src/ws_management/mod.rs @@ -3,13 +3,8 @@ pub mod header; pub use header::*; use crate::{ - cores::{ - Attribute, Tag, Time, WsUuid, - namespace::Namespace, - tag_name::{Action, Envelope}, - tag_value::Text, - }, - soap::{SoapEnvelope, body::SoapBody, header::SoapHeaders}, + cores::{Action, Attribute, Tag, Time, WsUuid, namespace::Namespace, tag_value::Text}, + soap::{Envelope, SoapEnvelope, body::SoapBody, header::SoapHeaders}, ws_addressing::AddressValue, }; @@ -107,7 +102,7 @@ impl WsMan { resource_body: SoapBody<'a>, option_set: Option, selector_set: Option, - ) -> Tag<'a, SoapEnvelope<'a>, Envelope> { + ) -> Envelope<'a> { self.invoke_with_operation_timeout( action, resource_uri, @@ -126,7 +121,7 @@ impl WsMan { option_set: Option, selector_set: Option, operation_timeout_secs: Option, - ) -> Tag<'a, SoapEnvelope<'a>, Envelope> { + ) -> Envelope<'a> { // Generate a unique message ID and operation ID for this request let message_id = uuid::Uuid::new_v4(); let operation_id = uuid::Uuid::new_v4(); @@ -142,8 +137,7 @@ impl WsMan { // Create the SOAP header with all required fields let header = SoapHeaders::builder() .action( - Tag::new(action.as_str().to_owned()) - .with_name(Action) + Action::new(action.as_str().to_owned()) .with_attribute(Attribute::MustUnderstand(true)), ) .data_locale( @@ -189,7 +183,7 @@ impl WsMan { // Convert to XML using Tag wrapper with proper namespaces - let mut soap = Tag::::new(envelope) + let mut soap = Envelope::new(envelope) .with_declaration(Namespace::SoapEnvelope2003) .with_declaration(Namespace::WsAddressing2004) .with_declaration(Namespace::DmtfWsmanSchema) diff --git a/crates/ironposh-winrm/tests/test_connect_request.rs b/crates/ironposh-winrm/tests/test_connect_request.rs index 9660117..d8da4cf 100644 --- a/crates/ironposh-winrm/tests/test_connect_request.rs +++ b/crates/ironposh-winrm/tests/test_connect_request.rs @@ -1,6 +1,6 @@ use ironposh_winrm::{ - cores::{Namespace, Tag, Text, tag_name::*}, - rsp::connect::ConnectValue, + cores::{Namespace, Tag, Text}, + rsp::connect::{ConnectTag, ConnectValue}, soap::{SoapEnvelope, body::SoapBody}, ws_management::{SelectorSetValue, WsAction, WsMan}, }; @@ -29,7 +29,7 @@ fn test_build_connect_envelope() { .with_declaration(Namespace::PowerShellRemoting), }; - let connect_tag = Tag::from_name(Connect) + let connect_tag = Tag::from_name(ConnectTag) .with_declaration(Namespace::WsmanShell) .with_value(connect_value); diff --git a/crates/ironposh-winrm/tests/test_disconnect_request.rs b/crates/ironposh-winrm/tests/test_disconnect_request.rs index b12c898..d349c5c 100644 --- a/crates/ironposh-winrm/tests/test_disconnect_request.rs +++ b/crates/ironposh-winrm/tests/test_disconnect_request.rs @@ -1,6 +1,6 @@ use ironposh_winrm::{ cores::{Empty, Namespace, Tag, Time, tag_name::*}, - rsp::disconnect::DisconnectValue, + rsp::disconnect::{DisconnectTag, DisconnectValue}, soap::{SoapEnvelope, body::SoapBody}, ws_management::{SelectorSetValue, WsAction, WsMan}, }; @@ -35,7 +35,7 @@ fn test_build_disconnect_envelope() { .idle_time_out(Tag::new(Time(180.0))) .build(); - let disconnect_tag = Tag::from_name(Disconnect) + let disconnect_tag = Tag::from_name(DisconnectTag) .with_declaration(Namespace::WsmanShell) .with_value(disconnect_value); @@ -78,7 +78,7 @@ fn test_build_disconnect_envelope_without_idle_timeout() { .to("http://10.10.0.3:5985/wsman".to_string()) .build(); - let disconnect_tag = Tag::from_name(Disconnect) + let disconnect_tag = Tag::from_name(DisconnectTag) .with_declaration(Namespace::WsmanShell) .with_value(DisconnectValue::builder().build()); @@ -107,7 +107,7 @@ fn test_build_reconnect_envelope() { .to("http://10.10.0.3:5985/wsman".to_string()) .build(); - let reconnect_tag = Tag::from_name(Reconnect) + let reconnect_tag = Tag::from_name(ReconnectTag) .with_declaration(Namespace::WsmanShell) .with_value(Empty); diff --git a/crates/ironposh-winrm/tests/test_initial_build_request.rs b/crates/ironposh-winrm/tests/test_initial_build_request.rs index 68fe7e4..0392704 100644 --- a/crates/ironposh-winrm/tests/test_initial_build_request.rs +++ b/crates/ironposh-winrm/tests/test_initial_build_request.rs @@ -1,9 +1,9 @@ use ironposh_winrm::{ cores::{Tag, tag_name::*, tag_value::Text}, - rsp::shell_value::ShellValue, - soap::{SoapEnvelope, body::SoapBody, header::SoapHeaders}, - ws_addressing::AddressValue, - ws_management::header::OptionSetValue, + rsp::shell_value::{ShellTag, ShellValue}, + soap::{Envelope, SoapEnvelope, body::SoapBody, header::SoapHeaders}, + ws_addressing::{AddressValue, ReplyToTag}, + ws_management::header::{OptionSetTag, OptionSetValue}, }; #[cfg(test)] @@ -26,7 +26,7 @@ mod tests { // Create shell tag with attributes let shell_tag = Tag::new(shell) - .with_name(Shell) + .with_name(ShellTag) .with_attribute(ironposh_winrm::cores::Attribute::ShellId( "2D6534D0-6B12-40E3-B773-CBA26459CFA8".into(), )) @@ -42,10 +42,10 @@ mod tests { url: Tag::new(Text::from( "http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous", )) - .with_name(Address) + .with_name(AddressTag) .with_attribute(ironposh_winrm::cores::Attribute::MustUnderstand(true)), }) - .with_name(ReplyTo) + .with_name(ReplyToTag) .with_attribute(ironposh_winrm::cores::Attribute::MustUnderstand(true)); // Build the complete SOAP envelope @@ -58,7 +58,7 @@ mod tests { Tag::new(Text::from( "http://schemas.xmlsoap.org/ws/2004/09/transfer/Create", )) - .with_name(Action) + .with_name(ActionTag) .with_attribute(ironposh_winrm::cores::Attribute::MustUnderstand(true)), ) .reply_to(reply_to_address) @@ -71,21 +71,21 @@ mod tests { Tag::new(Text::from( "http://schemas.microsoft.com/powershell/Microsoft.PowerShell", )) - .with_name(ResourceURI) + .with_name(ResourceURITag) .with_attribute(ironposh_winrm::cores::Attribute::MustUnderstand(true)), ) .max_envelope_size( Tag::new(512000) - .with_name(MaxEnvelopeSize) + .with_name(MaxEnvelopeSizeTag) .with_attribute(ironposh_winrm::cores::Attribute::MustUnderstand(true)), ) .locale( - Tag::new(()).with_name(Locale).with_attribute( + Tag::new(()).with_name(LocaleEmptyTag).with_attribute( ironposh_winrm::cores::Attribute::MustUnderstand(false), ), ) .data_locale( - Tag::new(()).with_name(DataLocale).with_attribute( + Tag::new(()).with_name(DataLocaleEmptyTag).with_attribute( ironposh_winrm::cores::Attribute::MustUnderstand(false), ), ) @@ -94,7 +94,7 @@ mod tests { uuid::Uuid::from_str("9EC885D6-F5A4-4771-9D47-4BDF7DAAEA8C") .expect("Failed to parse UUID"), ) - .with_name(SessionId) + .with_name(SessionIdTag) .with_attribute(ironposh_winrm::cores::Attribute::MustUnderstand(false)), ) .operation_id( @@ -102,25 +102,25 @@ mod tests { uuid::Uuid::from_str("73C4BCA6-7FF0-4AFE-B8C3-335FB19BA649") .expect("Failed to parse UUID"), ) - .with_name(OperationID) + .with_name(OperationIDTag) .with_attribute(ironposh_winrm::cores::Attribute::MustUnderstand(false)), ) .sequence_id( Tag::new(Text::from("1")) - .with_name(SequenceId) + .with_name(SequenceIdTag) .with_attribute(ironposh_winrm::cores::Attribute::MustUnderstand( false, )), ) .option_set( Tag::new(option_set_tag) - .with_name(OptionSet) + .with_name(OptionSetTag) .with_attribute(ironposh_winrm::cores::Attribute::MustUnderstand(true)), ) .operation_timeout(Time::from(60000)) .compression_type( Tag::new(Text::from("xpress")) - .with_name(CompressionType) + .with_name(CompressionTypeTag) .with_attribute(ironposh_winrm::cores::Attribute::MustUnderstand(true)), ) .build(), @@ -129,7 +129,7 @@ mod tests { .build(); // Convert envelope to Tag and add namespace declarations - let envelope: Tag<'_, _, Envelope> = envelope.into(); + let envelope: Envelope<'_> = envelope.into(); let envelope = envelope .with_declaration(ironposh_winrm::cores::namespace::Namespace::SoapEnvelope2003) .with_declaration(ironposh_winrm::cores::namespace::Namespace::WsAddressing2004) diff --git a/crates/ironposh-winrm/tests/test_receive_fix.rs b/crates/ironposh-winrm/tests/test_receive_fix.rs index bb8ba2c..ba1648e 100644 --- a/crates/ironposh-winrm/tests/test_receive_fix.rs +++ b/crates/ironposh-winrm/tests/test_receive_fix.rs @@ -1,6 +1,6 @@ use ironposh_winrm::{ - cores::{DesiredStream, Receive, Tag, Text}, - rsp::receive::ReceiveValue, + cores::{DesiredStreamTag, Tag, Text}, + rsp::receive::{ReceiveTag, ReceiveValue}, soap::SoapEnvelope, }; use ironposh_xml::{builder::Element, mapping::FromXml}; @@ -14,13 +14,13 @@ fn test_receive_with_single_desired_stream() { // Create a ReceiveValue with single DesiredStream containing space-separated streams let receive = ReceiveValue::builder() .desired_streams(vec![ - Tag::from_name(DesiredStream) + Tag::from_name(DesiredStreamTag) .with_value(Text::from("stdout stderr")) .with_attribute(ironposh_winrm::cores::Attribute::CommandId(command_id)), ]) .build(); - let receive_tag = Tag::from_name(Receive) + let receive_tag = Tag::from_name(ReceiveTag) .with_value(receive) .with_declaration(ironposh_winrm::cores::Namespace::WsmanShell); @@ -50,11 +50,11 @@ fn test_receive_shell_level_without_command_id() { // Test case: Shell-level receive without CommandId let receive = ReceiveValue::builder() .desired_streams(vec![ - Tag::from_name(DesiredStream).with_value(Text::from("stdout stderr")), + Tag::from_name(DesiredStreamTag).with_value(Text::from("stdout stderr")), ]) .build(); - let receive_tag = Tag::from_name(Receive) + let receive_tag = Tag::from_name(ReceiveTag) .with_value(receive) .with_declaration(ironposh_winrm::cores::Namespace::WsmanShell); diff --git a/crates/ironposh-winrm/tests/test_send_multiple_fragments.rs b/crates/ironposh-winrm/tests/test_send_multiple_fragments.rs index 28ff58d..2293a7d 100644 --- a/crates/ironposh-winrm/tests/test_send_multiple_fragments.rs +++ b/crates/ironposh-winrm/tests/test_send_multiple_fragments.rs @@ -1,6 +1,6 @@ use ironposh_winrm::{ - cores::{Attribute, Send, Tag, Text, tag_name::Stream}, - rsp::send::SendValue, + cores::{Attribute, StreamTag, Tag, Text}, + rsp::send::{SendTag, SendValue}, }; use ironposh_xml::builder::Element; use uuid::Uuid; @@ -19,10 +19,10 @@ fn test_send_with_multiple_stream_fragments() { let command_id = Uuid::new_v4(); // Create Stream tags for each fragment - let streams: Vec> = fragments + let streams: Vec> = fragments .iter() .map(|fragment| { - Tag::from_name(Stream) + Tag::from_name(StreamTag) .with_value(Text::from(fragment.as_str())) .with_attribute(Attribute::Name("stdin".into())) }) @@ -32,7 +32,7 @@ fn test_send_with_multiple_stream_fragments() { let send_value = SendValue::builder().streams(streams).build(); // Create Send tag with CommandId - let send_tag = Tag::from_name(Send) + let send_tag = Tag::from_name(SendTag) .with_value(send_value) .with_attribute(Attribute::CommandId(command_id)) .with_declaration(ironposh_winrm::cores::Namespace::WsmanShell); @@ -86,10 +86,10 @@ fn test_send_with_multiple_stream_fragments() { fn test_send_without_command_id() { let fragments = ["FRAGMENT1==".to_string(), "FRAGMENT2==".to_string()]; - let streams: Vec> = fragments + let streams: Vec> = fragments .iter() .map(|fragment| { - Tag::from_name(Stream) + Tag::from_name(StreamTag) .with_value(Text::from(fragment.as_str())) .with_attribute(Attribute::Name("stdin".into())) }) @@ -97,7 +97,7 @@ fn test_send_without_command_id() { let send_value = SendValue::builder().streams(streams).build(); - let send_tag = Tag::from_name(Send) + let send_tag = Tag::from_name(SendTag) .with_value(send_value) .with_declaration(ironposh_winrm::cores::Namespace::WsmanShell); @@ -128,10 +128,10 @@ fn test_send_large_response_multiple_fragments() { let command_id = Uuid::new_v4(); - let streams: Vec> = fragments + let streams: Vec> = fragments .iter() .map(|fragment| { - Tag::from_name(Stream) + Tag::from_name(StreamTag) .with_value(Text::from(fragment.as_str())) .with_attribute(Attribute::Name("stdin".into())) }) @@ -139,7 +139,7 @@ fn test_send_large_response_multiple_fragments() { let send_value = SendValue::builder().streams(streams).build(); - let send_tag = Tag::from_name(Send) + let send_tag = Tag::from_name(SendTag) .with_value(send_value) .with_attribute(Attribute::CommandId(command_id)) .with_declaration(ironposh_winrm::cores::Namespace::WsmanShell);