Skip to content

fix(winrm): descend into child for a tag whose value is a tag#34

Merged
irvingouj@Devolutions (irvingoujAtDevolution) merged 4 commits into
masterfrom
stack/xml-fromxml-nested-fix
Jun 25, 2026
Merged

fix(winrm): descend into child for a tag whose value is a tag#34
irvingouj@Devolutions (irvingoujAtDevolution) merged 4 commits into
masterfrom
stack/xml-fromxml-nested-fix

Conversation

@irvingoujAtDevolution

Copy link
Copy Markdown
Collaborator

Part 2/6. Base: stack/xml-fromxml-core-migrate (#33).

Fixes the Tag<Tag<..>, _> wrapper case (e.g. <CommandResponse> wrapping <CommandId>): from_xml must descend to the single named child rather than parse the wrapper as the inner tag. This was the SSPI e2e regression (matrix 4/10 → 10/10).

FromXml stack (review/merge bottom-up):

  1. core + winrm migration (feat(xml): namespace-correct FromXml core + winrm migration #33)
  2. nested-tag descend fix
  3. delete dead AnyTag/TagList + markers
  4. tag! macro + aliases
  5. harden / reject malformed XML
  6. tests

…sitor)

The deserialize side is where the visitor ceremony lived (accumulator
structs, finish(), NotSupposeToBeCalled). Replace it with a direct,
attribute-free derive:

- ironposh-xml `mapping`: `FromXml`/`ToXml` + `NodeExt` — element identity
  is the (namespace-URI, local-name) pair; the prefix is never compared.
- `#[derive(FromXml)]`: generates a flat `from_xml(node)` that matches each
  child by (URI, name) sourced from the field's `Tag<_, _, N>` type. No
  visitor struct, no finish(). Reuses the existing Tag/TagName types, so
  migrating a struct is a one-line derive add.

Proven on `DisconnectValue`: parses IdleTimeOut regardless of prefix, and
ignores a same-local-name child in the wrong namespace (the bug the old
visit_node had — it matched on local name only).

Additive: the existing XmlVisitor/SimpleXmlDeserialize path is untouched;
removing it across the winrm structs is the mechanical follow-up.
Deserialization across the WinRM/SOAP/WS-Management layer no longer uses the
accumulator-visitor pattern. Every type now implements a single direct
`ironposh_xml::mapping::FromXml::from_xml(node) -> Result<Self>`, and matching
is by `(namespace-URI, local-name)` via `NodeExt` — the prefix is never
compared, fixing the long-standing local-name-only matching bug.

Removed (no longer needed):
- `SimpleXmlDeserialize` derive + the `impl_xml_deserialize!` / `impl_tag_value!`
  macro_rules (the latter two were already dead).
- Every hand-written `XmlVisitor` impl: TagVisitor, TagList, AnyTag, the leaf
  value visitors (Text/Empty/WsUuid/Time/numerics/Unparsed), SoapEnvelope,
  Receive/ReceiveResponse, Send, SelectorSet/OptionSet, CommandLine, namespace
  visitors.
- The duplicate NodeDeserializer in winrm.

Converted:
- Structs whose fields are `Tag<_, _, N>` now `#[derive(FromXml)]` (one line);
  the value/leaf types and the irregular hand-written ones implement `FromXml`
  directly. Serialization (TagValue / Element builder) is unchanged.

Correctness fixes surfaced by namespace-aware matching:
- `creationXml` / `connectXml` / `connectResponseXml` carry the PowerShell
  namespace on the wire; their TagName definitions said `None`. Set to the real
  URI and taught the builder to emit a default-namespace element unprefixed
  (instead of erroring), keeping serialized output byte-identical.
- Unknown extension namespaces/elements are now ignored (SOAP must-ignore)
  rather than failing the whole envelope.

`ironposh-xml` keeps the legacy `XmlVisitor`/`XmlDeserialize` traits only
because ps_value's CLIXML primitive layer still rides on them; ps_value's
ComplexObject tree is untouched. Net: 310 insertions, 1804 deletions.
- rustfmt: import ordering/grouping on the touched files.
- clippy (-D warnings): drop the redundant `continue` from the FromXml derive
  output; merge the identical default-namespace match arms in the Element
  writer; use `Self` in the hand-written FromXml impls.
`Tag<Tag<W, M>, N>` (e.g. `command_response: <CommandResponse><CommandId/>`,
`signal: <Signal><SignalCode/>`) models an N element wrapping a single M child.
The new FromXml passed the parent node straight to the inner tag's from_xml,
which then checked the wrapper's name and failed
("Invalid tag: expected 'CommandId', found 'CommandResponse'").

Tag::from_xml now descends to the single N-named child when handed a node that
isn't itself N — restoring the old from_children semantics for nested tags.

This only surfaced on SSPI auth (Negotiate/Kerberos/NTLM), which exchanges
CommandId; Basic never hits that path, so unit fixtures missed it. Added a
unit regression test for the nested CommandResponse/CommandId shape.

Verified: real-server transport×auth×sealing matrix now 10/10 (was 4/10 on
this branch; master is 10/10). fmt + clippy + workspace tests all clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant