diff --git a/crates/ironposh-macros/src/lib.rs b/crates/ironposh-macros/src/lib.rs index d6b468b..232f71d 100644 --- a/crates/ironposh-macros/src/lib.rs +++ b/crates/ironposh-macros/src/lib.rs @@ -1291,10 +1291,10 @@ fn impl_from_xml(input: &DeriveInput) -> TokenStream2 { .map(|field| { let field_name = field.ident.as_ref().unwrap().clone(); let is_optional = is_option_type(&field.ty); - let tag_name_type = extract_tag_name_type(&field.ty); + let value_type = inner_value_type(&field.ty); SimpleFieldEntry { field_name, - tag_name_type, + value_type, is_optional, } }) @@ -1305,19 +1305,19 @@ fn impl_from_xml(input: &DeriveInput) -> TokenStream2 { quote! { let mut #f = None; } }); - // One namespace-correct match per field: identity is (URI, local-name). - let matchers = entries.iter().filter_map(|e| { + // 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. + let matchers = entries.iter().map(|e| { let f = &e.field_name; - e.tag_name_type.as_ref().map(|n| { - quote! { - if child.is_element_named( - ::NAMESPACE, - ::TAG_NAME, - ) { - #f = Some(ironposh_xml::mapping::FromXml::from_xml(child)?); - } + let ty = &e.value_type; + quote! { + if child.is_element_named( + <#ty as crate::cores::NamedTag>::NAMESPACE, + <#ty as crate::cores::NamedTag>::TAG_NAME, + ) { + #f = Some(ironposh_xml::mapping::FromXml::from_xml(child)?); } - }) + } }); let construct = entries.iter().map(|e| { @@ -1419,7 +1419,7 @@ fn impl_simple_tag_value(input: &DeriveInput) -> TokenStream2 { struct SimpleFieldEntry { field_name: Ident, - tag_name_type: Option, + value_type: Type, is_optional: bool, } @@ -1437,55 +1437,18 @@ fn is_option_type(ty: &Type) -> bool { false } -fn extract_tag_name_type(ty: &Type) -> Option { - // Try to extract TagName from Tag<'a, ValueType, TagName> or Option> - if let Type::Path(TypePath { path, .. }) = ty { - for segment in &path.segments { - if segment.ident == "Tag" || segment.ident == "Option" { - if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { - // For Option>, we need to look at the inner type - for arg in &args.args { - if let syn::GenericArgument::Type(inner_type) = arg { - if let Some(tag_name) = extract_tag_name_from_tag_type(inner_type) { - return Some(tag_name); - } - } - } - - // For Tag<'a, ValueType, TagName>, the third argument is TagName - if segment.ident == "Tag" && args.args.len() >= 3 { - if let syn::GenericArgument::Type(Type::Path(TypePath { path, .. })) = - &args.args[2] - { - if let Some(segment) = path.segments.last() { - return Some(segment.ident.clone()); - } - } - } - } - } - } - } - None -} - -fn extract_tag_name_from_tag_type(ty: &Type) -> Option { +/// The value a field carries: `Option` -> `T`, otherwise the type itself. +fn inner_value_type(ty: &Type) -> Type { if let Type::Path(TypePath { path, .. }) = ty { - for segment in &path.segments { - if segment.ident == "Tag" { + if let Some(segment) = path.segments.last() { + if segment.ident == "Option" { if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { - if args.args.len() >= 3 { - if let syn::GenericArgument::Type(Type::Path(TypePath { path, .. })) = - &args.args[2] - { - if let Some(segment) = path.segments.last() { - return Some(segment.ident.clone()); - } - } + if let Some(syn::GenericArgument::Type(inner)) = args.args.first() { + return inner.clone(); } } } } } - None + ty.clone() } diff --git a/crates/ironposh-winrm/src/cores/anytag.rs b/crates/ironposh-winrm/src/cores/anytag.rs deleted file mode 100644 index b331095..0000000 --- a/crates/ironposh-winrm/src/cores/anytag.rs +++ /dev/null @@ -1,147 +0,0 @@ -use crate::{ - cores::{Tag, TagList, TagName, tag_name::*, tag_value::Text}, - rsp::{receive::ReceiveValue, shell_value::ShellValue}, -}; - -#[macro_export] -macro_rules! define_any_tag { - ($enum_name:ident, $(($variant:ident, $tag_name:ty, $tag_type:ty)),* $(,)?) => { - #[expect(clippy::large_enum_variant)] - #[derive(Debug, Clone)] - pub enum $enum_name<'a> { - $($variant($tag_type),)* - } - - $( - impl<'a> std::convert::TryInto<$tag_type> for AnyTag<'a> { - type Error = ironposh_xml::XmlError; - - fn try_into(self) -> Result<$tag_type, Self::Error> { - match self { - $enum_name::$variant(tag) => Ok(tag), - _ => Err(ironposh_xml::XmlError::InvalidXml(format!( - "Cannot convert {:?} to any tag type", - self - ))), - } - } - } - - impl<'a> std::convert::From<$tag_type> for $enum_name<'a> { - fn from(tag: $tag_type) -> Self { - $enum_name::$variant(tag) - } - } - )* - - - impl<'a> $enum_name<'a> { - pub fn into_element(self) -> ironposh_xml::builder::Element<'a> { - match self { - $($enum_name::$variant(tag) => tag.into_element(),)* - } - } - } - - impl<'a> ironposh_xml::mapping::FromXml<'a> for $enum_name<'a> { - fn from_xml(node: ironposh_xml::parser::Node<'a, 'a>) -> Result { - use ironposh_xml::mapping::NodeExt; - // Dispatch by (namespace-URI, local-name) — prefix is irrelevant. - $( - if node.is_element_named(<$tag_name>::NAMESPACE, <$tag_name>::TAG_NAME) { - return Ok($enum_name::$variant( - <$tag_type as ironposh_xml::mapping::FromXml>::from_xml(node)?, - )); - } - )* - Err(ironposh_xml::XmlError::InvalidXml(format!( - "Unknown tag: {} (namespace: {:?})", - node.tag_name().name(), - node.tag_name().namespace() - ))) - } - } - }; -} - -// Define the `AnyTag` enum for the following purposes: -// Right now, since we are having all tag and tag name definations defined in compile time (i.e Sized, no dynamic trait objects), -// Hence, for the purpose of having a dynamic representation of any tag, we define `AnyTag`. -// This will allow us to have a single enum that can represent any tag type, which can be useful for generic processing of XML documents. -// while perserving the compile-time safety of tag definitions. -define_any_tag!( - AnyTag, - // SOAP elements - (Envelope, Envelope, Tag<'a, TagList<'a>, Envelope>), - (Header, Header, Tag<'a, TagList<'a>, Header>), - (Body, Body, Tag<'a, TagList<'a>, Body>), - // WS-Addressing headers - (Action, Action, Tag<'a, Text<'a>, Action>), - (To, To, Tag<'a, Text<'a>, To>), - (MessageID, MessageID, Tag<'a, Text<'a>, MessageID>), - (RelatesTo, RelatesTo, Tag<'a, Text<'a>, RelatesTo>), - (ReplyTo, ReplyTo, Tag<'a, TagList<'a>, ReplyTo>), - (FaultTo, FaultTo, Tag<'a, Text<'a>, FaultTo>), - (From, From, Tag<'a, Text<'a>, From>), - (Address, Address, Tag<'a, Text<'a>, Address>), - // PowerShell remoting shell elements - (ShellId, ShellId, Tag<'a, Text<'a>, ShellId>), - (Shell, Shell, Tag<'a, ShellValue<'a>, Shell>), - (Receive, Receive, Tag<'a, ReceiveValue<'a>, Receive>), - (Name, Name, Tag<'a, Text<'a>, Name>), - (ResourceUri, ResourceUri, Tag<'a, Text<'a>, ResourceUri>), - (Owner, Owner, Tag<'a, Text<'a>, Owner>), - (ClientIP, ClientIP, Tag<'a, Text<'a>, ClientIP>), - (ProcessId, ProcessId, Tag<'a, Text<'a>, ProcessId>), - (IdleTimeOut, IdleTimeOut, Tag<'a, Text<'a>, IdleTimeOut>), - (InputStreams, InputStreams, Tag<'a, Text<'a>, InputStreams>), - ( - OutputStreams, - OutputStreams, - Tag<'a, Text<'a>, OutputStreams> - ), - ( - MaxIdleTimeOut, - MaxIdleTimeOut, - Tag<'a, Text<'a>, MaxIdleTimeOut> - ), - (Locale, Locale, Tag<'a, Text<'a>, Locale>), - (DataLocale, DataLocale, Tag<'a, Text<'a>, DataLocale>), - ( - CompressionMode, - CompressionMode, - Tag<'a, Text<'a>, CompressionMode> - ), - ( - ProfileLoaded, - ProfileLoaded, - Tag<'a, Text<'a>, ProfileLoaded> - ), - (Encoding, Encoding, Tag<'a, Text<'a>, Encoding>), - (BufferMode, BufferMode, Tag<'a, Text<'a>, BufferMode>), - (State, State, Tag<'a, Text<'a>, State>), - (ShellRunTime, ShellRunTime, Tag<'a, Text<'a>, ShellRunTime>), - ( - ShellInactivity, - ShellInactivity, - Tag<'a, Text<'a>, ShellInactivity> - ), - (CreationXml, CreationXml, Tag<'a, TagList<'a>, CreationXml>), - (Version, Version, Tag<'a, Text<'a>, Version>), - (BA, BA, Tag<'a, Text<'a>, BA>), - // PowerShell Serialization Format - (I32TagName, I32TagName, Tag<'a, Text<'a>, I32TagName>), // 32-bit integer - (TN, TN, Tag<'a, TagList<'a>, TN>), - (T, T, Tag<'a, Text<'a>, T>), - (ToString, ToString, Tag<'a, Text<'a>, ToString>), - (DCT, DCT, Tag<'a, TagList<'a>, DCT>), - (En, En, Tag<'a, TagList<'a>, En>), - (Key, Key, Tag<'a, Text<'a>, Key>), - (Value, Value, Tag<'a, Text<'a>, Value>), - (Nil, Nil, Tag<'a, Text<'a>, Nil>), - (B, B, Tag<'a, Text<'a>, B>), - (S, S, Tag<'a, Text<'a>, S>), - // Complex objects - (Obj, Obj, Tag<'a, TagList<'a>, Obj>), - (MS, MS, Tag<'a, TagList<'a>, MS>), -); diff --git a/crates/ironposh-winrm/src/cores/mod.rs b/crates/ironposh-winrm/src/cores/mod.rs index fcf11fe..7d5274c 100644 --- a/crates/ironposh-winrm/src/cores/mod.rs +++ b/crates/ironposh-winrm/src/cores/mod.rs @@ -1,14 +1,11 @@ -pub mod anytag; pub mod attribute; pub mod namespace; pub mod tag; -pub mod tag_list; pub mod tag_name; pub mod tag_value; pub use attribute::*; pub use namespace::*; pub use tag::*; -pub use tag_list::*; pub use tag_name::*; pub use tag_value::*; diff --git a/crates/ironposh-winrm/src/cores/tag.rs b/crates/ironposh-winrm/src/cores/tag.rs index 1388026..b579f58 100644 --- a/crates/ironposh-winrm/src/cores/tag.rs +++ b/crates/ironposh-winrm/src/cores/tag.rs @@ -166,17 +166,23 @@ where { fn from_xml(node: ironposh_xml::parser::Node<'a, 'a>) -> Result { // Identity is the (namespace-URI, local-name) pair; the prefix is never - // consulted. A dispatcher (a derived struct, AnyTag, or a TagList) has - // usually already matched this, so the check is a cheap self-validation. - if !node.is_element_named(N::NAMESPACE, N::TAG_NAME) { - return Err(ironposh_xml::XmlError::XmlInvalidTag { - expected: N::TAG_NAME.to_string(), - found: node.tag_name().name().to_string(), - }); - } + // consulted. Usually a dispatcher already matched this element, so `node` + // *is* this tag. When a parent tag carries another tag as its value + // (`Tag, _>`), we're handed the parent instead — descend to the + // single N-named child. + 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)) + .ok_or_else(|| ironposh_xml::XmlError::XmlInvalidTag { + expected: N::TAG_NAME.to_string(), + found: node.tag_name().name().to_string(), + })? + }; - let value = V::from_xml(node)?; - let attributes = node + let value = V::from_xml(element)?; + let attributes = element .attributes() .filter_map(|attr| { Attribute::from_name_and_value(attr.name(), attr.value()) @@ -184,7 +190,7 @@ where .flatten() }) .collect(); - let namespaces_declaration = NamespaceDeclaration::from_xml(node)?; + let namespaces_declaration = NamespaceDeclaration::from_xml(element)?; Ok(Tag { value, @@ -196,6 +202,25 @@ where } } +/// A tag type's XML identity (name + namespace) exposed at the type level. +/// +/// `Tag<'a, V, N>` forwards to its `N: TagName`. Reading identity through this +/// trait — rather than naming `N` syntactically — lets `#[derive(FromXml)]` +/// work through type aliases like `pub type Get<'a> = Tag<'a, Text<'a>, GetTag>`. +pub trait NamedTag { + const TAG_NAME: &'static str; + const NAMESPACE: Option<&'static str>; +} + +impl<'a, V, N> NamedTag for Tag<'a, V, N> +where + V: TagValue<'a>, + N: TagName, +{ + const TAG_NAME: &'static str = N::TAG_NAME; + const NAMESPACE: Option<&'static str> = N::NAMESPACE; +} + impl<'a, V, N> AsRef for Tag<'a, V, N> where V: TagValue<'a>, @@ -231,3 +256,28 @@ impl_tag_from!(&'a str => Tag<'a, Text<'a>, N>); impl_tag_from!(String => Tag<'a, Text<'a>, N>); impl_tag_from!(u32 => Tag<'a, U32, N>); impl_tag_from!(uuid::Uuid => Tag<'a, WsUuid, N>); + +#[cfg(test)] +mod tests { + use super::*; + use crate::cores::{CommandId, CommandResponse}; + use ironposh_xml::parser::parse; + + const RSP: &str = "http://schemas.microsoft.com/wbem/wsman/1/windows/shell"; + + /// A `Tag` whose value is itself a `Tag` (`` wrapping a + /// `` child). `from_xml` must descend to the named child rather + /// than parse the wrapper as the inner tag. Regression for the SSPI e2e. + #[test] + fn nested_tag_value_descends_to_child() { + let uuid = "2D6534D0-6B12-40E3-B773-CBA26459CFA8"; + let xml = format!( + 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"); + assert_eq!(tag.value.value.0.to_string().to_uppercase(), uuid); + } +} diff --git a/crates/ironposh-winrm/src/cores/tag_list.rs b/crates/ironposh-winrm/src/cores/tag_list.rs deleted file mode 100644 index f8d98bb..0000000 --- a/crates/ironposh-winrm/src/cores/tag_list.rs +++ /dev/null @@ -1,47 +0,0 @@ -use ironposh_xml::{builder::Element, mapping::FromXml}; - -use crate::cores::{TagValue, anytag::AnyTag}; - -// This is just a temporary struct to hold a list of tags. -// to replace the actual TagValue going to be implemented for tags -#[derive(Debug, Clone, Default)] -pub struct TagList<'a> { - items: Vec>, -} - -impl<'a> TagList<'a> { - pub fn new() -> Self { - Self::default() - } - - pub fn add_tag(&mut self, tag: AnyTag<'a>) { - self.items.push(tag); - } - - pub fn with_tag(mut self, tag: AnyTag<'a>) -> Self { - self.add_tag(tag); - self - } - - pub fn inner(self) -> Vec> { - self.items - } -} - -impl<'a> TagValue<'a> for TagList<'a> { - fn append_to_element(self, element: Element<'a>) -> Element<'a> { - element.add_children(self.items.into_iter().map(AnyTag::into_element).collect()) - } -} - -impl<'a> FromXml<'a> for TagList<'a> { - fn from_xml(node: ironposh_xml::parser::Node<'a, 'a>) -> Result { - let mut items = Vec::new(); - for child in node.children() { - if child.is_element() { - items.push(AnyTag::from_xml(child)?); - } - } - Ok(TagList { items }) - } -} diff --git a/crates/ironposh-winrm/src/cores/tag_name.rs b/crates/ironposh-winrm/src/cores/tag_name.rs index 4d56dad..0504c6e 100644 --- a/crates/ironposh-winrm/src/cores/tag_name.rs +++ b/crates/ironposh-winrm/src/cores/tag_name.rs @@ -83,8 +83,6 @@ 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!(FaultTo, Some(Namespace::WsAddressing2004.uri())); -define_tagname!(From, Some(Namespace::WsAddressing2004.uri())); define_tagname!(Address, Some(Namespace::WsAddressing2004.uri())); define_tagname!(ReferenceParameters, Some(Namespace::WsAddressing2004.uri())); @@ -120,7 +118,6 @@ define_tagname!(GetStatus, Some(Namespace::DmtfWsmanSchema.uri())); define_tagname!(ResourceURI, Some(Namespace::DmtfWsmanSchema.uri())); define_tagname!(OperationTimeout, Some(Namespace::DmtfWsmanSchema.uri())); define_tagname!(MaxEnvelopeSize, Some(Namespace::DmtfWsmanSchema.uri())); -define_tagname!(FragmentTransfer, 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())); @@ -145,31 +142,3 @@ 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())); - -// PowerShell Remoting Protocol; -define_tagname!(Obj, None); -define_tagname!(MS, None); -define_tagname!(Version, None); -define_tagname!(BA, None); - -// PowerShell Serialization Format -// define_tagname!(I32, None); // 32-bit integer -define_custom_tagname!(I32TagName, "I32", None); // 32-bit integer -define_tagname!(TN, None); // Type Name -define_tagname!(T, None); // Type -define_custom_tagname!(ToString, "ToString", None); // ToString representation -define_tagname!(DCT, None); // Dictionary -define_tagname!(En, None); // Dictionary Entry -define_tagname!(Key, None); // Dictionary Key -define_tagname!(Value, None); // Dictionary Value -define_tagname!(Nil, None); // Null Value -define_tagname!(B, None); // Boolean -define_tagname!(S, None); // String - -// PowerShell InitRunspacepool Message Tags -define_tagname!(MinRunspaces, None); // Minimum number of runspaces -define_tagname!(MaxRunspaces, None); // Maximum number of runspaces -define_tagname!(PSThreadOptions, None); // PowerShell thread options -define_tagname!(ApartmentState, None); // Apartment state for runspace -define_tagname!(HostInfo, None); // Host information -define_tagname!(ApplicationArguments, None); // Application arguments diff --git a/crates/ironposh-winrm/src/soap/body.rs b/crates/ironposh-winrm/src/soap/body.rs index fd11b4d..c3da76e 100644 --- a/crates/ironposh-winrm/src/soap/body.rs +++ b/crates/ironposh-winrm/src/soap/body.rs @@ -29,12 +29,6 @@ pub struct SoapBody<'a> { pub delete: Option, Delete>>, #[builder(default, setter(into, strip_option))] pub enumerate: Option, Enumerate>>, - #[builder(default, setter(into, strip_option))] - pub pull: Option, Pull>>, - #[builder(default, setter(into, strip_option))] - pub release: Option, Release>>, - #[builder(default, setter(into, strip_option))] - pub get_status: Option, GetStatus>>, /// WS-Transfer operations #[builder(default, setter(into, strip_option))] @@ -44,8 +38,6 @@ pub struct SoapBody<'a> { #[builder(default, setter(into, strip_option))] pub shell: Option, Shell>>, #[builder(default, setter(into, strip_option))] - pub command: Option, Command>>, - #[builder(default, setter(into, strip_option))] pub command_line: Option>, #[builder(default, setter(into, strip_option))] pub receive: Option, Receive>>, diff --git a/crates/ironposh-winrm/tests/test_initial_build_request.rs b/crates/ironposh-winrm/tests/test_initial_build_request.rs index 4949d5a..68fe7e4 100644 --- a/crates/ironposh-winrm/tests/test_initial_build_request.rs +++ b/crates/ironposh-winrm/tests/test_initial_build_request.rs @@ -1,5 +1,5 @@ use ironposh_winrm::{ - cores::{Tag, TagList, tag_name::*, tag_value::Text}, + cores::{Tag, tag_name::*, tag_value::Text}, rsp::shell_value::ShellValue, soap::{SoapEnvelope, body::SoapBody, header::SoapHeaders}, ws_addressing::AddressValue, @@ -16,9 +16,6 @@ mod tests { #[test] fn test_build_soap_envelope_for_shell_creation() { - // Create an empty TagList for creation_xml (simplified for now) - let _creation_xml_content = TagList::new(); - // Build the Shell content for the body let shell = ShellValue::builder() .name("Runspace1") diff --git a/crates/ironposh-winrm/tests/test_parse_error_response.rs b/crates/ironposh-winrm/tests/test_parse_error_response.rs index 08bc1c3..bd99d71 100644 --- a/crates/ironposh-winrm/tests/test_parse_error_response.rs +++ b/crates/ironposh-winrm/tests/test_parse_error_response.rs @@ -110,10 +110,6 @@ mod tests { "Body should not have ResourceCreated in error" ); assert!(body.shell.is_none(), "Body should not have Shell in error"); - assert!( - body.command.is_none(), - "Body should not have Command in error" - ); assert!( body.receive.is_none(), "Body should not have Receive in error" diff --git a/crates/ironposh-winrm/tests/test_parse_receive_response.rs b/crates/ironposh-winrm/tests/test_parse_receive_response.rs index 5b05f19..497310d 100644 --- a/crates/ironposh-winrm/tests/test_parse_receive_response.rs +++ b/crates/ironposh-winrm/tests/test_parse_receive_response.rs @@ -140,7 +140,6 @@ mod tests { "Body should not have ResourceCreated" ); assert!(body.shell.is_none(), "Body should not have Shell"); - assert!(body.command.is_none(), "Body should not have Command"); assert!(body.receive.is_none(), "Body should not have Receive"); assert!( body.command_response.is_none(),