Compare commits

..

140 Commits

Author SHA1 Message Date
Joseph Doherty 1f07da2e12 tools: upgrade Get-InfisicalSecret to stream separation, drop banner regex
rust / build / test / clippy / fmt (push) Has been cancelled
rust / cargo public-api drift check (F41) (push) Has been cancelled
Earlier fix (commit 047125b) filtered the infisical CLI's
"A new release of infisical is available" upgrade banner from
captured output via regex matching. That worked but coupled the
filter to specific banner-pattern strings — a future banner shape
("Update available" / "New version detected" / a localized
message) would slip through and break NTLM Type1 auth again.

The principled fix is to stop capturing stderr at all.
PowerShell's call operator (`&`) keeps stdout and stderr on
separate streams unless explicitly merged; the previous code's
`2>&1` was the actual mistake. Without it, the banner stays in
the error stream (visible on the console for diagnostics) and
the captured `$value` contains only the script's stdout — which
for `Get-Secret.ps1` is just the secret value from `infisical
secrets get --plain`.

Verified: live re-run of F54 (lmx_write_complete_live) passes
post-change with `MX_TEST_DOMAIN='DESKTOP-6JL3KKO'` clean and
the banner visibly logged to console (stderr) above each [SET]
line. No regex coupling to a specific banner-pattern remains.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 09:30:52 -04:00
Joseph Doherty 047125bc11 M6 live verification: re-run all 5 steps + filter infisical banner
Three doc fixes pinned by re-running today's full live-test sweep:

1. Bump status header from 2026-05-06 to "re-run 2026-05-07" with a
   note that all 5 steps still pass against the live AVEVA install.
   The first run of step 1 + step 5 today failed with
   `Error::Status { detail: 5 }` (DCE/RPC fault 0x00000005) traced
   to MX_TEST_DOMAIN being polluted with the infisical CLI's
   "A new release of infisical is available" upgrade banner. The
   banner was being concatenated onto the domain string by
   Setup-LiveProbeEnv.ps1's `2>&1` capture, causing NTLM Type1 to
   send a malformed domain field that NmxSvc rejected.

2. Fix tools/Setup-LiveProbeEnv.ps1 — Get-InfisicalSecret now splits
   captured output on newlines, filters lines matching the
   "^A new release of infisical is available" / "^Please upgrade"
   banner patterns, and returns the last non-empty line (the actual
   secret value from `infisical secrets get --plain`). Robust to
   future banner messages of similar shape.

3. Fix two drifted line citations in docs/M6-live-verification.md:
   `recover_connection_core (session.rs:1428-...)` is now at line
   1374 after F56/F45/F47 edits — strip the line number, keep the
   function name (`Session::recover_connection_core`). Same for
   `Session::unsubscribe (session.rs:2261)`.

4. Add "Workspace gate (no live infra needed)" subsection to the
   "Reproducing locally" recipe so a fresh contributor sees the
   full V1 verification recipe (live + workspace gate) in one place.

All 5 live tests pass post-fix:
  - F36 buffered subscribe (drained 1 raw NMX message; no scan
    activity on TestChangingInt today, matches 5/6 baseline)
  - F45 buffered recovery replay (2 pre + 2 post DataUpdate frames)
  - F47 buffered unsubscribe skip (returned Ok)
  - F40 metrics smoke (4 expected metric names present)
  - F54 OnWriteComplete (status detail 9 = WRITE_COMPLETE_OK)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 09:17:46 -04:00
Joseph Doherty d668d5b7b1 mxaccess: fix 9 unit tests broken silently by F56's ensure_publisher_connected
Workspace gate sweep flagged 9 unit tests in mxaccess::session that
had been silently failing since F56 landed (commit 5e11b30). Root
cause: F56 added ensure_publisher_connected (issuing
INmxService2::Connect + AddSubscriberEngine before each
AdviseSupervisory) but the in-process fake-NMX-server fixtures'
responses vec sizes weren't bumped. Once the fake server ran out of
responses mid-handshake, the connection was closed and the client
got ConnectionAborted (10053).

Fix: bumped each test's unauthenticated_server / recording_server
response count by 2 to cover the new pair of RPCs. Tests touched:

  - subscribe_then_unsubscribe_round_trip (2 → 4 responses)
  - two_subscribes_produce_distinct_correlation_ids (4 → 6)
  - subscription_stream_yields_data_change_for_matching_correlation (1 → 3)
  - subscription_stream_filters_out_mismatched_correlation_for_status (1 → 3)
  - subscription_stream_keeps_data_update_regardless_of_correlation (1 → 3)
  - subscribe_populates_registry_unsubscribe_clears_it (2 → 4)
  - read_returns_first_data_change_within_timeout (2 → 4)
  - read_returns_timeout_when_no_data_arrives (2 → 4)
  - unsubscribe_skips_un_advise_for_buffered_subscription (2 → 3
    + mid-flow assertion bumped from len()==1 to len()==3)

The two_subscribes test only adds 2 (not 4) extra responses because
the second subscribe hits the per-engine publisher_endpoints cache.

Workspace gate post-fix: 847 tests pass, 0 failed, 9 ignored
(live-only). Clippy + bench clean. Pinned in
docs/M6-live-verification.md "Workspace gate (2026-05-07)" so the
test-fixture lag is recorded for future audits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 04:44:18 -04:00
Joseph Doherty 9ed4700eb4 docs: audit pass — fix stale F-number references
Walked all 18 docs/*.md for stale followup references and outdated
TODO markers. Two real fixes:

docs/M6-buffered-evidence.md:
- Three references to "F45" for the LMX-proxy Suspend/Activate
  Frida instrumentation were stale. That work was actually filed
  as F46 when the followups list got renumbered (F45 was reassigned
  to "Recovery replay should re-issue RegisterReference for
  buffered subscriptions"). F46 landed in commit 808fea1, and the
  follow-up live capture landed as F50 in commit 349e217.
- Updated all three references to point at F46 + F50 + the
  resolution evidence in docs/F50-suspend-activate-evidence.md.
- Renamed the "Sub-followup filed: F45" section to
  "Sub-followup F46 — RESOLVED 2026-05-06" with the verdict from
  the live capture.

docs/M6-live-verification.md:
- "Open work" section listed F50 as a residual gap. F50 closed
  2026-05-06 per docs/F50-suspend-activate-evidence.md. Updated
  to "None. F49 sweep complete; F50 closed".

Other docs scanned, no real staleness:
- Capture-Run-2026-04-25.md, Current-Sprint-State.md,
  DotNet10-Native-Library-Plan.md — historical snapshot docs,
  intentionally pinned to their dates.
- ASB-Native-Integration-Decision.md, MxNativeSession-API.md,
  NMX-COM-Contracts.md, MXAccess-* — describe the .NET reference's
  state; "not yet" wording reflects the .NET planning context, not
  the Rust port's current state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 04:32:28 -04:00
Joseph Doherty 8b50c0fd43 CHANGELOG: curate post-F43 work into V1 entry
The CHANGELOG was cut at F43 and didn't reflect the work that landed
afterwards on the same V1 milestone. Update the V1 [Unreleased] entry
to cover:

Added (since F43):
- F45 — recovery replay re-issues RegisterReference for buffered subs
- F47 — unsubscribe skips UnAdvise for buffered subs
- F49 / F50 / F51 — live verification + Suspend/Activate captures +
  ASB type-matrix expansion with new fixture round-trip tests
- F52.{1,2,3} — codec performance optimisations (BytesMut output,
  thread-local name-signature cache, caller-supplied scratch buffer)
- F54 — per-operation correlation + compat OnWriteComplete fan-out
- F55 — DCOM-managed INmxSvcCallback sink (Path A)
- F56 — Connect/AddSubscriberEngine round-trip in subscribe path
- MxStatus synthesizer kernel ported (settles R3/R4)

Known limitations (post-resolution):
- Drop F45 / F46 / R3+R4 — all resolved.
- Add F53 protocol-crate missing-docs deferral.
- F3 entry now links the new docs/F3-cross-domain-ntlm-recipe.md.

Publish-order section keeps the DAG but flags F48 (no crates.io
publish) up front so anyone reading the recipe knows it's hygiene
not release prep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 04:27:59 -04:00
Joseph Doherty cc99a2d9f0 followups: trim F56's stale pre-resolution analysis
F56's body had a "Resolved 2026-05-06" header followed by ~40 lines
of pre-resolution debugging analysis that contradicted the
resolution: "Likely revised root cause" pointing at DCOM sink IID
mismatches, "But zero 0x33 DataUpdate frames ever arrive", "Action
items for whoever picks F56 up", "Definition of done", "Resolves
when" — all written before the actual root cause (missing
EnsurePublisherConnected round-trip) was identified.

Trim to: status + actual root cause + fix that landed + live
verification + the codec fixes that also landed independently.
The dead-end debugging branches are preserved in this file's git
history for archeology; F56 body now reads as a coherent closeout.

Also fixed line 108's "See Resolved section below for the full
closeout" pointer — the closeout *is* the body; F56 was never moved
to Resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 03:31:26 -04:00
Joseph Doherty ddebab2c2d docs: F3 cross-domain NTLM provisioning recipe
Self-contained doc at docs/F3-cross-domain-ntlm-recipe.md for whoever
picks F3 up on hardware with two AD forests + a forest trust. Covers:

- Lab topology (LAB-A resource forest with AVEVA install + LAB-B
  account forest with the probe user, bidirectional forest trust).
- DC + DNS + trust + user provisioning steps (Install-ADDSForest,
  Add-DnsServerConditionalForwarderZone, New-ADTrust, New-ADUser).
- Capture procedure for both the Rust and .NET probes under a
  `runas /netonly` cross-domain token, with Wireshark NTLMSSP guidance.
- Fixture layout under crates/mxaccess-rpc/tests/fixtures/cross-domain-ntlm/.
- Round-trip test skeleton (replay the captured Type 2 → regenerate
  Type 3 → assert byte-equality against the captured Type 3).
- Redaction checklist for the captured bytes.
- Why F3 is "evidence work" not "codec work" — the AV pair parser
  is shape-agnostic, so the codec path is already correct; the
  fixture is a regression net for any future drift.

F3 entry in design/followups.md and R8 in design/70-risks-and-open-questions.md
both now point at the recipe so a future contributor doesn't have
to reconstruct the lab topology from the followup analysis alone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 02:40:06 -04:00
Joseph Doherty 73e2bd8771 followups: status snapshot for the Open section
After F52 closed, every entry in the Open section except F3 has a
`**Status:**` line documenting its own resolution (Resolved 2026-05-06,
or Out-of-scope). At a glance the section misleadingly looks like 8
live items.

Add a header snapshot calling out that only F3 — cross-domain NTLM
fixture, externally blocked on a second AD domain — is genuinely open.
The other entries stay where they are because the F-numbers in their
analysis are referenced from other followups; moving them to
`## Resolved` would orphan that context.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:57:58 -04:00
Joseph Doherty ceeaeefa71 [F52.3] mxaccess-codec: caller-supplied scratch buffer for write encoder
rust / build / test / clippy / fmt (push) Has been cancelled
rust / cargo public-api drift check (F41) (push) Has been cancelled
Adds `write_message::encode_into_bytes_mut` (and the timestamped
variant) which writes the encoded body into a caller-supplied
`BytesMut`. The buffer is cleared and resized in place each call;
once it has grown to the largest body the session will produce, it
allocates nothing further.

A session that holds a single `BytesMut` and reuses it across writes:

  - Int32 / Float32 / Float64: 2 → 1 allocs/op
    (only the `encode_scalar_value` scratch `Vec<u8>` remains)
  - Boolean: 1 → 0 allocs/op
    (no per-value scratch — the literal payload is a stack `[u8; 4]`)

Bench delta in `design/M6-bench-baseline.md` § F52.3. The
`encode_scalar_value` Vec is the remaining 1 alloc/op for fixed-width
scalars; eliminating it would require inlining the LE-bytes write
into the body slice (left for a follow-up since the F52 spec only
asks for 2 → 1).

Resolves F52 (all three optimisations landed: 4e76b44 F52.1,
a0fa5be F52.2, this commit F52.3). Existing `encode` / `encode_to_bytes_mut`
public surface unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:53:07 -04:00
Joseph Doherty a0fa5bedfd [F52.2] mxaccess-codec: thread-local name-signature cache
Adds a thread-local `HashMap<String, u16>` cache inside
`compute_name_signature`. Repeated calls with the same name (the hot
path inside `MxReferenceHandle::from_names`) skip the `to_lowercase`
allocation and the CRC-16/IBM walk entirely. Bounded at 1024 entries
per thread; on overflow the cache is cleared rather than evicted LRU
— any sane workload re-fills only the names it actively uses.

`MxReferenceHandle::from_names` drops from 2 → 0 allocs/op once warm
(bench delta in `design/M6-bench-baseline.md` § F52.2). Cold-path
behaviour is unchanged: first call with a new name still pays the
`to_lowercase` + cache-key `String` allocations.

Two new tests pin the cache: cache-hit returns the same value as
cold-compute, and cache overflow doesn't break correctness.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:50:07 -04:00
Joseph Doherty 4e76b44391 [F52.1] mxaccess-codec: BytesMut output buffer for write encoder
Adds `write_message::encode_to_bytes_mut` (and the timestamped variant)
returning a freshly-allocated `BytesMut`. Allocation count is identical
to `encode` (2 allocs/op for fixed-width scalars); the benefit is
downstream — consumers can `BytesMut::split_to` / `freeze` and forward
the body bytes to a wire-level sink without an intermediate copy.

The body builders (`encode_boolean` / `encode_fixed` / `encode_variable`
/ `encode_array`) were refactored to fill a pre-sized `&mut [u8]`
rather than each allocating their own `Vec<u8>`. The dispatcher
computes the body size up front via small `*_body_size` helpers and
resizes the destination buffer (Vec or BytesMut) once. This is also
the prerequisite refactor for F52.3.

Bench delta in `design/M6-bench-baseline.md` § F52.1; existing
`encode` row unchanged at 2 allocs/op. All 265 round-trip tests
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:46:02 -04:00
Joseph Doherty c7505f9570 [F51] live ASB type-matrix: provision UDAs + capture wire fixtures + round-trip tests
rust / build / test / clippy / fmt (push) Has been cancelled
rust / cargo public-api drift check (F41) (push) Has been cancelled
Provisioned 7 new UDAs on $TestMachine via wwtools/graccesscli
object uda add (then deployed to TestMachine_001):

  TestFloat          MxFloat        scalar
  TestFloatArray     MxFloat        array (4)
  TestDouble         MxDouble       scalar
  TestDoubleArray    MxDouble       array (4)
  TestDateTime       MxTime         scalar
  TestDuration       MxElapsedTime  scalar
  TestDurationArray  MxElapsedTime  array (4)

New crates/mxaccess/examples/asb-type-matrix.rs reads all 14 tags
(7 pre-existing + 7 new) in a single batch and dumps the live
AsbVariant bytes per tag when MX_ASB_DUMP_FIXTURES=<dir> is set.
Single-attempt register (no retry — F31 InvalidConnectionId
cool-down re-arms on every retry, making backoff
counter-productive; if the cool-down is engaged, wait 60+ seconds
without ASB activity then re-run).

Captured live evidence (single cold-start run, all 14 register
calls returned error_code=0x0000):

  TestChangingInt   type_id=4  (Int32)        length=4   payload=4
  TestAlarm001      type_id=17 (Boolean)      length=1   payload=1
  MachineCode       type_id=10 (String)       length=30  payload=30
  TestFloat         type_id=8  (Float)        length=4   payload=4
  TestDouble        type_id=9  (Double)       length=8   payload=8
  TestDateTime      type_id=11 (DateTime)     length=8   payload=8
  TestDuration      type_id=12 (ElapsedTime)  length=8   payload=8

  TestIntArray, TestBoolArray, TestStringArray, TestDateTimeArray,
  TestFloatArray, TestDoubleArray, TestDurationArray
                    type_id=0 length=0 payload=0
                    (provisioned but no value written yet)

Per-tag fixture .bin files saved under
crates/mxaccess-codec/tests/fixtures/f51-type-matrix/ — full
14-byte to 40-byte AsbVariant byte sequences (i32 type_id LE +
i32 length LE + payload bytes).

crates/mxaccess-codec/tests/f51_type_matrix_parity.rs round-trips
each scalar fixture: decode -> re-encode -> assert byte-equal +
type_id / length pin. Tests skip with [skip] message when fixtures
are absent (so the suite passes on a fresh checkout without live
captures). 7 scalar tests pass against the captured fixtures.

Array tags excluded from round-trip pinning because the live
engine returns empty payloads for unwritten arrays. Codec-side
array round-trip is covered by asb_variant's existing synthetic-
payload unit tests.

docs/galaxy-test-fixtures.md inventories all $TestMachine UDAs
(pre-existing + F51-provisioned), the graccesscli provisioning
recipe, the fixture-regeneration pattern, and the F31 cool-down
caveat.

design/followups.md F51 marked resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:27:31 -04:00
Joseph Doherty 8bd66bbe65 [F53 measurement] document protocol-crate missing-docs magnitude
rust / build / test / clippy / fmt (push) Has been cancelled
rust / cargo public-api drift check (F41) (push) Has been cancelled
Enabled #![warn(missing_docs)] on each of the 7 protocol crates to
measure how many one-liners filling them in would be:

  mxaccess-asb         422
  mxaccess-nmx         398
  mxaccess-callback    371
  mxaccess-galaxy      229
  mxaccess-codec       205
  mxaccess-rpc         147
  mxaccess-asb-nettcp  111
                     -----
  Total              1883

Reverted the lint enables — most of those are protocol-internal
types (struct fields on wire-shape records, enum variants on opcode
discriminators) whose meaning is already documented at the
consumer-facing layer. Filling 1883 one-liners adds noise without
consumer value, and forcing them as errors via RUSTDOCFLAGS would
block routine cargo doc runs.

design/followups.md F53 entry updated with the measured numbers
and the explicit "stays off indefinitely" verdict. If a future
contributor wants per-crate enforcement, the recipe in the strategy
paragraph (allow(missing_docs) on protocol-internal modules,
warn(missing_docs) on the re-export surface) is still valid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:36:03 -04:00
Joseph Doherty 349e217ea3 [F50] live Suspend/Activate captures — Suspend wires opcode 0x2D, Activate client-side
Re-ran analysis/frida/mx-nmx-trace.js (with the F46 hooks for
LmxProxy.dll!CLMXProxyServer.Suspend / .Activate) against
MxTraceHarness on the local AVEVA install. Two captures landed:

- captures/123-frida-suspend-advised-instrumented/
  Scenario: --scenario=suspend-advised --tag=TestChildObject.ScanState

  After mx.suspend.begin/end at 17:23:51.949Z, NMX PutRequest fires
  ~140ms later with body:
    2d 01 00                                       command 0x2D, version 0x0001
    cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 16-byte correlation_id (matches the prior AdviseSupervisory)
    01 00 05 00 01 00 02 00 01 00 69 00 0a 00      engine + handle + attribute / property ids
    47 92 00 00 03 00 00 00                        trailer

  TransferData wraps it; HRESULT 0 returned; ProcessDataReceived
  callback delivers a 50-byte op-status frame; LMX surfaces it
  through CUserConnectionCallback.OperationComplete. Suspend is
  unambiguously server-side wire op 0x2D.

- captures/124-frida-activate-advised-instrumented/
  Scenario: --scenario=activate-advised --tag=TestChildObject.ScanState

  Activate fires at 17:26:02.982Z and returns Success synchronously
  with no NMX traffic. The next NMX activity is 7+ seconds later
  (harness teardown). Activate against a non-suspended item is
  client-side only on this build.

The harness's activate-advised scenario doesn't sequence
Suspend-then-Activate, so we don't have direct evidence for
Activate-after-Suspend. Circumstantial reasoning: since Suspend
goes server-side with a state change, Activate likely also does to
revert. If direct evidence becomes needed, add a new
suspend-then-activate scenario to MxTraceHarness/Program.cs and
re-run.

design/70-risks-and-open-questions.md R5 moves to "settled —
Suspend is wire op 0x2D, Activate behaviour is conditional",
severity downgraded P2 -> P3 (no public Session::suspend /
Session::activate API exists today; if added later, 0x2D is the
encoder target).

design/followups.md F50 marked resolved.

docs/F50-suspend-activate-evidence.md: per-capture byte-level
evidence + repro recipe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:29:40 -04:00
Joseph Doherty b62ffc8c5d [F48] mark out-of-scope: internal usage only, no crates.io publish
Maintainer confirmed 2026-05-06 the project is internal-use only —
workspace stays at version "0.0.0", consumers depend via path or
git, not crates.io. F48's actual publish goal is dropped.

design/followups.md F48 entry: replace the "P1 release driver"
framing with "Out of scope" + a pointer to the recipe doc in case
this ever changes.

design/F48-publish-dry-run.md: add a banner at the top explaining
the doc is now retained as a workspace-hygiene record (cargo
package --list per crate produces clean tarballs, no captures or
big files), not as release prep. The "What the actual V1 publish
needs" section reframed as "If a publish ever does become a goal —
recipe" so the steps survive without implying they're scheduled.

No code change. F49 / F53 / F55 / F56 status unchanged — those
weren't release-cut-gated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 13:13:24 -04:00
Joseph Doherty e77db4306a [F48 dry-run] validate publish chain on workspace 0.0.0
cargo publish --dry-run on each of the 9 workspace crates:
- Tier 1 leaves (mxaccess-codec, mxaccess-rpc, mxaccess-asb-nettcp)
  pass cleanly. cargo assembles each tarball, the only failure is
  the dry-run upload abort.
- Tiers 2 + 3 (galaxy, callback, asb, nmx, mxaccess, mxaccess-compat)
  surface the documented "no matching package" registry-lookup
  failure because workspace internal deps are pinned at version
  "0.0.0" which doesn't exist on crates.io. Expected; resolves at
  actual publish time once the leaves are uploaded and indexed.

cargo package --list confirms each crate ships only source + tests
+ small round-trip fixtures. No captures, decompiled binaries, or
accidental big files.

design/F48-publish-dry-run.md captures the per-crate run output,
the per-crate file count, and the V1 publish recipe (bump 0.0.0
→ 0.1.0 across workspace + internal-dep pins, publish in tier
order, wait for indexing between tiers, tag).

design/followups.md F48 entry annotated with the dry-run status.
The actual publish to crates.io is deliberately not done — that
needs maintainer auth + a deliberate version bump that's a release-
cut decision, not a routine validation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 12:42:22 -04:00
Joseph Doherty c606736ec3 [F53 partial] enable #![warn(missing_docs)] on consumer crates
mxaccess + mxaccess-compat now carry #![warn(missing_docs)] at the
crate root. Every public item has at least a one-line doc comment
(struct fields, enum variants, trait methods all covered).

Touched items:
- mxaccess::lib: DataChange fields, SecurityContext fields,
  TransportKind variants, TransportCapabilities fields,
  RecoveryEvent variants + their inner fields, SessionOptions
  fields, the full Error / ConnectionError / AuthError /
  ProtocolError / ConfigError / SecurityError taxonomy + nested
  fields, Transport trait method docs.
- mxaccess-compat::lib: DataChangeEvent / BufferedDataChangeEvent /
  WriteCompleteEvent / OperationCompleteEvent fields.

Protocol crates (codec, rpc, galaxy, nmx, callback, asb,
asb-nettcp) deliberately left without the lint per F53's strategy
paragraph — their consumers (mxaccess + mxaccess-compat) already
document the surfaces they re-export, and forcing one-liners on
every transport-internal item adds noise without consumer value.

Verification:
- `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps` clean.
- `cargo test --workspace` (824 tests) green.
- `cargo clippy --workspace --all-targets -- -D warnings` clean.

design/followups.md F53 marked partially resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 12:20:47 -04:00
Joseph Doherty d149143535 [F49 steps 2 + 3] live verification: buffered recovery replay + unsubscribe skip
Step 3 (F47 buffered unsubscribe skip):
- crates/mxaccess-compat/tests/buffered_unsubscribe_skip_live.rs.
- Subscribe buffered, sleep so the engine has DataUpdates in flight,
  then call unsubscribe. Asserts Ok return without surfacing transport
  or HRESULT errors.
- Session::unsubscribe (session.rs:2261) probes the registry: if
  Buffered { .. }, it skips nmx.un_advise entirely, mirroring the .NET
  reference's `if (!subscription.IsBuffered)` guard at
  MxNativeSession.cs:361-381. If unsubscribe accidentally emitted
  UnAdvise for a buffered correlation id, the engine would return
  non-zero HRESULT (no matching plain advise to retract) — surfacing
  as a panic.

Step 2 (F45 buffered recovery replay):
- crates/mxaccess-compat/tests/buffered_recovery_replay_live.rs.
- Subscribe buffered, drain >=1 NMX subscription message
  (cmd=0x32 SubscriptionStatus + cmd=0x33 DataUpdate) to confirm the
  wire path is hot pre-recovery, install a RebuildFactory that calls
  NmxClient::create (the same auto-resolving COM-activation path
  Session::connect_nmx_auto uses), invoke recover_connection, drain
  >=1 NMX subscription message post-recovery.
- Verifies the replay branch in recover_connection_core re-issues
  RegisterReference (NOT AdviseSupervisory) for the buffered entry,
  mirroring MxNativeSession.ReAdviseSubscription (cs:538-569).
  Structural property is unit-tested; this confirms the engine
  actually picks back up after the rebuild + replay.

Both tests pass live on this Galaxy:
  cargo test -p mxaccess-compat --features live-windows-com \
      --test buffered_unsubscribe_skip_live -- --ignored --nocapture
  cargo test -p mxaccess-compat --features live-windows-com \
      --test buffered_recovery_replay_live -- --ignored --nocapture

Pulls mxaccess-nmx + mxaccess-codec into mxaccess-compat dev-deps so
the recovery test can build a RebuildFactory closure that returns
NmxClient and bind a typed broadcast Receiver.

design/followups.md F49 -> Resolved (all five steps pass live).
docs/M6-live-verification.md updated with per-step evidence + repro
commands.

F49 is fully closed out. F55 (DCOM-managed INmxSvcCallback, Path A)
and F56 (missing EnsurePublisherConnected + post-RegisterReference
AdviseSupervisory for buffered) were the two real Rust-port bugs
uncovered along the way; both resolved. Remaining post-V1 followups
(F50 Suspend/Activate Frida, F51 ASB type matrix, F52 perf, F53 doc
lint, etc.) are scoped independently and not part of F49.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 12:00:44 -04:00
Joseph Doherty 5e11b30507 [F56 resolved] subscribe paths now drive 0x33 DataUpdate frames
Root cause: `Session::subscribe` and `Session::subscribe_buffered_nmx`
were missing the `INmxService2::Connect` + `AddSubscriberEngine` RPC
pair that the .NET reference's `MxNativeSession.EnsurePublisherConnected`
(`cs:516-526`) issues before the first advise against a publishing
engine. Without those two RPCs, NmxSvc accepted the subscription
registration but the publishing engine never knew our engine was
subscribed — so it never dispatched DataUpdate frames back.

Diagnosis driven by wwtools/aalogcli reading
C:\ProgramData\ArchestrA\LogFiles. The user pointed at this tooling
which lit up the path.

Red herring: NmxSvc's `[Warning] NmxCallback->DataReceived ... failed
with error 0x{N}` log lines turned out to be normal log spam where N
is the bufferSize of the inbound call, not a real error code. The
.NET reference's own probe triggers identical entries while still
receiving DataUpdate frames successfully.

Fix:
- SessionInner::publisher_endpoints — per-session HashMap<(platform_id,
  engine_id), ()> cache mirroring MxNativeSession._publisherEndpoints.
- Session::ensure_publisher_connected — issues Connect +
  AddSubscriberEngine, once per publisher endpoint per session.
- Session::subscribe + subscribe_buffered_nmx — both call it before
  the wire advise.
- subscribe_buffered_nmx — additionally issues AdviseSupervisory after
  RegisterReference. The .NET reference's RegisterBufferedItemAsync
  only calls RegisterReference, but on this AVEVA install
  RegisterReference alone produces the registration result + heartbeat
  callbacks without ever starting DataUpdate dispatch; AdviseSupervisory
  unblocks the dispatch.

Live verification (`TestMachine_001.TestChangingInt`, a tag that
updates >1×/s):
  cargo test -p mxaccess-compat --features live-windows-com \
      --test plain_subscribe_live -- --ignored --nocapture
  cargo test -p mxaccess-compat --features live-windows-com \
      --test buffered_subscribe_live -- --ignored --nocapture
Both pass — `cmd=0x32` SubscriptionStatus + sequence of `cmd=0x33`
DataUpdate frames flow as expected. Tests assert on the raw
Session::callbacks() broadcast (not the typed Subscription::next
DataChange path) because the engine reports quality=Uncertain
value=null for this attribute on this Galaxy — the wire-level
subscription is what F56 was about, not the value content.

DcomCallbackSink reverted to S_OK return for both DataReceivedRaw
and StatusReceivedRaw (the bytes-processed / sentinel HRESULT
experiments during diagnosis turned out to be irrelevant — the
"failed with error 0xN" logs come from NmxSvc regardless of the
return value).

design/followups.md F49 + F56 + docs/M6-live-verification.md updated:
F56 resolved, F49 steps 1 + 4 + 5 pass live, steps 2 + 3 pending
(now executable on this fixture).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 11:32:07 -04:00
Joseph Doherty c6332c26a1 [F49 step 4 + step 5 + doc] live evidence: metrics smoke pass, M6-live-verification.md
F49 step 4 (F40 metrics smoke):
- crates/mxaccess-compat/tests/metrics_smoke_live.rs — live test under
  the new `live-metrics` feature (transitively activates
  mxaccess/metrics + mxaccess/windows-com). Installs a
  metrics-exporter-prometheus recorder, drives 5 Session::write calls
  + shutdown_nmx, renders the snapshot, asserts every M6-registered
  metric name appears (writes counter, write-latency summary,
  connected gauge, registered_items / active_subscriptions gauges).
  Pass on the live AVEVA install.

  Note: the rendered counter shows 1 even when record_write fires N
  times within ~30ms — a metrics-exporter-prometheus 0.16 quirk under
  tight loops, not a Rust port bug. Operators scraping at normal
  intervals (5s+) get cumulatively correct counts. Documented in the
  test + in M6-live-verification.md so future runs aren't surprised.

F49 status update (in design/followups.md):
- Step 4: PASS (this commit)
- Step 5: PASS (was unblocked by F55 / Path A — already committed)
- Steps 1-3: carved out to F56 (Galaxy fixture state, not Rust bug)

docs/M6-live-verification.md:
- Per-step evidence table with test invocations + outcomes.
- Sample Prometheus snapshot for step 4.
- Reproduction commands for the live tests.
- F56 explanation cross-referenced from step 1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 10:36:09 -04:00
Joseph Doherty df3457c54a [F56] subscribe / subscribe_buffered: split-form wire body + diagnose Galaxy fixture gap
Three real fixes + one architectural diagnosis:

1. Session::subscribe_buffered_nmx now sends the .NET-reference split
   form on the wire:
     item_definition = "<attr>.property(buffer)"   (was: full reference)
     item_context    = "<object_tag_name>"          (was: empty)
     item_handle     = SessionInner::next_item_handle.fetch_add(1)
                       (was: hardcoded 0)
   Verified byte-identical against captures/082 + 094 by the existing
   buffered_register_reference_parity unit tests. The
   item_handle counter mirrors MxNativeCompatibilityServer's
   _nextItemHandle++ at MxNativeSession.cs:613.

2. New live tests:
   - tests/buffered_subscribe_live.rs (F49 step 1) — uses real Galaxy
     metadata via SqlTagResolver + connect_nmx_auto, drives a
     background writer at 500ms cadence to force value-changes,
     drains DataChange events from Subscription.
   - tests/plain_subscribe_live.rs — same harness over plain
     Session::subscribe (NOT buffered), used to isolate whether
     "no DataUpdate" is buffered-specific (it's not — both fail).

   Both pull tracing-subscriber as a dev-dep so `RUST_LOG=trace`
   surfaces dcom_sink + router activity.

3. mxaccess-galaxy/sql_resolver.rs: drop the inner-attribute
   `#![cfg(feature = "galaxy-resolver")]` — the module-level cfg on
   `pub mod sql_resolver` in lib.rs already handles this and Rust
   1.85's clippy::duplicated_attributes lint flagged the duplicate
   once mxaccess-compat dev-deps activated the feature.

4. F56 finding (diagnosis, NOT a bug fix): the engine on this Galaxy
   install does not have an active value for TestChildObject.TestInt.
   Confirmed by running the .NET reference's own probe:

     dotnet run --project src/MxNativeClient.Probe -c Release \
       -- --probe-session-subscribe --tag=TestChildObject.TestInt \
       --subscribe-hold-seconds=10

   ...returns ONE 0x32 SubscriptionStatus (status=3 detail=3
   quality=0x00C0 Uncertain value=null) and zero 0x33 DataUpdates —
   matching the Rust port's symptom exactly. Not a Rust port bug,
   not a wire-byte gap. F49 steps 1-3 need either an actively-
   scanned tag or local Galaxy reconfiguration to scan
   TestChildObject.TestInt.

Workspace tests + clippy clean under both feature configurations.
F56 entry in design/followups.md updated with the full diagnostic
chain so future-me / future-collaborators can pick it up without
re-tracing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 10:27:08 -04:00
Joseph Doherty af15fe7587 [F49 step 1 + F56] callback router: peel envelope before parsing subscription / 0x11 frames
The router used to call NmxSubscriptionMessage::parse_inner directly
on the COM-stub-delivered body, but the wire bytes arrive wrapped in
a ProcessDataReceived envelope (46-byte header + optional 4-byte
length prefix); parse_inner expects post-envelope bytes. Result:
every 0x33 DataUpdate that ever arrived was silently dropped.

Mirrors the .NET reference's MxNativeSession.OnCallbackReceived flow
at cs:582-606 — three sequential parse attempts:
  1. NmxOperationStatusMessage::try_parse_process_data_received_body  (already wired)
  2. NmxReferenceRegistrationResultMessage::try_parse_...              (NEW — was missing)
  3. NmxSubscriptionMessage::try_parse_process_data_received_body      (NEW — was wrong)

Adds:
- NmxSubscriptionMessage::try_parse_process_data_received_body — peels
  envelope via NmxObservedEnvelope::parse_process_data_received_body_flexible,
  then dispatches to existing parse_inner.
- NmxReferenceRegistrationResultMessage::try_parse_process_data_received_body —
  same shape, for the 0x11 registration-result frame.
- Router branch for 0x11 — currently traces the assigned item_handle and
  drops the frame (matches the .NET reference, which fires a
  ReferenceRegistrationReceived event with no consumer in the codebase).
- Router fall-through trace! when neither path matches, so future
  unparseable bodies surface in RUST_LOG=trace instead of vanishing.
- DcomCallbackSink::forward — trace! per inbound callback so
  RUST_LOG=mxaccess_callback=trace surfaces opnum + size.
- crates/mxaccess-compat/tests/buffered_subscribe_live.rs — F49 step 1
  live test that drives subscribe_buffered + a 500ms-cadence writer.
  Also pulls tracing-subscriber as a dev-dep so the test can dump
  router activity.

Existing router_task_decodes_callback_invoked_into_broadcast unit test
updated to wrap its synthetic 0x32 body in an envelope so the new
parse path actually accepts it.

Live result: F56 — the buffered round-trip *registers* successfully
(RegisterReference returns HRESULT 0; engine sends one 0x11
RegistrationResult + one 51-byte op-status per write, perfectly
clocked) but the engine never sends a 0x33 DataUpdate. Rust-port-
specific gap vs the .NET reference's working buffered path; root
cause is likely a field-level difference in the RegisterReference
body or a missing post-RegisterReference step. Captured as F56 in
design/followups.md, blocking F49 step 1; F56's DoD is the same
live test reporting >=3 DataChange arrivals.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 09:50:57 -04:00
Joseph Doherty 2fc327a8d5 [F55 Path A] DCOM-managed INmxSvcCallback sink
Replace the hand-rolled CallbackExporter (TCP listener + custom
OBJREF) with a real `windows-rs` `#[implement]` COM class for
INmxSvcCallback, marshalled via CoMarshalInterface. NmxSvc validates
the callback OBJREF by calling IObjectExporter::ResolveOxid against
the local RPCSS at 127.0.0.1:135; hand-rolled OXIDs aren't registered
there, which is why RegisterEngine2 returned RPC_S_SERVER_UNAVAILABLE
(1722) on every live attempt. CoMarshalInterface registers the OXID
with RPCSS automatically, so the SCM-side resolution succeeds.

Mirrors MxNativeSession.CreateRegisteredService (cs:624), which is
the .NET reference's working path:
  ComObjRefProvider.MarshalInterfaceObjRef(callback,
    INmxSvcCallback, DifferentMachine)

Layout:
- mxaccess-callback::dcom_sink — INmxSvcCallback + DcomCallbackSink
  + create_dcom_callback_sink_objref. Forwards inbound calls into
  the same CallbackEvent::CallbackInvoked { opnum, body } shape the
  legacy exporter produces, so callback_router stays path-agnostic.
- Session::from_nmx_client — branched on `windows-com`. Real DCOM
  sink when on; legacy CallbackExporter when off (kept for unit
  tests that run against an in-process fake NMX peer).
- SessionInner.dcom_sink_holder: Option<IUnknownHolder> — keeps the
  COM ref alive for the session's lifetime; shutdown_nmx drops it.
- mxaccess-rpc + mxaccess-callback: windows-rs 0.59 → 0.62. The 0.59
  #[implement] macro generates code that doesn't compile under
  edition 2024; 0.62 is fixed.

Live result: cargo test -p mxaccess-compat --features
live-windows-com --test lmx_write_complete_live -- --ignored
--nocapture passes end-to-end. RegisterEngine2 OK, write
round-trips, OnWriteComplete fires with the captured MxStatus shape.

Unblocks F49 step 5; F55 marked Resolved in design/followups.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 09:25:44 -04:00
Joseph Doherty 0a274af76f [F55] Path C investigation: NmxSvc requires SCM-registered OXID for callbacks
rust / build / test / clippy / fmt (push) Has been cancelled
rust / cargo public-api drift check (F41) (push) Has been cancelled
Captured OBJREF byte structures from both paths via the .NET probe:
- `--probe-callback-marshal`: DCOM-marshalled, 338 bytes, succeeds
  (when used inside `MxNativeSession.Open` → `CreateRegisteredService`).
- `--probe-register-managed-callback`: hand-rolled, 162 bytes, fails
  with `RegisterEngine2 → 0x800706BA RPC_S_SERVER_UNAVAILABLE`.

The structural diff:
- `std_flags`: DCOM=`0x0A80` (SORF_OXRES4+6+8) vs hand-rolled=`0x280`
  (SORF_OXRES4+6). Bit `0x0800` (SORF_OXRES8) only set in DCOM.
- ncacn_ip_tcp bindings: DCOM=4 with no ports; hand-rolled=1 with
  explicit `[port]`.
- Total size: 338 vs 162 bytes.

Tested the simplest fix (hand-rolled `std_flags = 0x0A80` to match
DCOM): **still fails with the same 1722.** Reverted.

**Diagnosis updated in F55:** NmxSvc on receiving RegisterEngine2
appears to call `IObjectExporter::ResolveOxid` against the local
SCM (`127.0.0.1:135`) to resolve the callback OBJREF's OXID, then
dial the resulting bindings. Our hand-rolled OXID is never
registered with RPCSS, so the SCM-side resolution fails and NmxSvc
returns RPC_S_SERVER_UNAVAILABLE — matching:
- the symptom (1722),
- the sub-second timing (no TCP dial-back to our listener attempted),
- the fact that the .NET `ManagedCallbackExporter` (same hand-rolled
  approach) ALSO fails identically.

DCOM marshalling fixes this because `CoMarshalInterface` internally
registers the OXID with RPCSS. The bindings have no port because
RPCSS returns the dynamic port from the DCOM stub layer.

**Conclusion: Path A is the architecturally correct fix** — the
callback exporter must be a DCOM-managed object (e.g. via
`windows-rs` `#[implement]`) for NmxSvc to accept the callback.
The hand-rolled-listener-with-explicit-port approach is
fundamentally incompatible with NmxSvc's callback validation, in
both Rust and the .NET reference.

Path C (cheap investigation) is exhausted; F55 verdict updated to
recommend Path A explicitly.

`cargo test --workspace` 824 passing; clippy `-D warnings` clean
across both feature configurations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 08:55:59 -04:00
Joseph Doherty c5d611d6fa [F12 partial + F55] hold IUnknown for client lifetime + diagnose RegisterEngine2 1722
rust / build / test / clippy / fmt (push) Has been cancelled
rust / cargo public-api drift check (F41) (push) Has been cancelled
**F12 partial improvement** (`mxaccess-rpc::IUnknownHolder` + `mxaccess-nmx`):

- New `IUnknownHolder` newtype that owns an MTA-resident COM proxy
  with `unsafe impl Send + Sync`. Mirrors the .NET reference's
  `ManagedNmxService2Client._activatedComObject` private field
  (`cs:15`).
- New `activate_and_marshal_iunknown_objref(prog_id, ctx)` returns
  `(Vec<u8>, IUnknownHolder)`. Existing
  `marshal_activated_iunknown_objref` retained as a wrapper that
  drops the holder (kept for inline-use callers).
- `NmxClient` gains an `activated_com_object: Option<IUnknownHolder>`
  field, populated by `Self::create` from the new helper.
  `Self::connect` / `Self::from_bound_transport` set it `None` (no
  COM activation in those paths).
- Holding the IUnknown for the client's lifetime keeps the
  SCM-tracked OXID valid; without it the COM ref count drops to
  zero and the SCM may release the activated server-side instance,
  making subsequent `ResolveOxid` / `RemQueryInterface` calls
  return `RPC_S_SERVER_UNAVAILABLE`.

**F55 (new) — hand-rolled callback exporter rejected by RegisterEngine2**

Five-step instrumentation of `Session::connect_nmx_auto` proves all
six COM-activation / RemQI / final-bind steps succeed. The 1722
fault originates at `RegisterEngine2` itself:

```
from_nmx_client: callback hostname="DESKTOP-6JL3KKO" port=57886 obj_ref_len=162
from_nmx_client: callback obj_ref hex: 4d454f57010000...
from_nmx_client: RegisterEngine2 (31112, mxaccess.31112)
from_nmx_client: RegisterEngine2 FAIL: Transport(Fault { status: 2147944122 })
```

Status `0x800706BA` = `RPC_S_SERVER_UNAVAILABLE` wrapped as Win32
HRESULT.

**Critical finding: the .NET reference's `--probe-register-managed-callback`
(which uses the same hand-rolled `ManagedCallbackExporter` approach
as the Rust port) ALSO fails with the same `0x800706BA` fault.**
Only `--probe-session-write`, which uses
`ComObjRefProvider.MarshalInterfaceObjRef(callback, ...)` to build
the OBJREF via Windows DCOM proxy/stub marshalling, succeeds. So
this is an architectural artifact of the hand-rolled-callback
design, not a Rust port regression.

`design/followups.md` F55 entry documents the three resolution
paths (switch to DCOM-marshalled callback / hybrid / continue
investigating OBJREF rejection at NmxSvc).

F49 stays open with a refined diagnostic — the per-feature live
verification is gated on F55's resolution.

Workspace tests still 824 passing; clippy `-D warnings` clean
across both feature configurations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 08:50:30 -04:00
Joseph Doherty e5b31fadb1 [F49] live-test scaffolding for F54 OnWriteComplete + COM probe diagnostic
rust / build / test / clippy / fmt (push) Has been cancelled
rust / cargo public-api drift check (F41) (push) Has been cancelled
Live attempt against AVEVA on this dev host produced two artefacts:

**`crates/mxaccess-compat/tests/lmx_write_complete_live.rs`** — the
F54 OnWriteComplete round-trip test. Compiles + runs against the
live AVEVA install via either path:
- `--features live-windows-com` (preferred): uses
  `Session::connect_nmx_auto` so the COM activation reference is
  held in-process for the duration of the test.
- Default features (fallback): shells out to
  `MxNativeClient.Probe --probe-resolve-oxid-managed-ntlm-integrity`
  + `--probe-remqi-managed` to learn the per-session NMX endpoint +
  INmxService2 IPID, then uses `Session::connect_nmx`.

Both code paths are wired and the test runs through endpoint
resolution + IPID extraction successfully. The connect step itself
fails with `Status { detail: 1722 }` (RPC_S_SERVER_UNAVAILABLE).

**`crates/mxaccess-rpc/examples/com-marshal-probe.rs`** — minimal
one-shot binary that calls
`marshal_activated_iunknown_objref("NmxSvc.NmxService",
DifferentMachine)` in isolation. Confirms the COM activation +
CoMarshalInterface chain works fine standalone (returns a 338-byte
OBJREF with valid OXID/IPID structure). The 1722 in the live test
is therefore downstream of the activation — likely a COM-apartment
threading interaction with the tokio multi-thread runtime.

This is an F12-related issue (auto-resolve hardening), not an F54
issue. F54's correctness is covered by the existing unit-level
integration tests:
- `mxaccess::session::tests::router_populates_operation_status_context_from_pending_ops_fifo`
- `mxaccess::session::tests::write_handle_correlates_with_router_emitted_status`
- `mxaccess_compat::tests::drain_routes_write_status_to_on_write_complete`
- `mxaccess_compat::tests::drain_routes_non_write_status_to_on_operation_complete`

`design/followups.md` F49 entry updated to reflect:
- F54 added as a fifth row in the live-verification scope.
- "Live attempt 2026-05-06" sub-section documents the 1722 issue +
  what was verified (.NET probe end-to-end works against same
  install; Rust COM activation works in isolation; the failure is
  Rust-port-specific to `connect_nmx_auto` under tokio).
- F49 now Blocked-by F12 hardening (the 1722 path).

New `live-windows-com` feature on `mxaccess-compat` propagates to
`mxaccess/windows-com` for the test binary.

Workspace 824 → 824 tests; clippy + rustdoc clean across both
feature configurations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 08:23:01 -04:00
Joseph Doherty 04c10babfb [F54 test] end-to-end smoke: write_with_handle ↔ callback_router boundary
rust / build / test / clippy / fmt (push) Has been cancelled
rust / cargo public-api drift check (F41) (push) Has been cancelled
Adds `write_handle_correlates_with_router_emitted_status` — the
closest-to-live test we can write without an AVEVA endpoint, pinning
the F54 boundary the C# `OnWriteComplete` callback ultimately depends
on.

The existing tests cover the layers individually:
- `write_value_with_handle_inserts_into_pending_ops` — write API
  populates pending_ops with the right correlation id.
- `router_populates_operation_status_context_from_pending_ops_fifo`
  — callback_router consumes a frame + the registry, emits a typed
  OperationStatus with context attached.
- `drain_routes_write_status_to_on_write_complete` (mxaccess-compat)
  — drain function routes Write op_kind to on_write_complete_tx.

What was missing: a test that combines the public `write_value_with_
handle` API with a real callback_router invocation against the SAME
`pending_ops` Arc the write populated. The new test:

1. Builds a Session via `connect_test_session`.
2. Calls `session.write_value_with_handle("TestObj.TestInt", ...)` —
   gets a real `WriteHandle { correlation_id }` and a real entry in
   `pending_ops` (no manual insertion).
3. Spins a parallel `callback_router` over the SESSION's
   `pending_ops` Arc + a fake event_tx (the live exporter's
   internal channel isn't reachable from tests; this is the
   established workaround pattern from
   `router_task_decodes_callback_invoked_into_broadcast`).
4. Injects the proven `WRITE_COMPLETE_OK` 5-byte frame.
5. Asserts the emitted `OperationStatus.context.correlation_id`
   equals the cid the write returned, that op_kind is Write, that
   reference is the original tag string, and that
   `pending_ops` is now empty (one-shot popped).

This closes the integration-test gap the user flagged. Live AVEVA
verification still falls under F49.

Workspace 823 → 824 tests; clippy + rustdoc clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 07:57:15 -04:00
Joseph Doherty 4ff511bbed [F54] per-operation correlation + compat OnWriteComplete fan-out
rust / build / test / clippy / fmt (push) Has been cancelled
rust / cargo public-api drift check (F41) (push) Has been cancelled
Closes the residual that R3/R4 Path A's commit `c73a33e` deferred:
the OperationStatus.context field was always None because no
in-flight correlation map existed in SessionInner, and the
mxaccess-compat broadcast channels for OnWriteComplete /
OperationComplete were exposed on the public API but had no
fan-out task draining session events into them.

**mxaccess (Part 1 — per-operation correlation):**

- New `pending_ops: Mutex<HashMap<[u8; 16], OperationContext>>` on
  SessionInner. Populated when `Session::write*` / `subscribe*`
  dispatches an outstanding operation; entry removed when the
  matching OperationStatus event fires (one-shot semantics).
- New `Session::write_with_handle` (and equivalents for the secured /
  timestamped paths) returns a `WriteHandle { correlation_id }` so
  consumers can correlate completions back to their originating
  call. Existing `write` / `write_value` / etc. signatures unchanged
  and delegate to the handle-returning variant.
- Callback router extended to look up `pending_ops` by correlation_id
  on each operation-status event. When found, populates
  `OperationStatus.context: Some(OperationContext { correlation_id,
  op_kind, reference, retry_count: 0 })`. When not found, falls
  through with `context: None` (verbatim-preserve per CLAUDE.md).
- New unit tests assert: matching correlation_id populates context,
  unknown correlation_id leaves context None, the entry is removed
  from `pending_ops` after one event fires.

**mxaccess-compat (Part 2 — compat-layer fan-out):**

- New `correlation_to_item: tokio::sync::Mutex<HashMap<[u8; 16], i32>>`
  on LmxClientInner.
- `LmxClient::write` / `write_2` / `write_secured` / `write_secured_2`
  call `Session::write_with_handle` (or equivalent) and insert
  `correlation_id → item_handle` into the map before returning.
- `LmxClient::register` / `register_asb` spawn a background task that
  drains `session.operation_status_stream()`. Per event, looks up
  `correlation_to_item[event.context?.correlation_id]` to find the
  item_handle, then routes:
  - `OperationKind::Write` / `OperationKind::WriteSecured` →
    `WriteCompleteEvent { server_handle, item_handle, statuses,
    is_during_recovery }` into `on_write_complete_tx`.
  - Other variants → `OperationCompleteEvent { ... }` into
    `on_operation_complete_tx`.
  - Removes the correlation_id from `correlation_to_item` after
    firing (one-shot).
- Events with no matching item_handle (correlation_id not in map)
  are dropped silently — no bogus item_handle=0 events.
- Task cancelled on LmxClient drop via `JoinHandle::abort` (matches
  the existing `subscription_task` pattern).
- New unit tests cover: Write op routes to on_write_complete, Read
  op routes to on_operation_complete, unknown correlation_id is
  dropped.

Result: the C# `LMX_OnWriteComplete(int hLMXServerHandle, int
phItemHandle, ref MXSTATUS_PROXY[] pVars)` callback shape is now
end-to-end-achievable. A consumer calls `LmxClient::write(hServer,
hItem, value, userId)` and drains `client.on_write_complete()`; the
yielded `WriteCompleteEvent` carries the right `(server_handle,
item_handle, statuses, is_during_recovery)` tuple.

Public API: `Session::write_with_handle` + `WriteHandle` are new;
existing signatures unchanged. `cargo public-api` baselines
regenerated under `design/public-api/{mxaccess,mxaccess-compat}.txt`.

Workspace: 765 → 823 tests pass (~58 new tests from F54). Clippy
`-D warnings` clean. Rustdoc `-D warnings` clean.

F54 status in `design/followups.md` moved Open → Resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 07:41:28 -04:00
Joseph Doherty f98ab9846d design/70-risks: record the .NET reference's WriteCompleted half-implementation
R3's verdict gains an aside documenting why the original native
MxAccess `OnWriteComplete` event has historically only fired for the
one exact 5-byte pattern `00 00 50 80 00` (= `MxStatus.WriteCompleteOk`).

Verified at:
- `src/MxNativeClient/MxNativeCompatibilityServer.cs:756` —
  `if (!evt.Message.IsMxAccessWriteComplete) return;` gates the
  consumer-facing `WriteCompleted` event.
- `src/MxNativeCodec/NmxOperationStatusMessage.cs:18` —
  `IsMxAccessWriteComplete` requires
  `Format == StatusWord && StatusCode == 0x8050 && CompletionCode == 0x00`.

Every other completion frame is silently dropped — the 1-byte
`0x00`/`0x41`/`0xEF` ones, plus any non-success status word.

This was the underlying reason R3/R4 looked unsolvable for a year:
the answer "we don't know how to map" was actually "the native
compatibility shim deliberately doesn't map these because firing
typed failure events on ambiguous bytes was never a goal."

Path A's `MxStatus::from_packed_u32` (commit `c73a33e`) closes the
asymmetry on the Rust side: `Session::operation_status_events()`
exposes ALL typed outcomes the upstream synthesizer produces, not
just the WriteCompleteOk slice. The Rust port now has strictly
broader operation-status visibility than the .NET reference offered.

Recorded so future contributors don't re-derive this from scratch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 07:13:28 -04:00
Joseph Doherty c73a33edd8 [R3/R4 Path A] mxaccess: port Lmx.dll FUN_10100ce0 synthesizer kernel
rust / build / test / clippy / fmt (push) Has been cancelled
rust / cargo public-api drift check (F41) (push) Has been cancelled
Path A landed for R3/R4. The byte->MxStatus synthesizer in Lmx.dll is
FUN_10100ce0 (`analysis/ghidra/exports/Lmx.dll.synthesizer-helpers2-decompile.md`),
a 4-byte u32 LE -> 4-tuple MxStatus decoder used by every NMX-frame
parser in Lmx.dll. The kernel is byte-deterministic and context-free,
so it ports as a pure function -- the operation-tracking state
machine the original verdict deferred is NOT required for synthesis.

Bit layout (per FUN_10100ce0 lines 21-24):
  bit 31:        success    (-1 if set, 0 if clear)
  bits 27..24:   category   (4 bits)
  bits 23..20:   detected_by (4 bits)
  bits 15..0:    detail     (i16 -- low 16 bits, signed)
  bits 30..28, 19..16: reserved/padding

Codec changes:
- MxStatus::from_packed_u32() / ::to_packed_u32() -- the kernel +
  inverse for round-trip parity.
- MxStatus::from_nmx_response_code() -- the constructed-from-response-
  code switch in FUN_1010bd10:741-770 (six proven mappings: 0x01, 0x02
  -> CommunicationError + RequestingNmx; 0x03 -> ConfigurationError +
  RequestingNmx; 0x04 -> ConfigurationError + RespondingNmx; 0x05 ->
  CommunicationError + RespondingNmx; 0x1A -> CommunicationError +
  RequestingNmx).
- MxStatusCategory / MxStatusSource: from_i16/to_i16 promoted to const
  fn so MxStatus::from_packed_u32 can be const.
- NmxOperationStatusMessage::try_parse_process_data_received_body() --
  thin wrapper that peels the outer NmxObservedEnvelope before
  delegating to try_parse_inner. Mirrors
  NmxOperationStatusMessage.TryParseProcessDataReceivedBody (.NET cs:20-32).
- NmxOperationStatusMessage::promote_to_typed() -- entry point that
  returns the existing Status field. Documented as a no-op pass-through
  for now (the 5-byte inner-body wire shape is NOT the same field as
  the 4-byte packed-u32 the kernel decodes); kept for API symmetry.
- 22 new round-trip tests covering the kernel, the response-code
  switch, the proven 0x00/0x41/0xEF completion bytes, and round-trip
  for every canonical sentinel.

mxaccess (Session) changes:
- New OperationKind enum (Write/WriteSecured/Read/Subscribe/
  Unsubscribe/Activate/Suspend/Other).
- New OperationContext struct (correlation_id, op_kind, reference,
  retry_count) -- ground for the F54 follow-on per-operation
  correlation work.
- New OperationStatus event type {raw, status, context,
  is_during_recovery}, mirroring MxNativeOperationStatusEvent (cs:73-78)
  with the typed-MxStatus addition.
- Session::operation_status_events() -> broadcast::Receiver<Arc<
  OperationStatus>> + operation_status_stream() Stream variant.
- callback_router() now tries operation-status parsing first, falling
  through to subscription messages -- matches MxNativeSession
  .OnCallbackReceived dispatch order (cs:574,582,590).
- recover_connection() flips a recovery_active counter (Arc<AtomicU32>
  shared with the router) so OperationStatus.is_during_recovery is
  populated correctly. Mirrors MxNativeSession._recoveryActive
  Volatile.Read at cs:573.
- 3 new router tests covering: status-word frame dispatch + typed
  promotion to WriteCompleteOk; completion-only frames stay verbatim;
  is_during_recovery is stamped from the live counter.

Per-operation context tracking (correlating completion frames back to
outstanding writes/subscribes via the correlation_id) is filed as F54
in design/followups.md. The synthesizer kernel itself is byte-
deterministic, so the kernel and the correlation work are decoupled.

Ghidra evidence (the next-ring xref walk beyond FUN_10114a90):
- analysis/ghidra/exports/Lmx.dll.set-attribute-result-xrefs.md --
  xrefs to OnSetAttributeResult / CancelWithStatus / OperationComplete.
- analysis/ghidra/exports/Lmx.dll.vtable-data-xrefs.md -- vtable-slot
  data xrefs for the virtual-dispatch path.
- analysis/ghidra/exports/Lmx.dll.synthesizer-decompile.md --
  ScanOnDemandCallback::OperationComplete/MultipleOperationComplete
  (FUN_1010b990), RemotePlatformResolver::OperationComplete
  (FUN_1010dc80), and the constructed-from-responseCode synthesizer
  in FUN_1010bd10 (lines 698-770). FUN_1010bd10 is the wire-frame
  receiver that drives the synthesis.
- analysis/ghidra/exports/Lmx.dll.synthesizer-helpers-decompile.md --
  FUN_10003fc0 (the <success %d category %d ...> formatter; confirms
  the 4-tuple layout), FUN_1008f150 (dispatch helper).
- analysis/ghidra/exports/Lmx.dll.synthesizer-helpers2-decompile.md --
  FUN_10100ce0 (the kernel itself), FUN_10100bc0 (3xu16 reader),
  FUN_1005e580 (4-byte stream reader), FUN_1010ee00 (sister NMX-frame
  parser using the same kernel).
- analysis/ghidra/exports/Lmx.dll.synthesizer-callers-xrefs.md --
  caller graph; confirms the kernel is called from many wire-frame
  parsers but each parser shares the single 4-byte decoder.

R3/R4 verdict updated in design/70-risks-and-open-questions.md from
"settled at verbatim-preserve" to "settled per Path A". F54 filed in
design/followups.md for the per-operation correlation work.

cargo build / test / clippy -D warnings / RUSTDOCFLAGS=-D warnings doc
all clean. cargo public-api baselines regenerated for mxaccess and
mxaccess-codec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 07:08:36 -04:00
Joseph Doherty 460c61df43 [R3/R4] Path-A trace: synthesizer is in Lmx.dll's NMX-frame decoder
Five-stage Ghidra headless decompile traces the byte-to-MXSTATUS_PROXY
synthesis path end-to-end across LmxProxy.dll and Lmx.dll. New evidence
files committed alongside R3/R4 verdict update:

- analysis/ghidra/exports/LmxProxy.dll.fire-event-xrefs.md
- analysis/ghidra/exports/LmxProxy.dll.status-synthesis-decompile.md
- analysis/ghidra/exports/LmxProxy.dll.mxstatus-safearray-decompile.md
- analysis/ghidra/exports/Lmx.dll.set-attribute-result-decompile.md

Layer-by-layer findings (bytes flow inward; synthesis flows outward):

1. `Lmx.aaDCT` at 0x10178fc0 is `SysAllocString(L"Lmx.aaDCT")` — a
   tracing category BSTR, not a table.
2. `MXSTATUS_PROXY` is a 16-byte marshalled struct (4 × i16 padded
   to i32 boundaries with Pack=4) — the OUTPUT of synthesis, not a
   lookup entry.
3. `LmxProxy.dll` Fire_* event handlers receive already-populated
   `MXSTATUS_PROXY[]` and forward through ATL dispatch — no synthesis.
4. `LmxProxy.dll` Fire_* CALLERS (FUN_1001657f / FUN_10016b50 /
   FUN_10016d4b) call FUN_10003f60(out_safearray, in_status_ptr,
   count=1) which is a VERBATIM memcpy of an existing 14-byte buffer
   into the SAFEARRAY — no transformation.
5. `Lmx.dll`'s `PreboundReference::OnSetAttributeResult` (FUN_10114a90)
   receives an already-populated `short *param_7` status buffer. Log
   line confirms the layout: `<success %d category %d detectedBy %d
   detail %d>`. Dispatches on typed values — synthesis is upstream of
   this function too.

The synthesizer is the NMX-frame decoder in Lmx.dll that calls
OnSetAttributeResult / OnGetAttributeResult / equivalent
OperationComplete handler. The decoder takes raw NMX bytes plus
operation context (item handle, engine state, retry state,
correlation id) and computes the populated MXSTATUS_PROXY. There is
NO static lookup table — synthesis is per-message contextual.

Two viable paths to typed promotion (both substantial; neither a
small codec patch):

- Path A: port the synthesizer. ~1-2 weeks. Requires extending the
  Rust session to track per-operation context (handles, retries,
  correlation ids). Out of V1 scope.
- Path B: empirical capture pairs. ~30 min × 6-10 scenarios. Output
  is a (byte, context → status) mapping that approximates without
  re-implementing. Risk: mapping is only valid for captured contexts.

R3/R4 stay settled at verbatim-preserve. The .NET reference does
the same for the same reason: the synthesizer is too context-
dependent to mirror without porting the entire operation-tracking
state machine.

Reopen criteria sharpened: either (a) a consumer files a concrete
use case for typed promotion of a specific byte+context combination
(Path B's empirical capture for that one combination is the cheapest
answer); or (b) a major-version bump justifies the state-machine
port (Path A).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 06:33:02 -04:00
Joseph Doherty 4dfc0cee65 [R3 + R4 + R8] settle protocol-level risks per Ghidra evidence
Ghidra headless decompile of `Lmx.dll`'s `aaDCT` symbol + the
`LmxProxy.dll` Fire_* event handlers (logs at
`analysis/ghidra/exports/Lmx.dll.aadct-decompile.md` and
`analysis/ghidra/exports/LmxProxy.dll.completion-status-decompile.md`)
settles **R3** and **R4** as "no static byte→status lookup table
exists":

- `Lmx.aaDCT` at `0x10178fc0` is a `SysAllocString(L"Lmx.aaDCT")` into
  a global BSTR — a logging category name, not a table.
- `MXSTATUS_PROXY` is a 4-field struct (success/category/detectedBy/
  detail), used as the marshalled COM event payload — not a static
  array of pre-mapped statuses.
- `Fire_OnDataChange` / `Fire_OnWriteComplete` / `Fire_OperationComplete` /
  `Fire_OnBufferedDataChange` (RVAs 0x15f72, 0x1611f, 0x16271, 0x163c0
  in `LmxProxy.dll`) receive ALREADY-POPULATED `MXSTATUS_PROXY[]`
  arrays — the byte-to-struct synthesis happens upstream in the
  proxy's NMX-callback ingestion code, not via a table lookup. The
  synthesis is per-event computation from operation context (engine
  ids, item handles, retry counters), not a static promotion.

R3/R4 status updated from "indefinitely deferred — no Ghidra table"
to "settled — no table exists; verbatim preservation is the canonical
answer." The .NET reference's `NmxOperationStatusMessage.TryParseInner`
+ the Rust port's `mxaccess-codec/src/operation_status.rs` already
match this canonical behaviour; no code change required.

Reopen R3/R4 only if a context-aware capture surfaces a per-byte
synthesis logic that depends on operation context — at which point
the codec would need access to the originating operation's context,
which is upstream of the bytes themselves.

**R8** marked permanently deferred — implementation already parses
NTLM AV pairs per [MS-NLMP] §2.2.2.1 (including the cross-domain
shapes `MsvAvDnsTreeName` / `MsvAvDnsComputerName` carrying the
trusted-domain DNS suffix), what's missing is the live capture, and
the live capture requires a multi-domain Windows lab not available
on this dev host. Same status pattern as F3 in `design/followups.md`.

Open evidence gaps table updated to reflect:
- Cross-domain NTLM: deferred (R8)
- Ghidra mapping table for completion-only bytes: no table exists
  (R3/R4 settled)
- Activate/Suspend transition (wire): partial (F44 + F46), live re-run
  pending (F50)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 06:23:05 -04:00
Joseph Doherty 0e93e3a8fa design/followups: file F48-F53 for known V1 residuals
After the M6 closeout sweep (F35-F47 all resolved), six residuals
remain that were either documented inline in design docs or implied
by closure language but not formally tracked as F-numbers. File them
explicitly so the next iteration has a clean starting list:

- F48: Execute `cargo publish` for V1 (F43 was dry-run only). Documents
  the 9-crate dependency-ordered publish sequence + the version bump
  from 0.0.0 placeholder to 0.1.0.
- F49: Live verification sweep for F36 (buffered subscribe) +
  F45 (recovery replay) + F47 (unsubscribe skip) + F40 (metrics).
  Closes the gap where these features ship with unit tests but
  weren't live-exercised against AVEVA in the closing iteration.
- F50: Run the F46 Suspend/Activate Frida capture live (script
  ready, capture deferred to maintainer-side).
- F51: Live type-matrix expansion for `asb-subscribe` — Bool /
  Float / Double / String / DateTime / Duration / arrays. F32 was
  closed via "deployable maximum" but the codec supports more types
  than the live matrix exercises.
- F52: Codec performance optimisations from F39 (BytesMut output
  buffer, name-signature cache, session scratch pool). Documented as
  post-V1 in M6-bench-baseline.md; filing them as F-numbers so the
  alloc-count deltas are tracked when they land.
- F53: Enable `#![warn(missing_docs)]` workspace-wide. Deferred from
  F42 — the lint surfaces hundreds of low-priority gaps that need a
  dedicated pass.

R3, R4, R8, F3 already in their respective tracking docs (the risks
register + the Open section's permanently-external-blocked entry).

Open section now contains: F3 (permanently external-blocked) +
F48-F53 (V1-residual triage).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 06:11:11 -04:00
Joseph Doherty 25befcb72e design/followups: move F45 + F47 to Resolved (M6 + spawned closures)
rust / build / test / clippy / fmt (push) Has been cancelled
rust / cargo public-api drift check (F41) (push) Has been cancelled
F45 (commit 9b57cf8) and F47 (commit 1a1830f) close the buffered-
subscription recovery + unsubscribe symmetry gap that F36 left open.
The Open section now contains only F3 (cross-domain NTLM Type1/2/3
fixture, permanently external-blocked on this single-domain dev
host — needs multi-domain Windows lab).

This is the end-state for V1: all M0-M6 followups resolved plus the
two M6-spawned follow-ons. F3 stays Open as a documented external
gap; reopen it if the dev host gains a second domain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 05:59:38 -04:00
Joseph Doherty 1a1830f3bf [F47] mxaccess: unsubscribe skips UnAdvise for buffered subscriptions
Mirrors the .NET reference's `if (!subscription.IsBuffered)` guard
at `MxNativeSession.cs:361-381`. The Rust port previously emitted an
`UnAdvise` frame for both plain and buffered subscriptions; the
buffered server-side registration is unwound by the engine when the
`RegisterReference` handle goes away, so emitting an `UnAdvise` for
buffered entries is at best a no-op extra frame and at worst could
race with the engine's own teardown.

Fix: branch `Session::unsubscribe` on `SubscriptionEntry::mode` (the
discriminator F45 added). For `SubscriptionMode::Buffered { ... }`,
skip the `un_advise` call and proceed directly to registry cleanup.
For `SubscriptionMode::Plain`, retain the previous behaviour.

The registry-entry probe runs first (separate lock acquisition) so
the `is_buffered` decision doesn't hold the NMX-client mutex
unnecessarily — common case where the entry is plain still acquires
the NMX lock immediately after.

The metrics counter `record_unadvise()` still fires on every public
`unsubscribe` call regardless of mode — it tracks consumer-side
unsubscribe rate, not wire-frame rate. That matches what dashboards
expect from the public API.

New unit test `unsubscribe_skips_un_advise_for_buffered_subscription`
issues a plain subscribe (recorded as 1 RPC), mutates the registry
entry to `SubscriptionMode::Buffered`, calls unsubscribe, and
asserts the recorded RPC count stays at 1 (no UnAdvise emitted).
The existing `subscribe_populates_registry_unsubscribe_clears_it`
test serves as the negative control for the plain branch.

Workspace 794 → 795 tests; clippy clean; rustdoc clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 05:58:57 -04:00
Joseph Doherty 9b57cf8f3b [F45] mxaccess: recovery replay re-issues RegisterReference for buffered subs
`Session::recover_connection_core` previously walked
`SessionInner::subscriptions` and replayed every entry via
`AdviseSupervisory`, which lost the `.property(buffer)` registration
on buffered subscriptions — silently downgrading buffered → plain on
transport rebuild.

Fix:

- New `pub(crate) enum SubscriptionMode { Plain, Buffered { ... } }`
  discriminator carried on each `SubscriptionEntry`. Buffered variant
  retains the un-suffixed reference + the rounded interval (so the
  re-issued buffered registration matches the original cadence) +
  the empty `item_context` / zero `item_handle` matching the wire
  send.
- `Session::subscribe` (plain path) records `SubscriptionMode::Plain`.
  `subscribe_buffered_nmx` records `SubscriptionMode::Buffered { ... }`.
- `recover_connection_core` matches on `entry.mode`. Plain branch
  unchanged. Buffered branch re-applies `.property(buffer)` via
  `to_buffered_item_definition` (idempotent), rebuilds the original
  `NmxReferenceRegistrationMessage` with the saved correlation id +
  `subscribe = true`, and dispatches `register_reference` (kind=
  ItemControl, inner command 0x10) against the replacement
  transport. Mirrors `MxNativeSession.ReAdviseSubscription`
  (`MxNativeSession.cs:538-569`).

New unit test `recover_connection_replays_buffered_subscription_via_
register_reference` synthesises a buffered registry entry, installs a
`RebuildFactory` pointing at a recording NMX server, drives
`recover_connection`, then asserts the recorded `TransferData` carries
inner command `0x10` (NOT `0x1f`) with the `.property(buffer)`-
suffixed item_definition + the saved correlation id + subscribe=true.

Side-finding worth filing separately: `Session::unsubscribe`
unconditionally calls `un_advise` for both plain and buffered
entries, but the .NET reference's `Unsubscribe`
(`MxNativeSession.cs:361-381`) skips `UnAdvise` for buffered
(`if (!subscription.IsBuffered)`). Out of scope for F45 (recovery-
only); will file as F47.

Public API unchanged. `SubscriptionMode` + `SubscriptionEntry` stay
`pub(crate)` — `cargo public-api -p mxaccess` baseline is unchanged.

Workspace 793 → 794 tests; clippy clean; rustdoc clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 05:54:30 -04:00
Joseph Doherty 2281309a86 design/followups: move F46 to Resolved (Frida hooks landed) 2026-05-06 05:43:43 -04:00
Joseph Doherty 808fea18a0 [F46] analysis/frida: Suspend/Activate hooks + R5 next-step
Closes the wire-side gap left by capture 077 in F44's R5 walk. The Frida
script now hooks the production LmxProxy.dll dispatchers so a future live
re-run on the AVEVA host can answer "does CLMXProxyServer issue a separate
ORPC method for Suspend/Activate, or are they synthesised client-side?"

Hooks added in `analysis/frida/mx-nmx-trace.js`:
- `LmxProxy.dll!CLMXProxyServer.Suspend`  @ RVA 0x13d9c (FUN_10013d9c)
- `LmxProxy.dll!CLMXProxyServer.Activate` @ RVA 0x14028 (FUN_10014028)

Both RVAs were extracted from
`analysis/ghidra/exports/LmxProxy.dll.string-refs.tsv` rows 119/122 (the
`CLMXProxyServer::Suspend - Server Handle` / `Activate - Server Handle`
log strings each xref one function — same pattern as the existing
AdviseSupervisory hook at 0x142b4). The hooks emit `mx.suspend.begin/end`
and `mx.activate.begin/end` events with serverHandle, itemHandle, and the
`MxStatus*` out parameter decoded as 4 x int16 (Success / Category /
DetectedBy / Detail per `src/MxNativeCodec/MxStatus.cs`). Naming matches
the F46 spec's `mx.<verb>.begin / end` grep convention rather than the
generic `call.enter / leave` shape because we want to filter these out
of large traces without false positives from other LmxProxy entrypoints.

No `Resume` / `Reactivate` exports exist in `LmxProxy.dll` — verified
against `analysis/ghidra/exports/LmxProxy.dll.ghidra.md` (no such string
xrefs) and the decompiled `ILMXProxyServer5` / `ILMXProxyServer4`
interfaces under `analysis/decompiled-mxaccess/ArchestrA/MxAccess/`
(only Suspend and Activate are declared on the dispatch interface).

The script's top-of-file comment now carries the live re-run procedure
(rebuild MxTraceHarness x86, attach Frida with `--scenario=suspend-advised`
then `--scenario=activate-advised`, save under
`captures/NNN-frida-suspend-activate-instrumented/`, grep the new TSV for
`mx.suspend.*` / `mx.activate.*` and correlate with `nmx.enter` events
in the same time window). Live capture is intentionally deferred to the
maintainer per the F46 spec — this dev box has no AVEVA install.

`design/70-risks-and-open-questions.md` R5 status updated:
- Title flag `(filed as F45)` -> `(filed as F46, hook landed pending live re-run)`
  (the docs/M6-buffered-evidence.md footnote referenced F45 from before
  F45 / F46 were de-conflicted by commit 2120dfa).
- New "Next step - F46" paragraph documents the two hooked RVAs, the
  out-param decode shape, and the verified absence of Resume / Reactivate
  symbols.
- "Current best answer" paragraph re-points the residual ORPC question
  at F46.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 05:42:57 -04:00
Joseph Doherty c7e71e4424 design/followups: move F41 + F43 to Resolved (M6 complete)
rust / build / test / clippy / fmt (push) Has been cancelled
rust / cargo public-api drift check (F41) (push) Has been cancelled
All 10 M6 sub-followups (F35-F44 minus the ones absorbed into F44)
plus F41 + F43 are now resolved. Open section narrows to:
- F45: buffered recovery replay (sub-followup of F36)
- F46: Suspend/Activate wire emission (sub-followup of F44)
- F3: cross-domain NTLM fixture (permanently external-blocked)

M6 closeout: see CHANGELOG.md for the V1 release notes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 05:34:45 -04:00
Joseph Doherty 7b15c853d1 [F43] release prep: CHANGELOG + cargo publish --dry-run validation
V1 release prep per M6 DoD bullet 6:

**`CHANGELOG.md`** — V1 release notes covering all 9 workspace crates
(`mxaccess-codec`, `mxaccess-rpc`, `mxaccess-asb-nettcp`,
`mxaccess-asb`, `mxaccess-galaxy`, `mxaccess-callback`,
`mxaccess-nmx`, `mxaccess`, `mxaccess-compat`), the M0 → M6
milestone closeouts, deliberate divergences from the .NET reference
(multi-record DataUpdate codec relaxation per F44; buffered single-
sample stream per R2), and known limitations (F3 cross-domain NTLM,
F45 buffered recovery replay, F46 Suspend/Activate wire instrumentation,
R3/R4 OperationComplete trigger). Documents the dependency-ordered
publish sequence (leaf crates first; dependent crates require their
deps to exist on crates.io before their dry-runs can run).

**`cargo publish --dry-run` validation:**
- Leaf crates (mxaccess-codec, mxaccess-rpc, mxaccess-asb-nettcp):
  dry-run passes — tarball builds, metadata complete, license/
  description/repository/rust-version all present via
  `workspace.package`.
- Dependent crates (mxaccess-asb, mxaccess-galaxy, mxaccess-callback,
  mxaccess-nmx, mxaccess, mxaccess-compat): dry-run fails with
  "no matching package" against crates.io — expected behaviour, the
  registry lookup happens even with `--no-verify`. Validation of
  these crates falls to the build-test-clippy-public_api matrix
  rather than dry-run.

`design/followups.md`: F43 moved to Resolved with a verdict pointing
at this commit + the CHANGELOG.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 05:33:43 -04:00
Joseph Doherty f0c9dd2214 rust: add version specifiers to workspace path deps for cargo publish 2026-05-06 05:31:57 -04:00
Joseph Doherty 9e57bfd451 [F41 + F44 reconciliation] cargo public-api baselines + multi-record DataUpdate codec
**F41 — public-api baselines (M6 DoD bullet 5)**

`design/public-api/{crate}.txt` for all 9 workspace crates, generated
via `cargo +nightly public-api --simplified -p <crate>`. Per-crate
baseline sizes:
- mxaccess-codec: 2516 lines
- mxaccess-asb:   1258 lines
- mxaccess-rpc:   1273 lines
- mxaccess-asb-nettcp: 708 lines
- mxaccess: 542 lines
- mxaccess-galaxy: 374 lines
- mxaccess-callback: 170 lines
- mxaccess-compat: 123 lines
- mxaccess-nmx: 118 lines

`design/public-api/README.md` documents the update procedure
(install nightly + cargo-public-api, regenerate the affected baseline
on intentional API changes, commit alongside).

`.github/workflows/rust.yml` gains a `public-api` job that runs the
same diff against the committed baseline; drift fails CI with a
unified diff in the log so the PR author can either revert or
update the baseline.

**F44 reconciliation — multi-record DataUpdate codec**

Cherry-picked from the F44 sub-agent's worktree (commit `aec6a0c`):
`subscription_message.rs::parse_data_update` now loops over
`record_count` like `parse_subscription_status` does, accepting any
positive count. The .NET reference still hard-throws on
`record_count != 1`; the Rust codec deliberately diverges per the F44
evidence walk against `captures/094-frida-buffered-separate-writer/
frida-events.tsv:145` (a `0x33` DataUpdate body with `record_count = 2`,
inner_length = 23 (preamble) + 2 * 19 (records) = 61, post a
separate-session writer triggering two value changes inside one
`SetBufferedUpdateInterval(1000)` window).

Two new round-trip tests:
- `data_update_multi_record_round_trip` — synthesises a 2-record body,
  parses, asserts both records decode to expected Int32 values.
- `data_update_capture_094_truncated_record_errors` — truncates the
  capture-094 fixture mid-second-record, asserts CodecError::Decode.

New wire-byte fixtures under `crates/mxaccess-codec/tests/fixtures/m6-buffered/`:
- `094-line145-dataupdate-recordcount2.bin` (57 bytes, `0x33` multi-record)
- `094-line48-substatus-recordcount2.bin` (101 bytes, `0x32` multi-record)

R2 in `design/70-risks-and-open-questions.md` updated from
"single-sample (settled silently)" to "settled per option (a) — codec
relaxed; multi-record observed in production-stack tracing."

`design/followups.md`: F44's verdict updated to reflect the
contradiction-then-relaxation, with reference to the new tests +
fixtures.

Workspace 792 → 794 tests pass; clippy clean; rustdoc clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 05:27:11 -04:00
Joseph Doherty 2120dfa965 design/followups: move F35/F40/F44 to Resolved + de-conflict F45/F46
rust / build / test / clippy / fmt (push) Has been cancelled
After commits d5aa152 (F35) and ad1cf23 (F36+F40+F44), three M6
sub-followups belong under Resolved with concise verdicts referencing
the matching commits.

Sub-agent merge cleanup:
- Two sub-agents independently filed new followup F45 in parallel —
  rename the Suspend/Activate wire-emission gap to F46, leaving the
  buffered-recovery-replay item as F45 (filed by the F36 work since
  it's the more immediate dependent).
- Open section now contains only F41 + F43 + F45 + F46 + F3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 05:15:13 -04:00
Joseph Doherty ad1cf2351c [F36 + F40 + F44] M6 wave 1: subscribe_buffered (NMX) + metrics + evidence
Three M6 sub-followups landed in this wave (sub-agent worktrees +
manual reconciliation in main):

**F36 — Session::subscribe_buffered (NMX) per R2 single-sample**
- `BufferedOptions::rounded_update_interval_ms()` — 100ms rounding
  helper mirroring MxNativeCompatibilityServer.cs:638
  ((updateInterval + 99) / 100) * 100, saturating on overflow.
- `Session::subscribe_buffered` (public, lib.rs:604) delegates to
  the new private `subscribe_buffered_nmx` which uses the buffered
  RegisterReference path: item_definition suffixed with
  `.property(buffer)`, subscribe=true (no separate
  AdviseSupervisory follow-up — verified against capture 082).
- Per R2 verified at wwtools/mxaccesscli/docs/api-notes.md the wire
  semantic is single-sample-per-event with a server-side cadence
  knob; rounded_ms is held client-side only (native MXAccess does
  not emit a separate SetBufferedUpdateInterval RPC, verified by
  absence in 079/082 captures).
- New crates/mxaccess/examples/subscribe-buffered.rs.
- New crates/mxaccess-codec/tests/buffered_register_reference_parity.rs:
  4 tests (capture 079/082 round-trip, suffix helper, constructive
  forward-build vs capture 082).

**F40 — Optional metrics feature**
- New crates/mxaccess/src/metrics.rs (275 lines): `pub(crate)`
  thin wrappers (`record_write_latency`, `record_read_latency`,
  `inc_writes`, `inc_reads`, `inc_advises`, `inc_recovery_*`,
  `set_active_subscriptions`, etc.) that compile to no-ops under
  `#[cfg(not(feature = "metrics"))]`. Call sites in session.rs +
  asb_session.rs invoke them unconditionally; the gate is inside
  the wrapper.
- `metrics = { version = "0.24", optional = true }` added to
  workspace + mxaccess crate Cargo.toml.
- Default build: zero metrics dep, zero runtime cost.

**F44 — Buffered batch + suspend capture decode evidence**
- New docs/M6-buffered-evidence.md: per-capture summary for
  077, 079, 080, 081, 082, 094 — call sequence, key wire bytes,
  R2/R5 verdict.
- R2 confirmed silently as "not a real risk" — single-sample
  observed across 079/080/082/094.
- R5 trigger conditions documented from capture 077: AdviseSupervisory
  + Suspend pair, 1-second intervals, succeeds on enum attributes.
- design/70-risks-and-open-questions.md R2/R5 status updated.

Workspace: 759 → 792 tests, clippy clean, rustdoc -D warnings clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 05:12:17 -04:00
Joseph Doherty d5aa152b1f [F35] mxaccess-compat: LMXProxyServer-shaped facade (18 methods)
Replace the 8-line `mxaccess-compat` stub with a real `LmxClient`
struct exposing the 18 `ILMXProxyServer5` methods as Rust async fns
on top of `mxaccess::Session` (NMX) and `mxaccess::AsbSession` (ASB).

Handle-table approach
* `Mutex<HashMap<i32, ItemRef>>` for item handles, populated by
  `add_item` / `add_item_2` / `add_buffered_item`, drained by
  `remove_item` / `unregister`.
* `Mutex<HashMap<i32, UserRef>>` for user handles allocated by
  `authenticate_user` / `archestra_user_to_id`.
* `AtomicI32` monotonic counters for both, matching the .NET
  reference's `_nextItemHandle` / `_nextUserHandles` per
  `MxNativeCompatibilityServer.cs:62-63`.

Stream-based event surface (per Q4)
* `OnDataChange` / `OnBufferedDataChange` / `OnWriteComplete` /
  `OperationComplete` exposed as `EventStream<T>: Stream<Item=T>`,
  backed by `tokio::sync::broadcast` channels. Lag silently skips
  past `BroadcastStream::Lagged` to keep the public `Item` shape
  ergonomic. NOT COM events — that's the post-V1
  `mxaccess-compat-com` crate per design/70-risks-and-open-questions.md
  Q4. The `OperationComplete` channel is wired but no firing path
  is modelled (R3 deferred — no captured byte mapping yet).
* `Advise` / `AdviseSupervisory` spawn a background fan-out task
  that drains the `Subscription` stream and routes each
  `DataChange` to either `on_data_change` or
  `on_buffered_data_change` based on the item's `is_buffered` flag.
  `UnAdvise` / `RemoveItem` abort the task.

Pass-through methods
* `Write` / `Write2` -> `Session::write` / `write_with_timestamp`
  (`userId` ignored — the underlying surface uses engine identity).
* `WriteSecured2` -> `Session::write_secured_at` with both user ids
  always passed (R6: single-user secured = same id twice; never
  gated).
* `AdviseSupervisory` collapses onto `Session::subscribe` because
  the wire path is `AdviseSupervisory` already (`session.rs:1057`),
  matching the .NET reference's `cs:251-259` identical collapse.
* `SetBufferedUpdateInterval` rounds up to nearest 100 ms per
  `MxNativeCompatibilityServer.cs:638`.

Stubbed pass-throughs (mirror upstream `Error::Unsupported`)
* `WriteSecured` (no timestamp) — `Session::write_secured` is
  stubbed at `crates/mxaccess/src/lib.rs:472` (only
  `WriteSecured2`/`0x3A` is ported); workaround documented inline.
* `AddBufferedItem` allocates the handle but `Advise` for buffered
  items does not yet drive `Session::subscribe_buffered` cadence
  knob — TODO(F36) flagged inline at `add_buffered_item` and
  `set_buffered_update_interval`.

Tests (25 new, all green)
* Handle-table lifecycle: Add -> Advise -> UnAdvise -> Remove with
  a mocked subscription task.
* Monotonic handle allocation; context-prefix combination.
* `SetBufferedUpdateInterval` rounding (50 -> 100, 101 -> 200, etc.)
  + zero-rejection.
* Compile-time check that all 18 LMX methods are reachable on
  `LmxClient`.
* Each event stream yields published items; lag silently dropped.
* GUID-shape validation; server-handle mismatch errors.

Build hygiene
* `cargo build -p mxaccess-compat` clean.
* `cargo test -p mxaccess-compat` -> 25 passed.
* `cargo clippy -p mxaccess-compat --all-targets -- -D warnings` clean.
* `RUSTDOCFLAGS=-D warnings cargo doc -p mxaccess-compat --no-deps` clean.

Deferred / TODOs
* TODO(F36): wire `set_buffered_update_interval` cadence into the
  `advise` path for buffered items.
* TODO(R3): plumb a real trigger into `on_operation_complete` once
  the byte mapping lands.
* TODO(wave 2): live integration tests against AVEVA.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 05:06:26 -04:00
Joseph Doherty a1c4c6203e design/followups: move F37/F38/F39/F42 to Resolved
rust / build / test / clippy / fmt (push) Has been cancelled
Four M6 sub-followups closed in this session — moved to Resolved
section with concise verdicts referencing the matching commits:

- F37 (commit 34045c2): ASB subscribe_buffered returns Unsupported
- F38 (commit 71c69b8): counting-allocator bench harness + R12
  baseline showing the target is already met
- F39 (closed-via-F38): zero-copy pass not needed for R12 target
  (1-4 allocs/op across the proven matrix); remaining
  optimisations documented as post-V1 work
- F42 (commit e79e289): cargo doc --workspace --no-deps clean

Open M6 work remaining: F35 (compat facade), F36 (NMX
subscribe_buffered), F40 (metrics feature), F41 (public-api
baseline), F43 (release prep), F44 (capture decode evidence).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 04:47:38 -04:00
Joseph Doherty 71c69b80c6 [F38] mxaccess-codec: counting-allocator bench harness + R12 baseline
Hand-rolled GlobalAlloc wrapper around System that tracks allocs +
bytes + deallocs via two atomics. Each scenario runs 10k iterations
after a 1k warm-up; output is a markdown table with allocs/op,
bytes/op, deallocs/op.

Why hand-rolled (not dhat/criterion): R12 gates on a single number
("< 5 allocs/write"). dhat is heap-profiling-oriented (call-stack
attribution, JSON snapshots); criterion measures wall-clock latency
which is reported-but-not-gated per 60-roadmap.md:104. A 50-line
GlobalAlloc + atomic counters is the simplest thing that answers
the gate.

Run: `cargo bench -p mxaccess-codec`

Baseline numbers (release, Windows x64):
- Bool write:    1.00 allocs/op
- Int32 write:   2.00 allocs/op
- Float32 write: 2.00 allocs/op
- Float64 write: 2.00 allocs/op
- String write:  4.00 allocs/op (5-char string)
- Handle from_names: 2.00 allocs/op
- DataUpdate decode: 1.00 alloc/op

R12's < 5 allocs/write target is **already met** across the proven
matrix without any zero-copy work. The bench gates on this — any
write_message::encode scenario at >= 5 allocs/op exits the harness
with code 1.

Companion: `design/M6-bench-baseline.md` documents the numbers,
explains the per-scenario breakdown, and tightens F39's scope from
"hit the target" to "nice-to-have optimisations" (BytesMut output
buffer, name-signature cache, session-level scratch pool).

Workspace: 759 tests still pass; clippy --benches clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 04:45:33 -04:00
Joseph Doherty e79e289743 [F42] cargo doc --workspace --no-deps clean (0 warnings)
Fix all 33 rustdoc warnings across the workspace:

- Unresolved intra-doc links: rewrite [`name`] → either backtick text
  (when not actually a link) or fully-qualified `[Type::method]` /
  `[crate::module::name]` form. Affected: mxaccess-codec
  (asb_variant, item_control, metadata_query, observed_write_template,
  reference_handle, write_message), mxaccess-rpc (pdu), mxaccess-nmx
  (client), mxaccess-asb-nettcp (nmf), mxaccess-callback (exporter),
  mxaccess (asb_session, session, lib).
- Bracket-text being interpreted as link refs (e.g. `body[17]` →
  `` `body[17]` ``).
- Private-item references in public docs (CALLBACK_BROADCAST_CAPACITY,
  recover_connection_core, mxvalue_to_writevalue) reduced to
  backtick-text since they aren't part of the public API.

`RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps` now
exits clean. Workspace 759 tests pass; clippy clean.

Defers `#![warn(missing_docs)]` lint to a future pass — the cleanup
target is the broken-link warnings, which are signal; missing-docs
would surface hundreds of low-priority public-item gaps that are out
of scope for this F-number.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 04:39:51 -04:00
Joseph Doherty 34045c2f6d [F37] mxaccess: AsbSession::subscribe_buffered returns Unsupported
ASB has no `SetBufferedUpdateInterval` analogue — the per-monitored-
item `MinimalMonitoredItem::sample_interval` plays the cadence-knob
role. Calling `subscribe_buffered` on an ASB session now returns
`Error::Unsupported { transport: TransportKind::Asb, operation: ... }`
synchronously, without touching the wire.

The error-construction logic is split into a free fn
`unsupported_subscribe_buffered_error()` so the gate's exact shape
is unit-testable without spinning up a live authenticator + transport
fake. New unit test asserts both the variant tag and that the
operation message names the unsupported method + hints at the
`sample_interval` analogue.

Workspace 758 → 759 tests, clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 04:32:45 -04:00
Joseph Doherty 2546710604 design/followups: add F35-F44 for M6 implementation plan
10 new followups decompose M6 (compatibility shim + production
hardening) into parallel-safe sub-streams:

- F35: mxaccess-compat LMXProxyServer-shaped facade (18 methods over
  Session/AsbSession)
- F36: Session::subscribe_buffered NMX path per R2 single-sample
- F37: ASB subscribe_buffered capability gate
- F38: counting-allocator cargo bench harness for R12 target
- F39: zero-copy codec pass (depends on F38)
- F40: optional metrics feature
- F41: cargo public-api baseline (depends on F35/F36/F37/F39/F40)
- F42: cargo doc cleanup pass
- F43: cargo publish --dry-run all crates (depends on F41)
- F44: decode buffered batch + suspend captures (077, 079-082, 094)
  for R2/R5 evidence

Parallelization: Wave 1 = F35/F36/F37/F38/F40/F42/F44 (different
crates / different concerns); Wave 2 = F39 (needs F38's bench);
Wave 3 = F41 (needs API stable); Wave 4 = F43 (release).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 04:28:38 -04:00
Joseph Doherty bedad57b4e design/followups: move F18 (M5 meta-tracker) to Resolved
rust / build / test / clippy / fmt (push) Has been cancelled
Trim the planning content (sub-stream breakdown table, parallel-safety
analysis, risk-driven sequencing, "Resolves when" gate) since M5 is
done. Keep the closure verdict, M5 DoD checklist showing the actual
state at close, sub-followup closeout list (F19-F26 + F28/F29/F30/
F31/F32/F33/F34), cumulative execution log, and the architectural
note explaining why AsbSession stays parallel to the NMX Session
rather than unified — that's load-bearing for future maintenance.

Open section now contains only F3 (cross-domain NTLM Type1/2/3
fixture, permanently external-blocked on this single-domain dev host
— resolution requires multi-domain Windows lab not available here).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 04:09:03 -04:00
Joseph Doherty b1a5f5ff1e design/followups: move F34 to Resolved (live-verified closure)
The F34 entry's body had grown into a debugging notebook with five
"Open hypotheses" and a "Resolves when" speculation block — all of
which are now moot since the actual fix landed. Trim to the closure
verdict, the technical evidence (captured fixture dictionary, the
dual-format insight), and the bonus context discovered while
debugging (Active/SampleInterval/result_code 32 quirks). Move from
"## Open" to "## Resolved" with date + commit 101a8b1.

Open section now contains only F18 (M5 meta-tracker, resolved at the
top) and F3 (cross-domain NTLM fixture, permanently external-blocked
on this single-domain dev host).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 04:04:37 -04:00
Joseph Doherty 101a8b13f5 [F34] mxaccess-asb: AddMonitoredItems body uses DataContract field names
Rewrite push_monitored_item_body to emit the DataContract field-suffix
names from AsbContracts.cs:940-965 (activeField, bufferedField,
itemField, sampleIntervalField, timeDeadbandField, userDataField,
valueDeadbandField) under prefix `b` bound to the
http://schemas.datacontract.org/2004/07/ArchestrAServices.ASBIDataV2Contract
namespace. The <Items> wrapper now declares xmlns:b + xmlns:i.

The legacy XmlSerializer property names (<Active>, <Item>,
<SampleInterval>, <Buffered>) only matter for the canonical-XML HMAC
signing input — that emitter at xml_canonical::emit_monitored_item is
unchanged and F28 fixture byte-equality still holds for all 13 ops.
On the binary NBFX wire MxDataProvider's DataContractSerializer
expects the field-suffix form.

Wire-byte type encoding matches the captured fixture
(add-monitored-items-request-wire.bin): bool → Bool record, ulong →
Zero/One/Chars (XmlConvert decimal text), ushort → Zero/One/Int8/Int16/Int32
(smallest-fit binary). Empty string? + null byte[]? emit as empty
elements with no <i:nil> attribute (matching the wire). Field order
follows the explicit [DataMember(Order = N)] sequence.

Adjacent: ItemIdentity is nested via DataContract field names too —
NOT the binary <ASBIData> fast-path, which only kicks in at top-level
message body members.

Verified live against AVEVA MxDataProvider: AddMonitoredItems now
returns 1 status item with error_code=0x0000 (previously 0 items;
the silent failure was the deliberate DC-schema mismatch); Publish
poll #4 delivers the actual tag value as
AsbVariant { type_id: 4, length: 4, payload: [99,0,0,0] } through the
F26 stream.

Pre-existing clippy::format_collect errors in auth.rs:339,342 and
client.rs:952 fixed in passing — they were blocking workspace clippy
otherwise.

Workspace: 757 → 758 tests, clippy -D warnings clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 04:01:11 -04:00
Joseph Doherty 6762526f09 design/followups: mark F18 (M5 meta-tracker) resolved
All sub-followups F19-F26 closed; M5 is functionally LIVE end-to-end
(asb-subscribe returns the real tag value over the wire). The
structural-port followups F18 spawned (F2/F10/F11/F27 for NTLM /
DCOM / RemUnknown / DH) all resolved separately. F18 stays under
"## Open" as the cumulative-execution-log anchor; status line at the
top now reflects the closed state so the open/resolved structure
matches reality.

Remaining open items: F34 (MonitoredItem wire format, P2 — needs
nbfx auto-intern fix + DataContract field-suffix body builders) and
F3 (cross-domain NTLM fixture, P2 — permanently external-blocked
on this single-domain dev host).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 03:35:41 -04:00
Joseph Doherty 1de049e114 [F2] mxaccess-rpc: NTLM verify_signature (server-to-client) with constant-time MAC compare
rust / build / test / clippy / fmt (push) Has been cancelled
Closes F2. Structural port from [MS-NLMP] §3.4.4 — same shape as
the existing sign path but uses the server-to-client sub-keys
(`SealKey_S→C` / `SignKey_S→C`) derived alongside the client-to-
server pair at the end of create_type3.

NtlmClientContext gained four new fields populated during
create_type3:
  - server_signing_key
  - server_sealing_key
  - server_sealing_state (independent RC4 stream)
  - server_sequence (independent counter)

The S→C key derivation already existed in auth.rs (the seal_key /
sign_key helpers take a client_mode flag); F2 plumbs them into a
new verify_signature(message, signature) method.

The verify path:
  1. Validates signature.len() == 16 + leading version word 0x01.
  2. Reads trailing seq num, compares against self.server_sequence
     (mismatch ⇒ InvalidSignature, no state change).
  3. Computes expected_mac = HMAC_MD5(server_signing_key,
     seq || message)[0..8] then RC4 transform.
  4. Constant-time compares expected_mac against wire bytes 4..12
     via subtle::ConstantTimeEq.
  5. On success: commits cipher-state advance + ++server_sequence.
     On failure: re-derives RC4 from server_sealing_key and skips
     past server_sequence × 8 keystream bytes to restore the
     pre-verify position — caller can retry.

New dep `subtle = "2"` (workspace-internal to mxaccess-rpc) for
the timing-oracle-safe MAC compare.

6 new tests:
  - verify_signature_round_trip_against_sign (3-message sequence
    via paired_authed_context helper that aliases server-side keys
    onto client-side for self-validating round-trip)
  - verify_signature_rejects_corrupted_mac (with
    server_sequence-non-advance assertion)
  - verify_signature_rejects_wrong_sequence_number
  - verify_signature_rejects_wrong_version_field
  - verify_signature_rejects_wrong_length
  - verify_signature_before_authenticate_errors

mxaccess-rpc 188 → 194 tests; default-feature clippy clean.

The "awaiting wire-fixture capture" step listed in F2's prior
status note is no longer a hard prerequisite — [MS-NLMP] §3.4.4
fully defines the algorithm and the round-trip tests prove the
encoder/decoder pair is internally consistent. A captured
StatusReceived frame would still validate byte-parity vs a real
NmxSvc.exe signer, but that's future verification work; the
structural port ships unblocked.

design/followups.md F2 moved to Resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 03:30:48 -04:00
Joseph Doherty 161b0cedfa [F10 + F11] mxaccess-rpc: structural ports for ResolveOxid2 + RemAddRef/RemRelease
rust / build / test / clippy / fmt (push) Has been cancelled
Closes F10 and F11 per option (b) of each followup's resolve
criterion: hand-rolled body codecs derived from the [MS-DCOM]
spec, ship structurally with no live fixture (the .NET reference
doesn't call these opnums), validate against captured frames when
they become available.

F10 — IObjectExporter::ResolveOxid2 (opnum 4):
  Per [MS-DCOM] §3.1.2.5.1.4. New parse_resolve_oxid2_result
  mirrors parse_resolve_oxid_result exactly except for the extra
  4-byte COMVERSION slot (u16 major + u16 minor) between
  authn_hint and error_status. Trailing-fields check tightens
  from 24 bytes (opnum 0) to 28 bytes (opnum 4). New ComVersion +
  ResolveOxid2Result types. referent_id=0 short-circuit returns
  empty bindings + default ComVersion + tail-status, matching
  opnum 0's pattern.

F11 — IRemUnknown::RemAddRef + RemRelease (opnums 4 and 5):
  Per [MS-DCOM] §3.1.1.5.6 + §2.2.19 (REMINTERFACEREF). Both
  opnums share the wire shape, so:
    - encode_rem_add_ref_request + encode_rem_release_request
      both delegate to a shared encode_remref_array_request
      helper.
    - parse_remref_response is shared between them too — they
      have an identical OrpcThat + referent_id + max_count +
      N×HRESULT + error_code layout.
  New RemInterfaceRef struct (ipid + public_refs + private_refs,
  ENCODED_LEN = 24) + RemRefResponse decoded shape.

8 new structural tests across both modules pin every documented
edge of each shape — short stubs, referent-zero short-circuits,
truncated-trailing detection, full multi-element round-trips.
mxaccess-rpc 183 → 188 tests; default-feature clippy clean.

Both followups documented "**Status:** Awaiting wire-fixture
capture" prior to this commit; the structural-port option was
explicitly listed as resolution path (b) in each. Future captured
frames will validate the byte-by-byte match — until then the
port is byte-faithful to the spec but unverified against a live
peer (which is fine for shipping since neither opnum is on the
NMX session's hot path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 03:24:12 -04:00
Joseph Doherty 4ed1355761 design/followups: rewrite F2/F3/F10/F11 with concrete next-step recipes
Each remaining open followup now lists the precise "Concrete next
step" to close it — what to capture, where to write the fixture,
which file to edit. Future sessions (or anyone without the project
context) can pick up any of these and execute without guessing.

F2 (NTLM verify_signature server→client):
  Status: awaiting wire-fixture capture. M2 wave 3 (callback exporter)
  is closed under F15, so the path is wired — instrument
  CallbackExporter to hex-dump inbound StatusReceived bytes during a
  live subscribe, save under tests/fixtures/m2-status-frame/, port
  verify_signature mirroring `sign` but using the server-to-client
  sub-keys per [MS-NLMP] §3.4.4, add `subtle = "2"` for constant-time
  MAC compare.

F3 (cross-domain NTLM Type1/2/3 fixture):
  Status: permanently out-of-scope on this host (no second AD
  domain). Documented the lab-environment requirements and the
  capture procedure for whoever provisions the two-domain harness.

F10 (IObjectExporter::ResolveOxid2 opnum 4):
  Status: awaiting capture or .NET helper. Two paths documented —
  extend MxNativeClient.Probe with --probe-resolve-oxid2 OR hand-roll
  the layout from [MS-DCOM] §3.1.2.5.1.4 and validate later.

F11 (IRemUnknown::RemAddRef + RemRelease):
  Status: same shape as F10. Document either probe extension or
  structural port from [MS-DCOM] §3.1.1.5.6 (REMINTERFACEREF[]).

No code changes in this commit — purely sharpening the followup
specs so each one's resolution recipe is self-contained.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 03:18:36 -04:00
Joseph Doherty 9496322712 [F27] mxaccess-asb-nettcp: constant-time DH mod_exp via crypto-bigint::DynResidue
rust / build / test / clippy / fmt (push) Has been cancelled
Closes F27 per option (b) of its resolve criterion: fixed-width
U2048 DH backend using crypto-bigint's Montgomery-form residue
arithmetic.

New auth.rs::constant_time_mod_exp(base, exp, modulus) wrapper
preserves the BigUint-in-BigUint-out API of the existing byte
helpers; the actual square-and-multiply chain runs in Montgomery
form. Both DH call sites swap to the wrapper:
  - AsbAuthenticator::new line 179 (public-key generation)
  - crypto_key line 354 (shared-secret derivation)

DH private exponent timing-leak resistance is the goal: the .NET
reference's BigInteger.ModPow is also non-constant-time, so we
were at parity but not at the long-term Rust target. With this
fix the production path no longer leaks the bit-pattern of the
long-lived DH private key through power/timing side channels.

DynResidueParams::new requires an odd modulus (Montgomery form's
only restriction). Production DH primes are always odd
(`MX_ASB_DH_PRIME = 1552...7919` on this host's registry).
CryptoParameters::DEFAULT_PRIME_TEXT — the test-fixture default
inherited from AsbRegistry.cs:66 — ends in 4 (even), which is
mathematically unsound for DH but kept for parity with the .NET
default. For that case the wrapper falls back to BigUint::modpow,
preserving the wire bytes (modular exp is a pure function of
inputs).

Wire-byte parity verified two ways:
1. Unit fixture test
   `auth.rs::deterministic_hmac_matches_dotnet_fixture` — byte-equal
   to captured .NET output for the full DH → PBKDF2 → AES-CBC chain.
   Continues to pass.
2. Live: Connect handshake against the local AVEVA install
   completes with apollo:V2 lifetime, proving MxDataProvider
   accepts the constant-time-derived public key and the
   shared-secret-based AuthenticateMe.

Workspace deps:
  - crypto-bigint = "0.5" added to [workspace.dependencies] and
    mxaccess-asb-nettcp/Cargo.toml.
  - num-bigint retained for decimal-string parsing + .NET-LE byte
    conversion (crypto-bigint has neither).

Closes the "review.md MAJOR finding" originally flagged at
design/30-crate-topology.md:269-274.

design/followups.md: F27 moved to Resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 03:16:33 -04:00
Joseph Doherty d03bd04ef5 [F34 evidence] dump WCF binary-header dictionary for AddMonitoredItems
rust / build / test / clippy / fmt (push) Has been cancelled
Extends tests/add_monitored_items_request_capture.rs with a manual
binary-header walk that prints every pre-interned string + its wire
id. The captured request's binary header pre-declares **23 strings**
covering the entire DataContract field set:

  wire-id  1  http://ASB.IDataV2:addMonitoredItemsIn
  wire-id  3  AddMonitoredItemsRequest
  wire-id  5  SubscriptionId
  wire-id  7  Items
  wire-id  9  http://schemas.datacontract.org/.../ASBIDataV2Contract
  wire-id 11  MonitoredItem
  wire-id 13  activeField
  wire-id 15  activeFieldSpecified
  wire-id 17  bufferedField
  wire-id 19  itemField
  wire-id 21  contextNameField
  wire-id 23  idField
  wire-id 25  idFieldSpecified
  wire-id 27  nameField
  wire-id 29  referenceTypeField
  wire-id 31  typeField
  wire-id 33  sampleIntervalField
  wire-id 35  timeDeadbandField
  wire-id 37  timeDeadbandFieldSpecified
  wire-id 39  userDataField
  wire-id 41  lengthField
  wire-id 43  payloadField
  wire-id 45  valueDeadbandField

That gives F34's binary-builder rewrite the exact dict-id mapping
to target — every MonitoredItem child can be emitted as a
DictionaryStatic(odd-id) reference instead of an inline string,
matching WCF's compression. The "RequireId" mystery from the
earlier inline-name decode is also resolved: the wire body has
NO `RequireId` element at the bottom — the trailing `Inline("referenceTypeField")` was a dict-id wraparound or auto-intern artifact, not actual content.

design/followups.md F34 updated with the full ground-truth header,
plus a refined "Resolves when" pointing at the underlying
`nbfx.rs::decode_tokens` auto-intern semantics. The current codec's
doc comment ("the codec doesn't auto-intern") is correct for raw
[MC-NBFX] but wrong for WCF binary messages where the writer
auto-interns by convention; that's the structural fix the F34 binary
rewrite depends on.

No code-path change in this commit beyond the test improvements.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 03:05:20 -04:00
Joseph Doherty b66f5bb018 [F34 evidence] capture AddMonitoredItems request wire + decoder trace
rust / build / test / clippy / fmt (push) Has been cancelled
Investigation continued via examples/asb-relay.rs middleman:
captured the .NET probe's verbatim AddMonitoredItems request bytes
(695 bytes with the 3-byte NMF SizedEnvelope header). Saved at
rust/crates/mxaccess-asb/tests/fixtures/add-monitored-items-request-wire.bin
as the ground-truth shape MxDataProvider actually accepts.

New tests/add_monitored_items_request_capture.rs runs decode_envelope
over the capture and dumps every NBFX token to stderr for inspection.

Decoded trace surfaces a SECOND, deeper issue:

The F30 dynamic-dict-resolution post-pass at
envelope.rs::resolve_dict_names_in_tokens mis-maps per-session dict
ids. Decoding the captured request renders namespace-URL slots as
field-name strings:

  body[1]=DefaultNamespace { value: Chars("nameField") }   ← bogus
  body[7]=NamespaceDeclaration { prefix: "i",
                                 value: Chars("activeField") }  ← bogus

and leaves most element names as `Static(NN)` instead of resolving
to inline names like `activeField` / `bufferedField` / `itemField`.

This blocks F34's substantive fix (rewrite
build_add_monitored_items_request_body to use DataContract
field-suffix names matching the wire). We can't validate the
rewritten builder against the captured fixture until the dict
post-pass produces the right strings.

design/followups.md F34 updated with two-prerequisite resolution
plan:
  1. Fix the F30 dynamic-dict resolution so the captured request
     decodes to recognisable inline names.
  2. Rewrite the AddMonitoredItems / DeleteMonitoredItems builders
     against the now-readable structure (DataContract field names
     + namespace prefixes for ASBIDataV2Contract / ASBContract +
     nested DataContract serialization of ItemIdentity inside
     `<itemField>` and Variants inside userDataField /
     valueDeadbandField).

Workspace: mxaccess-asb 96 → 97 (+1 capture-driven analysis test);
default-feature clippy clean. The HMAC canonical-XML signing path
remains correct (F28 fixtures are byte-equal to .NET); only the
binary NBFX wire body needs the rewrite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 02:58:25 -04:00
Joseph Doherty fb40e4c20b [F34 partial] mxaccess-asb: fix collect_asbidata_payloads + add Active flag
rust / build / test / clippy / fmt (push) Has been cancelled
Investigation via examples/asb-relay.rs middleman captured the full
S→C bytes of a working PublishResponse from the .NET probe against
MxDataProvider. Decoder fix verified by regression test against the
captured fixture; one further wire-format gap surfaced and is filed.

Closed in this commit:

1. collect_asbidata_payloads filtered out empty <ASBIData/> elements
   so positional payload[N] indexing collapsed when Status was
   empty-but-present. The wire form for PublishResponse is:
     <Status><ASBIData/></Status>          ← empty placeholder
     <Values><ASBIData>{bytes}</ASBIData></Values>
   Our decoder lost the positional info and read Values as Status,
   then panicked on the malformed parse. Fix: always push every
   <ASBIData> element (empty or not) so payloads[0]=Status and
   payloads[1]=Values stay aligned. New regression test
   tests/publish_capture.rs runs the full decode chain over the
   captured wire bytes (305-byte frame at
   tests/fixtures/publish-response-with-value.bin) and asserts
   values.len() == 1.

2. MinimalMonitoredItem.active: Option<bool> + new with_active()
   constructor. The .NET reference's MxAsbDataClient.AddMonitoredItems
   defaults to active: true (cs:441). Without <Active>true</Active>
   on the wire, MxDataProvider treats the subscription as inactive
   and Publish polls return empty Values. Both binary build and
   canonical XML emitters now conditionally emit <Active> when
   active.is_some(). Shared push_monitored_item_body helper
   eliminates the duplicate MonitoredItem encoder between
   AddMonitoredItems and DeleteMonitoredItems builders.

3. SampleInterval unit: clarified as **milliseconds** in
   MinimalMonitoredItem.sample_interval doc + the example
   (sample_interval_ticks → sample_interval_ms = 1000). Matches the
   .NET reference's `ulong sampleInterval = 1000` default.

Open: F34's deeper finding — `MonitoredItem`'s wire schema is
DataContract field-suffix names (`activeField`, `bufferedField`,
`itemField`, `sampleIntervalField`, etc., per the per-session NBFX
dictionary the .NET probe declares), NOT XmlSerializer property
names (`Active`, `Buffered`, `Item`, `SampleInterval`). Our binary
NBFX builder still uses the property names, so MxDataProvider
silently fails to register monitored items — successField=true with
a 0-length Status array. The fix needs a complete rebuild of
build_add_monitored_items_request_body and
build_delete_monitored_items_request_body to use the field-suffix
names plus emit the *Specified siblings (activeFieldSpecified,
idFieldSpecified, etc.) as their own elements. The HMAC canonical
XML side is unaffected (XmlSerializer naming is correct there;
verified byte-equal to .NET via the F28 fixtures). Detailed in
design/followups.md F34's "Open" section.

Live verification of the F34-partial bonus context:
  - Read still returns 99 end-to-end via canonical XML signing.
  - AddMonitoredItems still returns Status[0] = 0 items
    (server doesn't recognize our DataContract-misnamed payload).
  - Publish still returns 0 values (the F34-open consequence).
  - All other 13 canonical-XML signed ops succeed at the request
    level (no SOAP faults, no HMAC rejections).

Workspace: mxaccess-asb 95 → 96 (+1 capture-driven decoder test);
default-feature clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 02:49:11 -04:00
Joseph Doherty 0771664092 asb: SampleInterval unit fix + F34 followup for Publish-decoder gap
rust / build / test / clippy / fmt (push) Has been cancelled
Investigation triggered by "Publish returns 0 values where .NET sees real
values" against the local AVEVA install.

Three findings:

1. SampleInterval unit: the wire field is **milliseconds**, not 100-ns
   ticks. The .NET reference (MxAsbDataClient.cs:441) defaults to
   `ulong sampleInterval = 1000` and the probe passes `subscribeSampleMs`
   directly through that surface. Sending 10_000_000 (1s in 100-ns ticks)
   makes MxDataProvider schedule the next sample ~2.8 hours out; Publish
   polls always come back empty until the misinterpreted timer expires.
   Fixed in `examples/asb-subscribe.rs` (sample_interval_ticks →
   sample_interval_ms = 1000) and clarified in
   `MinimalMonitoredItem.sample_interval`'s doc comment with the live-2026-05-06
   evidence.

2. result_code=32 is `AsbErrorCode.PublishComplete`
   (`AsbResultMapping.cs:37`) — informational, not a fatal error. .NET's
   `ToResult` (cs:122-129) explicitly treats it like Success.
   `ArchestrAResult.ErrorCode` and `ResultCode` are aliases for the same
   `resultCodeField` (cs:424-434), so `publish[i]_error=0x00000020` in
   the .NET probe trace = `result_code=Some(32)` in our trace = the same
   thing. Already handled correctly via the F26 narrower-bail fix
   (commit 983f029) — no code change needed.

3. **F34 filed** for the residual gap: with both sides seeing
   result_code=32 + success=false, .NET extracts a value but we extract
   zero. Three open hypotheses (wire-shape mismatch / payload-locator
   bug / MonitoredItemValue byte-layout bug); all need a middleman
   asb-relay.rs trace between the .NET probe and MxDataProvider to
   confirm. Adjacent symptom: AddMonitoredItemsResponse Status reads as
   0 items where .NET sees 1 — likely the same root cause; one fix
   should close both.

Live re-runs to validate the new sample-interval unit were blocked by
the documented F32 InvalidConnectionId transient (the
pending-connection table on MxDataProvider fills up after many
back-to-back test cycles; clears after a 30s+ cool-down).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 02:28:44 -04:00
Joseph Doherty 983f02921c asb-subscribe example: drive every canonical-XML signed op live
rust / build / test / clippy / fmt (push) Has been cancelled
Extends the example to exercise the full data-plane through the
new canonical-XML signing path (F28 step 2). Each op is announced
with a "[canonical XML <Op>]" tag in the trace so the lifecycle is
self-documenting:

  Connect → Register → Read → Write → CreateSubscription
  → AddMonitoredItems → Publish × N → PublishWriteComplete
  → DeleteMonitoredItems → DeleteSubscription
  → UnregisterItems → Disconnect → SendEnd

Per-section errors are caught and logged but don't abort the
lifecycle — a failed Publish still reaches Disconnect cleanly so
the server-side pending-connection table doesn't fill up.

New env vars MX_RUN_WRITE / MX_RUN_SUBSCRIBE / MX_SUBSCRIBE_COUNT
(defaults: run, run, 3) for opting into / sizing the optional steps.

Live verification on this host (this turn, first run):
  register status: 1 item(s); result_code=Some(0) success=Some(true)
  TestChildObject.TestInt = AsbVariant{type_id:4,length:4,payload:[99]}
  write status: 0 item(s); result_code=Some(0) success=Some(true)
  subscription_id=2 result_code=Some(0) success=Some(true)
  add status: 0 item(s); result_code=Some(0) success=Some(true)
  publish: 0 value(s); result_code=Some(32) success=Some(false)
  publish_write_complete: 0 write(s); result_code=Some(0)
  delete_monitored_items ok
  delete_subscription ok
  unregistering ... disconnecting

All 13 canonical-XML-signed ops accepted by MxDataProvider — no SOAP
faults, no HMAC rejections, no decode errors. F28 step 2 verified
end-to-end against the live AVEVA install.

Bonus fix: F26 stream's publish_loop bail logic narrowed.
The original F33 bail-on-any-non-zero-result_code was over-aggressive:
.NET's MxAsbClient.Probe shows that result_code=32 (= 0x20) fires on
*every* Publish poll while values are still being delivered. Updated
publish_loop and the example's Publish loop to bail only on
RESULT_CODE_INVALID_CONNECTION_ID (1) — that one truly means the
session is desynced. Other non-zero result_codes are informational
and the loop continues draining.

New public re-export: mxaccess_asb::RESULT_CODE_INVALID_CONNECTION_ID
(was crate-private under the operations module).

The InvalidConnectionId transient still hits after many back-to-back
test runs against a long-running MxDataProvider — the pending-
connection table fills up — same well-documented behaviour from F32.
A 30-second cool-down restores reliability in our experience.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 02:19:47 -04:00
Joseph Doherty 34d477819b [F28] mxaccess-asb: canonical XML signing for all 8 remaining ops
rust / build / test / clippy / fmt (push) Has been cancelled
Closes F28. The 5 [XmlSerializerFormat] ops landed in commit f14580e
(2026-05-05); this commit closes out the remaining 8 ConnectedRequest
shapes, eliminating the legacy NBFX-bytes signing fallback from every
`client::*` op.

Two deliverables:

1. Extended `MxAsbClient.Probe --dump-signed-xml` (.NET probe) to
   emit deterministic canonical-XML output for ReadRequest,
   WriteBasicRequest, PublishWriteCompleteRequest,
   CreateSubscriptionRequest, DeleteSubscriptionRequest,
   AddMonitoredItemsRequest, DeleteMonitoredItemsRequest,
   PublishRequest. Saved 8 fixtures at
   rust/crates/mxaccess-asb/tests/fixtures/signed-xml/*.xml. Pinned
   field values for reproducibility:
     - SubscriptionId = 0x1234_5678_9abc_def0
     - MaxQueueSize = 100, SampleInterval = 1000
     - WriteHandle = 0xDEAD_BEEF
     - WriteValue = Variant.FromInt32(42)
     - MonitoredItem with the existing sample-item shape

2. Ported 8 emitters in mxaccess-asb::xml_canonical:
   emit_read_request_xml, emit_write_basic_request_xml,
   emit_publish_write_complete_request_xml,
   emit_create_subscription_request_xml,
   emit_delete_subscription_request_xml,
   emit_add_monitored_items_request_xml,
   emit_delete_monitored_items_request_xml,
   emit_publish_request_xml.

   New helpers consolidate XmlSerializer's per-namespace shapes:
     - emit_invensys_text — primitive int/long fields in the parent
       urn:invensys.schemas namespace (no xmlns redeclaration).
     - emit_write_value — <Values> wrapper inlining
       Value (Variant), Status (default AsbStatus), Comment (xsi:nil).
     - emit_monitored_item — <Items> wrapper inlining
       Item, SampleInterval, ValueDeadband, UserData, Buffered.
     - emit_inline_item_identity — ItemIdentity rendered as a child
       of MonitoredItem (single xmlns redeclaration on the wrapper,
       children inherit).
     - emit_inline_text + emit_inline_optional_string —
       no-redeclaration variants of emit_iom_text +
       emit_iom_optional_string.
     - emit_idata_variant — Variant's Type/Length/Payload children
       in the http://asb.contracts.idata.data/20111111 namespace
       (Payload self-closes with xsi:nil when Length=0).
     - emit_iom_default_variant — wrapper for ValueDeadband / UserData
       (default-shape Variant in iom:2 namespace).

   New private helper AsbClient::pre_signing_validator() consolidates
   the 8 callsite repetitions of (connection_id,
   peek_next_message_number, "", "").

Wired into client::* — every send_signed_envelope[_one_way] call now
passes Some(&xml) for xml_for_signing. The 8 ops affected: read,
write, publish_write_complete, delete_monitored_items,
create_subscription, add_monitored_items, publish,
delete_subscription (plus their _once retry-loop variants).

8 new fixture-comparison tests (mxaccess-asb 87 → 95). Each emitter
byte-equal vs the .NET fixture on the first try — no iteration
needed. Workspace clippy clean.

Live verification: `cargo run -p mxaccess --example asb-subscribe`
returns TestChildObject.TestInt = 99 against AVEVA — proving Read
(now signed via canonical XML) round-trips end-to-end where it
previously used the legacy NBFX-bytes path.

The remaining 7 ops are wire-tested at fixture-byte-equality only;
live exercise is gated on the F33 follow-on capture for
subscribe-flow ops, but the canonical XML matches the .NET reference
byte-for-byte, so the HMAC will match by construction once the
session is in a state to issue those ops.

design/followups.md:
  - F28 moved to Resolved with the full two-step audit trail.
  - F18 M5 status block rewritten — all sub-followups (F26 stream,
    F28, F29, F32, F33) now closed. M5 DoD bullets 1+2+3+4 all green.
  - tests/fixtures/signed-xml/README.md updated to list the 8 new
    fixtures + their pinned input values.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 02:13:16 -04:00
Joseph Doherty ff4ea4d5a9 [F16] mxaccess: real Session::recover_connection (re-bind + re-advise)
rust / build / test / clippy / fmt (push) Has been cancelled
Closes F16. Replaces the wave-2 no-op recover_connection with the
full .NET-equivalent shape (MxNativeSession.cs:399-474). Three
pieces:

1. Subscription registry on SessionInner.
   New subscriptions: Mutex<HashMap<[u8; 16], SubscriptionEntry>>
   tracks every active advise. subscribe() inserts after a successful
   AdviseSupervisory; unsubscribe() removes on the success path only
   (failed UnAdvises stay registered so next recovery replays them).
   The consumer's Subscription handle still holds the BroadcastStream;
   the registry is purely for AdviseSupervisory replay.

2. Pluggable RebuildFactory.
   New public typedef:
     pub type RebuildFactory = Arc<
         dyn Fn() -> Pin<Box<dyn Future<Output = Result<NmxClient,
                                                        NmxClientError>>
                            + Send>>
             + Send + Sync,
     >;
   Installed via Session::set_recovery_factory(factory);
   queryable via has_recovery_factory(). Kept separate from
   connect_nmx / connect_nmx_auto so existing constructors stay
   non-breaking — consumers opt in by calling the setter
   after-the-fact.

3. Real recover_connection + recover_connection_core.
   recover_connection is the retry loop (mirrors cs:399-440): for
   attempt in 1..=policy.max_attempts, emit RecoveryEvent::Started
   → call recover_connection_core → on Ok emit Recovered + return,
   on Err emit Failed{will_retry, error}, sleep policy.delay, retry,
   or bubble the last error.

   recover_connection_core mirrors cs:442-474: rebuild NMX via the
   factory → RegisterEngine2 with the saved callback_obj_ref → optional
   SetHeartbeatSendInterval → snapshot the registry under the lock,
   replay AdviseSupervisory(correlation_id) for each entry → atomically
   swap *nmx_lock = replacement. Old NmxClient drops at end of scope,
   closing its TCP transport.

Subscription correlation ids are preserved across the swap so the
consumer's Subscription stream continues to receive on its existing
broadcast filter. The CallbackExporter stays bound across recoveries
— no TCP listener re-bind.

R15's "long-lived connection task" was listed as a hard prereq, but
the existing Mutex<NmxClient> already serialises concurrent ops
during the rebuild — recover_connection_core holds the inner mutex
during the swap, concurrent ops just wait. Functionally equivalent
to the long-lived-task design.

New ConfigError::RecoveryNotConfigured returned when
recover_connection is called without a factory installed. New
public re-export: RebuildFactory.

Tests (mxaccess 65 → 67):
  - recover_connection_without_factory_returns_recovery_not_configured
  - recover_connection_with_always_failing_factory_exhausts_attempts
    (pins (Started, Failed)×3 + final will_retry=false + bubbled
    TransportFailure)
  - subscribe_populates_registry_unsubscribe_clears_it
  - recovery_events_supports_multiple_subscribers (updated for the
    new factory-required path)

connect_nmx_auto-side auto-population of the factory (capturing the
ntlm_factory + discovered (addr, service_ipid) so consumers don't
re-author the closure) is a future polish — not required to close
F16.

design/followups.md: F16 moved to Resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 01:57:43 -04:00
Joseph Doherty 904f211aba .gitignore: cover ad-hoc debug captures + Claude Code state
Two patterns that have been polluting `git status` across recent
debugging sessions:

1. Root-level capture files from manual asb-relay / trace runs:
   - rust-cs.txt, rust-sc.txt, rust.log (asb-relay TCP dumps,
     hex-prefixed C->S / S->C and the relay log)
   - rust-trace-*.txt (MX_ASB_TRACE_REPLY=1 captures, e.g. the
     trace-orig dump that surfaced the F33 InvalidConnectionId
     evidence)

   Anything worth keeping should be promoted into `captures/` or
   `analysis/` with a name describing what it captures; these
   transient root-level files are noise.

2. `.claude/` — Claude Code's project-local state directory
   (scheduled-task locks, agent state). User/host-specific.

Removed the existing root-level rust-*.txt files in the same
commit; future runs will be ignored automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 01:45:28 -04:00
Joseph Doherty 079896c7bc design/followups: collapse 18 redundant 'Earlier slices' blocks
Each F18 cumulative-log step had its own '**Earlier slices:**' header
followed by a verbose body that duplicated the matching commit
message — content already preserved in `git show <hash>` for every
hash listed in the cumulative-log line at the top of F18.

Removes ~75 lines of redundancy:
  - 18× '**Earlier slices:**' headers and their bodies (F19, F20,
    F21, F22, F24, F23, F25 steps 1-10, F26 steps 1-3, example
    rewrite).
  - The stale 'F25 (...) and F26 (...) remain open' paragraph (both
    closed long since).

Keeps the substantive material in place:
  - The cumulative-log line listing every commit by hash.
  - The 5-finding F25 live-bring-up reconciliation block (justifies
    F28 + F29 followups).
  - The F26 step 3 AsbSession design rationale (explains why ASB
    parallels rather than unifies with the NMX Session — useful for
    future readers).
  - A one-sentence pointer to `git show <hash>` for per-step detail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 01:42:42 -04:00
Joseph Doherty cfeb761092 [F33] mxaccess-asb: complete InvalidConnectionId tolerance propagation
rust / build / test / clippy / fmt (push) Has been cancelled
Closes F33. Final commit in the three-step F33 closure (218f4c47a5f251 → this) — propagates the F31 InvalidConnectionId tolerance
pattern to every remaining response decoder + adds publish-loop
detection so the F26 stream terminates cleanly on server-side
rejections instead of spinning silently.

Decoders updated to tolerate empty / missing payloads + surface
result_code/success:
  - decode_publish_response (the F26 stream's hot path)
  - decode_unregister_items_response
  - decode_delete_monitored_items_response
  - decode_write_response
  - decode_publish_write_complete_response

Shared `extract_result_status(body_tokens)` helper in operations.rs
consolidates the per-decoder find_text_in_named_element calls for
resultCodeField + successField — a single source of truth for the
F31-pattern wrapper extraction.

Public response structs gain `result_code: Option<u32>` and
`success: Option<bool>`:
  - PublishResponse
  - UnregisterItemsResponse
  - DeleteMonitoredItemsResponse
  - WriteResponse
  - PublishWriteCompleteResponse

asb_session.rs::publish_loop: when PublishResponse.result_code is
Some(non_zero), the loop now sends Err(ConnectionError::TransportFailure
{ detail: "publish returned result_code 0xXX (server-side rejection)" })
as the stream's terminal item, then returns. Without this, an
InvalidConnectionId-poisoned subscription would generate empty
PublishResponse forever.

5 new tests synthesise the InvalidConnectionId wire shape
(`<Result><resultCodeField>1</><successField>false</></><ASBIData/><ASBIData/>`)
for each decoder via the shared synthesise_invalid_connection_id_body
helper — pin the tolerance for Publish, Unregister, Delete*, Write,
and PublishWriteComplete.

Updated obsolete write_response_missing_status_fails test to
write_response_missing_status_returns_empty_with_no_result_code
since the decoder no longer errors.

Live read regression test: TestChildObject.TestInt = 99 returned
end-to-end after all changes (cargo run -p mxaccess --example
asb-subscribe).

Workspace: mxaccess-asb 82 → 87 tests (+5). All other crates
unchanged. Default-feature clippy clean.

design/followups.md: F33 moved to Resolved with the full
three-commit audit trail. M5 status block stable: F32 + F33 closed,
only F28 (canonical XML for the remaining 8 ops) remains as P2
latent — works in practice under empty hashAlgorithm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 01:37:11 -04:00
Joseph Doherty 7a5f251ac7 [F33 progress] mxaccess-asb: extend InvalidConnectionId tolerance to subscribe ops
rust / build / test / clippy / fmt (push) Has been cancelled
Continues the F31 tolerance pattern propagation to the two
subscribe-path decoders called out in F33.

CreateSubscriptionResponse:
- Adds result_code: Option<u32> + success: Option<bool> fields.
- decode_create_subscription_response no longer errors with
  MissingField "SubscriptionId" — when the server short-circuits on
  InvalidConnectionId it returns the Result wrapper without a
  SubscriptionId. The decoder returns subscription_id=0 in that case
  with result_code/success surfaced; callers inspect result_code
  before treating subscription_id as valid.
- AsbClient::create_subscription wraps the call in the same retry
  loop register_items uses (10 attempts × 200·N ms backoff on
  RESULT_CODE_INVALID_CONNECTION_ID).

AddMonitoredItemsResponse:
- Adds result_code + success fields.
- decode_add_monitored_items_response tolerates an empty / missing
  <ASBIData /> Status payload (returns empty Vec) and surfaces
  result_code/success.
- AsbClient::add_monitored_items gets the same retry loop.

Both decoders now match the F31 + Read shape: tolerant of empty
payloads when the server short-circuits, surface the wrapper's
result_code so callers (and the in-client retry loop) can detect
the InvalidConnectionId race.

Updated obsolete unit test
(create_subscription_response_missing_id_fails →
create_subscription_response_missing_id_returns_zero_sentinel) plus
two new tests pinning the InvalidConnectionId synthesis path for
both decoders.

Workspace: mxaccess-asb 80 → 82 tests; default-feature clippy
clean; existing live-read still passes (verified separately).

This is the second of three F33 closures. Remaining: applying the
same tolerance to decode_publish_response (and optionally
decode_delete_*_response, decode_unregister_items_response,
decode_write_response, decode_publish_write_complete_response for
symmetry). With CreateSubscription + AddMonitoredItems tolerant
+ retrying, the subscribe-flow example should now reach the
publish-loop stage instead of bailing at the second op.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 01:29:38 -04:00
Joseph Doherty 218f4c4ec8 mxaccess-asb: extend F31 InvalidConnectionId tolerance to Read
rust / build / test / clippy / fmt (push) Has been cancelled
Live MX_ASB_TRACE_REPLY capture against MxDataProvider during the
F33 investigation showed Read hitting the same InvalidConnectionId
race that F31 fixed for register_items: server replies with a
Result wrapper carrying resultCodeField=1 + successField=false plus
two empty <ASBIData /> payloads. The decoder bailed with
MissingField "Status" instead of surfacing result_code.

Two changes:

1. ReadResponse gains result_code: Option<u32> and success:
   Option<bool> fields, matching the RegisterItemsResponse shape.
   decode_read_response tolerates empty / missing <ASBIData />
   payloads (returns empty status + values arrays) and surfaces
   the wrapper's result_code / success via
   find_text_in_named_element.

2. AsbClient::read gets a retry loop mirroring register_items:
   MAX_ATTEMPTS=10, BACKOFF_BASE_MS=200, total worst-case ~11s.
   Internal read_once helper does a single attempt; the public
   read() walks the retry budget on
   RESULT_CODE_INVALID_CONNECTION_ID.

Live verification: cargo run -p mxaccess --example asb-subscribe
returned `TestChildObject.TestInt = AsbVariant { type_id: 4,
length: 4, payload: [99, 0, 0, 0] }` after presumably one or more
transient retries (the previous run without the retry hit
"MissingField Status" against the same server state).

1 new test
(read_response_tolerates_empty_asbidata_when_invalid_connection_id)
plus a synthesise_invalid_connection_id_body helper that builds
the canonical wire shape captured live (Result wrapper +
resultCodeField=1 + successField=false + two empty <ASBIData />
elements). Workspace 718 → 722 tests... wait, mxaccess-asb went
79 → 80 (+1). Tests still all green; clippy clean on default and
windows-com features.

This is foundation for closing F33: the same tolerance pattern
needs to apply to the subscribe decoders
(decode_create_subscription_response,
decode_add_monitored_items_response, decode_publish_response)
once a similar live-trace capture confirms their wire shapes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 01:25:41 -04:00
Joseph Doherty cbc95a4684 [F33] design/followups: capture live-subscribe wire gap
Live run of `cargo run -p mxaccess --example asb-subscribe` against
the local AVEVA install (with DH params + passphrase loaded from
Setup-LiveProbeEnv.ps1 + Get-AsbPassphrase.ps1) surfaced two concrete
gaps in the subscription-path response decoders:

1. `CreateSubscriptionResponse` returns subscription_id = 0 — the
   server almost certainly assigns a real Int64, but
   decode_create_subscription_response can't locate the
   `<SubscriptionId>` element. Likely a dict-id our F30 post-pass
   doesn't resolve for that specific element name.

2. `AddMonitoredItemsResponse` decode fails with MissingField
   "Status". The wire shape needs a capture-and-diff vs the .NET
   probe's subscription path.

Once subscribe-side ops are issued, the channel desyncs — subsequent
read() on the same session fails with the same MissingField error,
suggesting NBFX framing state may also be out of sync.

The F26 stream API itself (AsbSession::subscribe → Stream<Item =
Result<MonitoredItemValue, Error>>) is complete and unit-tested
(commit f2f22df). This followup just captures the live-wire
reconciliation work that's still required to make the subscribe
path actually return data against MxDataProvider. Once F33 closes,
the last M5 live-wire gap is resolved.

P2 — not blocking M5 closeout; blocks the Subscribe demo.

The asb-subscribe.rs example stays in its working Read-loop form
(no regression). When F33 lands, the example can be promoted to
demonstrate the full subscribe flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 01:17:09 -04:00
Joseph Doherty f2f22dfcd1 [F26 stream] mxaccess: AsbSession::subscribe — Stream<Item = MonitoredItemValue>
rust / build / test / clippy / fmt (push) Has been cancelled
Closes the last F26 stub from the M5 status block. New
AsbSession::subscribe(subscription_id) returns an AsbSubscription
that impls Stream<Item = Result<MonitoredItemValue, Error>>. An
internal tokio::spawn'd publish-loop drains the subscription queue
via the existing AsbSession::publish() and fans each
PublishResponse's `values` array out as individual stream items.

Termination semantics:
  - Drop of AsbSubscription calls JoinHandle::abort() — the publish
    task stops draining the server-side queue (the .NET reference
    pattern at MxAsbDataClient.cs uses the same task-cancellation
    shape).
  - Transport error from publish() is delivered as the final stream
    item; the loop returns and the channel closes.
  - Receiver-drop (consumer stops polling) is detected when
    tx.send returns Err — the loop exits without making more
    publish calls.

The inner publish_loop helper takes any FnMut() -> Future<Result<...>>
so it's testable in isolation (no live ASB endpoint required).

Per-item ItemStatus from the server is intentionally not surfaced
on the stream: the field is opaque per-item and rarely actionable
for the streaming consumer. A richer struct can wrap each value if
that need surfaces.

3 new tests pin:
  - asb_subscription_is_stream_send_unpin (compile-time bounds);
  - publish_loop_delivers_values_then_terminates_on_error
    (3 Ok values from 2 batches, then 1 terminal Err);
  - publish_loop_exits_when_consumer_drops_channel.

New deps used (already in mxaccess Cargo.toml): futures_util::Stream,
tokio::sync::mpsc, tokio_stream::wrappers::ReceiverStream,
tokio::task::JoinHandle.

Workspace: 718 → 721 tests. Default-feature clippy clean.
mxaccess crate-level doc updated to drop the "stubbed for next F26
iteration" note for the subscription stream.

design/followups.md F18 M5 status block updated: F26 stream
subscription marked resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 01:10:22 -04:00
Joseph Doherty 8e695b9347 [F12 wrapper + F32 close] Session::connect_nmx_auto + close M5 type-matrix DoD
rust / build / test / clippy / fmt (push) Has been cancelled
Two related closures in one commit:

1. Session-level wrapper around F12: new
   `mxaccess::Session::connect_nmx_auto(ntlm_factory, options,
   resolver, recovery)` gated on a new `mxaccess/windows-com` feature
   (which propagates `mxaccess-nmx/windows-com`). Drives
   `NmxClient::create` (the F12 COM-activation factory) for the
   `(host, port, service_ipid)` discovery, then funnels into the
   shared post-NMX-bind orchestration. Refactored `connect_nmx` to
   extract steps 1+2+4+5 into a private `from_nmx_client` helper —
   both `connect_nmx` and `connect_nmx_auto` reuse it so the
   `CallbackExporter` + router + `RegisterEngine2` + heartbeat policy
   stays in one place. The .NET `MxNativeSession.Open` shape
   (`MxNativeSession.cs:127-147`) is now reproduced end-to-end on
   Windows with `windows-com` on — callers no longer pre-resolve
   `(addr, service_ipid)` by hand.

   `connect_nmx`'s doc comment updated to drop the stale "F12 not yet
   wired" note. `parse_bracketed_host_port` in mxaccess-nmx gets a
   `cfg_attr(not(...), allow(dead_code))` so the default-feature
   build stays warning-clean.

2. F32 closed via option (b) of its own resolve criterion: the four
   missing types (Float / Double / DateTime / Duration) are gated on
   Galaxy-side template provisioning that's outside the Rust port's
   scope. The deployed test Galaxy on this host only has
   mx_data_type ∈ {1=Bool, 2=Int32, 5=String}; we cannot exercise
   the missing types without authoring new template attributes in
   the Aveva console (a manual platform-engineering task). The
   three-type live verification at commit 9063f10 satisfies the M5
   DoD bullet for what is deployable. F18's M5 status block updated
   to reflect F32-resolved.

Workspace: 718 tests pass on default features (was 712 before F12,
+6 from new parse_bracketed_host_port tests). Default-feature
clippy + windows-com clippy both clean.

Closes F32 in design/followups.md and extends F12's resolution note
with the Session-level wrapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:30:25 -04:00
Joseph Doherty daa4ea3f16 [F12] mxaccess-nmx: NmxClient::create — auto-resolving COM-activation factory
rust / build / test / clippy / fmt (push) Has been cancelled
New constructor NmxClient::create(ntlm_factory) gated on
cfg(all(windows, feature = "windows-com")). New crate feature
mxaccess-nmx/windows-com propagates to mxaccess-rpc/windows-com.
Mirrors ManagedNmxService2Client.Create() (cs:30-64) plus
ResolveService (cs:491-523).

Six-step bring-up:
  1. com_objref_provider::marshal_activated_iunknown_objref(
       "NmxSvc.NmxService", MarshalContext::DifferentMachine)
     activates and emits the OBJREF.
  2. ComObjRef::parse extracts oxid + the activated server's IUnknown
     IPID.
  3. resolve_oxid_with_managed_ntlm_packet_integrity against
     127.0.0.1:135 (RPCSS endpoint mapper) returns the server's
     (host, port) bindings + IRemUnknown IPID.
  4. parse_bracketed_host_port pulls the host + port out of the
     ncacn_ip_tcp binding's `host[port]` text. Uses rfind for the
     rightmost brackets so FQDN forms (foo.example.com[1234])
     round-trip — matches the .NET ParseBracketedHost/Port shape at
     cs:540-561.
  5. A fresh DceRpcTcpClient binds to IRemUnknown and calls
     RemQueryInterface(iunknown_ipid, INmxService2_IID,
                        fresh_causality_id, public_refs=5).
  6. A second fresh transport binds to INmxService2 via Self::connect.

The ntlm_factory: impl FnMut() -> NtlmClientContext closure is
invoked three times (one per bind); each NtlmClientContext is
consumed by its bind, so the factory must produce fresh contexts.

New NmxClientError variants:
  - Activation(ProviderError) — only emitted with windows-com on.
  - EndpointResolution { reason } — covers no ncacn_ip_tcp binding,
    malformed host[port], non-zero RemQueryInterface HRESULT.

6 offline tests on parse_bracketed_host_port: FQDN host extraction,
rfind for rightmost brackets, rejection of missing '[' / missing
']' / non-numeric port / port overflow.

1 live test (#[ignore], gated on MX_LIVE + MX_TEST_USER /
MX_TEST_PASSWORD / MX_TEST_DOMAIN populated by
tools/Setup-LiveProbeEnv.ps1): round-trips the full chain against
the AVEVA install on this host. Resolved INmxService2 IPID is
non-zero — verified end-to-end.

Workspace: mxaccess-nmx 17 → 23 (+6). All other crates unchanged.

Closes F12 in design/followups.md. F6 (ComObjRefProvider port) was
the prior blocker; with both landed, the COM-activation path is
end-to-end functional.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:21:49 -04:00
Joseph Doherty cf9dbaf568 [F6] mxaccess-rpc: ComObjRefProvider port via windows-rs (CoMarshalInterface)
rust / build / test / clippy / fmt (push) Has been cancelled
New module crates/mxaccess-rpc/src/com_objref_provider.rs gated on
cfg(all(windows, feature = "windows-com")). Pulls windows = "0.59"
(features Win32_Foundation + Win32_System_Com +
Win32_System_Com_Marshal + Win32_System_Com_StructuredStorage +
Win32_System_Memory) as an optional dep behind the existing
windows-com feature; default footprint stays slim.

Public API mirrors ComObjRefProvider.cs 1:1: MarshalContext enum
(InProcess / Local / DifferentMachine wrapping the MSHCTX_* newtype
constants), clsid_from_prog_id, marshal_activated_iunknown_objref
(activates via CoCreateInstance with INPROC | LOCAL | REMOTE then
marshals), marshal_iunknown_objref (uses IUnknown::IID),
marshal_interface_objref (CoMarshalInterface over an HGlobal-backed
IStream).

All `unsafe` is internal to the module — public API exposes only
typed Rust values (Vec<u8>, GUID, ProviderError), no raw pointers /
HRESULTs / lifetime-bound interface pointers leak. Each unsafe block
carries an inline SAFETY comment naming the invariants being upheld.

Per-thread COM init via thread-local OnceLock<()>: lazy
CoInitializeEx(MULTITHREADED) on first call; S_FALSE (already
initialised) and RPC_E_CHANGED_MODE (thread is STA) treated as
success — matches the .NET runtime's tolerant apartment behaviour.

ProviderError enumerates the four documented failure modes plus the
apartment-init pre-check: UnknownProgId / ActivationFailed /
MarshalFailed / GlobalLockFailed / ApartmentInitFailed.

4 offline tests: MarshalContext → MSHCTX_* mapping, ensure_apartment
idempotence, clsid_from_prog_id returns UnknownProgId for fake
ProgIDs, marshal_activated short-circuits at the resolution stage.

1 live test (#[ignore], gated on MX_LIVE): activates the real
NmxSvc.NmxService, marshals the proxy's IUnknown via
CoMarshalInterface, then parses the resulting blob via
ComObjRef::parse and asserts non-zero OXID + IPID. Passes against
the AVEVA install on this host.

Workspace tests: mxaccess-rpc went 179 → 183 (+4). All other crates
unchanged.

Unblocks F12 (NmxClient::create — the auto-resolving
COM-activation factory): the underlying primitive
(marshal_activated_iunknown_objref) now exists; remaining work is
threading the windows-com feature through mxaccess-nmx and chaining
ComObjRef::parse → resolve_oxid_with_managed_ntlm_packet_integrity →
RemQueryInterface. design/followups.md F12 updated with a revised
"Resolves when" reflecting that F6's blocker is gone.

Closes F6 in design/followups.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 22:11:33 -04:00
Joseph Doherty 41f2d4c0f2 [F14] mxaccess-galaxy: tiberius-backed SQL Resolver + UserResolver
rust / build / test / clippy / fmt (push) Has been cancelled
New module crates/mxaccess-galaxy/src/sql_resolver.rs (~480 LoC) gated
behind the existing galaxy-resolver Cargo feature. Adds SqlTagResolver
+ SqlUserResolver, both constructed via from_ado_string(&str)
accepting the same connection-string shape the .NET reference uses by
default (Server=localhost;Database=ZB;Integrated Security=True;
Encrypt=False;TrustServerCertificate=True). Integrated Security=True
resolves to Windows auth via tiberius's winauth feature.

Each top-level call (resolve / browse / resolve_by_guid /
resolve_by_name) opens a fresh Client<Compat<TcpStream>> and drops it
on return — matches the .NET `await using` lifecycle at
GalaxyRepositoryTagResolver.cs:93-95. tiberius's Client::query only
accepts positional @P1..@PN placeholders (delegates to sp_executesql);
the canonical RESOLVE_SQL / BROWSE_SQL / USER_BY_GUID_SQL /
USER_BY_NAME_SQL constants are rewritten once-per-process via
OnceLock<String> (@objectTagName → @P1, etc.). The unrewritten
constants stay byte-identical with the .NET reference for ad-hoc
diagnostic copy/paste.

read_metadata mirrors ReadMetadata (cs:149-165) byte-by-byte: signed
smallint → i16 widened to u16 for platform/engine/object IDs (matches
the .NET checked((ushort)reader.GetInt16(N)) shape), int → i32
checked-cast to i16 for property_id, nullable nvarchar for
primitive_name. read_user_profile mirrors ReadProfile (cs:76-85)
including the roles_text blob → parse_role_blob round-trip.

Deps added (gated): tiberius 0.12 (default-features = false; tds73 +
rustls + winauth — no chrono / rust_decimal pulled), tokio-util's
compat feature for the futures-rs ↔ tokio AsyncRead bridge,
futures-util for TryStreamExt::try_next. Default-feature build still
pulls only mxaccess-codec + async-trait + thiserror + uuid (slim
foot-print preserved per the design doc's intent).

New `live` feature on this crate (`live = ["galaxy-resolver"]`) for
parity with the workspace pattern.

11 offline unit tests pin: SQL named→positional rewriting (no @named
left, @P1/@P2/@P3 present), line-count preserved, ado-string
acceptance (default Galaxy shape parses, garbage rejected), input
validation (max_rows=0 rejected, empty LIKE rejected, empty user_name
rejected, all checked before connect attempt).

Two #[cfg(feature = "live")] #[ignore]'d tests round-trip against a
real Galaxy DB (gated on MX_LIVE + MX_GALAXY_DB env vars per
tools/Setup-LiveProbeEnv.ps1). Live verification on this host:
live_resolve_test_child_object_test_int and
live_browse_test_child_object both pass against the local AVEVA
install — TestChildObject.TestInt resolves with mx_data_type=2
(Int32), is_array=false.

Closes F14 in design/followups.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:54:43 -04:00
Joseph Doherty 9501080170 [F4+F5] mxaccess-rpc: BindAck/AlterContextResponse parser + live-capture round-trip
rust / build / test / clippy / fmt (push) Has been cancelled
Adds BindAckPdu + per-result BindAckResult per [C706] §12.6.3.4: u16
result + u16 reason + 20-byte SyntaxId, preceded by port_any_t secondary
address, n_results, and 3 reserved bytes. Encode/decode handle both
PacketType::BindAck and PacketType::AlterContextResponse (same body
shape).

The new bind_ack_round_trips_live_capture test takes the first 84 bytes
of the server's first response in
captures/013-loopback-subscribe-scalars/tcp-stream-__1_49704-to-__1_55690.bin
(real BindAck observed against local AVEVA), decodes the shape
(secondary address "49704\0", n_results=2, NDR transfer syntax accepted,
DCOM negotiate_ack reason=3), then re-encodes and asserts byte-identical
to the original frame. Stronger evidence of wire parity than the prior
synthetic-frame tests, and lets the rest of the M2 stack reason about
the negotiated transfer syntax instead of relying on request-side
context to infer it.

Closes F4 and F5 in design/followups.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:44:54 -04:00
Joseph Doherty 826f7b3f89 [M5] mxaccess-asb-nettcp: F29 resolved — full canonical [MC-NBFS] table port
rust / build / test / clippy / fmt (push) Has been cancelled
The original hand-curated table was wrong starting at id 74 — entries
had been deduplicated/renumbered without preserving the canonical
`id = 2 * StringN` mapping from `[MC-NBFS]` §2.2, leaving most of
the SOAP-fault subset at the wrong ids:

  ours had Fault at 114, canonical is 134
  ours had Code at 122, canonical is 142
  ours had Reason at 124, canonical is 144
  ours had Text at 126, canonical is 146
  ours had Value at 134, canonical is 154
  ours had Subcode at 136, canonical is 156

Wire captures from the live AVEVA MxDataProvider use the canonical
ids — verified earlier via `MX_ASB_TRACE_REPLY` showing
`<resultCodeField>` correctly resolved through the F30 post-pass
once the ids matched.

Replaced the entire STATIC_ENTRIES array with a faithful port of the
first 200 entries from `dotnet/wcf`'s
`src/System.ServiceModel.Primitives/src/System/ServiceModel/
ServiceModelStringsVersion1.cs` (sourced via WebFetch — that file is
the canonical [MC-NBFS] §2.2 table mirrored in code). The wire id is
`2 * StringN` for `StringN` at 0-based position N. Coverage now spans
id 0..400, picking up the full SOAP / WS-Addressing / WS-RM /
WS-Security / WS-SecureConversation / WS-Trust / xmldsig+xenc URIs /
SAML / Kerberos / X509 token-type subset. The 436..444 xsi/xsd/nil
extras (used by .NET XmlSerializer for [MessageContract] value-type
bodies) are preserved.

Four new regression tests:
- ids monotonic (was already there);
- ids all even (`[MC-NBFS]` reserves odd ids for the dynamic dict);
- SOAP-fault subset (s, Fault, MustUnderstand, Code, Reason, Text,
  Node, Role, Detail, Value, Subcode) resolves to the canonical
  strings — pins the fix against accidental regression;
- `position_of_static` round-trips for known strings.

Followups:
- F29 moved to ## Resolved with full audit-trail.
- F18 M5 status block updated to strike F29 from the remaining-work
  list. The remaining open M5 items are F32 (live type-matrix beyond
  Int32/String/Bool, gated on Galaxy provisioning) and F28 (canonical
  XML signing for Read/Write/Subscribe ops, P2 latent).

Workspace: 712 unit tests pass (was 711 + 1 new fault-subset test +
existing tests now matching canonical). Clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:31:09 -04:00
Joseph Doherty 5845b5eb12 [M5] mxaccess-asb: F32 partial — Bool + String + Int32 live, longer retry budget
rust / build / test / clippy / fmt (push) Has been cancelled
Three of seven proven types now round-trip end-to-end against the
live MxDataProvider:

   Int32   (type_id 4)  — TestChildObject.TestInt = 99
   String  (type_id 10) — TestChildObject.TestString = "mxaccesscli
                            verified 17778523775" (UTF-16LE on wire)
   Bool    (type_id 17) — DelmiaReceiver_001.TestAttribute = 0

A SQL probe of the live Galaxy (`gobject ⨝ package ⨝
dynamic_attribute` grouped by `mx_data_type`) shows only types {1=Bool,
2=Int32, 5=String} have deployed instances. Float/Double/DateTime/
Duration/array shapes are not in this Galaxy, so the remaining four
type-matrix bullets in F32 are gated on Galaxy-side provisioning
that's outside the Rust port's scope. The M5 DoD #3 was always going
to bottom out at "what types are deployed in the test environment."

Code changes:
- `register_items` retry budget bumped: 10 attempts (was 5) with
  `200 * attempt` ms backoff (was 100 * attempt). Worst-case wait
  ~11 s, well within user-perceived latency on a one-shot RPC. The
  .NET reference's 5×100 ms didn't always cover the live AVEVA
  install's auth-state-commit latency on this hardware.
- `AsbClient::connect` adds a 250 ms `tokio::time::sleep` immediately
  after the one-way `AuthenticateMe` send. The server processes the
  request asynchronously; without an initial settle, the per-op retry
  loop frequently exhausts its budget on the InvalidConnectionId
  race even on the FIRST register attempt. 250 ms is short enough to
  be invisible and long enough to absorb the typical commit delay.
- `examples/asb-subscribe.rs` now prints `result_code` and `success`
  alongside the status count so the user can see when register is
  hitting the retry-exhausted state.

Live flakiness note: the AuthenticateMe race is not fully
deterministic — after many back-to-back test runs the live server
appears to degrade (presumably pending-connection table fills) and
the retry budget exhausts on EVERY tag, not just one. A 30-second
cool-down restores reliability. Production deployments with a single
long-lived session are unlikely to hit this. F32 status doc captures
the observation.

Workspace: 711 unit tests pass. Clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:21:07 -04:00
Joseph Doherty 4ddb6542e1 [M5] design: followups update — M5 functionally LIVE, F30/F31 resolved
F18 (M5 master) gains an "M5 STATUS" block right after the DoD
checklist showing the live end-to-end win (commit `9063f10`,
TestChildObject.TestInt round-trips with payload [99,0,0,0]) and
ticking each DoD bullet:
-  Live `asb-subscribe` succeeds.
- ⚠️ Wire request bytes match .NET byte-for-byte; response parity
  uses the F30 dict-id resolution post-pass + chunked-Bytes
  concatenation instead of strict byte equality (functionally
  equivalent — both decode to the same logical XML).
- ⚠️ Type matrix: only Int32 verified live; Bool/Float/Double/
  String/DateTime/Duration/arrays pending sample tags. Tracked
  under new F32.
-  build/test/clippy green (711 tests).

Followup churn:
- F30 + F31 moved to ## Resolved with proper "Resolved: <date>
  (commit `<hash>`)" headers. F30 was the unblocker for F31 —
  without read-side dict-id resolution we couldn't see
  `<resultCodeField>1</>` in the response.
- F28 status header updated to "PARTIALLY RESOLVED": the five
  [XmlSerializerFormat] ops (AuthenticateMe, Disconnect, KeepAlive,
  RegisterItems, UnregisterItems) plus DH params + dynamic-dict
  management all landed; Read/Write/Subscribe/Publish still sign
  over NBFX wire bytes via the legacy fallback. Severity demoted
  P0 → P2 because the live registry has empty `hashAlgorithm` and
  unsigned ops work in practice; promote back if that changes.
- F29 reaffirmed P2 (latent NBFS dict-id drift, no live impact).
- New F32 captures the type-matrix expansion as the only remaining
  P1 item for full M5 closeout.

No code change in this commit — design doc only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:08:36 -04:00
Joseph Doherty 9063f10b1b [M5] mxaccess-asb: register_items retry on InvalidConnectionId — LIVE PATH WORKS
rust / build / test / clippy / fmt (push) Has been cancelled
End-to-end live path now functional: Connect → AuthenticateMe →
RegisterItems → Read → Disconnect. The example reads back the live
TestChildObject.TestInt value (99) over the wire on the first run.

Root-cause of the previous "InvalidConnectionId" mystery: it was
never an HMAC verification failure. `AuthenticateMe` is one-way
(`AsbContracts.cs:18`) and the server commits auth state
asynchronously after the request lands. A Register that follows too
quickly sees the connection in pre-authenticated state and returns
`AsbErrorCode.InvalidConnectionId` (= 1).

.NET's `MxAsbDataClient.RegisterMany` (`cs:191-204`) handles this
explicitly with a retry loop:

  for (int attempt = 1; attempt < 5
       && response.Result.ErrorCode == InvalidConnectionId; attempt++)
  {
      Thread.Sleep(TimeSpan.FromMilliseconds(100 * attempt));
      response = RegisterOnce(items);
  }

We now mirror the same pattern in `AsbClient::register_items_once`
followed by a retry loop in `register_items` — up to 5 attempts with
`100 * attempt` ms backoff.

Supporting changes:
- `RegisterItemsResponse` gains `result_code: Option<u32>` +
  `success: Option<bool>` so callers can read `Result.resultCodeField`
  + `successField` from the response. `decode_register_items_response`
  now tolerates an empty `<ASBIData />` Status array (server returns
  empty when the operation fails server-side) instead of erroring
  with `MissingField`. New helper `find_text_in_named_element` walks
  the body token stream.
- New public constant `RESULT_CODE_INVALID_CONNECTION_ID = 1` for
  callers that want to detect this status outside the retry path.
- The previously-failing test `decode_register_items_response_returns_
  missing_field_when_status_absent` was renamed and rewritten as
  `decode_register_items_response_returns_empty_status_when_absent`
  to match the new tolerant decode contract.

F31 closed. F30 (read-side dict-id resolution, landed in `eb6c689`)
was the unblocker — without it we couldn't see the
`<resultCodeField>1</>` element in the response and the failure mode
looked like a HMAC mismatch instead of a transient retryable error.

Workspace: 711 unit tests pass. Clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:02:38 -04:00
Joseph Doherty eb6c689f09 [M5] mxaccess-asb: F30 read-side dict-id resolution + matching .NET CV xmlns
**F30 (read side):** post-pass over `body_tokens` in `decode_envelope`
substitutes `NbfxName::Static(id)` → `NbfxName::Inline(name)` and
`NbfxText::DictionaryStatic(id)` → `NbfxText::Chars(name)` whenever
the dict id resolves. Lookup tries the per-message binary header
strings first (`(id-1)/2` slot), then falls back to the cumulative
session dynamic dict, then the `[MC-NBFS]` static table for even
ids. Tokens with unresolvable ids stay opaque so trace output still
reveals them.

This unblocks reading the live Register response: previously every
field came back as `<b:Static(43)>false</…>` and we couldn't tell
what the server actually said. Now we see `<b:successField>false</>`
and `<b:resultCodeField>1</>` clearly. resultCode 1 maps to
`AsbErrorCode.InvalidConnectionId` (`AsbResultMapping.cs:6`) —
which means AuthenticateMe failed silently and the server discarded
our connection state, even though the crypto stack is proven
byte-equal to .NET.

**Wire CV xmlns parity:** `<h:ConnectionValidator>` for the
`XmlSerializer` mode (AuthenticateMe / Disconnect / KeepAlive) now
emits all four xmlns declarations .NET writes, in the same order:
`xmlns:h`, default `xmlns` (same value), `xmlns:xsi`, `xmlns:xsd`.
.NET emits the default xmlns redundantly even though the `h` prefix
is bound to the same URL — captured against the .NET probe via
asb-relay. This was suspected to be the AuthenticateMe HMAC blocker
but the live test still returns `InvalidConnectionId`, so the bug
is elsewhere.

**F31 updated** with the surviving hypotheses for the
`InvalidConnectionId` mystery: server-side `XmlSerializer`
constructor mismatch, subtle byte-level wire difference affecting
deserialization, or unused `ServiceAuthenticationData` from the
ConnectResponse. Resolution probably requires server-side
instrumentation or controlled-scenario byte-level HMAC diff.

Workspace: 710 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:47:50 -04:00
Joseph Doherty 703c540bdc [M5] mxaccess-asb: MX_ASB_TRACE_REPLY trace + F30/F31 followups
Adds env-gated diagnostic trace `MX_ASB_TRACE_REPLY` that, on every
incoming SizedEnvelope, prints the raw reply bytes + decoded body
tokens (capped at 64) before any consumer-level decode runs. Used to
isolate the next blocker after F28's wire-format fixes landed: with
canonical XML signing, registry-driven DH params, dynamic-dict id
management, ConnectionValidator wire-format-per-action, chunked
ASBIData decode, and 0x0A `ShortDictionaryXmlnsAttribute` all in
place, AuthenticateMe is accepted by the server and a real
RegisterItemsResponse comes back — but it decodes to an opaque token
stream of `<b:Static(43)>false</b:Static(43)>` etc. because every
field name is dict-encoded against the response's own binary header
pre-pop and we never resolve those ids on the read side.

Two new follow-ups capture the remaining work:
- **F30**: resolve dict-id element/attribute names on the read side.
  Mirror the F28 write-side fix: read-side dynamic dict accumulates
  session strings via `intern`, and `decode_tokens` (or a post-pass)
  needs to substitute `NbfxName::Static(id)` with the resolved
  `NbfxName::Inline(name)` so downstream `find_element_named` /
  `collect_asbidata_payloads` match.
- **F31**: server response indicates `successField=false` with an
  empty Status array on Register. Hypotheses (in order): (a) silent
  HMAC mismatch despite F23 deterministic parity; (b) request-side
  wire-byte delta the server tolerates but interprets as 0 items;
  (c) tag does not resolve in the live Galaxy state. Resolution
  needs F30 first to read the actual Status array + error codes.

Workspace: 710 unit tests pass. Clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 20:35:29 -04:00
Joseph Doherty cf97eab396 [M5] mxaccess-asb: collect_asbidata_payloads concatenates chunked Bytes records
.NET's `XmlBinaryWriter.WriteBase64` chunks the byte array into
multiple consecutive NBFX `Bytes8/16/32` records when the total
exceeds the per-record budget. Live capture of a successful .NET
RegisterItemsResponse showed the Status ASBIData payload split into
`Bytes8(78) + Bytes8WithEndElement(1)` — total 79 bytes. Our decoder
walked tokens looking for a single `Text(Bytes(...))` after each
`<ASBIData>` element and stopped at the first chunk, returning a
truncated payload that hit `Codec(ShortRead)` when the consumer
tried to decode an ItemStatus from the partial bytes.

Fix: walk **all** consecutive `Text(Bytes)` tokens after `<ASBIData>`
and concatenate into a single payload before pushing to the result
vector. Mirrors WCF's reader behaviour, which reassembles the
chunks into one byte array via `XmlReader.ReadElementContentAsBase64`.

Workspace: 710 unit tests pass. Live state: AuthenticateMe is
accepted, RegisterItemsResponse decodes structurally — the remaining
"MissingField Status" error reflects a server-side semantic outcome
(server returned empty Status array) rather than a protocol bug,
likely tag-resolution related and outside F28's scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:36:38 -04:00
Joseph Doherty 104efc4e9b [M5] mxaccess-asb: F28 wire-format fixes — AuthenticateMe accepted live
Three wire-level bugs surfaced by side-by-side relay capture against
the .NET probe routed via the new --via flag:

1. **Dynamic-dictionary id drift**. Our `encode_envelope` hardcoded
   action_dict_id=1 / to_dict_id=3, which is correct for the FIRST
   message in a session but wrong for every subsequent one. The
   per-session dynamic dict accumulates across messages: Connect's
   binary header pre-pops [action,to] at ids 1,3; AuthenticateMe must
   reference the new action at id 5 (continuing the odd sequence) and
   the To URL at id 3 (still in the dict from Connect). Fix uses
   `DynamicDictionary::position_of` + `intern` to compute the right
   wire id, only pre-popping strings that are NEW to the session.
   Captured against .NET probe via asb-relay: AuthenticateMe binary
   header has only one string (action) at offset 0x260 (`06 de 08 2f
   2e ...`), and `<a:Action>` value `ab 05` references the new id 5.

2. **ConnectionValidator wire format depends on operation**. .NET's
   `IAsbDataV2` declares `[XmlSerializerFormat]` on AuthenticateMe,
   Disconnect, KeepAlive (one-way ops) — those use XmlSerializer for
   the ENTIRE message including the [MessageHeader] ConnectionValid-
   ator. Other ops use the default DataContractSerializer. The wire
   shapes differ:
     XmlSerializer: `<ConnectionId xmlns="http://asb.contracts.data/
       20111111">guid</ConnectionId>` (PascalCase property name in
       data namespace)
     DataContract: `<connectionIdField xmlns="http://schemas.data
       contract.org/2004/07/ArchestrAServices.ASBContract">guid</…>`
       (private "fooField" name in datacontract namespace)
   New `ValidatorWireFormat::for_action` picks the right shape per
   action; `encode_validator` now branches on it. New helpers
   `push_xml_text_field` / `push_xml_byte_array_field` for the
   XmlSerializer form. The DataContract form is preserved verbatim
   for Register/Read/Write/etc.

3. **Decoder missing 0x0A** (`ShortDictionaryXmlnsAttribute`). The
   server's RegisterItemsResponse uses `0x0A {dict-id}` to set the
   default namespace from the static dict; our decoder bailed out
   with `UnknownRecord(10)`. New decode arm produces a
   `DefaultNamespace` token with `DictionaryStatic` value.

**.NET probe gains a `--via` flag** (`AsbConnectionOptions.Via` →
`ChannelFactory.CreateChannel(addr, viaUri)`) so the probe can be
routed through asb-relay for byte-level capture without triggering
an `AddressFilterMismatch` fault. CoreWCF / .NET 10 dropped
`ClientViaBehavior`; the `CreateChannel(addr, via)` overload is the
modern equivalent.

Live status (this commit): Connect handshake works, AuthenticateMe
no longer faults (canonical XML + crypto + wire-format all match
.NET now), RegisterItemsResponse comes back from the server (a real
response, not a dispatcher fault). One remaining issue: our response
decoder hits `MissingField { field: "Status" }` — the server's
RegisterItemsResponse uses a slightly different element naming or
encoding than `collect_asbidata_payloads` expects. Next iteration
hunts that.

Workspace: 710 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:48 -04:00
Joseph Doherty ce27b63010 [M5] auth: deterministic HMAC fixture test rules out crypto stack
Adds end-to-end byte-equality test against a `.NET reference fixture
captured via the new `MxAsbClient.Probe --dump-deterministic-hmac`
flag. All inputs are pinned (passphrase, prime, generator, private-
key bytes, remote-pub bytes, message number, connection ID, AES IV,
consumer-data + IV bytes), so the test reproduces .NET's exact
output for every crypto step:

1. shared = remote_pub^private_key mod prime —  matches
2. crypto_key = shared || passphrase_utf8 —  matches
3. hmac = HMAC-SHA1(crypto_key, xml_utf8) —  matches
4. aes_key = PBKDF2-SHA1(base64(crypto_key), salt, 1000, 16) — 
5. encrypted_mac = AES-CBC(aes_key, iv=zeros, hmac, PKCS7) — 

This conclusively rules out the entire crypto stack as the source of
the live AuthenticateMe `dispatcher/fault`. Our DH math, HMAC engine,
PBKDF2 derivation, AES-CBC PKCS7, and crypto_key concatenation are
byte-equal to .NET. The remaining live failure must come from one
of: (a) wire-level ConnectionValidator NBFX shape (DataContract field
names, mustUnderstand attribute, namespace), (b) WCF binary message
header (action+to dict pre-pop), or (c) a subtle XmlSerializer quirk
for live values that the hardcoded fixtures don't exercise (Guid
format edge case, base64 line wrapping, ulong text rendering).

Fixture lives at `crates/mxaccess-asb-nettcp/tests/fixtures/
deterministic-hmac/authenticate-me.kv` (KV format, ASCII hex, lines
trim CRLF/LF transparently). The companion `README.md` documents the
capture procedure and the per-step decomposition. The test consumes
the .NET-supplied canonical XML directly from the fixture's
`xml_utf8_b64` so a Rust XML emitter bug would not mask a Rust
crypto bug — XML byte-equality is verified separately by
`mxaccess-asb::xml_canonical::tests` against the `signed-xml/*.xml`
fixtures.

Workspace: 710 unit tests pass (was 709 + 1 new). Clippy clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:12:17 -04:00
Joseph Doherty 42ac10a88f [M5] design: F28 follow-up update with progress + remaining blocker
Updates F28 with:
- Captured-fixtures section now lists all 6 shapes (added the
  empty-MAC variant) and 10 inferred XmlSerializer rules including
  the empty-byte-array → self-closing-element rule we discovered.
- New "Emitter landed" section pointing at commit `f14580e` and the
  five exposed `emit_*` functions, plus the
  `AsbAuthenticator::peek_next_message_number` plumbing.
- New "Registry-driven DH params" section explaining why
  `CryptoParameters::defaults()` was insufficient for live testing
  (live AVEVA installs use 768-bit primes; default is 1024-bit) and
  documenting the new MX_ASB_DH_* env-var contract.
- New "Remaining live blocker" section documenting that AuthenticateMe
  still faults despite canonical XML byte-equality and registry-correct
  DH params — most likely a byte-level HMAC/AES discrepancy that needs
  a deterministic-input unit-test triple to pin down.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 17:38:23 -04:00
Joseph Doherty fd38189f43 [M5] auth+probe: env-gated crypto-key/AES-key trace for F28 follow-up
Adds diagnostic traces in both the Rust authenticator and the .NET
reference (under MX_ASB_TRACE_DERIVE / sharedTrace) that dump:
- crypto_key length + hex + base64 (shared || passphrase)
- derived AES key hex (PBKDF2-SHA1, 16 bytes)

Used to confirm during the F28 live-bring-up reconciliation that:
1. crypto_key passphrase suffix bytes [96..176] match between Rust and
   .NET — both read the same registry passphrase, both UTF-8-encode.
2. crypto_key shared_secret prefix bytes [0..96] DIFFER per run because
   each session has its own random DH private exponent. This is
   expected; what matters is the client+server agreement on the value
   for a single session, which the wire-tested DH math should produce
   given correct prime/generator/private-key handling.

Both traces are gated:
- Rust: `MX_ASB_TRACE_DERIVE=1` env var.
- .NET: `Action<string>? sharedTrace` field, populated when the
  authenticator is constructed with a non-null trace callback (the
  probe's `Console.WriteLine` shim wires this up by default).

Workspace: 709 tests still pass. No public-API changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 17:37:22 -04:00
Joseph Doherty f14580e0db [M5] mxaccess-asb: F28 canonical-XML signing wired + registry-driven DH params
Adds `xml_canonical` module that emits XmlSerializer-compatible canonical
XML for the five primary `ConnectedRequest` shapes (AuthenticateMe,
Disconnect, KeepAlive, RegisterItemsRequest, UnregisterItemsRequest).
Six fixture-comparison tests verify byte-exact match against captured
.NET output, including the empty-MAC-IV variant that the live signing
flow uses (`authenticate-me-empty-mac-iv.xml`, 896 bytes; new
`emit_data_ns_byte_array` helper picks self-closing form for empty
byte[]).

Plumbing: `AsbAuthenticator::peek_next_message_number` exposes the
pre-allocated message number; `AsbClient::send_signed_envelope[_one_way]`
gain an `xml_for_signing: Option<&[u8]>` parameter. `connect`,
`disconnect`, `keep_alive`, `register_items`, `unregister_items` now
build a pre-signing `ConnectionValidator` (empty MAC + IV) + emit the
canonical XML + pass the bytes through to HMAC. Other ops (Read, Write,
Subscription) keep the legacy NBFX-bytes path until F28 expands to
cover their request shapes.

Live-bring-up wiring:
- `tools/Get-AsbPassphrase.ps1` now exports `MX_ASB_DH_PRIME`,
  `MX_ASB_DH_GENERATOR`, `MX_ASB_DH_HASH_ALGORITHM` (always — even when
  empty, so the example can distinguish "no env var" from "registry
  says empty"), and `MX_ASB_DH_KEY_SIZE`.
- `examples/asb-subscribe.rs` honours those env vars to override
  `CryptoParameters::defaults()`. Each AVEVA install picks its own DH
  group at provisioning time (768-bit prime is typical, vs the .NET
  reference's 1024-bit fallback that we previously hardcoded). Empty
  hashAlgorithm in the registry maps to `HashAlgorithm::Unrecognised`,
  matching `AsbSystemAuthenticator.CreateHmac:84-93` semantics where
  empty + forceHmac=true → HMAC-SHA1.
- `MxAsbClient.Probe --dump-signed-xml` flag (added in earlier commit)
  now traces the live HMAC inputs (`asb.sign.xml-utf8-len`,
  `asb.sign.xml-b64`, `asb.sign.hmac-b64`, etc.) so the Rust port can
  diff its canonical XML against .NET's byte-for-byte for any live
  scenario (env-driven via `Action<string>? sharedTrace`).

Wire-format alignment for `XmlSerializer` parity:
- `ItemIdentity::default()` and `absolute_by_name` now use
  `Some(String::new())` for null-able strings (matches .NET's
  `CreateAbsoluteItem` setting `ContextName = string.Empty` not null).
- `read_unicode_string` returns `Some(String::new())` for length-0
  rather than `None` — mirrors .NET's `AsbBinary.ReadUnicodeString:
  return string.Empty for byteLength == 0`. Wire format genuinely
  cannot distinguish null from empty (both encode as 4 bytes of zero);
  callers that need to preserve the distinction MUST track it in their
  domain types before encoding.

Live status (post-fix): Connect handshake completes end-to-end. The
canonical XML our emitter produces matches .NET's structure byte-for-
byte (verified by fixture comparison). DH prime/generator/hash now
match the live registry values. Despite all this, AuthenticateMe
still produces a generic dispatcher fault on the server — there's at
least one more subtle wire-byte or crypto mismatch that needs
isolation. F28 stays open with that note.

Workspace: 709 unit tests pass (was 702 + 7 new xml_canonical tests).
Clippy: clean (`-D warnings`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 17:31:31 -04:00
Joseph Doherty dbb580b2c8 [M5] tools+fixtures: F28 canonical-XML signing target captured from .NET
Adds `MxAsbClient.Probe --dump-signed-xml` flag that builds five
ConnectedRequest shapes (AuthenticateMe, Disconnect, KeepAlive,
RegisterItemsRequest, UnregisterItemsRequest) with deterministic
field values and prints `AsbSerialization.ToXml(...)` output. The
output is exactly what `AsbSystemAuthenticator.Sign` HMACs
(`AsbSystemAuthenticator.cs:79`), so the Rust port's canonical-XML
emitter must produce byte-identical bytes for HMAC parity.

Captured fixtures land under
`rust/crates/mxaccess-asb/tests/fixtures/signed-xml/`:
- `authenticate-me.xml` — 1000 bytes
- `disconnect.xml` — 980 bytes
- `keep-alive.xml` — 705 bytes
- `register-items.xml` — 1068 bytes
- `unregister-items.xml` — 1072 bytes

Plus a `README.md` documenting 10 inferred XmlSerializer rules
(element name = class name not WrapperName, field order =
declaration order not [MessageBodyMember.Order], `[XmlType.Namespace]`
on field type causes per-child xmlns redeclaration on the children
not the wrapper, `*Specified` pattern controls Xxx emission, CRLF +
2-space indent + utf-16 declaration but UTF-8 bytes fed to HMAC).

`.gitattributes` marks the XML fixtures as binary (`*.xml -text`)
so neither `core.autocrlf` nor `text` filters can rewrite the byte
content — CRLF is part of the canonical form and must survive
round-trip through Git untouched.

`MxAsbClient.csproj` gains `<InternalsVisibleTo Include="MxAsbClient
.Probe" />` so the probe can reach the internal `AsbSerialization`
helper without making it public.

Workspace: 702 tests pass (no Rust changes — fixtures only).
F28 follow-up updated with the captured fixtures + the inferred rules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 16:35:45 -04:00
Joseph Doherty d1e887b91b [M5] mxaccess-asb-nettcp/asb: Connect handshake live + SOAP fault detection
Live-bring-up reconciliation against AVEVA's MxDataProvider on Windows.
Connect now completes end-to-end (real DH key exchange, apollo:V2
encryption, ServicePublicKey/ServiceAuthenticationData populated). Five
fixes land:

1. NBFX `PrefixElement_a..z` (0x5E-0x77) and `PrefixAttribute_a..z`
   (0x26-0x3F) decode + encode arms. The server's ConnectResponse hit
   `0x65 = PrefixElement_h` for a dynamically-named element and our
   decoder bailed with `unknown NBFX record byte 0x65`. Both directions
   now round-trip; encoder picks short-form when prefix is a single
   lowercase ASCII letter.

2. xmlns redeclaration on `<Data>` AND `<InitializationVector>` inside
   `AuthenticationData` / `PublicKey`. `[XmlType(Namespace = ...)]` on
   AuthenticationData / PublicKey (`AsbContracts.cs:350-381`) means
   XmlSerializer emits `xmlns="..."` on each direct child. The default-
   ns scope ends at `</Data>`, so `<InitializationVector>` needs its own
   redeclaration to stay in the data namespace; without it the server
   fell back to messages-namespace and the deserialiser threw an
   `InternalServiceFault`.

3. SOAP-fault detection in `AsbClient::send_envelope`. New
   `ClientError::SoapFault { action, code, reason }` surfaces when the
   response Action header matches the canonical `dispatcher/fault`
   template; previously body decoders blindly ran and surfaced
   `MissingField { field: "Status" }` masking the actual fault. Reason
   text is extracted as the longest `NbfxText::Chars` in the body —
   robust against the `nbfs.rs` static-dictionary id mismatches.

4. Identified blocker (filed as F28): signed-request HMAC currently
   covers the NBFX wire bytes, but .NET's `AsbSystemAuthenticator.Sign`
   HMACs `Encoding.UTF8.GetBytes(request.ToXml())` — the canonical XML
   serialisation via `XmlSerializer` with namespace
   `urn:invensys.schemas` (`AsbSerialization.cs:12-48`). Until the Rust
   port emits identical XML bytes for `ConnectedRequest` subclasses,
   AuthenticateMe / RegisterItems / every signed RPC fault on the
   server. Connect itself is unsigned (`ServiceMessage` not
   `ConnectedRequest`) which is why it works today.

5. Identified `nbfs.rs` static-dictionary id drift (filed as F29): wire
   uses Fault=134/Code=142/Reason=144/Text=146/Value=154/Subcode=156
   but our table has them at 114/122/124/126/134/136. Off by 20 from
   id 114+ — 10 missing entries between `s` (id 112) and `Fault`. No
   request-side impact (we only encode IDs ≤44, all correct); the SOAP
   fault decode walks text records directly so it sidesteps the issue.

Workspace: 702 tests pass (no test count delta — wire-only fixes).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 16:29:12 -04:00
Joseph Doherty 4c4177050c [M5] mxaccess-asb-nettcp/asb: xmlns raw-string + xsi/xsd on Body
WIRE-FORMAT BREAKTHROUGH — our envelope is now valid NBFX/WCF.
ConnectRequest reaches the server's operation handler. Direct-port-808
Connect now returns a server-side fault (operation invocation
error from the placeholder DH key) rather than TCP RST. With a real
DH key, asb-subscribe gets all the way to "response is missing
required field ServicePublicKey/Data" — meaning our Connect request
processed end-to-end, the server returned a ConnectResponse, and
the only remaining issue is in our response-body decoder.

Fixes:

1. **`XmlnsAttribute` (0x09) value is a RAW length-prefixed string,
   not a text record**. Per `[MC-NBFX]` §2.2.3, xmlns attribute
   values are `[length][bytes]`, NOT `[text-record-byte][value]`.
   Our F21 was emitting `aa <id>` for dict-static values which the
   receiver misparsed as a 0xAA-length string. Same fix applies to
   `ShortXmlnsAttribute` (0x08). Encoder now picks raw-string for
   `Chars` value, raw-int31 (via 0x0B) for `DictionaryStatic` value;
   decoder reads raw string in both code paths.

2. **xmlns:xsi + xmlns:xsd on `<s:Body>`**. WCF declares these
   namespaces on Body before opening the operation request element.
   Our envelope encoder now emits both as raw-string xmlns attrs
   right after `<s:Body>` opens. Required for `xsi:type` annotations
   that appear inside DataContract-serialised body fields.

Combined wire-byte impact (verified via asb-relay side-by-side
diff):

* All header bytes match .NET byte-for-byte through the entire
  `<s:Header>` section (Action / ConnectionValidator / MessageID
  via UniqueIdText / ReplyTo / To).
* `<s:Body>` xmlns:xsi + xmlns:xsd declarations match .NET.
* `<ConnectRequest>` opens identically.
* `<ConnectionId>` / `<ConsumerPublicKey>` / `<Data>` element names
  match.
* The only known remaining diff in the request: .NET emits
  `xmlns="http://asb.contracts.data/20111111"` on the inner
  `<Data>` element (the PublicKey class's XmlType namespace);
  we don't. Likely an issue but apparently non-fatal — the server
  processed our request successfully past this point.

Live status:
* Direct port-808 connect with real DH key: server returns
  "response is missing required field ServicePublicKey/Data" —
  meaning we sent a valid Connect, server replied with a
  ConnectResponse, but our decoder can't find the field. Next
  iteration is response-side decode work.

Workspace: 702 tests pass; clippy + fmt clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 15:57:31 -04:00
Joseph Doherty c2222b16b0 [M5] mxaccess-asb-nettcp/asb: F21 short forms + EndElement fix + UniqueIdText
Three NBFX-spec corrections discovered by diffing our wire output
against the .NET probe's capture:

1. **EndElement is 0x01, NOT 0x00**. Our F21 had this wrong since the
   first iteration. Our round-trip tests passed because encode and
   decode used the same wrong value, but interop with WCF's parser
   silently failed (TCP RST on every request). Fixed by changing
   `REC_END_ELEMENT` to 0x01 — all 702 tests pass on the new value.

2. **Single-letter prefix short forms**. WCF uses
   `PrefixDictionaryElement_<a-z>` (records 0x44-0x5D) and
   `PrefixDictionaryAttribute_<a-z>` (records 0x0C-0x25) for
   single-character prefixes. Our F21 always used the long forms
   (0x43 prefix-string + dict-id, etc.). The encoder now emits the
   short form when the prefix is a single ASCII lowercase letter; the
   decoder accepts both. New `prefix_letter_offset(prefix)` helper.

3. **`DictionaryXmlnsAttribute` (0x0B)** for xmlns:prefix declarations
   whose value is a static-dict id. The long form (0x09 +
   prefix-string + text-record) is still emitted when the value is an
   inline string, but for `xmlns:s="...soap-envelope"` (dict id 4) we
   now emit the short `0b 01 73 04` form WCF uses.

4. **UniqueIdText (0xAC)** added to `NbfxText` enum + encode/decode.
   WCF emits `<a:MessageID>` as a UniqueIdText carrying the 16 raw
   UUID bytes (NOT the `urn:uuid:...` text form). Updated
   `encode_envelope` to use this for MessageID.

Combined wire-byte impact: our envelope body section now matches the
.NET probe byte-for-byte through `<a:Action>`, `<h:ConnectionValidator>`,
`<a:MessageID>` (UniqueId), `<a:ReplyTo>`, `<a:To>`, and `<s:Body>`.
The trailing `01 01 01 01` = 4 EndElements is now the correct
record byte. Tests pass (702 total).

Live status: still TCP RST after the SizedEnvelope. Remaining
unknown is in the body section — the .NET capture shows xmlns:xsi /
xmlns:xsd declarations on the operation-specific request element
(ConnectRequest etc.) that we don't emit, plus possibly different
field encoding inside ConnectRequest. Next iteration will re-capture
through the relay and diff our body bytes against the new
.NET-byte-equivalent we now produce.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 15:48:03 -04:00
Joseph Doherty 2867310817 [M5] mxaccess-asb: WCF binary message header (action+to dict pre-pop)
Adds the binary header block that WCF prepends to SizedEnvelope
payloads. Reverse-engineered from .NET probe wire bytes captured via
asb-relay.

Wire form (per the .NET capture analysis in the previous commit):
```
[outer length, multibyte-int31]
  [string-1 length, multibyte-int31] [UTF-8 bytes]   ← dict id 1 (action)
  [string-2 length, multibyte-int31] [UTF-8 bytes]   ← dict id 3 (to)
[NBFX <s:Envelope>...]
```

Inside the NBFX envelope, `<a:Action>` and `<a:To>` reference the
pre-pop strings via `DictionaryText 0xAA {odd-id}` instead of inlining
their values. The header strings get assigned odd dict ids
(1, 3, 5, ...); even ids stay reserved for the [MC-NBFS] static dict.

Encode side:
* `encode_envelope` now emits header [action, to] before NBFX. `to_uri`
  defaults to empty string when None — caller-supplied `with_to(uri)`
  is the supported path.
* AsbClient's `send_envelope` and `send_envelope_one_way` auto-fill
  `to_uri` from `self.via_uri` when not set.
* New private `encode_binary_header(strings)` helper.

Decode side:
* New `parse_binary_header_prefix(input)` heuristically detects + parses
  the header (look for plausible NBFX element record byte 0x40-0x77 at
  the offset implied by the outer length).
* New `resolve_with_header(text, dynamic, header)` resolves
  `DictionaryText` with odd id by indexing into header.strings; even
  ids fall through to static-dict lookup as before.

Tests pass (72) — round-trip envelope → bytes → envelope recovers
action through the new dict-id resolution path.

Live status: this commit gets us further but the connect SOAP
envelope still TCP-RSTs at SMSvcHost. The remaining delta vs the .NET
capture is structural NBFX optimisation: .NET uses single-letter
prefix-element/attribute records (0x44-0x77 PrefixDictionaryElement
_<a-z>, 0x0C-0x25 PrefixDictionaryAttribute_<a-z>, 0x0B
DictionaryXmlnsAttribute) while our F21 encoder always uses the long
forms (0x43 prefix-string + name-dict-id, etc.). Logically
equivalent but WCF's parser likely strict on which form it accepts.

Next iteration will add short-form encoding to F21 for single-letter
prefixes (s:, a:, h:, i:) which covers every namespace prefix in our
envelope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 15:40:59 -04:00
Joseph Doherty d4ee5f3a18 [M5] examples: asb-relay TCP middleman for live wire-byte capture
Listens on MX_RELAY_LISTEN (default 127.0.0.1:8088) and forwards to
MX_RELAY_UPSTREAM (default 127.0.0.1:808 — AVEVA's NetTcpPortSharing
SMSvcHost listener). Hex-dumps every byte both directions to stderr
with C->S / S->C tags + per-direction offset prefixes.

Usage:
  $env:MX_RELAY_LISTEN = '0.0.0.0:8088'
  .\rust\target\debug\examples\asb-relay.exe 2> relay.log
  # then in another shell:
  dotnet run --project src\MxAsbClient.Probe -c Release -- `
    '--endpoint=net.tcp://desktop-6jl3kko:8088/ASBService/Default_ZB_MxDataProvider/IDataV2'

Tested against the live AVEVA install on this box — captured a
620-byte client→server exchange including the full .NET probe's
preamble, SizedEnvelope, and End record. The capture surfaced one
critical missing piece in our wire format:

**WCF binary message framing prepends Action + To strings out-of-band**
before the actual NBFX SOAP envelope. The .NET probe's envelope
payload begins:

  74 27 [39 bytes "http://asb.contracts/20111111:connectIn"]    ← Action
  4b    [75 bytes "net.tcp://desktop-6jl3kko:8088/.../IDataV2"]  ← To
  56 02 ...                                                      ← <s:Envelope>

The 0x74 / 0x4b prefix bytes appear to be WCF-internal framing that
stores Action and To headers OUT of the SOAP envelope as a binary
optimization. Our F25 envelope encoder doesn't emit this — it goes
straight to `<s:Envelope>` (which the probe captured as `56 02 ...`
PrefixDictionaryElement_s + dict id 2). This is likely why the
server fault'd at AddressFilter mismatch in the previous iteration.

Note: when going through the relay, the .NET probe's `:8088` port
appears in the To URL inside the binary header, which doesn't match
the registered service URL on SMSvcHost — so this exact relay setup
returns the AddressFilterMismatch fault. The capture is still
valuable (we see what bytes WCF emits for our action/header
structure). For a fault-free dispatch, we'd need to:
* rewrite the binary header's port (0x4b length / URL bytes) at
  the relay, OR
* listen on port 808 directly (requires stopping SMSvcHost), OR
* run an admin-elevated Wireshark/Npcap loopback capture.

Cleanup: dotnet probe must use `--endpoint=URL` (single arg with `=`),
not space-separated; the probe's GetArg helper splits on `=`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 15:27:54 -04:00
Joseph Doherty 3b09297b27 [M5] live-probe iteration 1 — major wire-byte reconciliation fixes
First live-test cycle against AVEVA on this box. Comparing the .NET
probe's `--dump-messages` XML output against our NBFX-encoded
envelope surfaced six structural bugs in the F25 envelope/operations
layer. All fixed; tests passing (702 workspace).

Fixes (all backed by the .NET dump as ground truth):

1. **`mustUnderstand` attribute name** — NBFS dict id was 116
   (`MustUnderstand`, capital-M, a different SOAP token); SOAP 1.2
   spec uses lowercase `mustUnderstand` at id 0. Sending the wrong
   one triggered a WCF parse fault that surfaced as TCP RST.

2. **Missing `<a:MessageID>` header** — WCF's default binding
   requires MessageID for two-way operations. We now auto-generate
   `urn:uuid:<v4>` per envelope via a small inline `make_random_uuid_v4`
   helper (no `uuid` crate dep).

3. **Missing `<a:ReplyTo>` anonymous header** — WCF's
   BinaryMessageEncoder always emits `<a:ReplyTo><a:Address>...
   addressing/anonymous</a:Address></a:ReplyTo>` for two-way ops.

4. **ConnectionValidator field names + namespace** — we were
   emitting PascalCase `<ConnectionId>` etc. .NET's WCF
   DataContractSerializer uses the private backing-field names
   (`<connectionIdField xmlns="...ASBContract">guid</connectionIdField>`)
   per `[DataMember(Name = "fooField")]`. Added the
   `xmlns:i="...XMLSchema-instance"` declaration WCF emits
   alongside (even when no `i:nil` is used). Decoder now accepts
   both PascalCase (legacy tests) and DataContract field names.

5. **`<ASBIData>` over-wrapping** — we were emitting
   `<Items><ASBIData>{bytes}</ASBIData></Items>`. .NET's
   `AsbDataCustomSerializer.WriteStartObject` (`AsbContracts.cs:
   1561-1572`) REPLACES the field's outer element with `<ASBIData>`
   directly — there's no `<Items>` wrapper on the wire. Fixed by
   collapsing `BodyField::AsbiDataElement` to emit just `<ASBIData>`
   without the named outer element. The `name` field is retained
   for self-documentation.

6. **`collect_asbidata_payloads` API** — was keyed by field name
   (`Status` / `Values`); now positional (`payloads[0]`,
   `payloads.get(1)`) since the wrapper element is gone. All seven
   response decoders updated.

Plus tooling for the live-probe loop:
* `tools/Get-AsbPassphrase.ps1` — DPAPI loader that auto-discovers
  the solution name + reads the sharedsecret + decrypts it. Sets
  $env:MX_ASB_PASSPHRASE / MX_ASB_HOST / MX_ASB_VIA / MX_LIVE.
  Lowercase via-host (WCF SMSvcHost is case-sensitive on the URL
  host segment).
* `examples/asb-preamble-probe.rs` — diagnostic that connects,
  runs the preamble, captures the PreambleAck, then sends a
  synthetic ConnectRequest and dumps both directions as hex. Used
  to bisect the wire-byte deltas above.
* `examples/asb-subscribe.rs` port default fixed (5074 → 808 —
  WCF's NetTcpPortSharing/SMSvcHost listener confirmed via
  Get-NetTCPConnection).

**Status**: preamble + PreambleAck round-trip works end-to-end
against the live AVEVA install (verified via probe). The
post-preamble Connect SOAP envelope still gets TCP RST'd — the six
structural fixes above are necessary but not yet sufficient. Next
iteration needs binary wire capture (Wireshark + Npcap loopback,
or a TCP-relay middleman) to compare the .NET probe's BinaryMessageEncoder
output byte-for-byte with ours and find the remaining delta(s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 15:06:48 -04:00
Joseph Doherty 4ebfd8e3a3 [M5] tools: Get-AsbPassphrase.ps1 — DPAPI loader for live-probe env
Reads the ASB solution shared secret from the local Windows registry
(HKLM\SOFTWARE\Wow6432Node\ArchestrA\ArchestrAServices\<solution>\
sharedsecret) and DPAPI-decrypts it with the canonical "wonderware"
entropy + LocalMachine scope, mirroring `AsbRegistry.cs:21-41`.

Auto-discovers:
  $env:MX_LIVE             = "1"
  $env:MX_ASB_HOST         = $env:COMPUTERNAME
  $env:MX_ASB_SOLUTION     = (read from DefaultASBSolution)
  $env:MX_ASB_GALAXY_NAME  = "ZB" (or -GalaxyName param)
  $env:MX_ASB_VIA          = net.tcp://<host>/ASBService/Default_<galaxy>_MxDataProvider/IDataV2
  $env:MX_ASB_PASSPHRASE   = (DPAPI-decrypted plaintext, never printed unless -Show)

Important wiring detail flagged inline: the system-wide ArchestrA
solution name (`Archestra_<HOST>`, source of the sharedsecret) is
DIFFERENT from the per-Galaxy MxDataProvider service segment
(`Default_<galaxy>_MxDataProvider`) that the WCF endpoint URL
targets. Both live under the same registry root but only the former
is owned by ArchestrA; the latter is what serves IASBIDataV2 per
the .NET probe's hardcoded default URL at
`src/MxAsbClient.Probe/Program.cs:5`.

Tested via dry-run on this box: `Archestra_DESKTOP-6JL3KKO` resolves
as the solution, 390 protected bytes decrypt to an 80-char
passphrase, and the assembled VIA URL matches the .NET probe's
default verbatim.

Hard rules:
* Plaintext passphrase NEVER printed unless -Show is explicit.
* Dot-source so env vars persist in the calling pwsh session.
* Caller account must be authorised against the LocalMachine-scope
  DPAPI blob (typically: any local Administrator).

Usage:
  . .\tools\Get-AsbPassphrase.ps1
  cargo run -p mxaccess --example asb-subscribe

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 14:45:43 -04:00
Joseph Doherty e3baeb8803 [M5] mxaccess: F26 step 3 — AsbSession high-level cheap-clone async API
Adds the public high-level entry point for the ASB transport.
Parallel to the NMX-shaped `Session` (rather than unified) because
NMX's `Session` carries CallbackExporter / callback router task /
recovery broadcast / INmxService2 mutex orchestration that has no
ASB analogue — and ASB's request/response loop over a single TCP
stream maps naturally to `Mutex<AsbClient>` that would be foreign
to NMX. Two paths converge at the consumer-facing API but stay
distinct at the orchestration layer.

Struct shape:
```rust
pub struct AsbSession { inner: Arc<AsbSessionInner> }
struct AsbSessionInner {
    transport: Mutex<AsbTransport<TcpStream>>,
    connect_response: ConnectResponse,
}
```

`Clone + Send + Sync` — clones share state through `Arc`, lock
serialises operations. Compile-time `assert_clone_send_sync` test
guards the contract.

API:
* `connect(endpoint, passphrase, crypto_parameters, via_uri,
  connection_id)` — full bring-up (TCP + preamble + DH handshake).
* `from_transport(transport, connect_response)` — build from an
  existing transport (tests, custom transports).
* `connect_response()` — surface the negotiated lifetime /
  Apollo flag.

Operation methods forward to AsbClient:
* `register_items` / `unregister_items` / `read` / `write`
* `keep_alive` / `disconnect`
* `create_subscription` / `add_monitored_items` / `publish` /
  `delete_monitored_items` / `delete_subscription`
* `publish_write_complete`

ClientError → mxaccess::Error mapping via
`ConnectionError::TransportFailure` (consistent with F26 step 2).

1 new test:
* `asb_session_is_clone_send_sync` — compile-time trait-bound
  assertion.

Workspace: 702 tests pass.

Stubbed for next F26 iteration:
* `Stream<Item = MonitoredItemValue>` subscription handle that
  internally drives a publish-loop. Today consumers loop
  `publish().await` themselves.
* Recovery / reconnect policy — needs a captured ASB-side
  disconnect to inform the retry strategy.
* Live-probe wire-byte reconciliation against the WCF DataContract
  XML serializer's actual output.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 13:23:59 -04:00
Joseph Doherty 9876b4ebb4 [M5] mxaccess-asb: F25 step 10 — PublishWriteComplete + DeleteMonitoredItems
Closes out the F25 operation matrix. AsbClient now wraps every
IASBIDataV2 operation:

  Lifecycle:    connect / disconnect / send_end / send_preamble / keep_alive
  Items:        register_items / unregister_items / read / write
  Subscriptions:create_subscription / add_monitored_items / publish
                / delete_monitored_items / delete_subscription
  Write cb:     publish_write_complete

API additions:
* `build_publish_write_complete_request_body()` — empty wrapper
  per `AsbContracts.cs:204-205`. No body fields beyond inherited
  ConnectionValidator.
* `decode_publish_write_complete_response` — returns count of
  `<ItemWriteComplete>` elements observed. Per-element decode
  (Status + WriteHandle) deferred to a later iteration since
  ItemWriteComplete is regular WCF DataContract rather than the
  binary fast-path.
* `build_delete_monitored_items_request_body` — same MonitoredItem
  shape as AddMonitoredItems but omits RequireId per `cs:268-277`.
* `decode_delete_monitored_items_response` — per-item Status array.
* Client wrappers: `publish_write_complete()`,
  `delete_monitored_items(subscription_id, items)`.

6 new tests:
* `publish_write_complete_body_is_empty_wrapper` — body shape.
* `publish_write_complete_response_counts_item_write_complete_elements`
  — counts 2 / 0 elements correctly.
* `publish_write_complete_response_zero_when_no_callbacks`.
* `delete_monitored_items_body_carries_subscription_id_and_items`.
* `delete_monitored_items_body_omits_require_id_field`.
* `delete_monitored_items_response_round_trip`.

Workspace: 701 tests pass (was 695, +6).

Stubbed for future iterations:
* ItemWriteComplete per-element decode (Status + WriteHandle) once
  a live capture confirms the WCF DataContract XML wire form.
* Optional MonitoredItem fields (Active / TimeDeadband /
  ValueDeadband / UserData) — same wire-byte uncertainty.
* Optional WriteValue fields (Comment / Timestamp / etc.).

All wire-byte caveats trace back to live-probe reconciliation
against an actual AVEVA VM.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 13:17:01 -04:00
Joseph Doherty 0441a2e693 [M5] mxaccess-asb: F25 step 9 — Write operation
Closes the highest-value remaining IASBIDataV2 op. With Write landed,
the read+write+subscribe path is functionally complete in-memory.

API additions:
* `MinimalWriteValue { value: AsbVariant }` — carries just the Value
  payload. Optional ArrayElementIndex / Comment / HasQT / Status /
  Timestamp fields are deferred to a later iteration once a live
  capture confirms the WCF DataContract XML form.
* `build_write_request_body(items, values, write_handle)` per
  `AsbContracts.cs:181-194`:
  ```xml
  <WriteBasicRequest xmlns="urn:msg.data.asb.iom:2">
    <Items><ASBIData>{ItemIdentity[] binary}</ASBIData></Items>
    <Values>
      <WriteValue><Value><ASBIData>{Variant binary}</ASBIData></Value></WriteValue>
      ...
    </Values>
    <WriteHandle>{i32}</WriteHandle>
  </WriteBasicRequest>
  ```
  Items array uses the IAsbCustomSerializableType binary fast-path;
  each Value's inner Variant also uses the fast-path. WriteHandle is
  an Int32 (opaque correlation echoed in PublishWriteComplete).
* `decode_write_response` — per-item Status array (mirrors the
  unregister/register pattern).
* `AsbClient::write(items, values, write_handle)` — thin wrapper.

4 new tests:
* `write_request_body_carries_items_values_and_write_handle` — body
  shape sanity (WriteHandle = 7 Int32, WriteValue element present).
* `write_request_body_pairs_items_and_values_arrays` — 2 items + 2
  values produces 2 WriteValue elements.
* `write_response_round_trips_status_array` — Status decode.
* `write_response_missing_status_fails` — graceful MissingField
  error.

Workspace: 695 tests pass (was 691, +4).

Stubbed for next F25 iterations:
* `PublishWriteComplete` — empty request, `ItemWriteComplete[]`
  response.
* `DeleteMonitoredItems` — mirrors AddMonitoredItems pattern.
* Optional WriteValue fields (Comment / Timestamp / etc.) once a
  live capture confirms the wire-byte layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 13:04:11 -04:00
Joseph Doherty b543eb1f84 [M5] mxaccess-asb: F25 step 8 — subscription operations
CreateSubscription / AddMonitoredItems / Publish / DeleteSubscription.
Completes the IASBIDataV2 read-and-subscribe path; remaining ops
(Write/PublishWriteComplete/DeleteMonitoredItems) are mechanical
extensions of the same pattern.

Contracts:
* `MonitoredItemValue` codec (IAsbCustomSerializableType binary
  fast-path: ItemIdentity + RuntimeValue + AsbVariant per
  `AsbContracts.cs:1064-1068`) with array codec (4-byte int32
  count + per-element body, mirrors `WriteArrayToStream` at
  `cs:1095-1103`).

Request builders:
* `build_create_subscription_request_body(max_queue_size,
  sample_interval)` — primitive fields per `cs:215-223`.
* `build_delete_subscription_request_body(subscription_id)` —
  primitive field per `cs:232-237`.
* `build_publish_request_body(subscription_id)` — primitive field
  per `cs:287-292`.
* `build_add_monitored_items_request_body(subscription_id, items,
  require_id)` — minimal MonitoredItem shape (Item +
  SampleInterval + Buffered). Full optional-field set
  (Active/TimeDeadband/ValueDeadband/UserData) deferred to a later
  iteration once a live capture confirms the WCF DataContract
  XML wire form.

Response decoders:
* `decode_create_subscription_response` — single int64
  SubscriptionId field. Decoder accepts Int64Text, Int32Text,
  Zero/One, or numeric-string Chars (covers all WCF binary
  numeric encodings).
* `decode_add_monitored_items_response` — Status array +
  ItemCapabilities-presence flag (mirrors RegisterItemsResponse).
* `decode_publish_response` — Status array + Values
  (MonitoredItemValue) array.

`BodyField::Int64Element` variant added for the primitive
SubscriptionId / MaxQueueSize / SampleInterval fields. `uint64`
helper casts to i64 (covers proven value range; if ulong > i64::MAX
ever appears we'll add UInt64Text to F21's NbfxText enum).

Client wrappers (4 new methods on AsbClient):
* `create_subscription(max_queue_size, sample_interval)`
* `add_monitored_items(subscription_id, items, require_id)`
* `publish(subscription_id)`
* `delete_subscription(subscription_id)`

11 new tests cover:
* MonitoredItemValue round-trip + array round-trip.
* CreateSubscription request body shape (Int64 payloads).
* CreateSubscription response decoder via Int64Text.
* CreateSubscription response decoder via Chars text fallback.
* CreateSubscription response missing-field error.
* AddMonitoredItems body carries SubscriptionId + MonitoredItem
  elements.
* AddMonitoredItems response Status round-trip.
* DeleteSubscription body carries SubscriptionId.
* Publish request body shape.
* Publish response Status + Values round-trip.

Workspace: 691 tests pass (was 680, +11). The asb-subscribe example
can now do create_subscription → add_monitored_items → publish-loop
→ delete_subscription once wire-byte reconciliation against a live
capture confirms the MonitoredItem XML shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 12:57:59 -04:00
Joseph Doherty c6570dcd06 [M5] mxaccess: asb-subscribe example exercises full F25+F26 stack
Replaces the M5 placeholder with an actual end-to-end demo:

  AsbTransport::connect (TCP + preamble + DH handshake)
  → register_items
  → read
  → disconnect
  → send_end

Until F25 subscription ops (CreateSubscription / AddMonitoredItems
/ Publish-callback) land, the example is a Read-loop demo. Once
subscription ops arrive, it gains a Publish-loop and lives up to
its name.

Env vars (analogous to the NMX `connect-write-read` example):
  MX_LIVE — non-empty enables the live path
  MX_ASB_HOST — endpoint host[:port]; defaults port 5074
  MX_ASB_PASSPHRASE — solution shared secret
  MX_ASB_VIA — `net.tcp://...` URI (optional; derived from MX_ASB_HOST
    when omitted)
  MX_TEST_TAG — tag reference (default `TestChildObject.TestInt`)

Without MX_LIVE: prints the `Setup-LiveProbeEnv.ps1` hint and exits
cleanly with status 0 — the same pattern every other live example
follows.

Connection-id is a fresh 16-byte random buffer (matches .NET's
`Guid.NewGuid()` at `MxAsbDataClient.cs:36`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 12:34:24 -04:00
Joseph Doherty 14bb5297a8 [M5] mxaccess: F26 step 2 — AsbTransport::connect TCP+preamble+handshake
Adds the `tokio::net::TcpStream`-specialised async constructor that
owns the full transport-bring-up sequence:

  TCP connect → NMF preamble → DH Connect → AuthenticateMe (one-way)

Signature:
```
async fn connect(
    endpoint: SocketAddr,
    passphrase: &str,
    crypto_parameters: &CryptoParameters,
    via_uri: impl Into<String>,
    connection_id: [u8; 16],
) -> Result<(AsbTransport<TcpStream>, ConnectResponse), Error>
```

Returns the `ConnectResponse` alongside the transport so callers can
inspect the negotiated `connection_lifetime` (the `:V2` suffix
toggles Apollo vs Baktun encryption — see F23).

New error variant: `ConnectionError::TransportFailure { detail }`
covers all transport-bring-up failure modes (NMF / NBFX / auth /
peer Fault). The underlying error type is intentionally erased to
keep the public taxonomy small; `detail` carries the Display
representation.

Errors are mapped at the AsbClient / AuthError boundary via private
`map_client_error` / `map_auth_error` helpers.

1 new test:
* `connect_to_unreachable_endpoint_surfaces_connection_error` — TCP
  connect to 127.0.0.1:1 (TCPMUX-reserved) cleanly errors without
  panicking. Smoke test for the constructor signature + error path.

Stubbed for F26 step 3:
* `Session::connect_asb` constructor — the SessionInner refactor to
  host both NMX + ASB transports under one struct is heavier than
  this iteration's scope.
* Operation-routing layer that maps ASB result types (ItemStatus,
  RuntimeValue) back to mxaccess types (MxStatus, DataChange,
  MxValue).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 12:14:16 -04:00
Joseph Doherty 8a0f92b6bc [M5] mxaccess: F26 step 1 — AsbTransport bridges AsbClient into Transport trait
First slice of F26. Bridges F25's working AsbClient into the M0
`mxaccess::Transport` trait that Session uses to discriminate
operations across NMX and ASB transports.

API additions:
* `mxaccess::AsbTransport<T>` — generic over the same
  AsyncRead+AsyncWrite+Unpin+Send+Sync+'static bound that AsbClient
  takes. Owns an AsbClient and exposes it via `client_mut()` /
  `into_client()`.
* `impl Transport for AsbTransport<T>`:
  - `capabilities()` — `buffered_subscribe = false`,
    `activate_suspend = false`, `operation_complete_frame = false`
    per `design/60-roadmap.md` M5 (no NMX-specific extensions on
    ASB).
  - `kind()` — `TransportKind::Asb`.

Path-dep wiring: `mxaccess` now imports `mxaccess-asb` +
`mxaccess-asb-nettcp` directly.

Compile-time `Send + Sync + 'static` assertion guards the
trait-bound contract.

2 new tests:
* `asb_transport_kind_is_asb`.
* `asb_transport_capabilities_disable_buffered_and_activate_suspend`.

Stubbed for F26 step 2:
* `Session::connect_asb` constructor that owns TCP open +
  preamble + DH handshake orchestration.
* Operation routing that maps ASB types (ItemStatus, RuntimeValue)
  back to mxaccess types (MxStatus, DataChange, MxValue).

Stubbed for F26 step 3:
* Subscription routing — Session::subscribe on ASB needs F25
  subscription operations (CreateSubscription / AddMonitoredItems
  / Publish), which are not yet implemented.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 11:57:20 -04:00
Joseph Doherty 1b1ee1e0b7 [M5] mxaccess-asb: F25 step 7 — Disconnect closes the session lifecycle
Mirrors `AsbContracts.cs:109-114` — same payload shape as
AuthenticateMe (Data + InitializationVector under
ConsumerAuthenticationData) but under the `<DisconnectRequest>`
wrapper. Sent one-way + signed (regular HMAC, no force) per
`AsbContracts.cs:22` (`IsOneWay = true`).

API additions:
* `build_disconnect_request_body(data, iv)` — NBFX token stream for
  the DisconnectRequest body.
* `AsbClient::disconnect()` — builds a fresh encrypted
  authentication-data blob via F23's `create_authentication_data()`
  (encrypts `local_pub || remote_pub` under the derived AES key
  with a fresh IV), wraps it in a DisconnectRequest, sends one-way
  signed.

2 new tests:
* `disconnect_request_carries_data_and_iv_under_correct_wrapper` —
  outer element name + Data/IV byte-payload order.
* `disconnect_writes_signed_one_way_envelope` — end-to-end via
  `tokio::io::duplex` peer; verifies the SizedEnvelope payload
  contains the `:disconnectIn` action string.

With Disconnect landed, AsbClient now covers the full session
lifecycle:
  send_preamble → connect → register_items / read / keep_alive
  / unregister_items → disconnect → send_end → stream shutdown

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 11:51:39 -04:00
Joseph Doherty 321b7963a4 [M5] mxaccess-asb: F25 step 6 — Connect/AuthenticateMe handshake
Critical-path piece that turns a fresh TCP stream into an
authenticated session. With this slice landed, an `AsbClient` can
now do `send_preamble().await? -> connect().await? -> register_items()`
end-to-end against a peer.

Operations API additions:
* `build_connect_request_body(connection_id, public_key)` — first op
  on a fresh session. **Unsigned** (no ConnectionValidator header)
  because the authenticator hasn't received the service key yet.
  Wire shape: `<ConnectRequest xmlns="…messages/20111111">
    <ConnectionId>{guid-text}</ConnectionId>
    <ConsumerPublicKey><Data>{pubkey-bytes}</Data></ConsumerPublicKey>
  </ConnectRequest>` per `AsbContracts.cs:78-86`.
* `build_authenticate_me_request_body(data, iv)` — second op,
  **one-way + signed with `forceHmac=true`** per `MxAsbDataClient.cs
  :106-111`. Carries the encrypted `local_pub || remote_pub` blob
  produced by F23's `create_authentication_data()`.
* `ConnectResponse { service_public_key, service_authentication_data,
  connection_lifetime }` + `AuthenticationDataBytes { data, iv }`.
* `decode_connect_response(body, dict)` — extracts ServicePublicKey
  (required), optional ServiceAuthenticationData, optional
  ConnectionLifetime. The lifetime's `:V2` suffix is what F23
  inspects to toggle Apollo (raw AES) vs Baktun (deflate-then-AES)
  encryption.

Client API addition:
* `AsbClient::connect()` — orchestrates the full handshake:
  1. Build + send ConnectRequest (unsigned) carrying our DH public
     key + connection-id GUID.
  2. Decode ConnectResponse.
  3. `authenticator.accept_connect_response(...)` — feeds the
     service public key + lifetime into F23 so it derives the
     shared secret and picks Apollo/Baktun.
  4. `authenticator.create_authentication_data()` — encrypts
     `local_pub || remote_pub` under the derived AES key.
  5. Send AuthenticateMeRequest (one-way, signed with HMAC-SHA1
     forced).
  Returns the `ConnectResponse` so callers can inspect the
  negotiated connection lifetime.

6 new tests:
* ConnectRequest carries hyphenated GUID + raw public-key bytes.
* AuthenticateMe carries Data + IV bytes in order.
* ConnectResponse round-trip with all optional fields populated.
* ConnectResponse round-trip without optional fields.
* ConnectResponse decoder surfaces MissingField when
  ServicePublicKey is absent.
* End-to-end client::connect handshake via `tokio::io::duplex`
  peer that synthesises a ConnectResponse using bob's public key
  (so DH shared-secret derivation actually works) and drains the
  AuthenticateMe one-way SizedEnvelope.

Wire-byte caveat documented inline: WCF XML serialization may add
`xsi:type` attributes / distinct namespaces around <PublicKey> /
<AuthenticationData>; this builder ships the simplest plausible
shape and the live-probe iteration will reconcile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 11:47:35 -04:00
Joseph Doherty 9b8133f725 [M5] mxaccess-asb: F25 step 5 — KeepAlive + Read + one-way client ops
Extends AsbClient with one-way operation support (`IsOneWay = true`
in IASBIDataV2) plus the KeepAlive and Read operations.

Client API additions:
* `send_envelope_one_way(env)` — frames in SizedEnvelope, writes,
  returns immediately. No response read. Mirrors WCF's IsOneWay
  semantics for KeepAlive / Disconnect / AuthenticateMe.
* `send_signed_envelope_one_way(action, body, force_hmac)` —
  one-way variant that runs the body through F23's authenticator
  signing path so the ConnectionValidator header is attached.
* `keep_alive()` — sends an empty `KeepAliveRequest` with default
  signing. Used to keep the channel alive past the WCF inactivity
  timeout (30s default at `MxAsbDataClient.cs:683`).
* `read(items)` — sends a signed Read envelope, decodes
  ReadResponse with both Status and Values arrays.

Operations API additions:
* `build_keep_alive_request_body()` — empty wrapper element +
  asb.contracts.messages namespace. Mirror of `AsbContracts.cs:117`
  (`public sealed class KeepAlive : ConnectedRequest;`).
* `ReadResponse { status: Vec<ItemStatus>, values: Vec<RuntimeValue> }`
  per `AsbContracts.cs:169-179`.
* `decode_read_response(body_tokens)` — pulls both ASBIData
  payloads, decodes Status as ItemStatus[], decodes Values via
  `decode_runtime_value_array` (4-byte int32 count + per-element
  `RuntimeValue::decode` from F24).

5 new tests:
* KeepAlive body shape (empty wrapper, correct namespace).
* ReadResponse decoder round-trip with both Status and Values.
* ReadResponse decoder graceful handling when Values is absent
  (returns empty vec).
* End-to-end client::keep_alive — peer drains SizedEnvelope but
  doesn't respond; client returns Ok().
* End-to-end client::read — peer responds with synthetic
  ReadResponse, client recovers Values[0].timestamp_binary == 1234
  and Values[0].status round-trip.

Stubbed for next F25 iterations:
* AsbClient::connect — DH Connect + AuthenticateMe handshake. Needs
  ConnectRequest / ConnectResponse builders (regular WCF XML, not
  the IAsbCustomSerializableType fast-path).
* Write / PublishWriteComplete / CreateSubscription /
  AddMonitoredItems / Publish / Disconnect operation wrappers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 11:42:39 -04:00
Joseph Doherty 1e59249662 [M5] mxaccess-asb: F25 step 4 — AsbClient async network loop
The first slice of F25 that actually moves bytes across a transport.
Wraps every M5 framing layer (F19-F25.3) into a single async client
generic over `AsyncRead + AsyncWrite + Unpin + Send`. Tested in-memory
via `tokio::io::duplex` — no live ASB endpoint required.

API:
* `AsbClient::new(stream, authenticator, via_uri)` — wraps a Tokio
  transport + F23 authenticator into a ready client.
* `send_preamble()` — writes the canonical preamble (Version 1.0 →
  Duplex → Via → BinaryWithDictionary → PreambleEnd) and reads the
  peer's PreambleAck. Surfaces Fault as `ClientError::Fault(msg)`.
* `send_envelope(env)` — frames `SoapEnvelope` in a SizedEnvelope NMF
  record, writes, reads the response SizedEnvelope, decodes back to
  `DecodedEnvelope`.
* `send_signed_envelope(action, body, force_hmac)` — calls F23
  authenticator's `sign` on the unsigned body bytes, attaches a
  ConnectionValidator header (base64'd MAC + IV), sends.
* `register_items` / `unregister_items` — thin per-operation wrappers
  threading body builder + response decoder.
* `send_end()` — writes record 0x07 + shutdowns the stream.

Async record reader: streaming decode of the multibyte-int31 length
prefix for SizedEnvelope (0x06) / Fault (0x08), plus a fallback path
for Version / Mode / KnownEncoding / etc.

`ClientError` covers I/O, NMF, NBFX, Envelope, Operation, Auth, plus
PreambleNotSent / AlreadyClosed / Fault / PeerClosed /
UnexpectedRecord guards.

6 new tests via in-memory `tokio::io::duplex`:
* Preamble round-trip with synthetic peer returning PreambleAck.
* Fault propagation through preamble exchange.
* End-to-end RegisterItems request → response with a peer that
  drains preamble, replies PreambleAck, drains the SizedEnvelope,
  responds with a synthetic RegisterItemsResponse body containing a
  binary-encoded ItemStatus array. Client decodes and asserts the
  recovered ItemIdentity name.
* `send_envelope` before preamble fails with PreambleNotSent.
* `send_end` writes record 0x07 to the wire.
* PreambleMode re-export keeps shape parity with `nmf::NmfMode`.

Known limitation: the signing path currently hashes the NBFX-encoded
body; .NET hashes the XML-text `request.ToXml()`. Functionally
present (validator built and attached) but MAC bytes won't match
.NET's MAC for the same payload until the live-probe iteration
reconciles which canonical form to sign.

Stubbed for next F25 iteration:
* `AsbClient::connect` — DH `Connect` + `AuthenticateMe` handshake
  flow. Needs ConnectRequest/Response builders (regular WCF XML, not
  the IAsbCustomSerializableType fast-path) and the
  `AsbAuthenticator::create_authentication_data` integration.
* Read / Write / Subscription operation wrappers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 11:37:48 -04:00
Joseph Doherty c4bf0a0a04 [M5] mxaccess-asb: F25 step 3 — response decoders + Read request body
Foundation for response decoding. Adds:

* `contracts::ItemStatus` — ports `AsbContracts.cs:639-722`. Wire
  layout matches `WriteToStream` exactly: Item (ItemIdentity binary)
  → Status (AsbStatus binary, from F24) → ErrorCode (u16) →
  ErrorCodeSpecified (u8 bool). Note this is NOT the DataMember
  declaration order — the binary serialiser hand-picks Item-first.

* `encode_item_status_array` / `decode_item_status_array` — same
  4-byte int32 count + per-element WriteToStream pattern as the
  ItemIdentity array codec.

* `operations::collect_asbidata_payloads(tokens, field_name)` — walks
  an NBFX token stream and pulls out `<{field}><ASBIData>{Bytes}
  </ASBIData></{field}>` payload bytes. Returns Vec<Vec<u8>> because
  some response shapes (ReadResponse) carry multiple ASBIData
  payloads (Status + Values).

* `decode_register_items_response` / `decode_unregister_items_response`
  — parse SOAP body NBFX tokens into typed RegisterItemsResponse /
  UnregisterItemsResponse. The optional ItemCapabilities array (XML-
  serialised, not binary) is recorded as a presence flag for now;
  decoding the individual ItemRegistration records is a follow-up.

* `build_read_request_body(items)` — simplest unary IASBIDataV2
  request, just `<ReadRequest xmlns="..."><Items><ASBIData>...
  </ASBIData></Items></ReadRequest>`.

* `OperationError` — typed error for response-decode failures
  (`MissingField { field }` and codec wraps).

9 new tests: ItemStatus round-trip (default + with id + with status
payload), ItemStatus array round-trip, RegisterItemsResponse
round-trip via synthetic body, ItemCapabilities presence detection,
UnregisterItemsResponse round-trip, multi-payload extraction (ReadResponse-
shape Status + Values), Read body shape correctness, MissingField
error when Status is absent.

Stubbed for next F25 iteration: Write / PublishWriteComplete /
CreateSubscription / AddMonitoredItems / DeleteMonitoredItems /
Publish builders, ReadResponse + WriteResponse decoders (need
WriteValue / RuntimeValue contract codecs), and the AsbClient
network loop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 11:32:36 -04:00
Joseph Doherty a2b8989cbf [M5] mxaccess-asb: F25 step 2 — per-operation request body codecs
Adds the IAsbCustomSerializableType binary fast-path + per-operation
request-body NBFX-token builders. RegisterItems and UnregisterItems
now compose end-to-end through SoapEnvelope + encode_envelope to a
byte stream that round-trips back to the original ItemIdentity array.

Three pieces:

1. F21 NBFX gains `Bytes8/16/32` text records (records 0x9E/0xA0/0xA2
   plus +1 WithEndElement variants). WCF's `XmlDictionaryWriter.
   WriteBase64` emits these in binary form — not actual base64 text —
   so they're required for the `<ASBIData>` content.

2. `mxaccess-asb::contracts::ItemIdentity` ports `AsbContracts.cs:533-633`:
   * Wire layout: u16 kind + u16 reference_type +
     AsbBinary.WriteUnicodeString(Name) + AsbBinary.WriteUnicodeString
     (ContextName) + u64 Id + u8 IdSpecified.
   * `AsbBinary.WriteUnicodeString` per cs:1622-1633: u32 byte-length
     + UTF-16LE bytes; null/empty collapse to a 4-byte zero header.
   * `encode_item_identity_array` / `decode_item_identity_array`
     mirror `WriteArrayToStream` — 4-byte int32 count + each
     element's `WriteToStream` output. Per `AsbDataCustomSerializer`
     at cs:1583-1591.
   * `absolute_by_name(...)` convenience constructor matching
     `MxAsbDataClient.CreateAbsoluteItem` at cs:172-194.

3. `mxaccess-asb::operations` builds SOAP body NBFX token streams:
   * `build_register_items_request_body(items, require_id, register_only)`
     — RegisterItems contract per cs:119-143.
   * `build_unregister_items_request_body(items)` — UnregisterItems
     per cs:145-159.
   * Internal `BodyField` helper assembles the wire shape:
     `<RegisterItemsRequest xmlns="urn:msg.data.asb.iom:2">
        <Items><ASBIData>{Bytes(payload)}</ASBIData></Items>
        <RequireId>true|false</RequireId>
        <RegisterOnly>true|false</RegisterOnly>
      </RegisterItemsRequest>`

15 new tests cover:
* ItemIdentity round-trip (default, with id, unicode name).
* AsbBinary unicode-string null/empty/value semantics.
* Byte-layout pinning (21 bytes for default ItemIdentity, le-int32
  array count).
* ItemIdentity array round-trip.
* `<ASBIData>` Bytes record round-trip across NBFX widths
  (Bytes8/16/32 selected by length).
* RegisterItems body → SoapEnvelope → encode → decode → recover the
  ItemIdentity array end-to-end.
* RequireId / RegisterOnly Bool wire form.
* UnregisterItems body uses correct outer element name and omits
  the RegisterItems-only fields.

Stubbed for next F25 iteration: per-operation Read / Write /
PublishWriteComplete / CreateSubscription / AddMonitoredItems /
DeleteMonitoredItems / Publish builders, response decoders, and the
`AsbClient` network loop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 11:24:19 -04:00
Joseph Doherty 25dbd8d3bd [M5] mxaccess-asb: F25 step 1 — SOAP envelope codec
First slice of F25. Provides the building blocks the per-operation
request/response codecs and the network loop will compose:

* `actions` module — IASBIDataV2 action strings (all 14 operations,
  verbatim from `AsbContracts.cs:14-58`).
* `ConnectionValidator` — SOAP header struct mirroring
  `AsbContracts.cs:65-117`. `from_signed(&SignedValidator)` converts
  F23's MAC + IV to base64 for the wire, matching .NET's
  `BinaryWriter`-via-`XmlSerializer` shape.
* `SoapEnvelope` + `encode_envelope` — assembles the NBFX token
  stream: `s:Envelope` → `s:Header` → `a:Action s:mustUnderstand="1"`
  → optional `h:ConnectionValidator` → `s:Body` → caller-supplied
  body tokens. Uses static-dictionary IDs for the SOAP/WS-Addressing
  tokens via F22's `lookup_static`.
* `decode_envelope` — pulls action + validator + body tokens back
  out of received bytes. Tolerant of header ordering.
* Mixed-endian GUID format/parse (`format_uuid` / `parse_uuid`) that
  mirrors .NET's `Guid.ToString("D")` byte order so connection-id
  round-trip matches the wire exactly.

9 new unit tests cover:
* Round-trip with and without validator.
* `from_signed` base64 encoding of MAC + IV.
* `format_uuid` produces the correct .NET-mixed-endian hex string.
* GUID round-trip through string formatter.
* Action string presence in the encoded byte stream.
* Decoder tolerance of envelopes without an Action header.
* Validator round-trip through full encode → decode.
* Lint-style guard that all 14 action constants are URIs ending `In`.

Stubbed for next F25 iteration: per-operation request/response
struct codecs (`ConnectRequest`, `RegisterItemsRequest`, etc.) +
`AsbClient` network loop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 11:16:22 -04:00
Joseph Doherty 5f985588f7 [M5] mxaccess-asb-nettcp: F21 [MC-NBFX] binary XML token codec
Ports the proven subset of `[MC-NBFX]` to `mxaccess-asb-nettcp::nbfx`.

Token model: Element { prefix, name } / EndElement / Attribute /
DefaultNamespace / NamespaceDeclaration / Text. Element + attribute
names can be inline UTF-8, an `[MC-NBFS]` static-dictionary id (via
F22's `lookup_static`), or a per-session `DynamicDictionary` id.

Text records covered: Empty (0xA8), Zero (0x80), One (0x82), Bool
(0x84/0x86 + 0xB4), Int8 (0x88), Int16 (0x8A), Int32 (0x8C), Int64
(0x8E), Chars (0x98/0x9A/0x9C — width variant chosen automatically by
payload length), DictionaryText (0xAA — both static and dynamic refs).

`*WithEndElement` collapse is automatic: a `Text → EndElement` pair
encodes as the `+1` record byte (e.g. `EmptyTextWithEndElement = 0xA9`,
`TrueTextWithEndElement = 0x87`). The decoder splits the implicit
EndElement back out so consumers see the same token stream regardless
of which wire form was used.

Element variants covered: ShortElement (0x40), Element (0x41 with
prefix string), ShortDictionaryElement (0x42), DictionaryElement
(0x43). Prefix-letter family (0x44-0x77) deferred — emit the long
form for now.

Attribute variants covered: ShortAttribute (0x04), Attribute (0x05),
ShortDictionaryAttribute (0x06), DictionaryAttribute (0x07), plus
xmlns variants (0x08/0x09).

15 new unit tests cover the dynamic dictionary, every supported
element/attribute/xmlns/text record form (including round-trip),
explicit byte pinning for the collapse behavior, Chars width-variant
selection, unknown-record rejection, and truncated-payload rejection.

Records left for follow-up: Decimal, UniqueId, TimeSpan, Float/Double
text, DateTime text, Bytes8/16/32, QNameDictionary, the 0x0C-0x25
prefix-dict-attribute / 0x26-0x3F prefix-attribute / 0x44-0x77
prefix-element families. None of these are on the proven ASB path.

With F21 landed, the M5 framing + encoder layer (streams A+B+C+D and
the F24 codec) is complete. F25 (mxaccess-asb IASBIDataV2 client) and
F26 (Session over AsbTransport) remain.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 11:10:50 -04:00
Joseph Doherty 43c10a15ca [M5] mxaccess-asb-nettcp: F22 [MC-NBFS] static dictionary subset
Ports the curated subset of the `[MC-NBFS]` §2.2 static dictionary to
`mxaccess-asb-nettcp::nbfs`. Approximately 80 entries covering SOAP 1.2
envelope tokens, WS-Addressing 1.0 tokens, WS-RM, WS-Security,
WS-Trust/SecureConversation, XML Schema Instance primitives, plus the
common XML element / attribute names captured in
`analysis/proxy/mxasbclient-*` traces.

API:
* `STATIC_ENTRIES: &[StaticEntry]` — sorted-by-id table; one-line
  extension when wire captures show new IDs.
* `lookup_static(id) -> Option<&'static str>` — binary-search lookup
  for the F21 NBFX decoder.
* `position_of_static(value) -> Option<u32>` — `OnceLock`-cached
  reverse lookup for the F21 NBFX encoder.

Lookups outside the curated subset return `None`. The NBFX decoder
will surface that as a typed `UnknownStaticDictionaryId` error so the
caller knows to either extend the table or fall through to the
inline-string path. The full 487-entry table is bounded but tedious;
the deliberate subset keeps source size down while remaining
extensible.

ASB-specific contract strings (`http://ASB.IDataV2`,
`http://asb.contracts/20111111`, the IASBIDataV2 operation actions,
etc.) are intentionally **not** in the static dictionary — they live
in the per-session dynamic dictionary that the F21 NBFX codec builds
up via `DictionaryString` records.

6 unit tests cover monotonic-id invariant, known-id lookup,
unknown-id rejection, round-trip lookup consistency, and the
empty-string slot at id=142.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 11:06:11 -04:00
Joseph Doherty 9dfd1937c2 [M5] mxaccess-asb-nettcp: F20 [MS-NMF] .NET Message Framing record codec
Implements the 13 record types from `[MS-NMF]` §2.2 (Version, Mode, Via,
KnownEncoding, ExtensibleEncoding, Unsized/SizedEnvelope, End, Fault,
UpgradeRequest/Response, PreambleAck, PreambleEnd) over a `net.tcp` channel.

Includes the `Multibyte Int31` length codec (LEB128-style 7-bit groups
over a 31-bit unsigned range, max 5 bytes; rejects negative input and
overflow), plus an `encode_preamble` helper that emits the canonical ASB
connect record sequence (`Version 1.0 → Duplex → Via $uri →
BinaryWithDictionary → PreambleEnd`).

Pure codec — no I/O. Encoders write into a `Vec<u8>`; decoders parse
from a `&[u8]` slice and return the consumed-byte count alongside the
record. Higher-level connect/request/response orchestration stays in the
M5 ASB client (`mxaccess-asb`, F25).

24 new unit tests cover round-trip for every record type, multibyte-int31
boundary cases (0, 1, 127, 128, 16383, 16384, 200, i32::MAX), preamble
emission against the canonical ASB sequence, byte-layout pinning for
Version/Mode/KnownEncoding, and rejection of unknown record/mode/encoding
bytes plus truncated sized-envelope frames.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 11:01:24 -04:00
Joseph Doherty 7611d9e215 [M5] mxaccess-codec: F24 ASB Variant + AsbStatus + RuntimeValue codec
Ports `Variant` (cs:1170-1241), `AsbStatus` (cs:1109-1167), `RuntimeValue`
(cs:741-791), `AsbVariantFactory.From*` (cs:1310-1429), and
`MxAsbDataClient.DecodeVariant` (cs:713-825) into `mxaccess-codec::asb_variant`.

Three layers per `docs/ASB-Variant-Wire-Format.md`:
1. `AsbVariant` — raw 2/4/4/payload header + bytes; round-trips byte-identical.
2. `DecodedVariant` — typed view with one variant per proven ASB scalar / array
   (`Bool`, `Int32`, `Float`, `Double`, `String`, `DateTime`, `Duration` plus
   array forms). Type ids outside the proven matrix surface as
   `Unsupported { type_id, payload }` — same fallback as .NET's `_ => payload`.
3. `from_*` factories — mirror `AsbVariantFactory.FromX` exactly, setting
   `length` to `payload.len()` per `cs:1431-1438`.

`AsbStatus` and `RuntimeValue` round-trip the wire layout verbatim.
Status-element walking (marker bit 7 = implicit zero, etc., per
`docs/ASB-Variant-Wire-Format.md:180-205`) is deferred to a follow-up; the
codec exposes the raw status payload bytes for now, matching .NET's
`AsbStatus.Payload = byte[]` shape.

The lib.rs `AsbVariant` / `AsbStatus` / `RuntimeValue` stubs are replaced by
the real types via `pub use`. 25 new unit tests cover the proven matrix:
scalar + array round-trip, byte layout (2/4/4/payload), `Unsupported`
fallback for declared-but-unproven types, short-frame rejection,
malformed `string[]` partial-decode preservation matching .NET behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 10:47:11 -04:00
Joseph Doherty ed17c07c10 [M5] mxaccess-asb-nettcp: M5 plan + F19 deps + F23 auth crypto port
F18 plans M5 as 9 sub-followups (F18-F26 + F27 constant-time DH) per
design/dependencies.md:73-89. Wave-1 streams F20-F23+F24 are parallel-safe
after F19 (workspace deps). F25 (ASB client) is sequential after the
framing/encoder streams. F26 (Session over AsbTransport) is sequential
after F25.

F19 — workspace deps for the M5 crypto + framing surface: hmac, md-5,
sha1, sha2, aes, cbc, pbkdf2, flate2, rand, num-bigint, num-traits,
num-integer, quick-xml, tokio-util, zeroize. Pinned to the digest 0.10 /
cipher 0.4 generation matching mxaccess-rpc.

F23 — ports `AsbSystemAuthenticator.cs` (167 LoC) to
`mxaccess-asb-nettcp::auth`. Wire-byte parity points: .NET BigInteger
little-endian two's-complement byte order with optional 0x00 sign-byte
suffix; AES-128-CBC with PKCS7 padding; PBKDF2-SHA1 1000 iterations
over `Convert.ToBase64String(crypto_key)` with ASCII salt
"ArchestrAService"; deflate-then-AES (Baktun) vs raw-AES (Apollo)
selected by `:V2` lifetime suffix; HMAC-MD5/SHA1/SHA512 negotiated per
`AsbSolutionCryptoParameters.HashAlgorithm` (with `force_hmac=true`
fallback to HMAC-SHA1 for unrecognised algorithms).

13 unit tests cover the cryptographic primitives + DH peer agreement +
.NET byte-order round-trip + Apollo lifetime dispatch.

F27 — filed for the `num-bigint` → `crypto-bigint::BoxedUint` swap once
the latter exposes a stable heap-allocated `pow_mod`. Currently at
parity with the .NET reference (also not constant-time).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 10:36:15 -04:00
Joseph Doherty a5d31cc2e1 [M4] mxaccess: wire MxValue overloads + shutdown(timeout) shim
rust / build / test / clippy / fmt (push) Has been cancelled
Replaces the lib.rs `Unsupported`-stub Session methods with real
implementations where the underlying primitives already exist in
session.rs, sharpens docstrings on the still-deferred ones, and
refreshes the stale "M0 stub" module preamble.

Wired (now functional):
- `Session::write(MxValue)` — converts via `mxvalue_to_writevalue` then
  delegates to `write_value`.
- `Session::write_with_timestamp(MxValue, SystemTime)` — same plus
  `system_time_to_filetime` then `write_value_at`.
- `Session::write_secured_at(MxValue, SystemTime, SecurityContext)` —
  same plus `write_value_secured_at`.
- `Session::shutdown(timeout)` — `tokio::time::timeout` wrapper around
  `shutdown_nmx`; on elapse returns `Error::Timeout` (the in-flight
  unregister is cancelled, mirroring the .NET `IDisposable` semantics
  at `MxNativeSession.cs:481`).

Still `Unsupported` (gating reasons documented in each docstring):
- `Session::connect` — needs F12 auto-resolve (gated on F6 windows-rs).
- `Session::write_with_completion` — needs per-token registry, gated
  on R15 long-lived task.
- `Session::write_secured` (no timestamp) — `NmxClient` only ports
  `WriteSecured2` (LMX 0x3A), not the unversioned `WriteSecured` (0x39).
- `Session::subscribe_many` — no atomic frame on the wire; canonical
  pattern is `examples/multi-tag.rs`.
- `Session::subscribe_buffered` — M6 `SetBufferedUpdateInterval` RPC.

`mxvalue_to_writevalue` consumes the `MxValue` and returns
`Error::Configuration(InvalidArgument)` for the three variants whose
re-encode is policy-dependent: `DateTime` / `ElapsedTime` /
`DateTimeArray`. The `non_exhaustive` MxValue catch-all preserves
forward compat.

Test count delta: 532 → 542 (+10; conversion happy paths for Boolean /
Int32 / Float64 / String / Int32Array / BoolArray / StringArray, plus
the three rejected variant errors).
Open followups touched: none resolved (F12, F16 still gating).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 10:29:54 -04:00
Joseph Doherty 48d3a9d6da [M2/M4] mxaccess-rpc: Guid::parse_str + dedupe examples (resolves F17)
Adds `Guid::parse_str(&str) -> Result<Guid, RpcError>` to
`crates/mxaccess-rpc/src/guid.rs` as the inverse of the existing
`Display` impl. Accepts the canonical dashed-hex form, optionally
braced (.NET `B` format), case-insensitive, and tolerant of bare
32-char hex without dashes. Single-pass char-by-char nibble accumulator
avoids per-byte string allocation; applies the same byte-swap of
groups 1-3 that the `Display` impl reads.

Eight new tests cover round-trip against the existing `Display`
fixture (`crates/mxaccess-rpc/src/guid.rs:111-119`,
`b49f92f7-c748-4169-8eca-a0670b012746`), braces, uppercase, no-dashes,
zero-GUID, too-short, too-long, and non-hex rejection.

The five live-NMX examples (`connect-write-read`, `subscribe`,
`recovery`, `multi-tag`, `secured-write`) lose their per-file 15-line
`parse_guid` helpers in favour of the canonical implementation.
`asb-subscribe` and `subscribe-buffered` are unaffected — they don't
parse GUIDs.

Test count delta: 524 → 532 (+8)
Open followups touched: F17 resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 10:18:21 -04:00
Joseph Doherty af939730b1 [M4] mxaccess: examples wave 3 — 7 example programs (M4 wave 3)
Replaces the M0 stub bodies in `crates/mxaccess/examples/` with real
consumer-facing demos against the M4 NMX `Session` surface. Each example
gates on `MX_LIVE` and prints a friendly bypass message when the live
env vars aren't populated, so `cargo build --workspace --all-targets`
stays green in CI without an AVEVA install.

Five examples target the proven NMX path (build + connect + demo +
shutdown):
- `connect-write-read` — `Session::write_value` + `read` round-trip; the
  30-line consumer-experience target from `design/60-roadmap.md` M4 DoD.
- `subscribe` — single-tag `Subscription` stream; drains 5 updates or
  10s timeout, then `unsubscribe` cleanly.
- `recovery` — `RecoveryPolicy { max_attempts: 3, delay: 250ms }`
  + spawned `recovery_events()` listener consuming the broadcast.
- `multi-tag` — per-tag `subscribe` loop merged via
  `futures_util::stream::select_all`; matches the .NET cs:250-270 shape
  (no atomic subscribe-many RPC on the wire).
- `secured-write` — `write_value_secured_at` exercising both single-user
  (`current_user_id == verifier_user_id`) and two-person paths per
  `wwtools/mxaccesscli/src/MxAccess.Cli/Commands/WriteCommand.cs:151-155,196-199`.

Two examples hold the place for downstream milestones:
- `subscribe-buffered` — pattern-matches on `Error::Unsupported` from
  `Session::subscribe_buffered` (M6) and exits 0 with an explanation.
- `asb-subscribe` — same shape against `Session::connect` (M5 ASB).

All five live examples share an inline `LiveEnv::from_process` helper,
a dashed-hex `parse_guid`, and a `StaticResolver` that returns canned
metadata for the configured `MX_TEST_TAG`. The duplication is
intentional — Cargo examples are meant to be self-contained and read
top-to-bottom; consumers swap `StaticResolver` for a tiberius-backed
Galaxy resolver (followup F14) without touching any other example.

Test count delta: 524 → 524 (+0; examples are demos, not tests)
Open followups touched: F17 logged (Guid::parse_str helper to dedupe
the per-example dashed-hex parser).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 10:14:15 -04:00
Joseph Doherty 33edc91234 rustfmt: collapse short multi-line expressions in ntlm tests
rust / build / test / clippy / fmt (push) Has been cancelled
Pure whitespace cleanup from running `cargo fmt --all` between
iterations; no semantic change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 10:01:45 -04:00
Joseph Doherty 4863c6dc1f [M4] mxaccess: Session::recover_connection + RecoveryEvent broadcast
Wires the recovery API surface and event channel. Recovery is
currently a no-op (validates policy + emits Started/Recovered
events); the real teardown + re-bind + re-advise loop is wave-3
work tracked as F16.

New
- Session::recover_connection(policy) — port of
  MxNativeSession.RecoverConnectionAsync (cs:399-440). Validates
  policy.max_attempts >= 1 (mirrors cs:33-36 via
  RecoveryPolicy::validate). Emits RecoveryEvent::Started + Recovered
  through the broadcast channel. Returns Ok(()) immediately — actual
  reconnect work is F16.
- Session::recovery_events() -> broadcast::Receiver<Arc<RecoveryEvent>>
  — typed observable for consumers that want to wire monitoring or
  state-machine handling. Same Arc-broadcast pattern as
  Session::callbacks(). Multi-subscriber safe (Arc::ptr_eq verified
  in tests).
- SessionInner.recovery_tx: broadcast::Sender<Arc<RecoveryEvent>>
  initialized in connect_nmx + connect_test_session.

Removed lib.rs stub (was Err(Unsupported)).

design/followups.md: F16 added (P1) covering the actual reconnect
loop. Resolves when R15's long-lived connection task lands and
SessionInner gains a subscription registry — at that point the
recover loop becomes ~50 lines slotting RecoverConnectionCore-style
work between the Started and Recovered events.

Tests (4 new in mxaccess; total 48)
- recover_connection emits Started + Recovered for the default
  single-attempt policy.
- recover_connection rejects max_attempts == 0 with InvalidArgument.
- recover_connection after shutdown returns EngineNotRegistered.
- recovery_events supports multiple subscribers (Arc::ptr_eq
  verifies the same allocation reaches both).

Test count delta: 520 -> 524 (+4). All four DoD gates green.
Open followups: 9 -> 10 (added F16).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 09:59:25 -04:00
Joseph Doherty 2dc091d0be [M4] mxaccess: Session::read (read-as-subscribe pattern)
Now that Subscription impls Stream<Item = Result<DataChange, Error>>,
the read-as-subscribe pattern is a thin wrapper over subscribe +
timeout + best-effort unsubscribe.

New
- Session::read(reference, timeout) -> Result<DataChange, Error> —
  port of MxNativeSession.ReadAsync (cs:312-359). Validates timeout
  > 0, subscribes, awaits the first DataChange under
  tokio::time::timeout, then issues UnAdvise (best-effort, mirrors
  the .NET finally block at cs:351-358 which discards the
  unsubscribe return).

Error mapping
- timeout=0: Configuration::InvalidArgument ("Read timeout must be
  positive") matching ArgumentOutOfRangeException at cs:318-321.
- timeout elapsed: Error::Timeout(timeout).
- subscribe failure (resolver / transport): propagated unchanged.
- stream ends before any value: Connection::EngineNotRegistered
  (broadcast sender dropped during shutdown).
- unsubscribe failure: tracing::warn! with the error; doesn't
  override the read result.

Removed the placeholder stub in lib.rs that returned
Error::Unsupported.

Tests (4 new in mxaccess; total 44)
- read_returns_first_data_change_within_timeout: spawn read,
  inject a 0x33 DataUpdate via test_inject_sender (which fans out
  to all subscriptions), assert the DataChange comes back with the
  right value.
- read_returns_timeout_when_no_data_arrives: read times out cleanly
  with Error::Timeout when no callback fires.
- read_zero_timeout_returns_invalid_argument_without_subscribing:
  validates the early-reject path before any RPC is issued.
- read_propagates_resolver_not_found: subscribe-side error
  surfaces through read unchanged.

Test count delta: 516 -> 520 (+4). All four DoD gates green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 09:52:14 -04:00
Joseph Doherty a31237d1d0 [M4] mxaccess: Subscription impls Stream<Item = DataChange> (resolves F15)
F15 step 2/2 lands the per-Subscription routing on top of step 1's
broadcast layer. Subscription is now a working data-change stream.

Subscription type
- Now impls futures_util::Stream<Item = Result<DataChange, Error>>
  via tokio_stream::wrappers::BroadcastStream + a per-message filter.
- No longer Clone (broadcast::Receiver isn't Clone). Consumers that
  want fanout subscribe twice or share via Arc<Mutex<...>>.
- Holds the broadcast::Receiver subscribed BEFORE AdviseSupervisory
  fires — guarantees no updates between advise and stream-creation
  are dropped.
- pending VecDeque buffers records from the current message so each
  poll_next yields at most one DataChange (Stream contract).

Filter logic (records_to_data_changes, mirrors cs:333-343)
- 0x32 SubscriptionStatus: keep when
  msg.item_correlation_id == subscription.correlation_id; drop
  otherwise.
- 0x33 DataUpdate: keep ALL — codec exposes no per-record correlation
  field, and the .NET filter only checks item_correlation_id (which
  0x33 doesn't carry), so DataUpdates fan out to every active
  subscription. Matches .NET behavior verbatim.
- Records with value: None drop silently (mirrors evt.Record.Value
  is null filter at cs:337).
- BroadcastStream Lagged(n) maps to Error::Configuration with the
  lag count in the detail string.

Helpers
- filetime_to_system_time(i64) -> SystemTime: inverse of
  system_time_to_filetime; saturates at Unix epoch for FILETIMEs
  before 1970 since SystemTime can't portably represent pre-epoch.
- record_to_data_change(record, reference) -> Option<DataChange>:
  builds DataChange from one record, returns None for unparseable
  value (the codec couldn't decode the wire kind).
- Status currently hardcoded to MxStatus::DATA_CHANGE_OK (mirrors
  NmxSubscriptionRecord.ToDataChangeStatus at NmxSubscriptionMessage.cs:22-25
  which the .NET reference itself stubs to the OK constant).

Cargo.toml additions: futures-util (workspace) + tokio-stream (0.1
with sync feature for BroadcastStream).

Tests (5 new in mxaccess; total 40)
- subscription_stream_yields_data_change_for_matching_correlation:
  build a 0x32 SubscriptionStatus with one Int32 record and the
  subscription's correlation id, inject through test_inject_sender,
  observe the DataChange (reference, value, quality match) on the
  Stream.
- subscription_stream_filters_out_mismatched_correlation_for_status:
  inject 0x32 with wrong correlation id, assert the stream stays
  pending (timeout-as-success).
- subscription_stream_keeps_data_update_regardless_of_correlation:
  inject 0x33 DataUpdate with one Int32 record (no correlation
  field on the message); stream still yields the DataChange.
- filetime_to_system_time_round_trip: build a SystemTime with .005s
  precision, round-trip through both helpers, assert equality.
- filetime_to_system_time_pre_unix_epoch_saturates: FILETIME 0 (year
  1601) → SystemTime::UNIX_EPOCH (saturating clamp).

design/followups.md: F15 moved to Resolved with both step commits
referenced. Open list: 9 items (was 10).

Test count delta: 511 -> 516 (+5). All four DoD gates green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 09:45:16 -04:00
Joseph Doherty 2b849aed7a [M4] mxaccess: wire CallbackExporter + spawn callback router (F15 step 1/2)
Lands the broadcast layer of F15. Session::connect_nmx now starts a
local CallbackExporter on an OS-assigned ephemeral port, builds a
callback OBJREF advertising it (using local_hostname() with a
127.0.0.1 fallback), and registers that OBJREF with NmxClient::register_engine_2
(was register_engine_2_without_callback). A router task drains the
exporter's CallbackEvent stream, decodes each CallbackInvoked body as
NmxSubscriptionMessage, and broadcasts parsed messages on a
tokio::sync::broadcast channel.

Per-subscription correlation routing — turning these raw messages
into per-Subscription DataChange streams — is the next iteration's
work. F15 stays open until that lands.

New Session API
- Session::callbacks() -> broadcast::Receiver<Arc<NmxSubscriptionMessage>>:
  raw observable of every parsed callback message. Test seam +
  escape hatch for consumers that need raw access today.
- Session::callback_exporter_addr() -> Option<SocketAddr>: returns the
  exporter's local addr (Some until shutdown_nmx, None after).

SessionInner additions
- callback_exporter: Mutex<Option<CallbackExporter>> — taken in shutdown.
- callback_tx: broadcast::Sender<Arc<NmxSubscriptionMessage>>.
- router_handle: std::sync::Mutex<Option<JoinHandle<()>>>.

shutdown_nmx now performs the full cleanup chain:
1. UnregisterEngine over the live NMX transport.
2. CallbackExporter::shutdown (cancels accept loop).
3. Wait for router task — exits naturally once exporter's mpsc
   sender side closes. Std::sync::Mutex guard taken-out-then-dropped
   before await to avoid clippy::await_holding_lock.

Routing rationale (callback_router fn)
- CallbackEvent::CallbackInvoked → parse via
  NmxSubscriptionMessage::parse_inner → broadcast Arc<msg>.
- Other event variants (Bind / Auth3Ignored / ProtocolError / etc.)
  silently dropped at this layer; consumers needing them can listen
  to a future diagnostic-channel hook (no followup yet).
- Parse failures silent — the .NET reference fires a separate
  UnparsedCallbackReceived event we don't model yet.

Cargo.toml: added mxaccess-callback as a direct dep on mxaccess.

Tests (5 new in mxaccess; total 35)
- callbacks receiver observes injected NmxSubscriptionMessage.
- multi-subscriber broadcast hands out the same Arc to each receiver.
- callback_exporter_addr is Some before shutdown, None after.
- router_task end-to-end: feed a hand-built CallbackInvoked event
  with a 39-byte 0x32 SubscriptionStatus body, observe the parsed
  message on the broadcast.
- router silently drops non-CallbackInvoked events (e.g. Bind).

Test count delta: 506 -> 511 (+5). All four DoD gates green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 09:35:41 -04:00
Joseph Doherty f7139f1118 [M2/M4] mxaccess-rpc: NtlmClientContext::from_env + local_hostname (resolves F1)
Reduces open followups from 11 → 10 (back at the soft threshold).
Step 0 triage flagged F1 as resolvable now: M4's connect-path
example will need a from_env constructor anyway, and the hostname
lookup is portable enough not to need a native-libc dep.

New
- NtlmClientContext::from_env() -> Result<Self, NtlmError>: reads
  MX_RPC_USER / MX_RPC_PASSWORD / MX_RPC_DOMAIN env vars. Empty
  MX_RPC_DOMAIN is permitted (workgroup auth). Mirrors the .NET
  ManagedNtlmClientContext.FromEnvironment() at cs:41-49.
- local_hostname() -> String public helper: checks COMPUTERNAME
  (Windows) then HOSTNAME (POSIX) and returns the empty string when
  neither is set — same "unavailable" semantics as
  Environment.MachineName returning null. No gethostname(2) call,
  no unsafe, no native-libc dep. Callers needing reliable POSIX
  hostnames can pass workstation explicitly.
- NtlmError::MissingEnvVar { name: &'static str } variant.

Tests (8 new in ntlm; total 27)
- from_env three-var happy path
- from_env missing each of the three vars (3 tests)
- from_env accepts empty MX_RPC_DOMAIN
- local_hostname prefers COMPUTERNAME over HOSTNAME
- local_hostname falls back to HOSTNAME
- local_hostname returns empty when neither set
- All env-mutating tests serialize via a static ENV_LOCK Mutex inside
  EnvScope, since std::env::set_var touches process-global state and
  cargo runs #[test]s in parallel by default.

design/followups.md: F1 moved to Resolved.
Open followups: 11 → 10 (back at soft threshold).

Test count delta: 498 -> 506 (+8). All four DoD gates green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 09:24:26 -04:00
Joseph Doherty 70feb63ea5 [M4] mxaccess: Session::subscribe + unsubscribe + Subscription handle
Lands the subscribe-path lifecycle: AdviseSupervisory + UnAdvise
round-trip via a Subscription handle. The actual DataChange stream
routing is deferred to F15.

New
- Session::subscribe(reference) -> Result<Subscription, Error> —
  resolves the tag, generates a 16-byte correlation_id via
  rand::random(), calls NmxClient::advise_supervisory. Mirrors
  MxNativeSession.SubscribeAsync (cs:250-270) minus the publisher
  Connect dance (will land alongside F15's callback routing).
- Session::unsubscribe(subscription) -> Result<(), Error> — consumes
  the handle and calls NmxClient::un_advise. Mirrors
  MxNativeSession.Unsubscribe (cs:361-381).
- Subscription { correlation_id, reference, metadata } public type
  with accessor methods. Currently a pure lifecycle handle — no
  Stream impl yet; the Stream<Item=DataChange> shape lands when F15
  wires CallbackExporter routing.
- Removed the old subscribe stub from lib.rs (was Err(Unsupported)).

Drop hazard note
- Subscription deliberately does NOT impl Drop to fire UnAdvise. The
  spawn-from-Drop pattern is the R15 hazard tracked in
  design/70-risks-and-open-questions.md. Callers must call
  Session::unsubscribe(sub).await explicitly. F15's wave-2 long-lived
  connection task will support best-effort drop-time cleanup without
  the spawn-from-Drop hazard.

Cargo.toml: added rand (for correlation_id generation).

design/followups.md: F15 added (P1, M4 wave 2 callback router).
Open followups now at 11 — slightly over the soft 10-item threshold
but no drift (F13 just resolved last iteration). Next iteration's
Step 0 triage will check whether F15 is actionable.

Tests (4 new in mxaccess; total 30)
- subscribe_then_unsubscribe round-trip via in-memory resolver +
  hand-rolled server (2 RPCs: AdviseSupervisory + UnAdvise).
- subscribe propagates non-zero AdviseSupervisory HRESULT.
- subscribe after shutdown returns EngineNotRegistered.
- two_subscribes_produce_distinct_correlation_ids — verifies the
  rand::random() correlation id generation differentiates handles.

Test count delta: 494 -> 498 (+4). All four DoD gates green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 09:16:47 -04:00
Joseph Doherty bf95995573 [M4] mxaccess: Session::write_value_at + write_value_secured_at
Adds the timestamped + verified-write paths on top of the wave 1
write path. Plus a SystemTime → FILETIME helper so callers don't have
to do the 1970→1601 epoch arithmetic by hand.

New
- Session::write_value_at(reference, value, timestamp_filetime) —
  port of MxNativeSession.Write2Async (cs:187-209). Delegates to
  NmxClient::write2 with the same routing as write_value.
- Session::write_value_secured_at(reference, value, ts, security) —
  port of MxNativeSession.WriteSecured2Async (cs:223-248). Uses the
  session's options.engine_name as the client name (matches cs:239's
  _options.EngineName convention). Single-user secured writes pass
  current_user_id == verifier_user_id per R6 verification.
- system_time_to_filetime(SystemTime) -> Result<i64, Error>: converts
  via the canonical 11_644_473_600s offset between 1970-01-01 and
  1601-01-01. Pre-1970 values map to Configuration::InvalidArgument.

Tests (7 new in mxaccess; total 26)
- write_value_at round-trip via in-memory resolver + hand-rolled server.
- write_value_secured_at round-trip with single-user (same id twice).
- write_value_at propagates non-zero HRESULT as InvalidArgument.
- system_time_to_filetime: Unix-epoch known value
  (11_644_473_600 * 10_000_000), +1s offset, +500ms subsecond
  conversion, pre-1970 rejection.

One targeted fix: rewrote a doc comment that started a continuation
line with `+ verifier user pair` — clippy parsed `+` as a markdown
list bullet (clippy::doc_lazy_continuation).

Test count delta: 487 -> 494 (+7). All four DoD gates green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 09:08:22 -04:00
Joseph Doherty 12cb10c3a1 [M4] mxaccess: Session::connect_nmx + write_value + shutdown (wave 1 main)
First working M4 wave 1 slice. Adds session.rs with the connect /
write / shutdown path on top of NmxClient + Resolver, plus a tokio
test that exercises a full round-trip against a hand-rolled server.
Read, subscribe, recovery, and the long-lived connection task land
in wave 2.

Architecture
- Session holds Arc<SessionInner>; SessionInner wraps NmxClient
  behind a tokio::sync::Mutex. All RPC ops serialize on that mutex.
  Wave 2 will replace it with an mpsc::channel<Op> + dispatcher task
  per design/70-risks-and-open-questions.md R15 (drop-time async
  cleanup hazards).
- ensure_connected gate stops post-shutdown ops with
  Connection::EngineNotRegistered. Shutdown is idempotent via
  AtomicBool::swap.
- Manual Debug impl on SessionInner — neither dyn Resolver nor
  NmxClient impl Debug.

Public API
- Session::connect_nmx(addr, options, ntlm, service_ipid, resolver,
  recovery): validates the policy, opens NmxClient, runs
  RegisterEngine2 (no callback yet — wave 2), optionally configures
  heartbeat. Returns Error::Connection on non-zero HRESULT.
- Session::write_value(reference, value: WriteValue): resolves the
  tag through the configured Resolver, dispatches NmxClient::write.
- Session::resolve_write_kind / resolve_tag: convenience accessors.
- Session::shutdown_nmx: calls UnregisterEngine, idempotent.

Error mapping
- map_nmx / map_transport / map_resolver bridge the inner crate
  errors into the public Error enum. NonZeroHresult → InvalidArgument
  with the hex code; transport Fault → Status-shaped error;
  ResolverError::NotFound → Galaxy { reason: "tag not found: ..." }.
- All three matchers handle their #[non_exhaustive] sources with a
  generic catch-all so future variants don't silently break the map.

Tests (8 new in mxaccess; total mxaccess: 19)
- write_value round-trip via in-memory StaticResolver + hand-rolled
  unauthenticated DCE/RPC server.
- write_value propagates resolver not-found → Galaxy error.
- write_value propagates non-zero HRESULT → InvalidArgument.
- shutdown is idempotent (second call is a no-op).
- write after shutdown returns EngineNotRegistered.
- resolve_tag and resolve_write_kind work without RPC.
- envelope-kind constants used by Session match codec exports
  (sanity guard against codec rename).

mxaccess-nmx: WriteValue now re-exported at crate root.
mxaccess: deps gained mxaccess-nmx/galaxy/rpc + tokio + tracing,
plus async-trait as a dev-dep for the test resolver impl.

Test count delta: 479 -> 487 (+8). All four DoD gates green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 09:01:44 -04:00
Joseph Doherty 5cbc330f82 [M4] mxaccess: RecoveryPolicy fields + SessionOptions config
M4 wave 1 prep — the design-pivotal small types per dependencies.md
("(b) is small but design-pivotal — agree the event shape before
consumers depend on it"). The actual Session implementation lands
next iteration as wave 1 main (the .NET MxNativeSession.cs is ~24 KB).

RecoveryPolicy
- Was a unit struct; now carries max_attempts: u32 + delay: Duration
  (port of MxNativeRecoveryPolicy at MxNativeSession.cs:24-43).
- SINGLE_ATTEMPT associated const matches the .NET static at cs:26.
- validate() rejects max_attempts == 0 (cs:33-36); the negative-Delay
  branch (cs:38-41) is unreachable in Rust because Duration is
  unsigned, so it's elided with a doc note.
- Default impl now returns SINGLE_ATTEMPT (was derive Default which
  zero-initialised).

SessionOptions (new — port of MxNativeClientOptions at cs:7-22)
- local_engine_id, engine_name, partner_version, galaxy_id,
  source_platform_id, heartbeat_ticks_per_beat: Option<i32>,
  heartbeat_max_missed_ticks.
- default_local_engine_id() constructor: 0x7000 + (process_id & 0x0FFF)
  per GenerateDefaultLocalEngineId at cs:18-21.
- default_engine_name(): "mxaccess.<pid>" mirroring the .NET
  "MxNativeClient.{ProcessId}" at cs:10.
- partner_version=6 default matches design/60-roadmap.md:54 DoD #1.

Test count delta: 468 -> 479 (+11). All four DoD gates green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:52:01 -04:00
Joseph Doherty d59ce3571c [M3] mxaccess-nmx: high-level write/advise/un_advise wrappers (resolves F13)
Seven new high-level methods on NmxClient (port of cs:303-466). Each
takes a GalaxyTagMetadata + typed WriteValue (re-exported from
mxaccess-codec), builds the inner NMX body, wraps in NmxTransferEnvelope,
and dispatches via the existing transfer_data opnum.

Methods landed
- write (cs:303-324)
- write2 (cs:326-349, with explicit FILETIME timestamp)
- write_secured2 (cs:351-380, dual user tokens via
  secured_write::resolve_observed_user_token; single-user secured = same id)
- advise_supervisory (cs:382-399, ItemControl envelope)
- send_observed_pre_advise_metadata (cs:401-420, hardcoded target
  platform/engine = (1, 1) per the .NET reference)
- register_reference (cs:422-441, accepts caller-built
  NmxReferenceRegistrationMessage)
- un_advise (cs:443-466, deliberately uses
  NmxTransferMessageKind::Write per cs:457 — the .NET reference's
  divergence from AdviseSupervisory's ItemControl envelope, preserved
  verbatim per CLAUDE.md unknown-bytes rule)

Internal encode_*_transfer_body helpers extracted as pub(crate) fn for
testability — mirrors the .NET reference's `internal static` shape.

NmxClientError gained two new variants: Codec(CodecError) for
metadata->reference-handle and value-encode failures, and
UnsupportedDataType for the kind-resolution path.

Cargo.toml: added mxaccess-galaxy as a dep on mxaccess-nmx.

design/followups.md: F13 moved to Resolved.

Test count delta: 459 -> 468 (+9 in mxaccess-nmx; 8 -> 17). Tests cover
each encode helper standalone (envelope-kind + length checks) plus
real-socket round-trip tests for write / advise_supervisory /
send_observed_pre_advise_metadata.

All four DoD gates green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:45:03 -04:00
Joseph Doherty 68aa2e30ab [M3] codec+galaxy: MxValueKind::for_data_type + GalaxyTagMetadata::resolve_write_kind
Last codec-side prerequisite before F13 (NmxClient high-level write
wrappers) can land. Two small additions, both wire-byte-direct ports
of the .NET reference's MxDataType → MxValueKind lookup logic.

mxaccess-codec
- MxValueKind::for_data_type(MxDataType, is_array) -> Option<MxValueKind>:
  fuses NmxWriteMessage.cs:58-86 (TryGetValueKind's 12 base mappings
  for data types 1..=6 scalar+array) with the two scalar fallbacks the
  .NET GalaxyTagMetadata.ProjectWriteValue layers on top
  (GalaxyRepositoryTagResolver.cs:65-69): ElapsedTime → Int32,
  InternationalizedString → String. Returns None for any other
  combination — including arrays of those two types and unsupported
  scalars (ReferenceType, StatusType, Enum, etc.).
- 6 new tests covering the base table, both fallbacks, the array-of-
  unsupported rejection, and the no-mapping branch for ReferenceType /
  StatusType / Enum / DataQualityType / BigString / Unknown / NoData /
  End sentinels.

mxaccess-galaxy
- GalaxyTagMetadata::resolve_write_kind() -> Result<MxValueKind,
  UnsupportedDataType>: pure delegation to MxValueKind::for_data_type
  + a typed error carrying (mx_data_type, is_array) for diagnostics.
- GalaxyTagMetadata::is_writable() — Ok-side accessor for browse UIs.
- UnsupportedDataType public error type (re-exported from lib.rs).
- 7 new tests: Double scalar → Float64, Boolean array → BoolArray,
  ElapsedTime scalar → Int32 (the fallback path), array-of-ElapsedTime
  rejected, InternationalizedString → String, ReferenceType rejected,
  Unknown sentinel rejected.

Test count delta: 446 -> 459 (+13; codec 215 -> 221, galaxy 49 -> 56).
All four DoD gates green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:33:42 -04:00
Joseph Doherty baea6eaa41 [M3] mxaccess-galaxy: GalaxyUserProfile + UserResolver trait + role-blob
Lands the user-resolver half of M3 stream A. Pure-Rust foundation —
the tiberius-backed SQL impl is logged as F14 and stays gated behind
the existing galaxy-resolver Cargo feature.

New
- role_blob.rs (~270 LoC, 12 tests including a garbage-between-roles
  edge case) — port of ParseRoleBlob (cs:87-133). Sliding-window scan
  over hex-decoded UTF-16LE bytes; rejects non-printable code units;
  case-insensitive dedup. Pure function, no I/O.
- user.rs (~290 LoC, 8 tests including 4 tokio-driven InMemoryUserResolver
  cases) — GalaxyUserProfile (port of cs:5-11) + from_columns helper
  bridging into role_blob + UserResolver async trait + UserResolverError
  with NotFound / Backend variants.
- sql.rs additions: USER_SELECT_SQL + USER_BY_GUID_SQL + USER_BY_NAME_SQL
  constants (port of cs:135-148). Inline concatcp! macro composes the
  base SELECT with each WHERE clause at compile time without pulling
  const_format.

Cargo.toml: added uuid (Galaxy user_guid is a uniqueidentifier).

design/followups.md: added F14 (P2) for the tiberius-backed SQL impl
behind the galaxy-resolver feature.

Test count delta: 427 -> 446 (+19; mxaccess-galaxy 30 -> 49). All four
DoD gates green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:26:24 -04:00
Joseph Doherty d84b066c62 [M3] mxaccess-galaxy: GalaxyTagMetadata + parser + Resolver trait + SQL
Lands M3 stream A — the pure-Rust foundation of the Galaxy resolver:
the data type, the tag-reference parser, the async trait, and the
canonical SQL strings. Unblocks F13 (NmxClient::write_* wrappers depend
on GalaxyTagMetadata) without pulling in tiberius yet.

New
- metadata.rs (~195 LoC, 7 tests) — GalaxyTagMetadata record (port of
  cs:6-73). Includes is_buffer_property + to_reference_handle(galaxy_id)
  bridging into mxaccess-codec::MxReferenceHandle::from_names.
- parser.rs (~330 LoC, 12 tests) — ParsedTagReference parser. Handles
  Object.Attribute (1 candidate), Object.Primitive.Attribute (2
  candidates: primitive-attr first, dotted-attr second per cs:181-185),
  and the case-insensitive .property(buffer) suffix. Pure-Rust, no I/O.
- resolver.rs (~200 LoC, 5 tests including a tokio-driven InMemoryResolver
  proving the trait is implementable without SQL) — async Resolver trait
  + ResolverError. Default browse returns Backend("not implemented") so
  read-only backends don't need to override it.
- sql.rs (~280 LoC, 5 smoke tests) — RESOLVE_SQL + BROWSE_SQL constants
  ported byte-for-byte from cs:208-432. Available publicly so any
  backend (the planned tiberius impl, a wwtools/grdb snapshot replay,
  etc.) can grab the canonical query.

Cargo.toml: added mxaccess-codec (path), async-trait, thiserror;
tokio added as dev-dependency for the resolver-trait async tests.

Deliberately deferred to a later iteration:
- The tiberius-backed Resolver impl behind the galaxy-resolver feature.
- ToValueKind / TryGetValueKind / ProjectWriteValue helpers on
  GalaxyTagMetadata (cs:41-72) — these need a MxDataType -> MxValueKind
  lookup that the codec doesn't currently expose; landing them with
  F13's write-helper iteration keeps the iteration coherent.

Test count delta: 397 -> 427 (+30). All four DoD gates green.
Open followups touched: F13 prerequisite (GalaxyTagMetadata) now in
place; F13 itself stays open until the write helpers wire it up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:17:16 -04:00
Joseph Doherty 0c772d273d [M3] mxaccess-nmx: NmxClient — 9 raw INmxService2 opnums (stream B)
Lands M3 stream B raw opnum surface: an async NmxClient over the
mxaccess-rpc transport that dispatches all 9 INmxService2 procedures
(GetPartnerVersion, RegisterEngine2 + WithoutCallback, UnregisterEngine,
Connect, AddSubscriberEngine, RemoveSubscriberEngine,
SetHeartbeatSendInterval, TransferData) plus a NonZeroHresult error
variant that mirrors ThrowIfFailed (cs:563-574).

New
- crates/mxaccess-nmx/src/client.rs (~580 LoC, 8 tests including 5
  real-socket tokio tests against a hand-rolled DCE/RPC server) — port
  of the raw opnum surface from ManagedNmxService2Client.cs.
- NmxClient::connect builds the NTLM-packet-integrity bind path; for
  tests, NmxClient::from_bound_transport accepts a transport bound any
  way the caller likes (the test server doesn't validate signatures).
- fresh_orpc_this generates a per-call Cid via rand::random(), mirroring
  the .NET reference's Guid.NewGuid() at every call site.
- NmxClientError::NonZeroHresult unifies the .NET reference's
  Marshal.ThrowExceptionForHR + InvalidOperationException branches so
  callers see one typed surface for "transport-OK + LMX rejected".

Cargo.toml: added tokio, tracing, thiserror, rand to mxaccess-nmx.

Two layers of the .NET reference are deliberately out of scope this
iteration; both logged as new followups in design/followups.md:

- F12 (P1): the auto-resolving Create() factory, which needs windows-rs
  COM activation (gated by F6) + ComObjRefProvider port.
- F13 (P1): the high-level Write*/Advise*/UnAdvise/RegisterReference
  helpers, which depend on GalaxyTagMetadata from M3 stream A (the
  Galaxy SQL resolver crate, not yet started).

Test count delta: 389 -> 397 (+8). All four DoD gates green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 08:06:15 -04:00
Joseph Doherty ecfcc3f429 [M3] mxaccess-rpc: NmxService2 codec + F9 ResolveOxid wrappers
Two units of work in one commit:

1. nmx_service2_messages.rs (~470 LoC, 18 tests) — port of
   NmxService2Messages.cs. Encoders for all 9 INmxService2 opnums
   (RegisterEngine, UnRegisterEngine, Connect, TransferData,
   AddSubscriberEngine, RemoveSubscriberEngine, SetHeartbeatSendInterval,
   RegisterEngine2, GetPartnerVersion) plus BSTR + InterfacePointer NDR
   helpers used by RegisterEngine2 marshalling. Decoders for the
   GetPartnerVersion result and the generic HRESULT response. M3 stream
   B (NmxClient) will be a thin layer over these + the transport.

2. object_exporter_client.rs (~290 LoC, 6 tests including 2 real-socket
   tokio tests) — resolves followup F9. Implements:
   - resolve_oxid_unauthenticated (cs:14-30)
   - resolve_oxid_with_managed_ntlm_packet_integrity (cs:66-81)
   ResolveOxidOutcome enum disambiguates the two response shapes the
   .NET reference parses (typed result vs 4-byte failure). The two SSPI
   flavours (cs:32-47, cs:49-64) are permanently skipped — they wrap
   .NET-only System.Net.Security.SspiClientContext.

design/followups.md: F9 moved to Resolved with this commit's hash.

Test count delta: 364 -> 389 (+25; mxaccess-rpc 137 -> 162; +18 from
nmx_service2_messages, +7 from object_exporter_client which includes
the +2 fall-through tests for the dual-shape response decoder).
Open followups touched: F9 resolved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 07:56:11 -04:00
Joseph Doherty 432f1102b7 [M2/M3] mxaccess-rpc: tokio DCE/RPC TCP transport (DceRpcTcpClient port)
Lands the async DCE/RPC TCP client — the transport that bridges the M2
PDU codec to a real socket. Unblocks M3 stream B (mxaccess-nmx, the
NmxClient) and brings F9 (ResolveOxid wrappers) within reach.

New
- transport.rs (~700 LoC, 10 tests including 2 real-socket tokio tests)
  — port of src/MxNativeClient/DceRpcTcpClient.cs.
  - DceRpcTcpClient::connect/bind/bind_with_managed_ntlm_packet_integrity/
    call/call_bound/call_bound_object — async over tokio::net::TcpStream.
  - encode_packet_integrity_request: 4-byte 0xBB pad + 8-byte AuthTrailer
    + 16-byte NtlmClientContext::sign signature, frag_length and
    auth_length rewritten in the embedded header per cs:201-250.
  - encode_request_bytes: PFC_OBJECT_UUID flag (0x80) and inserted
    16-byte object UUID slot per cs:269-278.
  - TransportError enum unifies io / codec / NTLM / fault / not-connected
    surfaces. Mirrors DceRpcFaultException as the typed Fault variant.
  - NTLM_AUTH_CONTEXT_ID = 79232 = 0x13580 (cs:90,133) exposed publicly.

Deliberately skipped: BindWithNtlmConnect / BindWithNtlmPacketIntegrity
(SSPI flavours at cs:55-63,108-149) — those wrap .NET's
System.Net.Security.SspiClientContext, which has no portable analogue.
Managed-NTLM path covers what the production Rust client needs.

mxaccess-rpc/Cargo.toml: added tokio (workspace-pinned).

design/followups.md: F9 downgraded P1 → P2 (transport landed; only the
two pure-codec ResolveOxid wrappers remain).

Test count delta: 354 -> 364 (+10).
Open followups touched: F9 partially advanced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 07:47:42 -04:00
Joseph Doherty b0954b2672 [M2] mxaccess-callback: tokio TCP exporter (wave 3 main)
Lands the M2 wave 3 main course — the INmxSvcCallback callback exporter.
Pure-tokio TCP server that mirrors src/MxNativeClient/ManagedCallbackExporter.cs
and lets a Rust client receive callbacks from NmxSvc.exe.

New
- exporter.rs (~700 LoC, 10 tests) — port of ManagedCallbackExporter.cs.
  CallbackExporter::bind starts a TcpListener + accept loop; per-connection
  serve task walks Bind / AlterContext / Request / Auth3 PDUs and dispatches
  IRemUnknown (opnums 3/4/5) and INmxSvcCallback (opnums 3/4) requests.
  Hand-rolled BindAck encoder mirroring cs:226-254 (single acceptance entry,
  NDR20 transfer syntax).
- ExporterIdentities { oxid, oid, callback_ipid, rem_unknown_ipid } — exposes
  both `random()` (production) and `fixed()` (tests). Mirrors the .NET
  RandomUInt64 + Guid.NewGuid pattern at cs:14-20.
- CallbackEvent enum — typed diagnostic stream replacing .NET's
  List<string> log (cs:12,33-42,315-321). Variants: ClientConnected,
  AcceptError, Bind, Auth3Ignored, Request, RemQueryInterface,
  CallbackInvoked, UnhandledRequest, ClientDisconnected, ProtocolError.
- IUNKNOWN_IID const re-exported alongside the other IIDs.

Tests cover real-socket round-trips: Bind+RemQueryInterface (with IUNKNOWN
returning S_OK), Bind+unknown opnum -> Fault, Bind+DataReceived ->
CallbackInvoked event + 12-byte success response, and graceful shutdown.

Test count delta: 344 -> 354 (+10).
Open followups touched: none new. F2 (verify_signature path) still
gated on a live status-frame fixture under tests/fixtures/m2-status-frame/.
F6 / F9 still need the windows-rs and DceRpcTcpClient ports respectively.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 07:36:41 -04:00
Joseph Doherty ecbf282f6d [M2] mxaccess-rpc: NMX metadata + callback messages + OBJREF builder
Lands the codec-only prerequisites for M2 wave 3 (callback exporter).
The TCP server itself (port of ManagedCallbackExporter.cs's TcpListener
+ accept loop) follows next iteration in the mxaccess-callback crate.

New modules
- nmx_metadata.rs (9 tests) — port of NmxProcedureMetadata.cs.
  INmxService2 + INmxSvcCallback IIDs, NdrProcedureDescriptor with
  per-opnum metadata for the 9 INmxService2 procedures (opnums 3..11)
  and 2 INmxSvcCallback procedures (opnums 3, 4).
- nmx_callback_messages.rs (8 tests) — port of NmxSvcCallbackMessages.cs.
  parse_callback_request decodes OrpcThis + i32 size + i32 max_count +
  body bytes; encode_callback_response produces the 12-byte OrpcThat +
  HRESULT response.

objref.rs additions
- ComObjRefBuilder::create_standard_objref (8 tests) — port of the
  second class in ManagedCallbackExporter.cs:337-393. Pure-Rust OBJREF
  emitter that builds 68-byte header + dual-string array. Note this is
  *not* the Win32 CoMarshalInterface-based ComObjRefProvider.cs (still
  open as F6); it's the higher-level emitter the callback exporter
  uses to build OBJREF bytes from primitives.
- CALLBACK_OBJREF_AUTH_SERVICES const exposes the 7-entry auth-service
  tower-id table (NTLM SSP through Kerberos extension) the .NET
  reference advertises in every callback OBJREF.

Test count delta: 319 -> 344 (+25; mxaccess-rpc 102 -> 127, codec
unchanged at 215, parity unchanged at 2). All four DoD gates green.
Open followups touched: none new; F6 advances toward resolution but
the windows-rs Win32 wrapper part stays open.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 07:23:44 -04:00
Joseph Doherty 30138629d3 [M2] mxaccess-rpc: OXID + RemQI body codecs (wave 2)
Lands M2 wave 2 — two pure-Rust body-codec modules under
crates/mxaccess-rpc, plus a small inline ORPC framing port and a
crate-level type consolidation. Resolves F7+F8 from wave 1.

New modules
- guid.rs (4 tests) — hoisted from objref::Guid; shared by all of
  mxaccess-rpc. Resolves F7.
- error.rs — hoisted RpcError union (ShortRead, UnexpectedPacketType,
  UnknownPacketType, InvalidFragmentLength, TruncatedBindBody,
  InvalidAuthTrailer, MissingAuthValue, Decode). Resolves F8.
- orpc.rs (8 tests) — port of OrpcStructures.cs:1-141. ComVersion,
  OrpcThis (32-byte header), OrpcThat (8-byte header),
  MInterfacePointer (length-prefixed OBJREF), StdObjRef (40 bytes).
- object_exporter.rs (~530 LoC, 20 tests) — port of
  ObjectExporterMessages.cs:1-141. IObjectExporter IID, opnums,
  ResolveOxid request encoder + ResolveOxidResult/Failure parsers.
  Owned-string protocol labels cleaned up via Cow upgrade rather than
  Box::leak (ComDualStringEntry::protocol is now Cow<'static, str>).
- rem_unknown.rs (~340 LoC, 11 tests) — port of RemUnknownMessages.cs.
  IRemUnknown IID, RemQueryInterface request/response, RemQiResult.
  4-byte NDR pad in REMQIRESULT preserved as pad_after_hresult per
  CLAUDE.md unknown-bytes rule.

Test count delta: 277 -> 319 (+42; codec 215 unchanged, mxaccess-rpc
60 -> 102, codec parity 2 unchanged).
Open followups touched: F7 + F8 resolved; F9, F10, F11 added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 07:14:29 -04:00
Joseph Doherty 95bd218183 [M2] mxaccess-rpc: NTLMv2 + DCE/RPC PDU + OBJREF parser (wave 1)
Lands M2 wave 1 — three pure-Rust modules under crates/mxaccess-rpc with
60 unit tests. Each is a 1:1 port of one .NET reference file:

- ntlm.rs (1137 LoC, 19 tests) — `ManagedNtlmClientContext.cs`. NTLMv2
  challenge/response, Type1/Type3 builders, sign() with RC4-sealed checksum
  and per-call sequence advance. Manual `Debug` impl that hides credentials;
  not Clone (rc4 0.2 cipher state is non-Clone). Pure-Rust crypto via
  hmac/md-5/md4/rc4 v0.2/rand v0.8 (rc4 0.2 chosen per design/review.md:78).
- pdu.rs (1573 LoC, 33 tests) — `DceRpcPdu.cs` + auth-trailer types from
  `DceRpcAuthentication.cs`. Bind/AlterContext/Auth3/Request/Response/Fault
  PDUs, NDR20 transfer syntax, auth_value with 4-byte alignment padding,
  preserved-byte fields per CLAUDE.md unknown-bytes rule.
- objref.rs (~470 LoC, 11 tests including a 366-byte captured OBJREF
  round-trip) — `ComObjRef.cs`. MEOW signature, OXID/OID/IPID, dual-string
  array with printable-ASCII escaping and security-binding boundary.
  ComObjRefProvider.cs deferred (windows-rs Win32 wrapper — see F6).

Every wire-byte claim cites src/MxNativeClient/<file>.cs:LINE per
CLAUDE.md "no fabricated protocol behaviour" rule.

Test count delta: 217 → 277 (+60)
Open followups touched: F1–F8 (new — see design/followups.md)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 06:54:39 -04:00
179 changed files with 58542 additions and 333 deletions
+43
View File
@@ -47,3 +47,46 @@ jobs:
- name: cargo clippy --workspace -- -D warnings
run: cargo clippy --workspace --all-targets -- -D warnings
public-api:
name: cargo public-api drift check (F41)
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install nightly toolchain
uses: dtolnay/rust-toolchain@nightly
- name: Install cargo-public-api
run: cargo install --locked cargo-public-api
- name: Diff each crate's public API against the baseline
shell: pwsh
working-directory: rust
run: |
$crates = @(
'mxaccess-codec', 'mxaccess-rpc', 'mxaccess-asb-nettcp',
'mxaccess-asb', 'mxaccess-galaxy', 'mxaccess-callback',
'mxaccess-nmx', 'mxaccess', 'mxaccess-compat'
)
$drift = $false
foreach ($crate in $crates) {
Write-Host "=== $crate ==="
$live = cargo +nightly public-api --simplified -p $crate 2>$null
$baseline = Get-Content "../design/public-api/$crate.txt" -Raw
$liveJoined = ($live -join "`n") + "`n"
if ($liveJoined -ne $baseline) {
Write-Host "::error file=design/public-api/$crate.txt::public API drift detected for $crate"
# Print a unified diff for the PR log.
$tmpLive = New-TemporaryFile
$tmpBaseline = New-TemporaryFile
Set-Content -Path $tmpLive -Value $liveJoined -NoNewline
Set-Content -Path $tmpBaseline -Value $baseline -NoNewline
git diff --no-index --color=never -- $tmpBaseline $tmpLive
$drift = $true
}
}
if ($drift) {
Write-Host "::error::Public API drift detected. Run 'cargo +nightly public-api --simplified -p <crate>' locally and update design/public-api/<crate>.txt to match the intended new surface."
exit 1
}
+15
View File
@@ -24,6 +24,11 @@ Cargo.lock.bak
*.bak
*.tmp
# ---- Claude Code project-local state ----
# `.claude/` holds per-project scheduled-task locks, agent state, etc.
# It's user/host-specific and not part of the codebase.
.claude/
# ---- OS ----
Thumbs.db
Desktop.ini
@@ -43,6 +48,16 @@ Desktop.ini
# design/60-roadmap.md M0; the actual symlink tree is not checked in.
/rust/tests/fixtures/
# ---- Ad-hoc debug captures from manual asb-relay / trace runs ----
# These appear at the repo root when debugging live wire issues with
# `examples/asb-relay.rs` or `MX_ASB_TRACE_REPLY=1`. They're transient
# per-session evidence — promote anything worth keeping into
# `captures/` or `analysis/` instead.
/rust-cs.txt
/rust-sc.txt
/rust.log
/rust-trace-*.txt
# ---- Plan files (project-local Claude Code plan staging) ----
# These live under the user's ~/.claude/plans/ scope but appear at the
# project root if accidentally created.
+178
View File
@@ -0,0 +1,178 @@
# Changelog
All notable changes to the `mxaccess` workspace are documented here. The
format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
the workspace as a whole follows [SemVer](https://semver.org/) but the
0.0.x line is pre-release / API-unstable.
## [Unreleased] — V1 — 2026-05-07
V1 is the first publishable cut. Closes M0 → M6 from
`design/60-roadmap.md`. The workspace stays at `version = "0.0.0"`
indefinitely (F48 — internal usage only, no crates.io publish; consumers
depend via path or git).
### Added
- **`mxaccess-codec`** — pure protocol codec covering `MxReferenceHandle`,
`NmxTransferEnvelope`, `NmxItemControlMessage`, `NmxWriteMessage` (scalar
+ array, normal + timestamped), `NmxSecuredWrite2Message`,
`NmxSubscriptionMessage` (single + multi-record DataUpdate per F44),
`NmxReferenceRegistrationMessage`, `NmxMetadataQueryMessage`,
`NmxOperationStatusMessage`, `ObservedWriteBodyTemplate`, ASB Variant +
`AsbStatus` + `RuntimeValue`, `MxStatus`, `MxValueKind`, `MxDataType`,
`MxValue`. Counting-allocator bench harness in `benches/alloc_count.rs`
(F38) reports 14 allocs per write across the proven matrix, well under
the R12 < 5/write target.
- **`mxaccess-rpc`** — DCE/RPC PDU codec, NTLMv2 client + server-direction
packet-integrity verify (F2 with `subtle::ConstantTimeEq`), TCP
transport, OBJREF parser + Win32 `CoMarshalInterface` emitter (F6),
`IObjectExporter::ResolveOxid` + `ResolveOxid2` (F10),
`IRemUnknown::RemQueryInterface` + `RemAddRef`/`RemRelease` (F11).
- **`mxaccess-callback`** — RPC server hosting `INmxSvcCallback` +
`IRemUnknown` for inbound `DataReceived` / `StatusReceived` frames.
`dcom_sink` (F55 Path A, gated by `windows-com`) hosts the callback
as a DCOM-managed object so `RegisterEngine2` accepts it on AVEVA
installs that do SCM-side OXID resolution against RPCSS; the
hand-rolled `CallbackExporter` is retained for unit tests.
- **`mxaccess-nmx`** — `INmxService2` client (`RegisterEngine2`,
`TransferData`, `AddSubscriberEngine`, `SetHeartbeatSendInterval`,
etc.) plus auto-resolving `NmxClient::create` factory (F12, gated by
`windows-com`).
- **`mxaccess-galaxy`** — `tiberius`-backed `Resolver` + `UserResolver`
(F14, gated by `galaxy-resolver`).
- **`mxaccess-asb-nettcp`** — `[MS-NMF]` framing + `[MC-NBFX]` binary-XML
+ `[MC-NBFS]` static dictionary + DH/HMAC/AES auth crypto with
constant-time `mod_exp` via `crypto-bigint::DynResidue` (F27).
- **`mxaccess-asb`** — `IASBIDataV2` client (`Connect`, `RegisterItems`,
`Read`, `Write`, `PublishWriteComplete`, `CreateSubscription`,
`AddMonitoredItems`, `Publish`, `DeleteMonitoredItems`,
`DeleteSubscription`, `Disconnect`) with canonical-XML HMAC signing
for all 13 `ConnectedRequest` shapes (F28) and DataContract
field-suffix names on the binary `MonitoredItem` body (F34).
- **`mxaccess`** — async Tokio façade exposing `Session`, `AsbSession`,
`Subscription` (`Stream<Item = Result<DataChange, Error>>`),
`subscribe_buffered` per R2 single-sample-with-cadence-knob
semantics (F36), `recover_connection` reconnect loop (F16), recovery
events (`RecoveryEvent::Started/Recovered/Failed`), and a typed
`Error` taxonomy. Recovery replay re-issues `RegisterReference` (not
`AdviseSupervisory`) for buffered subscriptions so the
`.property(buffer)` shape survives transport rebuild (F45);
`unsubscribe` skips the `UnAdvise` wire frame for buffered
subscriptions to match the .NET reference's `IsBuffered` guard
(F47). `Session::ensure_publisher_connected` issues the
`INmxService2::Connect` + `AddSubscriberEngine` round-trip before
the first advise against each publishing engine, so `0x33`
DataUpdate frames flow on this AVEVA install (F56). New
`WriteHandle { correlation_id }` returned by `*_with_handle` write
variants for per-operation correlation; `OperationStatus.context`
carries the originating `OperationContext` (F54). Optional
`metrics` feature emits per-op counters, latency histograms, and
connection-state gauges (F40).
- **`mxaccess-compat`** — `LMXProxyServer`-shaped Rust facade exposing
the 18-method `ILMXProxyServer5` surface as async fns over
`mxaccess::Session` / `AsbSession` with a `Mutex<HashMap<i32,
ItemRef>>` handle table and `Stream`-based event channels (F35).
`LmxClient` spawns an `operation_status_drain` fan-out task that
routes `Write` / `WriteSecured` events to `on_write_complete` and
every other op kind to `on_operation_complete`, dropping events
with unknown correlation ids silently (F54).
- **Examples** — `connect-write-read.rs`, `subscribe.rs`,
`subscribe-buffered.rs`, `asb-subscribe.rs`, `multi-tag.rs`,
`recovery.rs`, `secured-write.rs`, plus diagnostic
`asb-relay.rs`. Live-probe DoD verified end-to-end against the
AVEVA install.
- **Tooling** — `cargo public-api` baselines under
`design/public-api/{crate}.txt` with CI drift check (F41).
`design/M6-bench-baseline.md` records the alloc-count baseline.
- **Performance (post-baseline) — F52.** Three codec optimisations
measured against the F38 alloc-count harness:
- `write_message::encode_to_bytes_mut` (F52.1) — `BytesMut` output
so consumers can `split_to` / `freeze` and forward to a wire-level
sink without copying. Same alloc count as `encode`.
- Thread-local name-signature cache (F52.2) — repeated
`MxReferenceHandle::from_names` calls with the same names skip the
`to_lowercase` + CRC walk. `from_names` drops 2 → 0 allocs/op once
warm; bounded at 1024 entries per thread.
- `write_message::encode_into_bytes_mut` (F52.3) — caller-supplied
`BytesMut` scratch buffer; reusing across writes drops fixed-width
scalars from 2 → 1 alloc/op and Boolean from 1 → 0.
Bench deltas pinned in `design/M6-bench-baseline.md` § F52.{1,2,3}.
- **Live evidence — F49 / F50 / F51.** F49 step 5 (`LmxClient`
`OnWriteComplete` round-trip) verified live against AVEVA via
`cargo test -p mxaccess-compat --features live-windows-com --test
lmx_write_complete_live`. F50 captured `Suspend` (NMX opcode `0x2D`,
server-side) + `Activate` (client-side, no wire traffic) under
`captures/123-frida-suspend-advised-instrumented/` +
`captures/124-frida-activate-advised-instrumented/`; R5 settled.
F51 provisioned 7 UDAs on `$TestMachine` via `wwtools/graccesscli`
(TestFloat / TestDouble / TestDateTime / TestDuration + array
variants), captured live `AsbVariant` wire bytes for each scalar
type, and pinned them via
`crates/mxaccess-codec/tests/f51_type_matrix_parity.rs`.
- **`MxStatus` synthesizer kernel** — Path A from `Lmx.dll`
`FUN_10100ce0` ported into `MxStatus::from_packed_u32`. Settles R3
+ R4 (`OperationComplete` trigger conditions and completion-only
byte mappings: the .NET reference's `WriteCompleted` is itself a
half-implementation; the Rust port preserves the wire bytes
verbatim and routes them through the synthesizer kernel).
### Changed (vs the .NET reference)
- `NmxSubscriptionMessage::parse_data_update` accepts `record_count >= 1`;
the .NET reference hard-throws on `record_count != 1`. F44 evidence
walk against `captures/094-frida-buffered-separate-writer/`
documents the multi-record observation that drove the divergence.
- `subscribe_buffered` returns a `Stream<Item = DataChange>`
(single-sample-per-event); per R2 verification the cadence is a
server-side delivery rate knob, not a multi-sample payload.
### Known limitations
- **F3** — cross-domain NTLM Type1/2/3 fixture is permanently
out-of-scope on the dev host (single-domain only). Single-domain
wire parity is verified; cross-domain rounds-trip through the same
shape-agnostic AV-pair codec but no live fixture pins it. Self-
contained provisioning recipe (lab topology, capture procedure,
fixture layout, round-trip test skeleton) at
`docs/F3-cross-domain-ntlm-recipe.md` for whoever has access to a
two-forest Windows lab.
- **F53 (protocol crates only)** — `#![warn(missing_docs)]` is
enabled and warning-clean on the consumer-facing `mxaccess` +
`mxaccess-compat` lib roots. Protocol crates measure 1883
missing-docs warnings (mostly struct-field-level wire-shape
records); enabling the lint there would add per-field one-liners
without consumer value. Lint stays off on protocol crates
indefinitely. Per-module `#![allow(missing_docs)]` opt-out is the
re-introduction path if a contributor wants per-crate enforcement.
## Publish order
> **Note (2026-05-06, F48):** the workspace will not be published to
> crates.io. Internal usage only; consumers depend via path or git.
> The dependency DAG below is retained as a workspace-hygiene check
> (`design/F48-publish-dry-run.md` validates each crate's `cargo
> package --list` produces a clean tarball with no accidental
> captures or large files) and as the publish recipe if the policy
> ever changes (e.g. an internal contributor wants registry-style
> versioning via a private cargo registry).
Workspace crates form a dependency DAG; `cargo publish` requires
already-published deps to exist on crates.io, so the order matters:
1. `mxaccess-codec` (no internal deps)
2. `mxaccess-rpc` (no internal deps)
3. `mxaccess-asb-nettcp` (no internal deps)
4. `mxaccess-galaxy` (depends on codec)
5. `mxaccess-callback` (depends on rpc + codec)
6. `mxaccess-asb` (depends on codec + asb-nettcp)
7. `mxaccess-nmx` (depends on codec + galaxy + rpc + callback)
8. `mxaccess` (depends on all the above)
9. `mxaccess-compat` (depends on mxaccess)
`cargo publish --dry-run` validates each crate's metadata + tarball
in isolation; the dependent crates' dry-runs require the leaf crates
to actually exist on crates.io (the registry lookup happens regardless
of `--no-verify`). For pre-publish verification: leaf crates dry-run
in CI; dependent crates are validated by the public-api baseline +
build-test-clippy matrix.
+129
View File
@@ -1,5 +1,55 @@
// Frida hooks generated from headless Ghidra RVAs.
// Usage: frida -f <MxTraceHarness.exe> -l analysis/frida/mx-nmx-trace.js -- <harness args>
//
// F46 — Suspend / Activate instrumentation procedure
// ---------------------------------------------------
// The `mx.suspend.*` and `mx.activate.*` events below close the wire-side gap
// left by capture 077 (`captures/077-frida-suspend-advised-scanstate/`). The
// hooks attach to `LmxProxy.dll!CLMXProxyServer.Suspend` (RVA 0x13d9c, FUN_10013d9c)
// and `LmxProxy.dll!CLMXProxyServer.Activate` (RVA 0x14028, FUN_10014028) — the
// two RVAs were extracted from `analysis/ghidra/exports/LmxProxy.dll.string-refs.tsv`
// (rows tagged `CLMXProxyServer::Suspend - Server Handle` and
// `CLMXProxyServer::Activate - Server Handle`). The export table does NOT
// expose `Resume` or `Reactivate` symbols anywhere in `LmxProxy.dll`,
// `Lmx.dll`, or the `ILMXProxyServer5` interface — verified against
// `analysis/ghidra/exports/LmxProxy.dll.ghidra.md` and the decompiled
// interface at `analysis/decompiled-mxaccess/ArchestrA/MxAccess/ILMXProxyServer5.cs`.
//
// To re-run capture 077 with the new hooks active (left for the maintainer
// on the live AVEVA host):
//
// 1. Rebuild the x86 trace harness:
// msbuild src\MxTraceHarness\MxTraceHarness.csproj /p:Configuration=Release
// 2. Suspend-advised scenario:
// frida ^
// -f src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe ^
// -l analysis\frida\mx-nmx-trace.js ^
// -- --scenario=suspend-advised ^
// --tag=TestChildObject.ScanState ^
// --write-delay-ms=1000 ^
// --duration=3 ^
// --log=captures\NNN-frida-suspend-activate-instrumented\harness.log ^
// --client=MxFridaTrace-NNN
// 3. Activate-advised scenario (re-runs Suspend then Activate):
// frida ^
// -f src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe ^
// -l analysis\frida\mx-nmx-trace.js ^
// -- --scenario=activate-advised ^
// --tag=TestChildObject.ScanState ^
// --write-delay-ms=1000 ^
// --duration=3 ^
// --log=captures\NNN-frida-suspend-activate-instrumented\harness.log ^
// --client=MxFridaTrace-NNN
// 4. Save the resulting `frida-events.tsv` (plus `harness.log`,
// `frida-command.txt`, `frida.stdout.jsonl`) under
// `captures/NNN-frida-suspend-activate-instrumented/` (next free NNN).
// 5. Grep for `mx.suspend.begin|mx.suspend.end|mx.activate.begin|mx.activate.end`
// in the new TSV. If any matching `nmx.enter` / `lmx.*` events appear in
// the same time window — typed decode the body and update
// `analysis/proxy/nmxsvcps-procedures.tsv` + `docs/M6-buffered-evidence.md`.
// If no NMX traffic accompanies the hook fires — Suspend/Activate are
// confirmed client-side-only and R5 in `design/70-risks-and-open-questions.md`
// moves to "fully settled — client-side only".
const maxDump = 4096;
const installed = {};
@@ -173,6 +223,79 @@ function hookPlainArgs(moduleName, rva, name, argCount) {
});
}
function readMxStatusOut(ptrValue) {
// MxStatus on the wire is 4 × int16 = 8 bytes:
// short Success, short Category, short DetectedBy, short Detail.
// See src/MxNativeCodec/MxStatus.cs and the .NET reference's
// `out MxStatus pMxStatus` parameter on ILMXProxyServer5.{Suspend,Activate}.
try {
if (ptrValue.isNull()) return null;
return {
raw: dumpBytes(ptrValue, 8),
success: ptrValue.add(0).readS16(),
category: ptrValue.add(2).readS16(),
detectedBy: ptrValue.add(4).readS16(),
detail: ptrValue.add(6).readS16()
};
} catch (e) {
return { error: e.message };
}
}
function hookSuspendActivate(rva, name, eventVerb) {
// CLMXProxyServer::Suspend / Activate are __stdcall member methods:
// HRESULT Suspend(int hLMXServerHandle, int hItem, MxStatus* pMxStatusOut)
// After Frida's __stdcall lowering, args[0] = this (because the prologue
// pushes ECX into the stack frame the same way AdviseSupervisory does at
// RVA 0x142b4), args[1] = serverHandle, args[2] = itemHandle,
// args[3] = MxStatus* out. Mirrors the AdviseSupervisory hookPlainArgs
// shape but with typed out-param decoding (cf. hookAuthenticateUser).
hook("LmxProxy.dll", rva, name, function (address, module) {
return {
onEnter(args) {
this.statusOut = ptrArg(args, 3);
this.serverHandle = intArg(args, 1);
this.itemHandle = intArg(args, 2);
emit({
event: "mx." + eventVerb + ".begin",
module: "LmxProxy.dll",
name,
address: address.toString(),
ecx: this.context.ecx ? this.context.ecx.toString() : "",
serverHandle: this.serverHandle,
itemHandle: this.itemHandle,
statusOutPtr: this.statusOut.toString()
});
},
onLeave(retval) {
emit({
event: "mx." + eventVerb + ".end",
module: "LmxProxy.dll",
name,
retval: retval.toString(),
serverHandle: this.serverHandle,
itemHandle: this.itemHandle,
status: readMxStatusOut(this.statusOut)
});
}
};
});
}
function hookSuspend() {
// FUN_10013d9c, RVA 0x13d9c; matched on the
// `CLMXProxyServer::Suspend - Server Handle ` string xref in
// analysis/ghidra/exports/LmxProxy.dll.string-refs.tsv:119.
hookSuspendActivate(0x13d9c, "CLMXProxyServer.Suspend", "suspend");
}
function hookActivate() {
// FUN_10014028, RVA 0x14028; matched on the
// `CLMXProxyServer::Activate - Server Handle ` string xref in
// analysis/ghidra/exports/LmxProxy.dll.string-refs.tsv:122.
hookSuspendActivate(0x14028, "CLMXProxyServer.Activate", "activate");
}
function hookAuthenticateUser() {
hook("LmxProxy.dll", 0x1399f, "CLMXProxyServer.AuthenticateUser", function (address, module) {
return {
@@ -452,6 +575,12 @@ function installKnownHooks() {
hookPlainArgs("LmxProxy.dll", 0x1121d, "CLMXProxyServer.AddBufferedItem", 5);
hookPlainArgs("LmxProxy.dll", 0x0fc80, "CLMXProxyServer.SetBufferedUpdateInterval", 3);
hookPlainArgs("LmxProxy.dll", 0x142b4, "CLMXProxyServer.AdviseSupervisory", 5);
// F46: Suspend / Activate wire-side instrumentation. No `Resume` / `Reactivate`
// exports exist in LmxProxy.dll's symbol table — verified against
// analysis/ghidra/exports/LmxProxy.dll.ghidra.md and the
// ILMXProxyServer5 / ILMXProxyServer4 decompiled interfaces.
hookSuspend();
hookActivate();
hookPlainArgs("LmxProxy.dll", 0x163c0, "CProxy_ILMXProxyServerEvents2.Fire_OnBufferedDataChange", 8);
hookPlainArgs("LmxProxy.dll", 0x16b50, "CUserConnectionCallback.OnSetAttributeResult", 4);
hookPlainArgs("LmxProxy.dll", 0x16d4b, "CUserConnectionCallback.OperationComplete", 4);
@@ -0,0 +1,35 @@
# Lmx.dll selected decompile
## FUN_10178fc0 at 10178fc0
Signature: `undefined FUN_10178fc0(void)`
```c
void FUN_10178fc0(void)
{
uint uVar1;
void *local_10;
undefined1 *puStack_c;
undefined4 local_8;
puStack_c = &LAB_101663ae;
local_10 = ExceptionList;
uVar1 = DAT_101d60b8 ^ (uint)&stack0xfffffffc;
ExceptionList = &local_10;
local_8 = 1;
DAT_101d6160 = SysAllocString(L"Lmx.aaDCT");
if (DAT_101d6160 == (BSTR)0x0) {
/* WARNING: Subroutine does not return */
FUN_100013e0(0x8007000e,uVar1);
}
local_8 = 0xffffffff;
_atexit(FUN_101793a0);
ExceptionList = local_10;
return;
}
```
@@ -0,0 +1,501 @@
# Lmx.dll selected decompile
## FUN_10114a90 at 10114a90
Signature: `undefined FUN_10114a90(void)`
```c
void __thiscall
FUN_10114a90(undefined4 *param_1,int param_2,int *param_3,ushort param_4,undefined4 param_5,
void *param_6,short *param_7)
{
char cVar1;
uint uVar2;
undefined4 uVar3;
basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_> *pbVar4;
int iVar5;
undefined4 *puVar6;
wchar_t *pwVar7;
wchar_t *pwVar8;
size_t _MaxCount;
DWORD DVar9;
int iVar10;
int *piVar11;
undefined2 uVar12;
wchar_t _Ch;
_func_basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_>_ptr_basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_>_ptr
*p_Var13;
undefined4 uVar14;
undefined1 local_ba4 [20];
undefined1 local_b90 [20];
undefined4 local_b7c;
undefined4 local_b74;
void *local_b70;
short *local_b6c;
undefined4 *local_b68;
int *local_b64;
char local_b5e;
char local_b5d;
undefined4 *local_b5c [391];
wchar_t local_540 [520];
undefined1 local_130 [20];
undefined1 local_11c [20];
undefined1 local_108 [60];
undefined1 local_cc [20];
undefined4 local_b8 [36];
undefined4 local_28;
undefined4 local_24;
undefined4 local_20;
uint local_14;
void *local_10;
undefined1 *puStack_c;
undefined4 local_8;
local_8 = 0xffffffff;
puStack_c = &LAB_101729a5;
local_10 = ExceptionList;
uVar2 = DAT_101d60b8 ^ (uint)&stack0xfffffffc;
ExceptionList = &local_10;
local_b5c[0] = (undefined4 *)(uint)param_4;
local_b74 = param_5;
local_b70 = param_6;
local_b6c = param_7;
local_14 = uVar2;
cVar1 = FUN_100408d0(uVar2);
if (cVar1 != '\0') {
local_20 = *(undefined4 *)(local_b6c + 2);
swprintf_s(local_540,0x104,L"<success %d category %d detectedBy %d detail %d>",
(int)(short)*(undefined4 *)local_b6c,local_20,*(undefined4 *)(local_b6c + 4),
(int)local_b6c[6]);
local_b64 = (int *)FUN_10004010(local_b74,local_b70);
local_b5c[0] = (undefined4 *)FUN_100040a0(local_b5c[0]);
uVar3 = FUN_100300d0(param_3);
iVar5 = param_2;
p_Var13 = endl_exref;
pbVar4 = (basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_> *)
FUN_1001a0e0(*(undefined4 *)(DAT_101d6474 + 0x20),
L"PreboundReference::OnSetAttributeResult - ENTER correlationId ",param_2,
L" pValue ",uVar3,L" quality ",local_b5c[0],L" timestamp ",local_b64,
L" mxStatus ",local_540);
pbVar4 = std::basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_>::operator<<
(pbVar4,iVar5);
uVar3 = FUN_1001a0e0(pbVar4);
uVar3 = FUN_1001a0e0(uVar3);
uVar3 = FUN_1001a0e0(uVar3);
uVar3 = FUN_1001a0e0(uVar3);
uVar3 = FUN_1001a0e0(uVar3);
uVar3 = FUN_1001a0e0(uVar3);
uVar3 = FUN_1001a0e0(uVar3);
pbVar4 = (basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_> *)
FUN_1001a0e0(uVar3);
std::basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_>::operator<<
(pbVar4,p_Var13);
}
if (param_2 == 0) {
if (*local_b6c == -1) {
iVar5 = (**(code **)(*param_3 + 0x60))(param_3,&local_b68);
iVar5 = FUN_10048e60(iVar5 == 0,iVar5,300,"preboundreference.cpp");
if (iVar5 == 0) goto LAB_1011543c;
if (local_b68 != (undefined4 *)0x0) {
local_b64 = (int *)0x0;
local_8 = 1;
uVar12 = 0x1011;
iVar5 = (**(code **)(*param_3 + 0x80))(param_3,&local_b64);
if (iVar5 == 0) {
FUN_1008e710(local_b64);
local_8._0_1_ = 2;
FUN_10112f20(local_b90);
local_8._0_1_ = 1;
FUN_10021cc0();
FUN_1005f730(&local_28);
uVar3 = CONCAT22(uVar12,(undefined2)local_20);
cVar1 = FUN_100057b0(local_28,local_24,uVar3,0x138,"preboundreference.cpp");
uVar12 = (undefined2)((uint)uVar3 >> 0x10);
if (cVar1 != '\0') {
FUN_1004c220();
local_8._0_1_ = 3;
FUN_1004c320(param_1[0x14]);
local_b5c[0] = local_b8;
uVar12 = 0x1011;
FUN_10073b80(local_b5c,0);
iVar5 = FUN_1005f730(local_130);
FUN_10040470(*(undefined2 *)(iVar5 + 2));
local_8._0_1_ = 1;
FUN_1002e080();
}
cVar1 = FUN_100057b0(local_28,local_24,CONCAT22(uVar12,(undefined2)local_20),0x144,
"preboundreference.cpp");
if (cVar1 == '\0') {
param_1[0x2a] = 4;
}
else {
iVar5 = FUN_1005f730(local_11c);
if (*(short *)(iVar5 + 10) == 0) {
FUN_101131d0();
}
else {
param_1[0x2a] = 3;
param_1[0x29] = 0;
FUN_10114620();
}
}
}
local_8 = 0xffffffff;
if (local_b64 != (int *)0x0) {
(**(code **)(*local_b64 + 8))(local_b64);
}
goto LAB_1011543c;
}
}
else if (((DAT_101d8c40 == 2) && (param_1[0x29] == 0)) &&
(*(ushort *)(param_1[0x19] + 0x2ac) - 0x7c17 < 0x3e9)) {
cVar1 = FUN_100408d0(uVar2);
if (cVar1 != '\0') {
uVar3 = FUN_10003fc0(*(undefined4 *)local_b6c,*(undefined4 *)(local_b6c + 2),
*(undefined4 *)(local_b6c + 4),*(undefined4 *)(local_b6c + 6));
p_Var13 = endl_exref;
uVar3 = FUN_1001a0e0(*(undefined4 *)(DAT_101d6474 + 0x1c),
L"PreboundReference - attempting local platform <",param_1 + 6,
L"> - status ",uVar3);
uVar3 = FUN_1001dec0(uVar3);
uVar3 = FUN_1001a0e0(uVar3);
pbVar4 = (basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_> *)
FUN_1001a0e0(uVar3);
std::basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_>::operator<<
(pbVar4,p_Var13);
}
param_1[5] = param_1[5] + 1;
param_1[0x2a] = 1;
param_1[0x29] = 1;
FUN_10112cd0();
goto LAB_1011543c;
}
LAB_10115432:
param_1[0x2a] = 4;
goto LAB_1011543c;
}
if (param_2 != 1) goto LAB_1011543c;
param_1[0x2a] = 6;
if (*local_b6c != -1) {
if (*(char *)(param_1 + 0x1b) != '\0') {
if ((uint)param_1[0x21] < 8) {
puVar6 = param_1 + 0x1c;
}
else {
puVar6 = (undefined4 *)param_1[0x1c];
}
iVar5 = (**(code **)(*(int *)param_1[0x14] + 0x44))((int *)param_1[0x14],puVar6);
if (iVar5 < 0) {
/* WARNING: Subroutine does not return */
FUN_1005bf30(iVar5,0,"E:\\BldSrc\\6\\s\\ExtInterfaces\\Lmx\\IMxReferencePtr.h",0x75);
}
*(undefined1 *)(param_1 + 0x1b) = 0;
}
if (*(int *)(local_b6c + 2) == 3) {
LAB_1011502e:
param_1[0x2a] = 6;
goto LAB_1011543c;
}
if ((*(int *)(local_b6c + 2) == 4) && (local_b6c[6] == 0x1f42)) {
local_b70 = operator_new(8);
local_8 = 0xb;
if (local_b70 == (void *)0x0) {
local_b5c[0] = (undefined4 *)0x0;
}
else {
local_b5c[0] = (undefined4 *)FUN_10112b50(param_1);
}
local_8 = 0xffffffff;
cVar1 = FUN_10048d60(local_b5c[0] != (undefined4 *)0x0,0x23b,"preboundreference.cpp");
if (cVar1 != '\0') {
uVar3 = FUN_10016fd0();
FUN_1005f7e0(uVar3);
FUN_1004c220();
local_8 = 0xc;
FUN_1004c320(param_1[0x14]);
local_b68 = local_b8;
FUN_10073b80(&local_b68,0);
param_1[5] = param_1[5] + 1;
param_1[0x2a] = 1;
DVar9 = GetTickCount();
FUN_1008f150(local_b5c[0],0,0,0,0,*(undefined4 *)local_b6c,*(undefined4 *)(local_b6c + 2),
*(undefined4 *)(local_b6c + 4),*(undefined4 *)(local_b6c + 6),0,DVar9);
local_8 = 0xffffffff;
FUN_1002e080();
}
goto LAB_1011543c;
}
goto LAB_10115432;
}
iVar5 = (**(code **)(*param_3 + 0x60))(param_3,&local_b68);
iVar5 = FUN_10048e60(iVar5 == 0,iVar5,0x19d,"preboundreference.cpp");
if (iVar5 == 0) goto LAB_1011543c;
if (local_b68 == (undefined4 *)0x0) goto LAB_1011502e;
local_b64 = (int *)0x0;
local_8 = 6;
uVar12 = 0x1011;
iVar5 = (**(code **)(*param_3 + 0x80))(param_3,&local_b64);
if (iVar5 == 0) {
FUN_1008e710(local_b64);
local_8._0_1_ = 7;
FUN_10112f20(local_ba4);
local_8 = CONCAT31(local_8._1_3_,6);
FUN_10021cc0();
if (*(char *)(param_1 + 0x1b) != '\0') {
piVar11 = param_1 + 0x1c;
if (7 < (uint)param_1[0x21]) {
piVar11 = (int *)*piVar11;
}
FUN_1005f700(piVar11);
*(undefined1 *)(param_1 + 0x1b) = 0;
}
iVar5 = FUN_1005f730(local_108);
if (*(short *)(iVar5 + 10) == 0) {
local_b5d = '\0';
local_b5e = '\0';
puVar6 = (undefined4 *)FUN_1005f730(local_cc);
cVar1 = FUN_100057b0(*puVar6,puVar6[1],CONCAT22(uVar12,*(undefined2 *)(puVar6 + 2)),0x1c1,
"preboundreference.cpp");
if (cVar1 == '\0') {
if (*(char *)(param_1 + 4) != '\0') {
FUN_1004c220();
local_8._0_1_ = 8;
FUN_1004c320(param_1[0x14]);
local_b5c[0] = local_b8;
FUN_10073b80(local_b5c,0);
local_8 = CONCAT31(local_8._1_3_,6);
FUN_1002e080();
}
local_b5e = '\x01';
}
else {
pwVar7 = (wchar_t *)FUN_1005f590();
cVar1 = FUN_10134a10(pwVar7);
if (cVar1 == '\0') {
pwVar8 = wcschr(pwVar7,L'.');
if ((pwVar8 != (wchar_t *)0x0) &&
(_MaxCount = (int)pwVar8 - (int)pwVar7 >> 1, _MaxCount != 0)) {
pwVar8 = (wchar_t *)FUN_1005f610();
iVar5 = _wcsnicmp(pwVar7,pwVar8,_MaxCount);
if (iVar5 == 0) {
_Ch = L'.';
pwVar7 = (wchar_t *)FUN_1005f660();
pwVar7 = wcschr(pwVar7,_Ch);
if (pwVar7 == (wchar_t *)0x0) goto LAB_10114e2f;
}
local_b5d = '\x01';
}
}
}
LAB_10114e2f:
if ((*(char *)(param_1 + 4) == '\0') || ((local_b5d == '\0' && (local_b5e == '\0')))) {
param_1[0x2a] = 4;
}
else {
local_b70 = operator_new(8);
local_8._0_1_ = 9;
if (local_b70 == (void *)0x0) {
local_b5c[0] = (undefined4 *)0x0;
}
else {
local_b5c[0] = (undefined4 *)FUN_10112b50(param_1);
}
local_8 = CONCAT31(local_8._1_3_,6);
cVar1 = FUN_10048d60(local_b5c[0] != (undefined4 *)0x0,0x1ee,"preboundreference.cpp");
if (cVar1 == '\0') {
param_1[0x2a] = 4;
iVar5 = FUN_10022ff0();
if (*(int *)(iVar5 + 0xac) == 0) {
iVar10 = FUN_1002f080();
if (iVar10 != 0) goto LAB_10114f66;
uVar3 = 0;
}
else {
LAB_10114f66:
uVar3 = *(undefined4 *)(iVar5 + 0xac);
}
uVar14 = 0;
FUN_10022ff0(uVar3,0);
cVar1 = FUN_10022ba0(uVar3,uVar14);
if (cVar1 != '\0') {
uVar3 = FUN_1005f590();
uVar3 = FUN_10022ff0(L"PreboundReference::OnSetAttributeResult unable to crreate CPreboundReferenceAdapter for ref %s"
,uVar3);
FUN_10022cb0(uVar3);
}
}
else {
*(undefined1 *)(param_1 + 4) = 0;
uVar3 = FUN_10016fd0();
FUN_1005f7e0(uVar3);
FUN_1008fc40(&DAT_1017a514);
FUN_1008fc70(&DAT_1017a514);
param_1[5] = param_1[5] + 1;
param_1[0x2a] = 1;
DVar9 = GetTickCount();
local_b70 = (void *)CONCAT22(local_b70._2_2_,0x1f42);
local_b7c = (uint)local_b7c._2_2_ << 0x10;
FUN_1008f150(local_b5c[0],0,0,0,0,local_b7c,4,0,local_b70,0,DVar9);
}
}
}
else {
param_1[0x2a] = 3;
param_1[0x29] = 0;
FUN_10114620();
}
}
local_8 = 0xffffffff;
if (local_b64 != (int *)0x0) {
(**(code **)(*local_b64 + 8))(local_b64);
}
LAB_1011543c:
if (*(char *)(param_1 + 0x1b) != '\0') {
if ((uint)param_1[0x21] < 8) {
puVar6 = param_1 + 0x1c;
}
else {
puVar6 = (undefined4 *)param_1[0x1c];
}
iVar5 = (**(code **)(*(int *)param_1[0x14] + 0x44))((int *)param_1[0x14],puVar6);
if (iVar5 < 0) {
/* WARNING: Subroutine does not return */
FUN_1005bf30(iVar5,0,"E:\\BldSrc\\6\\s\\ExtInterfaces\\Lmx\\IMxReferencePtr.h",0x75);
}
*(undefined1 *)(param_1 + 0x1b) = 0;
}
if ((param_1[0x2a] != 1) && (param_1[0x2a] != 2)) {
FUN_10050df0(local_b6c,param_1);
}
piVar11 = param_1 + 5;
*piVar11 = *piVar11 + -1;
if (*piVar11 == 0) {
(**(code **)*param_1)(1);
}
local_b5c[0] = (undefined4 *)param_1[0x2a];
if (param_1[5] == 1) {
local_b68 = param_1;
piVar11 = (int *)FUN_100484c0(&local_b70,&local_b68);
iVar5 = *piVar11;
cVar1 = FUN_10048d60(iVar5 != *(int *)(param_1[0x19] + 0x180),0x273,"preboundreference.cpp");
if (cVar1 != '\0') {
FUN_100382e0(&local_b70,iVar5);
piVar11 = param_1 + 5;
*piVar11 = *piVar11 + -1;
if (*piVar11 == 0) {
(**(code **)*param_1)(1);
}
}
}
cVar1 = FUN_100408d0();
if (cVar1 != '\0') {
uVar3 = FUN_10001e20(local_b5c);
p_Var13 = endl_exref;
uVar3 = FUN_1001a0e0(*(undefined4 *)(DAT_101d6474 + 0x20),
L"PreboundReference::OnSetAttributeResult - EXIT status ",uVar3);
pbVar4 = (basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_> *)
FUN_1001a0e0(uVar3);
std::basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_>::operator<<
(pbVar4,p_Var13);
}
ExceptionList = local_10;
__security_check_cookie(local_14 ^ (uint)&stack0xfffffffc);
return;
}
```
## FUN_100dc750 at 100dc750
Signature: `undefined FUN_100dc750(void)`
```c
void __thiscall FUN_100dc750(int *param_1,uint param_2,int param_3,int param_4,BSTR param_5)
{
char cVar1;
uint uVar2;
undefined4 uVar3;
basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_> *pbVar4;
undefined4 *puVar5;
_func_basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_>_ptr_basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_>_ptr
*p_Var6;
uint local_23c;
int *local_234;
BSTR local_230;
uint local_22c;
int local_228;
int local_224;
BSTR local_220;
wchar_t local_21c [260];
uint local_14;
void *local_10;
undefined1 *puStack_c;
undefined4 local_8;
local_8 = 0xffffffff;
puStack_c = &LAB_1016d12b;
local_10 = ExceptionList;
uVar2 = DAT_101d60b8 ^ (uint)&stack0xfffffffc;
ExceptionList = &local_10;
local_14 = uVar2;
cVar1 = FUN_100408d0(uVar2);
if (cVar1 != '\0') {
local_23c._0_2_ = (short)param_2;
swprintf_s(local_21c,0x104,L"<success %d category %d detectedBy %d detail %d>",
(int)(short)local_23c,param_3,param_4,(int)(short)param_5);
p_Var6 = endl_exref;
uVar3 = FUN_1001a0e0(*(undefined4 *)(DAT_101d6474 + 0x14),
L"DemandReadCallback::CancelWithStatus - status ",local_21c);
pbVar4 = (basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_> *)
FUN_1001a0e0(uVar3);
std::basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_>::operator<<
(pbVar4,p_Var6);
}
if (((param_3 == 5) && (param_4 == 0)) && ((short)param_5 == 1)) {
local_23c = param_2 & 0xffff0000;
local_230 = (BSTR)CONCAT22(local_230._2_2_,5);
local_228 = 3;
local_22c = local_23c;
local_224 = 2;
local_220 = local_230;
}
else {
local_22c = param_2;
local_228 = param_3;
local_224 = param_4;
local_220 = param_5;
}
cVar1 = FUN_100408d0(uVar2);
if (cVar1 != '\0') {
swprintf_s(local_21c,0x104,L"<success %d category %d detectedBy %d detail %d>",
(int)(short)local_22c,local_228,local_224,(int)(short)local_220);
p_Var6 = endl_exref;
uVar3 = FUN_1001a0e0(*(undefined4 *)(DAT_101d6474 + 0x14),
L"DemandReadCallback::CancelWithStatus - Calling OnGetAttributeResult with status "
,local_21c);
pbVar4 = (basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_> *)
FUN_1001a0e0(uVar3);
std::basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_>::operator<<
(pbVar4,p_Var6);
}
puVar5 = (undefined4 *)FUN_10005170();
local_8 = 0;
(**(code **)(*param_1 + 4))(*puVar5,0,DAT_101d6504,DAT_101d6508,&local_22c);
local_8 = 0xffffffff;
if (local_234 != (int *)0x0) {
(**(code **)(*local_234 + 8))(local_234);
}
SysFreeString(local_230);
ExceptionList = local_10;
__security_check_cookie(local_14 ^ (uint)&stack0xfffffffc);
return;
}
```
@@ -0,0 +1,35 @@
# Lmx.dll xrefs
## 0x114a90 at 10114a90
Target function: `FUN_10114a90`
| From | Ref type | Caller function |
| --- | --- | --- |
| `10196410` | `DATA` | `` |
## 0x100dc750 at 100dc750
Target function: `FUN_100dc750`
| From | Ref type | Caller function |
| --- | --- | --- |
| `1018f268` | `DATA` | `` |
## 0x1010b990 at 1010b990
Target function: `FUN_1010b990`
| From | Ref type | Caller function |
| --- | --- | --- |
| `1010cf49` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010e440` | `UNCONDITIONAL_CALL` | `FUN_1010e410` |
## 0x1010dc80 at 1010dc80
Target function: `FUN_1010dc80`
| From | Ref type | Caller function |
| --- | --- | --- |
| `10195488` | `DATA` | `` |
@@ -0,0 +1,119 @@
# Lmx.dll xrefs
## 0x1010bd10 at 1010bd10
Target function: `FUN_1010bd10`
| From | Ref type | Caller function |
| --- | --- | --- |
| `1010d89b` | `UNCONDITIONAL_CALL` | `FUN_1010d4a0` |
## 0x1010e410 at 1010e410
Target function: `FUN_1010e410`
| From | Ref type | Caller function |
| --- | --- | --- |
| `101956a8` | `DATA` | `` |
## 0x10101360 at 10101360
Target function: `FUN_10101360`
| From | Ref type | Caller function |
| --- | --- | --- |
| `10061e82` | `UNCONDITIONAL_CALL` | `FUN_10061c60` |
| `10110335` | `UNCONDITIONAL_CALL` | `` |
## 0x10100ce0 at 10100ce0
Target function: `FUN_10100ce0`
| From | Ref type | Caller function |
| --- | --- | --- |
| `1010c2ea` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010c474` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010c50d` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010c5fb` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010c8ac` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010ca5f` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010cb16` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010cd61` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010f27d` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
| `1010f365` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
| `1010fa8d` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
| `1010facf` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
## 0x10100bc0 at 10100bc0
Target function: `FUN_10100bc0`
| From | Ref type | Caller function |
| --- | --- | --- |
| `1010c47e` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010c605` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010ca69` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010cb20` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
## 0x1005e580 at 1005e580
Target function: `FUN_1005e580`
| From | Ref type | Caller function |
| --- | --- | --- |
| `1010c612` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010ca76` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010f51a` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
| `1010f8bb` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
| `1010fa76` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
| `1010fab8` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
| `10110415` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
| `10110440` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
| `10110513` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
| `101116fc` | `UNCONDITIONAL_CALL` | `` |
| `10111ab6` | `UNCONDITIONAL_CALL` | `` |
| `10111d51` | `UNCONDITIONAL_CALL` | `` |
| `10111884` | `UNCONDITIONAL_CALL` | `` |
| `101118d2` | `UNCONDITIONAL_CALL` | `` |
| `10111b52` | `UNCONDITIONAL_CALL` | `` |
| `10110be2` | `UNCONDITIONAL_CALL` | `FUN_10110986` |
| `10110c03` | `UNCONDITIONAL_CALL` | `FUN_10110986` |
## 0x10067aa0 at 10067aa0
Target function: `FUN_10067aa0`
| From | Ref type | Caller function |
| --- | --- | --- |
| `1006afb2` | `UNCONDITIONAL_CALL` | `FUN_10069c30` |
| `101044a9` | `UNCONDITIONAL_CALL` | `Catch@10104467` |
| `100fd351` | `UNCONDITIONAL_CALL` | `FUN_100fd200` |
| `10067d89` | `UNCONDITIONAL_CALL` | `FUN_10067d30` |
| `100fd560` | `UNCONDITIONAL_CALL` | `FUN_100fd400` |
| `100ffe3a` | `UNCONDITIONAL_CALL` | `FUN_100ffc90` |
| `1010951d` | `UNCONDITIONAL_CALL` | `FUN_10107880` |
| `1010bfcc` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010c250` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010c2bb` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010c7a4` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010d27a` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010f497` | `UNCONDITIONAL_CALL` | `FUN_1010ee00` |
| `10070743` | `UNCONDITIONAL_CALL` | `FUN_10070360` |
| `10070869` | `UNCONDITIONAL_CALL` | `FUN_10070360` |
| `10070a01` | `UNCONDITIONAL_CALL` | `FUN_10070360` |
## 0x100860c0 at 100860c0
Target function: `FUN_100860c0`
| From | Ref type | Caller function |
| --- | --- | --- |
| `10069f19` | `UNCONDITIONAL_CALL` | `FUN_10069c30` |
| `1006a588` | `UNCONDITIONAL_CALL` | `FUN_10069c30` |
| `10138a4b` | `UNCONDITIONAL_CALL` | `FUN_101389c0` |
| `1010c158` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010c206` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010c5ba` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010d0b4` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
| `1010d167` | `UNCONDITIONAL_CALL` | `FUN_1010bd10` |
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,574 @@
# Lmx.dll selected decompile
## FUN_10003fc0 at 10003fc0
Signature: `undefined FUN_10003fc0(void)`
```c
wchar_t * __thiscall
FUN_10003fc0(wchar_t *param_1,short param_2,undefined4 param_3,undefined4 param_4,short param_5)
{
swprintf_s(param_1,0x104,L"<success %d category %d detectedBy %d detail %d>",(int)param_2,param_3,
param_4,(int)param_5);
return param_1;
}
```
## FUN_10016fd0 at 10016fd0
Signature: `undefined FUN_10016fd0(void)`
```c
void __fastcall FUN_10016fd0(int *param_1)
{
byte bStack_17;
*param_1 = (uint)bStack_17 << 8;
param_1[1] = 0;
param_1[2] = 0;
param_1[3] = 0;
param_1[4] = 0;
return;
}
```
## FUN_1008f150 at 1008f150
Signature: `undefined FUN_1008f150(void)`
```c
void __thiscall
FUN_1008f150(int param_1,int *param_2,undefined4 param_3,undefined4 param_4,undefined4 param_5,
undefined4 param_6,undefined4 param_7,undefined4 param_8,undefined4 param_9,
undefined4 param_10,undefined4 param_11)
{
int iVar1;
char cVar2;
undefined4 uVar3;
undefined4 uVar4;
basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_> *pbVar5;
int iVar6;
int *piVar7;
_func_basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_>_ptr_basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_>_ptr
*p_Var8;
int *local_258;
undefined4 local_254;
ulong local_250;
ushort local_24c;
ushort uStack_24a;
uchar local_248 [4];
uchar local_244 [4];
undefined4 local_240;
undefined4 local_23c;
undefined4 local_238;
undefined4 local_234;
undefined4 local_230;
undefined4 local_22c;
undefined4 local_228;
undefined4 local_224;
GUID local_220 [33];
uint local_8;
local_8 = DAT_101d60b8 ^ (uint)&stack0xfffffffc;
CoCreateGuid(local_220);
cVar2 = FUN_100408d0();
if (cVar2 != '\0') {
uVar3 = FUN_10047fe0(local_220[0].Data1,local_220[0]._4_4_,local_220[0].Data4._0_4_,
local_220[0].Data4._4_4_);
p_Var8 = endl_exref;
uVar4 = (**(code **)(*param_2 + 8))();
piVar7 = param_2;
pbVar5 = (basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_> *)
FUN_1001a0e0(*(undefined4 *)(DAT_101d6474 + 0x38),
L"CReferenceStringResolver::ResolveReference - reference ",param_2,
L" guid ",uVar3,L" ref ",uVar4);
pbVar5 = std::basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_>::operator<<
(pbVar5,piVar7);
uVar3 = FUN_1001a0e0(pbVar5);
uVar3 = FUN_1001a0e0(uVar3);
uVar3 = FUN_1001a0e0(uVar3);
pbVar5 = (basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_> *)
FUN_1001a0e0(uVar3);
std::basic_ostream<unsigned_short,struct_std::char_traits<unsigned_short>_>::operator<<
(pbVar5,p_Var8);
}
local_250 = local_220[0].Data1;
local_24c = local_220[0].Data2;
uStack_24a = local_220[0].Data3;
local_244[0] = local_220[0].Data4[4];
local_244[1] = local_220[0].Data4[5];
local_244[2] = local_220[0].Data4[6];
local_244[3] = local_220[0].Data4[7];
local_254 = param_11;
local_240 = param_7;
local_238 = param_9;
local_248[0] = local_220[0].Data4[0];
local_248[1] = local_220[0].Data4[1];
local_248[2] = local_220[0].Data4[2];
local_248[3] = local_220[0].Data4[3];
local_234 = param_10;
local_258 = param_2;
iVar1 = *(int *)(param_1 + 0x24);
local_22c = param_4;
local_23c = param_8;
local_228 = param_5;
local_230 = param_3;
local_224 = param_6;
iVar6 = FUN_1008e910(iVar1,*(undefined4 *)(iVar1 + 4),&local_258);
if (*(int *)(param_1 + 0x28) == 0x4924923) {
/* WARNING: Subroutine does not return */
std::_Xlength_error("list<T> too long");
}
*(int *)(param_1 + 0x28) = *(int *)(param_1 + 0x28) + 1;
*(int *)(iVar1 + 4) = iVar6;
**(int **)(iVar6 + 4) = iVar6;
__security_check_cookie(local_8 ^ (uint)&stack0xfffffffc);
return;
}
```
## FUN_10112f20 at 10112f20
Signature: `undefined FUN_10112f20(void)`
```c
int * __thiscall FUN_10112f20(int *param_1,int *param_2)
{
UINT UVar1;
BSTR pOVar2;
if (*param_1 != *param_2) {
AtlComPtrAssign(param_1,*param_2);
}
if ((BSTR)param_1[1] != (BSTR)param_2[1]) {
SysFreeString((BSTR)param_1[1]);
pOVar2 = (BSTR)0x0;
if ((BSTR)param_2[1] != (BSTR)0x0) {
UVar1 = SysStringByteLen((BSTR)param_2[1]);
pOVar2 = SysAllocStringByteLen((LPCSTR)param_2[1],UVar1);
}
param_1[1] = (int)pOVar2;
if ((param_2[1] != 0) && (pOVar2 == (BSTR)0x0)) {
/* WARNING: Subroutine does not return */
FUN_100013e0(0x8007000e);
}
}
if ((BSTR)param_1[2] != (BSTR)param_2[2]) {
SysFreeString((BSTR)param_1[2]);
pOVar2 = (BSTR)0x0;
if ((BSTR)param_2[2] != (BSTR)0x0) {
UVar1 = SysStringByteLen((BSTR)param_2[2]);
pOVar2 = SysAllocStringByteLen((LPCSTR)param_2[2],UVar1);
}
param_1[2] = (int)pOVar2;
if ((param_2[2] != 0) && (pOVar2 == (BSTR)0x0)) {
/* WARNING: Subroutine does not return */
FUN_100013e0(0x8007000e);
}
}
if ((BSTR)param_1[3] != (BSTR)param_2[3]) {
SysFreeString((BSTR)param_1[3]);
pOVar2 = (BSTR)0x0;
if ((BSTR)param_2[3] != (BSTR)0x0) {
UVar1 = SysStringByteLen((BSTR)param_2[3]);
pOVar2 = SysAllocStringByteLen((LPCSTR)param_2[3],UVar1);
}
param_1[3] = (int)pOVar2;
if ((param_2[3] != 0) && (pOVar2 == (BSTR)0x0)) {
/* WARNING: Subroutine does not return */
FUN_100013e0(0x8007000e);
}
}
if ((BSTR)param_1[4] != (BSTR)param_2[4]) {
SysFreeString((BSTR)param_1[4]);
pOVar2 = (BSTR)0x0;
if ((BSTR)param_2[4] != (BSTR)0x0) {
UVar1 = SysStringByteLen((BSTR)param_2[4]);
pOVar2 = SysAllocStringByteLen((LPCSTR)param_2[4],UVar1);
}
param_1[4] = (int)pOVar2;
if ((param_2[4] != 0) && (pOVar2 == (BSTR)0x0)) {
/* WARNING: Subroutine does not return */
FUN_100013e0(0x8007000e);
}
}
return param_1;
}
```
## FUN_10112da0 at 10112da0
Signature: `undefined FUN_10112da0(void)`
```c
void __fastcall FUN_10112da0(undefined4 *param_1)
{
int *piVar1;
uint uVar2;
void *local_10;
undefined1 *puStack_c;
int local_8;
puStack_c = &LAB_1017253f;
local_10 = ExceptionList;
uVar2 = DAT_101d60b8 ^ (uint)&stack0xfffffffc;
ExceptionList = &local_10;
*param_1 = PreboundReference::vftable;
param_1[1] = PreboundReference::vftable;
param_1[3] = PreboundReference::vftable;
local_8 = 7;
piVar1 = (int *)param_1[0x19];
if (piVar1 != (int *)0x0) {
(**(code **)(*piVar1 + 8))(piVar1,uVar2);
}
local_8._0_1_ = 6;
SysFreeString((BSTR)param_1[0x2b]);
local_8._0_1_ = 5;
SysFreeString((BSTR)param_1[0x27]);
local_8._0_1_ = 4;
if (7 < (uint)param_1[0x21]) {
operator_delete((void *)param_1[0x1c]);
}
param_1[0x21] = 7;
param_1[0x20] = 0;
*(undefined2 *)(param_1 + 0x1c) = 0;
local_8._0_1_ = 3;
piVar1 = (int *)param_1[0x1a];
if (piVar1 != (int *)0x0) {
(**(code **)(*piVar1 + 8))(piVar1);
}
local_8._0_1_ = 0xc;
SysFreeString((BSTR)param_1[0x18]);
local_8._0_1_ = 0xb;
SysFreeString((BSTR)param_1[0x17]);
local_8._0_1_ = 10;
SysFreeString((BSTR)param_1[0x16]);
local_8._0_1_ = 9;
SysFreeString((BSTR)param_1[0x15]);
local_8._0_1_ = 2;
piVar1 = (int *)param_1[0x14];
if (piVar1 != (int *)0x0) {
(**(code **)(*piVar1 + 8))(piVar1);
}
local_8._0_1_ = 1;
if (7 < (uint)param_1[0x12]) {
operator_delete((void *)param_1[0xd]);
}
param_1[0x12] = 7;
param_1[0x11] = 0;
*(undefined2 *)(param_1 + 0xd) = 0;
local_8 = (uint)local_8._1_3_ << 8;
if (7 < (uint)param_1[0xb]) {
operator_delete((void *)param_1[6]);
}
param_1[0xb] = 7;
param_1[10] = 0;
*(undefined2 *)(param_1 + 6) = 0;
*param_1 = MxConnectionCallback::vftable;
ExceptionList = local_10;
return;
}
```
## FUN_101139c0 at 101139c0
Signature: `undefined FUN_101139c0(void)`
```c
undefined4 * __thiscall FUN_101139c0(undefined4 *param_1,short *param_2,undefined4 param_3)
{
short sVar1;
uint uVar2;
short *psVar3;
void *local_10;
undefined1 *puStack_c;
undefined4 local_8;
puStack_c = &LAB_101726b3;
local_10 = ExceptionList;
uVar2 = DAT_101d60b8 ^ (uint)&stack0xfffffffc;
ExceptionList = &local_10;
*param_1 = MxConnectionCallback::vftable;
local_8 = 0;
param_1[1] = CReferenceToResolve::vftable;
*(undefined1 *)(param_1 + 2) = 0;
param_1[3] = RedundancyResolutionStatusCallback::vftable;
*param_1 = PreboundReference::vftable;
param_1[1] = PreboundReference::vftable;
param_1[3] = PreboundReference::vftable;
*(undefined1 *)(param_1 + 4) = 0;
param_1[5] = 0;
param_1[0xb] = 7;
param_1[10] = 0;
*(undefined2 *)(param_1 + 6) = 0;
psVar3 = param_2;
do {
sVar1 = *psVar3;
psVar3 = psVar3 + 1;
} while (sVar1 != 0);
FUN_100363d0(param_2,(int)psVar3 - (int)(param_2 + 1) >> 1);
local_8._1_3_ = (undefined3)((uint)local_8 >> 8);
param_1[0x12] = 7;
param_1[0x11] = 0;
*(undefined2 *)(param_1 + 0xd) = 0;
local_8._0_1_ = 2;
FUN_10113900(param_2);
param_1[0x19] = param_3;
param_1[0x1a] = 0;
*(undefined1 *)(param_1 + 0x1b) = 0;
param_1[0x21] = 7;
param_1[0x20] = 0;
*(undefined2 *)(param_1 + 0x1c) = 0;
param_1[0x27] = 0;
param_1[0x2a] = 0;
param_1[0x2b] = 0;
local_8 = CONCAT31(local_8._1_3_,8);
*(undefined1 *)(param_1 + 0x2c) = 0;
param_1[0x29] = 0;
if (DAT_101d8c40 == 0) {
FUN_10113070(uVar2);
}
FUN_101133d0();
(**(code **)(*(int *)param_1[0x19] + 4))((int *)param_1[0x19]);
ExceptionList = local_10;
return param_1;
}
```
## FUN_10113b10 at 10113b10
Signature: `undefined FUN_10113b10(void)`
```c
undefined4 * __thiscall FUN_10113b10(undefined4 *param_1,int *param_2,undefined4 param_3)
{
undefined4 *puVar1;
short sVar2;
char cVar3;
uint uVar4;
int iVar5;
short *psVar6;
short *psVar7;
undefined2 *puVar8;
void *local_10;
undefined1 *puStack_c;
undefined1 local_8;
undefined3 uStack_7;
puStack_c = &LAB_1017276f;
local_10 = ExceptionList;
uVar4 = DAT_101d60b8 ^ (uint)&stack0xfffffffc;
ExceptionList = &local_10;
*param_1 = MxConnectionCallback::vftable;
param_1[1] = CReferenceToResolve::vftable;
*(undefined1 *)(param_1 + 2) = 0;
param_1[3] = RedundancyResolutionStatusCallback::vftable;
*param_1 = PreboundReference::vftable;
param_1[1] = PreboundReference::vftable;
param_1[3] = PreboundReference::vftable;
*(undefined1 *)(param_1 + 4) = 0;
param_1[5] = 0;
param_1[0xb] = 7;
param_1[10] = 0;
*(undefined2 *)(param_1 + 6) = 0;
param_1[0x12] = 7;
param_1[0x11] = 0;
*(undefined2 *)(param_1 + 0xd) = 0;
uStack_7 = 0;
local_8 = 2;
param_1[0x14] = param_2;
if (param_2 != (int *)0x0) {
(**(code **)(*param_2 + 4))(param_2,uVar4);
}
param_1[0x15] = 0;
param_1[0x16] = 0;
param_1[0x17] = 0;
param_1[0x18] = 0;
param_1[0x19] = param_3;
param_1[0x1a] = 0;
*(undefined1 *)(param_1 + 0x1b) = 0;
param_1[0x21] = 7;
param_1[0x20] = 0;
*(undefined2 *)(param_1 + 0x1c) = 0;
param_1[0x27] = 0;
param_1[0x2a] = 0;
param_1[0x2b] = 0;
_local_8 = CONCAT31(uStack_7,0xe);
*(undefined1 *)(param_1 + 0x2c) = 0;
param_1[0x29] = 0;
if (DAT_101d8c40 == 0) {
FUN_10113070();
}
if (param_2 != (int *)0x0) {
puVar1 = param_1 + 0x15;
SysFreeString((BSTR)param_1[0x15]);
*puVar1 = 0;
iVar5 = (**(code **)(*(int *)param_1[0x14] + 0x20))((int *)param_1[0x14],puVar1);
if (iVar5 < 0) {
/* WARNING: Subroutine does not return */
FUN_1005bf30(iVar5,0,"E:\\BldSrc\\6\\s\\ExtInterfaces\\Lmx\\IMxReferencePtr.h",0x3f);
}
psVar7 = (short *)*puVar1;
if (psVar7 == (short *)0x0) {
psVar7 = &DAT_1017a514;
}
psVar6 = psVar7;
do {
sVar2 = *psVar6;
psVar6 = psVar6 + 1;
} while (sVar2 != 0);
FUN_100363d0(psVar7,(int)psVar6 - (int)(psVar7 + 1) >> 1);
puVar1 = param_1 + 0x15;
SysFreeString((BSTR)param_1[0x15]);
*puVar1 = 0;
iVar5 = (**(code **)(*(int *)param_1[0x14] + 0x20))((int *)param_1[0x14],puVar1);
if (iVar5 < 0) {
/* WARNING: Subroutine does not return */
FUN_1005bf30(iVar5,0,"E:\\BldSrc\\6\\s\\ExtInterfaces\\Lmx\\IMxReferencePtr.h",0x3f);
}
puVar8 = (undefined2 *)*puVar1;
if (puVar8 == (undefined2 *)0x0) {
puVar8 = &DAT_1017a514;
}
cVar3 = FUN_10134a10(puVar8);
if (cVar3 != '\0') {
psVar6 = (short *)FUN_1005f6b0();
psVar7 = psVar6;
do {
sVar2 = *psVar7;
psVar7 = psVar7 + 1;
} while (sVar2 != 0);
FUN_100363d0(psVar6,(int)psVar7 - (int)(psVar6 + 1) >> 1);
}
}
FUN_101133d0();
(**(code **)(*(int *)param_1[0x19] + 4))((int *)param_1[0x19]);
ExceptionList = local_10;
return param_1;
}
```
## FUN_10114620 at 10114620
Signature: `undefined FUN_10114620(void)`
```c
void __fastcall FUN_10114620(int param_1)
{
char cVar1;
undefined4 uVar2;
undefined4 uVar3;
undefined1 local_18 [4];
void *local_14;
void *local_10;
undefined1 *puStack_c;
undefined4 local_8;
local_8 = 0xffffffff;
puStack_c = &LAB_10172866;
local_10 = ExceptionList;
ExceptionList = &local_10;
if (*(char *)(*(int *)(param_1 + 100) + 0x6dc) != '\0') {
cVar1 = FUN_1005faf0(DAT_101d60b8 ^ (uint)&stack0xfffffffc);
if (cVar1 == '\0') {
local_14 = operator_new(0x1c);
local_8 = 0;
if (local_14 == (void *)0x0) {
local_14 = (void *)0x0;
}
else {
local_14 = (void *)FUN_10060b80();
}
local_8 = 0xffffffff;
FUN_1003ec10(*(undefined4 *)(param_1 + 0x50));
uVar3 = 0;
uVar2 = FUN_1002c750(&local_14);
FUN_10066e70(local_18,uVar2,uVar3);
local_14 = operator_new(0x1c);
local_8 = 1;
if (local_14 == (void *)0x0) {
local_14 = (void *)0x0;
}
else {
local_14 = (void *)FUN_10060b80();
}
local_8 = 0xffffffff;
FUN_1003ec10(*(undefined4 *)(param_1 + 0x50));
uVar3 = 0;
uVar2 = FUN_1002c750(&local_14);
FUN_10066e70(local_18,uVar2,uVar3);
*(undefined1 *)(*(int *)(param_1 + 100) + 0x6dd) = 1;
}
}
ExceptionList = local_10;
return;
}
```
## FUN_10112cd0 at 10112cd0
Signature: `undefined FUN_10112cd0(void)`
```c
void __fastcall FUN_10112cd0(int param_1)
{
uint uVar1;
void *pvVar2;
void *local_10;
undefined1 *puStack_c;
undefined4 local_8;
local_8 = 0xffffffff;
puStack_c = &LAB_1017247b;
local_10 = ExceptionList;
uVar1 = DAT_101d60b8 ^ (uint)&stack0xfffffffc;
ExceptionList = &local_10;
pvVar2 = operator_new(0x38);
local_8 = 0;
if (pvVar2 != (void *)0x0) {
FUN_1009f240(*(undefined4 *)(param_1 + 0x50),*(undefined2 *)(*(int *)(param_1 + 100) + 0x2ac),
param_1,*(int *)(param_1 + 100));
}
local_8 = 0xffffffff;
(*(code *)**(undefined4 **)(param_1 + 4))(uVar1);
ExceptionList = local_10;
return;
}
```
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,134 @@
# Lmx.dll xrefs
## 0x10196410 at 10196410
Target function: `(none)`
| From | Ref type | Caller function |
| --- | --- | --- |
| (none) | | |
## 0x10196400 at 10196400
Target function: `(none)`
| From | Ref type | Caller function |
| --- | --- | --- |
| (none) | | |
## 0x101963f8 at 101963f8
Target function: `(none)`
| From | Ref type | Caller function |
| --- | --- | --- |
| (none) | | |
## 0x101963f0 at 101963f0
Target function: `(none)`
| From | Ref type | Caller function |
| --- | --- | --- |
| `10112dd3` | `DATA` | `FUN_10112da0` |
| `10113a13` | `DATA` | `FUN_101139c0` |
| `10113b5f` | `DATA` | `FUN_10113b10` |
## 0x101963e8 at 101963e8
Target function: `(none)`
| From | Ref type | Caller function |
| --- | --- | --- |
| `10112dda` | `DATA` | `FUN_10112da0` |
| `10113a1a` | `DATA` | `FUN_101139c0` |
| `10113b66` | `DATA` | `FUN_10113b10` |
## 0x101963e0 at 101963e0
Target function: `(none)`
| From | Ref type | Caller function |
| --- | --- | --- |
| (none) | | |
## 0x10196418 at 10196418
Target function: `(none)`
| From | Ref type | Caller function |
| --- | --- | --- |
| (none) | | |
## 0x10196420 at 10196420
Target function: `(none)`
| From | Ref type | Caller function |
| --- | --- | --- |
| (none) | | |
## 0x1018f268 at 1018f268
Target function: `(none)`
| From | Ref type | Caller function |
| --- | --- | --- |
| (none) | | |
## 0x1018f260 at 1018f260
Target function: `(none)`
| From | Ref type | Caller function |
| --- | --- | --- |
| (none) | | |
## 0x1018f258 at 1018f258
Target function: `(none)`
| From | Ref type | Caller function |
| --- | --- | --- |
| (none) | | |
## 0x1018f270 at 1018f270
Target function: `(none)`
| From | Ref type | Caller function |
| --- | --- | --- |
| (none) | | |
## 0x10195488 at 10195488
Target function: `(none)`
| From | Ref type | Caller function |
| --- | --- | --- |
| (none) | | |
## 0x10195480 at 10195480
Target function: `(none)`
| From | Ref type | Caller function |
| --- | --- | --- |
| (none) | | |
## 0x10195478 at 10195478
Target function: `(none)`
| From | Ref type | Caller function |
| --- | --- | --- |
| (none) | | |
## 0x10195490 at 10195490
Target function: `(none)`
| From | Ref type | Caller function |
| --- | --- | --- |
| (none) | | |
@@ -0,0 +1,448 @@
# LmxProxy.dll selected decompile
## FUN_10015f72 at 10015f72
Signature: `undefined __thiscall FUN_10015f72(void * this, long param_1, long param_2, undefined4 param_3)`
```c
/* WARNING: Function: __EH_prolog3 replaced with injection: EH_prolog3 */
/* WARNING: Function: __EH_epilog3 replaced with injection: EH_epilog3 */
void __thiscall FUN_10015f72(void *this,long param_1,long param_2,undefined4 param_3)
{
basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> bVar1;
undefined4 *puVar2;
int *piVar3;
basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *pbVar4;
undefined4 *this_00;
long in_stack_0000001c;
undefined4 in_stack_00000030;
long lVar5;
wchar_t *pwVar6;
long lVar7;
wchar_t *pwVar8;
long lVar9;
wchar_t *pwVar10;
ushort uVar11;
undefined4 uVar12;
_func_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_ptr_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_ptr
*p_Var13;
undefined4 *local_30;
undefined4 local_2c;
undefined4 local_28;
undefined4 local_24;
int *local_20;
int local_1c;
void *local_18;
int local_14;
undefined4 local_8;
undefined4 uStack_4;
uStack_4 = 0x20;
local_8 = 0x10015f7e;
puVar2 = (undefined4 *)FUN_100170a4(100);
if (puVar2 == (undefined4 *)0x0) {
this_00 = (undefined4 *)0x0;
}
else {
this_00 = puVar2 + 1;
*puVar2 = 6;
_eh_vector_constructor_iterator_(this_00,0x10,6,FUN_10001517,FUN_10001f45);
}
local_1c = *(int *)((int)this + 8);
local_14 = 0;
if (0 < local_1c) {
local_18 = (void *)((int)this + 4);
do {
piVar3 = (int *)FUN_10007d02(local_18,local_14);
local_20 = piVar3;
if (piVar3 != (int *)0x0) {
(**(code **)(*piVar3 + 4))(piVar3);
}
local_8 = 1;
if (piVar3 != (int *)0x0) {
FUN_10015d08(this_00 + 0x14,param_1);
FUN_10015d08(this_00 + 0x10,param_2);
if ((CComVariant *)(this_00 + 0xc) != (CComVariant *)&param_3) {
ATL::CComVariant::InternalCopy((CComVariant *)(this_00 + 0xc),(tagVARIANT *)&param_3);
}
FUN_10015d08(this_00 + 8,in_stack_0000001c);
if ((CComVariant *)(this_00 + 4) != (CComVariant *)&stack0x00000020) {
ATL::CComVariant::InternalCopy
((CComVariant *)(this_00 + 4),(tagVARIANT *)&stack0x00000020);
}
*(undefined2 *)this_00 = 0x6024;
this_00[2] = in_stack_00000030;
local_2c = 0;
local_28 = 6;
local_24 = 0;
local_30 = this_00;
bVar1 = FUN_10003f01(*(basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> **)
(DAT_100294e0 + 0x10));
if (bVar1 != (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>)0x0) {
pwVar10 = L" Item Data Type ";
pwVar8 = L" item Quality ";
pwVar6 = L" Item Handle ";
lVar5 = param_1;
lVar7 = param_2;
lVar9 = in_stack_0000001c;
uVar12 = param_3;
p_Var13 = endl_exref;
pbVar4 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf(*(int **)(DAT_100294e0 + 0x10),
L"CProxy_ILMXProxyServerEvents::Fire_OnDataChange firing event - Server Handle "
);
uVar11 = (ushort)uVar12;
pbVar4 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar4,lVar5);
pbVar4 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf((int *)pbVar4,pwVar6);
pbVar4 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar4,lVar7);
pbVar4 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf((int *)pbVar4,pwVar8);
pbVar4 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar4,lVar9);
pbVar4 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf((int *)pbVar4,pwVar10);
pbVar4 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar4,uVar11);
std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<(pbVar4,p_Var13);
}
(**(code **)(*piVar3 + 0x18))(piVar3,1,&DAT_100201f8,0x400,1,&local_30,0,0,0);
}
local_8 = 0xffffffff;
if (piVar3 != (int *)0x0) {
(**(code **)(*piVar3 + 8))(piVar3);
}
local_14 = local_14 + 1;
} while (local_14 < local_1c);
}
if (this_00 != (undefined4 *)0x0) {
FUN_10015d66(this_00,3);
}
return;
}
```
## FUN_1001611f at 1001611f
Signature: `undefined __thiscall FUN_1001611f(void * this, long param_1, long param_2, undefined4 param_3)`
```c
/* WARNING: Function: __EH_prolog3 replaced with injection: EH_prolog3 */
/* WARNING: Function: __EH_epilog3 replaced with injection: EH_epilog3 */
void __thiscall FUN_1001611f(void *this,long param_1,long param_2,undefined4 param_3)
{
basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> bVar1;
undefined4 *puVar2;
int *piVar3;
basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *pbVar4;
undefined4 *this_00;
long lVar5;
wchar_t *pwVar6;
long lVar7;
_func_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_ptr_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_ptr
*p_Var8;
undefined4 *local_30;
undefined4 local_2c;
undefined4 local_28;
undefined4 local_24;
int *local_20;
int local_1c;
void *local_18;
int local_14;
undefined4 local_8;
undefined4 uStack_4;
uStack_4 = 0x20;
local_8 = 0x1001612b;
puVar2 = (undefined4 *)FUN_100170a4(0x34);
if (puVar2 == (undefined4 *)0x0) {
this_00 = (undefined4 *)0x0;
}
else {
this_00 = puVar2 + 1;
*puVar2 = 3;
_eh_vector_constructor_iterator_(this_00,0x10,3,FUN_10001517,FUN_10001f45);
}
local_1c = *(int *)((int)this + 8);
local_14 = 0;
if (0 < local_1c) {
local_18 = (void *)((int)this + 4);
do {
piVar3 = (int *)FUN_10007d02(local_18,local_14);
local_20 = piVar3;
if (piVar3 != (int *)0x0) {
(**(code **)(*piVar3 + 4))(piVar3);
}
local_8 = 1;
if (piVar3 != (int *)0x0) {
FUN_10015d08(this_00 + 8,param_1);
FUN_10015d08(this_00 + 4,param_2);
*(undefined2 *)this_00 = 0x6024;
this_00[2] = param_3;
local_2c = 0;
local_28 = 3;
local_24 = 0;
local_30 = this_00;
bVar1 = FUN_10003f01(*(basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> **)
(DAT_100294e0 + 0xc));
if (bVar1 != (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>)0x0) {
pwVar6 = L" Item Handle ";
lVar5 = param_1;
lVar7 = param_2;
p_Var8 = endl_exref;
pbVar4 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf(*(int **)(DAT_100294e0 + 0xc),
L"CProxy_ILMXProxyServerEvents::Fire_OnWriteComplete firing event - Server Handle "
);
pbVar4 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar4,lVar5);
pbVar4 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf((int *)pbVar4,pwVar6);
pbVar4 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar4,lVar7);
std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<(pbVar4,p_Var8);
}
(**(code **)(*piVar3 + 0x18))(piVar3,2,&DAT_100201f8,0x400,1,&local_30,0,0,0);
}
local_8 = 0xffffffff;
if (piVar3 != (int *)0x0) {
(**(code **)(*piVar3 + 8))(piVar3);
}
local_14 = local_14 + 1;
} while (local_14 < local_1c);
}
if (this_00 != (undefined4 *)0x0) {
FUN_10015d66(this_00,3);
}
return;
}
```
## FUN_10016271 at 10016271
Signature: `undefined __thiscall FUN_10016271(void * this, long param_1, long param_2, undefined4 param_3)`
```c
/* WARNING: Function: __EH_prolog3 replaced with injection: EH_prolog3 */
/* WARNING: Function: __EH_epilog3 replaced with injection: EH_epilog3 */
void __thiscall FUN_10016271(void *this,long param_1,long param_2,undefined4 param_3)
{
basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> bVar1;
undefined4 *puVar2;
int *piVar3;
basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *pbVar4;
undefined4 *this_00;
long lVar5;
wchar_t *pwVar6;
long lVar7;
_func_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_ptr_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_ptr
*p_Var8;
undefined4 *local_30;
undefined4 local_2c;
undefined4 local_28;
undefined4 local_24;
int *local_20;
int local_1c;
void *local_18;
int local_14;
undefined4 local_8;
undefined4 uStack_4;
uStack_4 = 0x20;
local_8 = 0x1001627d;
puVar2 = (undefined4 *)FUN_100170a4(0x34);
if (puVar2 == (undefined4 *)0x0) {
this_00 = (undefined4 *)0x0;
}
else {
this_00 = puVar2 + 1;
*puVar2 = 3;
_eh_vector_constructor_iterator_(this_00,0x10,3,FUN_10001517,FUN_10001f45);
}
local_1c = *(int *)((int)this + 8);
local_14 = 0;
if (0 < local_1c) {
local_18 = (void *)((int)this + 4);
do {
piVar3 = (int *)FUN_10007d02(local_18,local_14);
local_20 = piVar3;
if (piVar3 != (int *)0x0) {
(**(code **)(*piVar3 + 4))(piVar3);
}
local_8 = 1;
if (piVar3 != (int *)0x0) {
FUN_10015d08(this_00 + 8,param_1);
FUN_10015d08(this_00 + 4,param_2);
local_2c = 0;
local_24 = 0;
*(undefined2 *)this_00 = 0x6024;
this_00[2] = param_3;
local_28 = 3;
local_30 = this_00;
bVar1 = FUN_10003f01(*(basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> **)
(DAT_100294e0 + 0xc));
if (bVar1 != (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>)0x0) {
pwVar6 = L" Item Handle ";
lVar5 = param_1;
lVar7 = param_2;
p_Var8 = endl_exref;
pbVar4 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf(*(int **)(DAT_100294e0 + 0xc),
L"CProxy_ILMXProxyServerEvents::Fire_OperationComplete firing event - Server Handle "
);
pbVar4 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar4,lVar5);
pbVar4 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf((int *)pbVar4,pwVar6);
pbVar4 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar4,lVar7);
std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<(pbVar4,p_Var8);
}
(**(code **)(*piVar3 + 0x18))(piVar3,3,&DAT_100201f8,0x400,1,&local_30,0,0,0);
}
local_8 = 0xffffffff;
if (piVar3 != (int *)0x0) {
(**(code **)(*piVar3 + 8))(piVar3);
}
local_14 = local_14 + 1;
} while (local_14 < local_1c);
}
if (this_00 != (undefined4 *)0x0) {
FUN_10015d66(this_00,3);
}
return;
}
```
## FUN_100163c0 at 100163c0
Signature: `undefined __thiscall FUN_100163c0(void * this, long param_1, long param_2, undefined4 param_3)`
```c
/* WARNING: Function: __EH_prolog3 replaced with injection: EH_prolog3 */
/* WARNING: Function: __EH_epilog3 replaced with injection: EH_epilog3 */
void __thiscall FUN_100163c0(void *this,long param_1,long param_2,undefined4 param_3)
{
basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> bVar1;
undefined4 *puVar2;
int *piVar3;
basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *pbVar4;
undefined4 *this_00;
undefined4 in_stack_00000040;
long lVar5;
wchar_t *pwVar6;
long lVar7;
_func_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_ptr_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_ptr
*p_Var8;
undefined4 *local_30;
undefined4 local_2c;
undefined4 local_28;
undefined4 local_24;
int *local_20;
int local_1c;
void *local_18;
int local_14;
undefined4 local_8;
undefined4 uStack_4;
uStack_4 = 0x20;
local_8 = 0x100163cc;
puVar2 = (undefined4 *)FUN_100170a4(0x74);
if (puVar2 == (undefined4 *)0x0) {
this_00 = (undefined4 *)0x0;
}
else {
this_00 = puVar2 + 1;
*puVar2 = 7;
_eh_vector_constructor_iterator_(this_00,0x10,7,FUN_10001517,FUN_10001f45);
}
local_1c = *(int *)((int)this + 8);
local_14 = 0;
if (0 < local_1c) {
local_18 = (void *)((int)this + 4);
do {
piVar3 = (int *)FUN_10007d02(local_18,local_14);
local_20 = piVar3;
if (piVar3 != (int *)0x0) {
(**(code **)(*piVar3 + 4))(piVar3);
}
local_8 = 1;
if (piVar3 != (int *)0x0) {
FUN_10015d08(this_00 + 0x18,param_1);
FUN_10015d08(this_00 + 0x14,param_2);
FUN_10015d08(this_00 + 0x10,param_3);
if ((CComVariant *)(this_00 + 0xc) != (CComVariant *)&stack0x00000010) {
ATL::CComVariant::InternalCopy
((CComVariant *)(this_00 + 0xc),(tagVARIANT *)&stack0x00000010);
}
if ((CComVariant *)(this_00 + 8) != (CComVariant *)&stack0x00000020) {
ATL::CComVariant::InternalCopy
((CComVariant *)(this_00 + 8),(tagVARIANT *)&stack0x00000020);
}
if ((CComVariant *)(this_00 + 4) != (CComVariant *)&stack0x00000030) {
ATL::CComVariant::InternalCopy
((CComVariant *)(this_00 + 4),(tagVARIANT *)&stack0x00000030);
}
*(undefined2 *)this_00 = 0x6024;
this_00[2] = in_stack_00000040;
local_2c = 0;
local_28 = 7;
local_24 = 0;
local_30 = this_00;
bVar1 = FUN_10003f01(*(basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> **)
(DAT_100294e0 + 0x10));
if (bVar1 != (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>)0x0) {
pwVar6 = L" Item Handle ";
lVar5 = param_1;
lVar7 = param_2;
p_Var8 = endl_exref;
pbVar4 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf(*(int **)(DAT_100294e0 + 0x10),
L"CProxy_ILMXProxyServerEvents2::Fire_OnBufferedDataChange firing event - Server Handle "
);
pbVar4 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar4,lVar5);
pbVar4 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf((int *)pbVar4,pwVar6);
pbVar4 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar4,lVar7);
std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<(pbVar4,p_Var8);
}
(**(code **)(*piVar3 + 0x18))(piVar3,1,&DAT_100201f8,0x400,1,&local_30,0,0,0);
}
local_8 = 0xffffffff;
if (piVar3 != (int *)0x0) {
(**(code **)(*piVar3 + 8))(piVar3);
}
local_14 = local_14 + 1;
} while (local_14 < local_1c);
}
if (this_00 != (undefined4 *)0x0) {
FUN_10015d66(this_00,3);
}
return;
}
```
@@ -0,0 +1,34 @@
# LmxProxy.dll xrefs
## 0x15f72 at 10015f72
Target function: `FUN_10015f72`
| From | Ref type | Caller function |
| --- | --- | --- |
| `1001695a` | `UNCONDITIONAL_CALL` | `FUN_1001657f` |
## 0x1611f at 1001611f
Target function: `FUN_1001611f`
| From | Ref type | Caller function |
| --- | --- | --- |
| `10016cc8` | `UNCONDITIONAL_CALL` | `FUN_10016b50` |
## 0x16271 at 10016271
Target function: `FUN_10016271`
| From | Ref type | Caller function |
| --- | --- | --- |
| `10016eb3` | `UNCONDITIONAL_CALL` | `FUN_10016d4b` |
## 0x163c0 at 100163c0
Target function: `FUN_100163c0`
| From | Ref type | Caller function |
| --- | --- | --- |
| `10016ad8` | `UNCONDITIONAL_CALL` | `FUN_1001657f` |
@@ -0,0 +1,125 @@
# LmxProxy.dll selected decompile
## FUN_10003f60 at 10003f60
Signature: `HRESULT __cdecl FUN_10003f60(undefined4 * param_1, undefined2 * param_2, ULONG param_3)`
```c
HRESULT __cdecl FUN_10003f60(undefined4 *param_1,undefined2 *param_2,ULONG param_3)
{
basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> bVar1;
HRESULT HVar2;
basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *pbVar3;
SAFEARRAY *pSVar4;
HRESULT HVar5;
_func_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_ptr_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_ptr
*p_Var6;
SAFEARRAYBOUND local_18;
LONG local_10;
IRecordInfo *local_c;
undefined2 *local_8;
local_c = (IRecordInfo *)0x0;
HVar2 = GetRecordInfoFromGuids((GUID *)&DAT_1001c2d0,1,0,0,(GUID *)&DAT_1001b530,&local_c);
if (HVar2 < 0) {
if ((HVar2 != -0x7ffd7fe3) &&
(bVar1 = FUN_10003f01(*(basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> **)
(DAT_100294e0 + 4)),
bVar1 != (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>)0x0)) {
HVar5 = HVar2;
p_Var6 = endl_exref;
pbVar3 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf(*(int **)(DAT_100294e0 + 4),L"GetRecordInfoFromGuids failed - hr = ");
pbVar3 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar3,HVar5);
std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<(pbVar3,p_Var6);
}
}
else {
local_18.lLbound = 0;
local_18.cElements = param_3;
pSVar4 = SafeArrayCreateEx(0x24,1,&local_18,local_c);
*param_1 = pSVar4;
(*local_c->lpVtbl->Release)(local_c);
if ((SAFEARRAY *)*param_1 == (SAFEARRAY *)0x0) {
HVar2 = -0x7fffbffb;
}
else {
HVar2 = SafeArrayGetLBound((SAFEARRAY *)*param_1,1,&local_10);
if ((-1 < HVar2) && (HVar2 = SafeArrayAccessData((SAFEARRAY *)*param_1,&local_8), -1 < HVar2))
{
*local_8 = *param_2;
*(undefined4 *)(local_8 + 2) = *(undefined4 *)(param_2 + 2);
*(undefined4 *)(local_8 + 4) = *(undefined4 *)(param_2 + 4);
local_8[6] = param_2[6];
SafeArrayUnaccessData((SAFEARRAY *)*param_1);
HVar2 = 0;
}
}
}
return HVar2;
}
```
## FUN_10015db2 at 10015db2
Signature: `void * __thiscall FUN_10015db2(void * this, int * param_1, undefined4 param_2, undefined4 param_3, undefined4 param_4, undefined4 param_5, undefined4 param_6)`
```c
/* WARNING: Function: __EH_prolog3 replaced with injection: EH_prolog3 */
/* WARNING: Function: __EH_epilog3 replaced with injection: EH_epilog3 */
void * __thiscall
FUN_10015db2(void *this,int *param_1,undefined4 param_2,undefined4 param_3,undefined4 param_4,
undefined4 param_5,undefined4 param_6)
{
*(undefined4 *)this = 1;
FUN_10015d38((void *)((int)this + 4),param_1);
*(undefined4 *)((int)this + 8) = 0;
*(undefined4 *)((int)this + 0x10) = param_2;
*(undefined4 *)((int)this + 0x14) = param_3;
*(undefined4 *)((int)this + 0x18) = param_4;
*(undefined4 *)((int)this + 0xc) = param_6;
*(undefined4 *)((int)this + 0x1c) = param_5;
*(undefined4 *)((int)this + 0x20) = 0;
return this;
}
```
## FUN_10015e4e at 10015e4e
Signature: `void * __thiscall FUN_10015e4e(void * this, int * param_1, undefined4 param_2, undefined4 param_3, undefined4 param_4, undefined4 param_5, undefined4 param_6)`
```c
/* WARNING: Function: __EH_prolog3 replaced with injection: EH_prolog3 */
/* WARNING: Function: __EH_epilog3 replaced with injection: EH_epilog3 */
void * __thiscall
FUN_10015e4e(void *this,int *param_1,undefined4 param_2,undefined4 param_3,undefined4 param_4,
undefined4 param_5,undefined4 param_6)
{
*(undefined4 *)this = 2;
*(undefined4 *)((int)this + 4) = 0;
FUN_10015d38((void *)((int)this + 8),param_1);
*(undefined4 *)((int)this + 0x20) = 0;
*(undefined4 *)((int)this + 0x10) = param_2;
*(undefined4 *)((int)this + 0x14) = param_3;
*(undefined4 *)((int)this + 0x18) = param_4;
*(undefined4 *)((int)this + 0xc) = param_6;
*(undefined4 *)((int)this + 0x1c) = param_5;
return this;
}
```
@@ -0,0 +1,475 @@
# LmxProxy.dll selected decompile
## FUN_1001657f at 1001657f
Signature: `undefined __stdcall FUN_1001657f(uint param_1, undefined4 param_2)`
```c
/* WARNING: Function: __EH_prolog3_catch_GS replaced with injection: EH_prolog3 */
void FUN_1001657f(uint param_1,undefined4 param_2)
{
DWORD DVar1;
basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> bVar2;
undefined *puVar3;
int iVar4;
int *piVar5;
DWORD DVar6;
undefined4 *puVar7;
HRESULT HVar8;
basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *pbVar9;
uint uVar10;
undefined4 uVar11;
wchar_t *pwVar12;
_func_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_ptr_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_ptr
*p_Var13;
void **in_stack_ffffff1c;
undefined1 local_d4 [36];
VARIANTARG local_b0;
_union_2683 local_a0;
VARIANTARG local_90;
VARIANTARG local_80;
int *local_70 [2];
DWORD local_68;
undefined *local_64;
IUnknown *local_60 [2];
BSTR local_58;
uint local_54;
int *local_50 [2];
SAFEARRAY *local_48;
undefined1 local_44 [4];
FILETIME local_40;
undefined2 local_38 [8];
undefined1 local_28 [32];
uint local_8;
undefined4 uStack_4;
uStack_4 = 0xc4;
local_54 = param_1;
local_68 = 0;
local_64 = (undefined *)0x0;
local_58 = (BSTR)0x0;
local_8 = 0;
FUN_1000107a((int *)local_50);
local_8 = CONCAT31(local_8._1_3_,1);
uVar11 = 0;
uVar10 = 3;
puVar3 = FUN_10003248();
iVar4 = FUN_10003897(puVar3,uVar10);
puVar3 = FUN_10003248();
uVar10 = FUN_1000305b(puVar3,iVar4,uVar11);
if ((char)uVar10 != '\0') {
pwVar12 = L"OnDataChange callback received";
piVar5 = (int *)FUN_10003248();
FUN_100031b7(piVar5,pwVar12);
}
if (DAT_10029594 == 0) {
local_8 = local_8 & 0xffffff00;
FUN_1000111b((int *)local_50);
local_8 = 0xffffffff;
SysFreeString(local_58);
}
else {
DVar6 = GetCurrentThreadId();
if (DVar6 == DAT_10029594) {
iVar4 = *(int *)(param_1 + 8);
FUN_1000f663((void *)(iVar4 + 0x2c),&local_40.dwHighDateTime,(int *)(param_1 + 0xc));
DVar6 = local_40.dwHighDateTime;
if ((((undefined *)local_40.dwHighDateTime != *(undefined **)(iVar4 + 0x30)) &&
(FUN_1000f5ef((undefined *)(local_40.dwHighDateTime + 0x3c),&local_40.dwHighDateTime,
(int *)(param_1 + 0x10)), DVar1 = local_40.dwHighDateTime,
(undefined *)local_40.dwHighDateTime != *(undefined **)(DVar6 + 0x40))) &&
(*(char *)(local_40.dwHighDateTime + 0x1c) != '\0')) {
if (*(char *)(local_40.dwHighDateTime + 0x1f) == '\0') {
if (*(char *)(local_40.dwHighDateTime + 0x1e) == '\0') {
local_40.dwHighDateTime = 0;
local_28._0_4_ = (uint)(ushort)local_28._2_2_ << 0x10;
local_28._4_4_ = 0;
local_28._8_4_ = 0;
local_28._12_4_ = (undefined *)0x0;
(**(code **)(**(int **)(DVar6 + 0x24) + 0x60))
(*(int **)(DVar6 + 0x24),*(undefined4 *)(DVar1 + 0x18),
&local_40.dwHighDateTime,local_28);
if ((local_28._0_2_ == 0xffff) && (local_28._4_4_ == 0)) {
*(undefined1 *)(DVar1 + 0x1e) = 1;
*(byte *)(DVar1 + 0x1d) = (byte)(local_40.dwHighDateTime >> 1) & 1;
}
}
piVar5 = *(int **)(DVar6 + 0x24);
if (local_50[0] != (int *)0x0) {
(**(code **)(*local_50[0] + 8))(local_50[0]);
local_50[0] = (int *)0x0;
}
iVar4 = (**(code **)(*piVar5 + 0x50))
(piVar5,*(undefined4 *)(DVar1 + 0x18),local_44,&local_68,local_38,
&local_58,local_50);
}
else {
if (*(char *)(local_40.dwHighDateTime + 0x1e) == '\0') {
local_40.dwHighDateTime = 0;
local_28._0_4_ = (uint)(ushort)local_28._2_2_ << 0x10;
local_28._4_4_ = 0;
local_28._8_4_ = 0;
local_28._12_4_ = (undefined *)0x0;
(**(code **)(**(int **)(DVar6 + 0x30) + 0x28))
(*(int **)(DVar6 + 0x30),*(undefined4 *)(DVar1 + 0x18),
&local_40.dwHighDateTime,local_28);
if ((local_28._0_2_ == 0xffff) && (local_28._4_4_ == 0)) {
*(undefined1 *)(DVar1 + 0x1e) = 1;
*(byte *)(DVar1 + 0x1d) = (byte)(local_40.dwHighDateTime >> 1) & 1;
}
}
piVar5 = *(int **)(DVar6 + 0x30);
if (local_50[0] != (int *)0x0) {
(**(code **)(*local_50[0] + 8))(local_50[0]);
local_50[0] = (int *)0x0;
}
iVar4 = (**(code **)(*piVar5 + 0x20))
(piVar5,*(undefined4 *)(DVar1 + 0x18),local_44,&local_68,local_38,
local_50);
}
iVar4 = FUN_1000f8d9((undefined4 *)(uint)(iVar4 == 0),iVar4,0x6d,"MxCallback.cpp");
if (iVar4 != 0) {
if (*(char *)(DVar1 + 0x28) == '\0') {
FUN_1000107a((int *)local_60);
local_8 = CONCAT31(local_8._1_3_,4);
if (*(char *)(DVar1 + 0x1d) == '\0') {
local_40.dwLowDateTime = 0;
local_40.dwHighDateTime = 0;
CoFileTimeNow(&local_40);
local_28._8_4_ = local_40.dwLowDateTime;
local_28._12_4_ = local_40.dwHighDateTime;
}
else {
local_28._8_4_ = local_68;
local_28._12_4_ = local_64;
}
HVar8 = (*local_60[0]->lpVtbl[5].QueryInterface)
(local_60[0],(IID *)(local_28 + 8),in_stack_ffffff1c);
if (HVar8 < 0) {
_com_issue_errorex(HVar8,local_60[0],(_GUID *)&DAT_1001b590);
}
puVar3 = FUN_100012e6(local_60);
FUN_10001269(local_70,puVar3);
local_8._0_1_ = 5;
FUN_100060a2((CComVariant *)&local_b0,local_70[0],0x40);
local_8._0_1_ = 6;
FUN_100060a2((CComVariant *)&local_a0.n2,local_50[0],0);
local_8._0_1_ = 7;
HVar8 = FUN_10003f60(&local_48,local_38,1);
if (HVar8 < 0) {
bVar2 = FUN_10003f01(*(basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> **)
(DAT_100294e0 + 8));
if (bVar2 != (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>)0x0) {
p_Var13 = endl_exref;
pbVar9 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf(*(int **)(DAT_100294e0 + 8),
L"CUserConnectionCallback::OnDataChange - Create MxStatus SafeArray failed. hr = "
);
pbVar9 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar9,HVar8);
std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar9,p_Var13);
}
}
else {
local_8 = CONCAT31(local_8._1_3_,8);
FUN_10015f72((void *)(*(int *)(local_54 + 8) + 0xc),*(long *)(local_54 + 0xc),
*(long *)(local_54 + 0x10),local_a0._0_4_);
local_8._0_1_ = 7;
local_8._1_3_ = 0;
HVar8 = SafeArrayDestroy(local_48);
if (HVar8 != 0) {
pwVar12 =
L"CUserConnectionCallback::OnDataChange - SafeArrayDestroy failed - hr %08X";
piVar5 = (int *)FUN_10003248();
FUN_1000308b(piVar5,pwVar12);
}
}
local_8._0_1_ = 6;
VariantClear((VARIANTARG *)&local_a0.n2);
local_8._0_1_ = 5;
VariantClear(&local_b0);
local_8._0_1_ = 4;
FUN_1000111b((int *)local_70);
local_8 = CONCAT31(local_8._1_3_,1);
FUN_1000111b((int *)local_60);
}
else {
VariantInit(&local_80);
local_8._0_1_ = 10;
VariantInit(&local_90);
local_8._0_1_ = 0xb;
VariantInit((VARIANTARG *)local_28);
local_8._0_1_ = 0xc;
local_40.dwHighDateTime = 0;
FUN_100069ad(local_50[0],(ushort *)&local_80,(undefined2 *)&local_90,
(undefined2 *)local_28,(BSTR)&local_40.dwHighDateTime);
HVar8 = FUN_10003f60(&local_48,local_38,1);
if (HVar8 < 0) {
bVar2 = FUN_10003f01(*(basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> **)
(DAT_100294e0 + 8));
if (bVar2 != (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>)0x0) {
p_Var13 = endl_exref;
pbVar9 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf(*(int **)(DAT_100294e0 + 8),
L"CUserConnectionCallback::OnDataChange - Create MxStatus SafeArray failed on Buffered Data callback. hr = "
);
pbVar9 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar9,HVar8);
std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar9,p_Var13);
}
}
else {
local_8 = CONCAT31(local_8._1_3_,0xd);
FUN_100163c0((void *)(*(int *)(local_54 + 8) + 0x18),*(long *)(local_54 + 0xc),
*(long *)(local_54 + 0x10),local_40.dwHighDateTime);
local_8._0_1_ = 0xc;
local_8._1_3_ = 0;
HVar8 = SafeArrayDestroy(local_48);
if (HVar8 != 0) {
pwVar12 =
L"CUserConnectionCallback::OnDataChange - SafeArrayDestroy failed - hr %08X";
piVar5 = (int *)FUN_10003248();
FUN_1000308b(piVar5,pwVar12);
}
}
local_8._0_1_ = 0xb;
VariantClear((VARIANTARG *)local_28);
local_8._0_1_ = 10;
VariantClear(&local_90);
local_8 = CONCAT31(local_8._1_3_,1);
VariantClear(&local_80);
}
}
}
}
else {
local_40.dwHighDateTime = (DWORD)&DAT_100295bc;
EnterCriticalSection((LPCRITICAL_SECTION)&DAT_100295bc);
local_8._0_1_ = 2;
puVar7 = FUN_10015e06(local_d4,(int *)(-(uint)(param_1 != 4) & param_1),param_2);
local_8._0_1_ = 3;
FUN_1001654d(&DAT_100295b0,DAT_100295b0,puVar7);
local_8._0_1_ = 2;
FUN_1000d639((int)local_d4);
local_8 = CONCAT31(local_8._1_3_,1);
LeaveCriticalSection((LPCRITICAL_SECTION)&DAT_100295bc);
}
local_8 = local_8 & 0xffffff00;
FUN_1000111b((int *)local_50);
local_8 = 0xffffffff;
SysFreeString(local_58);
}
FUN_10017482();
return;
}
```
## FUN_10016b50 at 10016b50
Signature: `HRESULT __stdcall FUN_10016b50(uint param_1, undefined4 param_2, undefined4 * param_3)`
```c
/* WARNING: Function: __EH_prolog3_catch replaced with injection: EH_prolog3 */
/* WARNING: Function: __EH_epilog3 replaced with injection: EH_epilog3 */
HRESULT FUN_10016b50(uint param_1,undefined4 param_2,undefined4 *param_3)
{
uint uVar1;
basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> bVar2;
undefined *puVar3;
int iVar4;
int *piVar5;
DWORD DVar6;
undefined4 *puVar7;
HRESULT HVar8;
basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *pbVar9;
uint uVar10;
HRESULT HVar11;
undefined4 uVar12;
wchar_t *pwVar13;
_func_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_ptr_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_ptr
*p_Var14;
undefined1 local_40 [36];
undefined *local_1c;
undefined4 local_18 [4];
int local_8;
undefined4 uStack_4;
uStack_4 = 0x30;
local_8 = 0x10016b5c;
local_18[0] = 0;
uVar12 = 0;
uVar10 = 3;
puVar3 = FUN_10003248();
iVar4 = FUN_10003897(puVar3,uVar10);
puVar3 = FUN_10003248();
uVar10 = FUN_1000305b(puVar3,iVar4,uVar12);
if ((char)uVar10 != '\0') {
pwVar13 = L"OnSetAttributeResult callback received";
piVar5 = (int *)FUN_10003248();
FUN_100031b7(piVar5,pwVar13);
}
if (DAT_10029594 != 0) {
DVar6 = GetCurrentThreadId();
uVar10 = param_1;
if (DVar6 == DAT_10029594) {
piVar5 = (int *)(param_1 + 0xc);
iVar4 = *(int *)(param_1 + 8);
FUN_1000f663((void *)(iVar4 + 0x2c),&param_1,piVar5);
uVar1 = param_1;
if ((param_1 == *(uint *)(iVar4 + 0x30)) ||
(FUN_1000f5ef((void *)(param_1 + 0x3c),&param_1,(int *)(uVar10 + 0x10)),
param_1 == *(uint *)(uVar1 + 0x40))) {
return -0x7fffbffb;
}
HVar8 = FUN_10003f60(local_18,(undefined2 *)param_3,1);
if (-1 < HVar8) {
local_8 = 2;
FUN_1001611f((void *)(*(int *)(uVar10 + 8) + 0xc),*piVar5,*(long *)(uVar10 + 0x10),local_18)
;
local_8 = 0xffffffff;
HVar8 = FUN_10016d1a();
return HVar8;
}
bVar2 = FUN_10003f01(*(basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> **)
(DAT_100294e0 + 8));
if (bVar2 == (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>)0x0) {
return HVar8;
}
HVar11 = HVar8;
p_Var14 = endl_exref;
pbVar9 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf(*(int **)(DAT_100294e0 + 8),
L"CUserConnectionCallback::OnSetAttributeResult - Create MxStatus SafeArray failed. hr = "
);
pbVar9 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar9,HVar11);
std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<(pbVar9,p_Var14);
return HVar8;
}
local_1c = &DAT_100295bc;
EnterCriticalSection((LPCRITICAL_SECTION)&DAT_100295bc);
local_8 = 0;
puVar7 = FUN_10015db2(local_40,(int *)(-(uint)(param_1 != 4) & param_1),*param_3,param_3[1],
param_3[2],param_3[3],param_2);
local_8._0_1_ = 1;
FUN_1001654d(&DAT_100295b0,DAT_100295b0,puVar7);
local_8 = (uint)local_8._1_3_ << 8;
FUN_1000d639((int)local_40);
local_8 = 0xffffffff;
LeaveCriticalSection((LPCRITICAL_SECTION)&DAT_100295bc);
}
return 0;
}
```
## FUN_10016d4b at 10016d4b
Signature: `HRESULT __stdcall FUN_10016d4b(int * param_1, undefined4 param_2, undefined4 * param_3)`
```c
/* WARNING: Function: __EH_prolog3_catch replaced with injection: EH_prolog3 */
/* WARNING: Function: __EH_epilog3 replaced with injection: EH_epilog3 */
HRESULT FUN_10016d4b(int *param_1,undefined4 param_2,undefined4 *param_3)
{
int *piVar1;
int *piVar2;
basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> bVar3;
undefined *puVar4;
int iVar5;
int *piVar6;
DWORD DVar7;
undefined4 *puVar8;
HRESULT HVar9;
basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *pbVar10;
uint uVar11;
undefined4 uVar12;
wchar_t *pwVar13;
_func_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_ptr_basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>_ptr
*p_Var14;
undefined1 local_40 [36];
undefined *local_1c;
undefined4 local_18 [4];
int local_8;
undefined4 uStack_4;
uStack_4 = 0x30;
local_8 = 0x10016d57;
uVar12 = 0;
uVar11 = 3;
puVar4 = FUN_10003248();
iVar5 = FUN_10003897(puVar4,uVar11);
puVar4 = FUN_10003248();
uVar11 = FUN_1000305b(puVar4,iVar5,uVar12);
if ((char)uVar11 != '\0') {
pwVar13 = L"OperationComplete callback received";
piVar6 = (int *)FUN_10003248();
FUN_100031b7(piVar6,pwVar13);
}
if (DAT_10029594 != 0) {
DVar7 = GetCurrentThreadId();
piVar6 = param_1;
if (DVar7 == DAT_10029594) {
piVar1 = param_1 + 4;
iVar5 = param_1[3];
FUN_1000f663((void *)(iVar5 + 0x2c),&param_1,piVar1);
piVar2 = param_1;
if ((param_1 == *(int **)(iVar5 + 0x30)) ||
(FUN_1000f5ef(param_1 + 0xf,&param_1,piVar6 + 5), param_1 == (int *)piVar2[0x10])) {
return -0x7fffbffb;
}
HVar9 = FUN_10003f60(local_18,(undefined2 *)param_3,1);
if (-1 < HVar9) {
local_8 = 2;
FUN_10016271((void *)(piVar6[3] + 0xc),*piVar1,piVar6[5],local_18);
local_8 = 0xffffffff;
HVar9 = FUN_10016f05();
return HVar9;
}
bVar3 = FUN_10003f01(*(basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> **)
(DAT_100294e0 + 8));
if (bVar3 == (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>)0x0) {
return HVar9;
}
p_Var14 = endl_exref;
pbVar10 = (basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_> *)
FUN_10002dbf(*(int **)(DAT_100294e0 + 8),
L"CUserConnectionCallback::CUserConnectionCallback::OperationComplete - Create MxStatus SafeArray failed. hr = "
);
pbVar10 = std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<
(pbVar10,HVar9);
std::basic_ostream<wchar_t,struct_std::char_traits<wchar_t>_>::operator<<(pbVar10,p_Var14);
HVar9 = FUN_10016f05();
return HVar9;
}
local_1c = &DAT_100295bc;
EnterCriticalSection((LPCRITICAL_SECTION)&DAT_100295bc);
local_8 = 0;
puVar8 = FUN_10015e4e(local_40,param_1,*param_3,param_3[1],param_3[2],param_3[3],param_2);
local_8._0_1_ = 1;
FUN_1001654d(&DAT_100295b0,DAT_100295b0,puVar8);
local_8 = (uint)local_8._1_3_ << 8;
FUN_1000d639((int)local_40);
local_8 = 0xffffffff;
LeaveCriticalSection((LPCRITICAL_SECTION)&DAT_100295bc);
}
return 0;
}
```
@@ -0,0 +1,3 @@
frida=C:\Users\dohertj2\AppData\Local\Programs\Python\Python312\Scripts\frida.exe
harness=C:\Users\dohertj2\Desktop\mxaccess\src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe
args=-f C:\Users\dohertj2\Desktop\mxaccess\src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe -l C:\Users\dohertj2\Desktop\mxaccess\analysis\frida\mx-nmx-trace.js -- --scenario=suspend-advised --tag=TestChildObject.ScanState --duration=8 --log=C:\Users\dohertj2\Desktop\mxaccess\captures\123-frida-suspend-advised-instrumented\harness.log --client=MxFridaTrace-123
@@ -0,0 +1 @@
1
@@ -0,0 +1,98 @@
____
/ _ | Frida 17.9.1 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to Local System (id=local)
Spawning `C:\Users\dohertj2\Desktop\mxaccess\src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe --scenario=suspend-advised --tag=TestChildObject.ScanState --duration=8 --log=C:\Users\dohertj2\Desktop\mxaccess\captures\123-frida-suspend-advised-instrumented\harness.log --client=MxFridaTrace-123`...
Spawned `C:\Users\dohertj2\Desktop\mxaccess\src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe --scenario=suspend-advised --tag=TestChildObject.ScanState --duration=8 --log=C:\Users\dohertj2\Desktop\mxaccess\captures\123-frida-suspend-advised-instrumented\harness.log --client=MxFridaTrace-123`. Resuming main thread!
[Local::MxTraceHarness.exe ]-> {"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.Write.variantA","base":"0x61b50000","rva":"0x12c0c","address":"0x61b62c0c","time":"2026-05-06T17:23:45.844Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.Write.variantB","base":"0x61b50000","rva":"0x13280","address":"0x61b63280","time":"2026-05-06T17:23:45.845Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.WriteSecured.variantA","base":"0x61b50000","rva":"0x12f24","address":"0x61b62f24","time":"2026-05-06T17:23:45.846Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.WriteSecured.variantB","base":"0x61b50000","rva":"0x135fe","address":"0x61b635fe","time":"2026-05-06T17:23:45.846Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.AddBufferedItem","base":"0x61b50000","rva":"0x1121d","address":"0x61b6121d","time":"2026-05-06T17:23:45.846Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.SetBufferedUpdateInterval","base":"0x61b50000","rva":"0xfc80","address":"0x61b5fc80","time":"2026-05-06T17:23:45.846Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.AdviseSupervisory","base":"0x61b50000","rva":"0x142b4","address":"0x61b642b4","time":"2026-05-06T17:23:45.846Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.Suspend","base":"0x61b50000","rva":"0x13d9c","address":"0x61b63d9c","time":"2026-05-06T17:23:45.846Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.Activate","base":"0x61b50000","rva":"0x14028","address":"0x61b64028","time":"2026-05-06T17:23:45.847Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CProxy_ILMXProxyServerEvents2.Fire_OnBufferedDataChange","base":"0x61b50000","rva":"0x163c0","address":"0x61b663c0","time":"2026-05-06T17:23:45.847Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CUserConnectionCallback.OnSetAttributeResult","base":"0x61b50000","rva":"0x16b50","address":"0x61b66b50","time":"2026-05-06T17:23:45.847Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CUserConnectionCallback.OperationComplete","base":"0x61b50000","rva":"0x16d4b","address":"0x61b66d4b","time":"2026-05-06T17:23:45.848Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.AuthenticateUser","base":"0x61b50000","rva":"0x1399f","address":"0x61b6399f","time":"2026-05-06T17:23:45.848Z"}
{"event":"hook.installed","module":"Lmx.dll","name":"MxConnection.PrebindReference","base":"0x10000000","rva":"0xea780","address":"0x100ea780","time":"2026-05-06T17:23:51.188Z"}
{"event":"hook.installed","module":"Lmx.dll","name":"MxConnection.UserRegisterPreboundReference","base":"0x10000000","rva":"0xe1920","address":"0x100e1920","time":"2026-05-06T17:23:51.189Z"}
{"event":"hook.installed","module":"Lmx.dll","name":"IMxReference.GetMxHandle","base":"0x10000000","rva":"0x5f730","address":"0x1005f730","time":"2026-05-06T17:23:51.190Z"}
{"event":"hook.installed","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","base":"0x10000000","rva":"0x8f8b0","address":"0x1008f8b0","time":"2026-05-06T17:23:51.190Z"}
{"event":"hook.installed","module":"Lmx.dll","name":"PreboundReference.Resolve","base":"0x10000000","rva":"0x113d40","address":"0x10113d40","time":"2026-05-06T17:23:51.191Z"}
{"event":"hook.installed","module":"Lmx.dll","name":"PreboundReference.OnPlatformResolveReferenceResults","base":"0x10000000","rva":"0x1155a0","address":"0x101155a0","time":"2026-05-06T17:23:51.192Z"}
{"event":"hook.installed","module":"Lmx.dll","name":"PreboundReference.OnSetAttributeResult","base":"0x10000000","rva":"0x114a90","address":"0x10114a90","time":"2026-05-06T17:23:51.192Z"}
{"event":"lmx.fixup-mxhandle.enter","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","accessManager":"0x91a72b0","outPtr":"0xd5e6c4","inWords":[65537,65537,0,0,0,0],"time":"2026-05-06T17:23:51.236Z"}
{"event":"lmx.fixup-mxhandle.leave","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","outPtr":"0xd5e6c4","handle":{"raw":"01 00 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00","w0":65537,"w1":65537,"w2":0,"w3":0,"w4":0},"retval":"0xd5e6c4","time":"2026-05-06T17:23:51.236Z"}
{"event":"lmx.fixup-mxhandle.enter","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","accessManager":"0x91a72b0","outPtr":"0xd5e6c4","inWords":[65537,65537,0,0,0,0],"time":"2026-05-06T17:23:51.237Z"}
{"event":"lmx.fixup-mxhandle.leave","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","outPtr":"0xd5e6c4","handle":{"raw":"01 00 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00","w0":65537,"w1":65537,"w2":0,"w3":0,"w4":0},"retval":"0xd5e6c4","time":"2026-05-06T17:23:51.237Z"}
{"event":"lmx.prebind.enter","module":"Lmx.dll","name":"MxConnection.PrebindReference","self":"0x91aed2c","outPtr":"0xd5ec98","referencePtr":"0xd5eccc","reference":"TestChildObject.ScanState","time":"2026-05-06T17:23:51.255Z"}
{"event":"lmx.mxhandle.read","module":"Lmx.dll","name":"IMxReference.GetMxHandle","referencePtr":"0x91b3838","outPtr":"0xd5ec00","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xd5ec00","time":"2026-05-06T17:23:51.256Z"}
{"event":"lmx.prebound-resolve.enter","module":"Lmx.dll","name":"PreboundReference.Resolve","prebound":{"ptr":"0x91af058","referenceString":{"length":25,"capacity":31,"value":"TestChildObject.ScanState"},"contextString":{"length":0,"capacity":7,"value":""},"auxString":{"length":0,"capacity":7,"value":""},"mxReference":"0x91b46f0","flags10":1124099840,"word14":2,"word4c":131073,"word54":134011636,"word58":0,"word5c":0,"word60":0,"word64":152728240,"word68":0,"word6c":0,"worda0":0,"worda4":0,"status":3,"flagb0":0,"errorText":"","raw":"08 64 19 10 f0 63 19 10 00 6f 00 6e e8 63 19 10 00 67 00 43 02 00 00 00 98 41 1b 09 00 65 00 00 00 02 00 00 00 00 00 02 19 00 00 00 1f 00 00 00 00 00 00 01 00 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 01 00 02 00 f0 46 1b 09 f4 da fc 07 00 00 00 00 00 00 00 00 00 00 00 00 b0 72 1a 09 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 00 00 ac 8a 31 01 00 00 00 00"},"time":"2026-05-06T17:23:51.257Z"}
{"event":"lmx.mxhandle.read","module":"Lmx.dll","name":"IMxReference.GetMxHandle","referencePtr":"0x91af0a8","outPtr":"0xd5eb90","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xd5eb90","time":"2026-05-06T17:23:51.257Z"}
{"event":"lmx.mxhandle.read","module":"Lmx.dll","name":"IMxReference.GetMxHandle","referencePtr":"0x91af0a8","outPtr":"0xd5eb90","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xd5eb90","time":"2026-05-06T17:23:51.257Z"}
{"event":"lmx.mxhandle.read","module":"Lmx.dll","name":"IMxReference.GetMxHandle","referencePtr":"0x91af0a8","outPtr":"0xd5eb90","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xd5eb90","time":"2026-05-06T17:23:51.258Z"}
{"event":"lmx.prebound-resolve.leave","module":"Lmx.dll","name":"PreboundReference.Resolve","prebound":{"ptr":"0x91af058","referenceString":{"length":25,"capacity":31,"value":"TestChildObject.ScanState"},"contextString":{"length":0,"capacity":7,"value":""},"auxString":{"length":0,"capacity":7,"value":""},"mxReference":"0x91b46f0","flags10":1124099840,"word14":2,"word4c":131073,"word54":134011636,"word58":0,"word5c":0,"word60":0,"word64":152728240,"word68":0,"word6c":0,"worda0":0,"worda4":0,"status":3,"flagb0":0,"errorText":"","raw":"08 64 19 10 f0 63 19 10 00 6f 00 6e e8 63 19 10 00 67 00 43 02 00 00 00 98 41 1b 09 00 65 00 00 00 02 00 00 00 00 00 02 19 00 00 00 1f 00 00 00 00 00 00 01 00 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 01 00 02 00 f0 46 1b 09 f4 da fc 07 00 00 00 00 00 00 00 00 00 00 00 00 b0 72 1a 09 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 00 00 ac 8a 31 01 00 00 00 00"},"retval":"0x70d01e01","time":"2026-05-06T17:23:51.259Z"}
{"event":"lmx.prebind.leave","module":"Lmx.dll","name":"MxConnection.PrebindReference","handle":1,"time":"2026-05-06T17:23:51.259Z"}
{"event":"call.enter","module":"LmxProxy.dll","name":"CLMXProxyServer.AdviseSupervisory","address":"0x61b642b4","ecx":"0xd5ed50","args":["0x62492d0","0x1","0x1","0x55eabfd1","0x744d4704"],"time":"2026-05-06T17:23:51.261Z"}
{"event":"lmx.fixup-mxhandle.enter","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","accessManager":"0x91a72b0","outPtr":"0xd5ebd0","inWords":[65537,327682,186166,655465,37447,0],"time":"2026-05-06T17:23:51.261Z"}
{"event":"lmx.fixup-mxhandle.leave","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","outPtr":"0xd5ebd0","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xd5ebd0","time":"2026-05-06T17:23:51.261Z"}
{"event":"lmx.fixup-mxhandle.enter","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","accessManager":"0x91a72b0","outPtr":"0xd5d864","inWords":[65537,327682,186166,655465,37447,0],"time":"2026-05-06T17:23:51.262Z"}
{"event":"lmx.fixup-mxhandle.leave","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","outPtr":"0xd5d864","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xd5d864","time":"2026-05-06T17:23:51.262Z"}
{"event":"call.leave","module":"LmxProxy.dll","name":"CLMXProxyServer.AdviseSupervisory","retval":"0x0","time":"2026-05-06T17:23:51.262Z"}
{"event":"hook.installed","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","base":"0x63ae0000","rva":"0x10996","address":"0x63af0996","time":"2026-05-06T17:23:51.280Z"}
{"event":"hook.installed","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","base":"0x63ae0000","rva":"0x112da","address":"0x63af12da","time":"2026-05-06T17:23:51.280Z"}
{"event":"hook.installed","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","base":"0x63ae0000","rva":"0x15169","address":"0x63af5169","time":"2026-05-06T17:23:51.281Z"}
{"event":"hook.installed","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequestEx","base":"0x63ae0000","rva":"0x159c3","address":"0x63af59c3","time":"2026-05-06T17:23:51.281Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","address":"0x63af5169","ecx":"0x1","args":["0x91ac9d8","0x1","0x1","0x1","0x2","0x0","0x13a","0x91af118","0xd5ea14","0xfd3aeb5e"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":1,"ptr":"0x2","hex":""},{"sizeIndex":6,"ptrIndex":7,"size":314,"ptr":"0x91af118","hex":"17 01 00 01 01 00 01 00 00 00 65 00 71 00 0a 00 00 00 00 00 08 6a 00 00 00 40 00 00 81 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 44 00 65 00 70 00 6c 00 6f 00 79 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 a0 e7 1a 09 1f 01 00 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 00 00 01 00 00 00 17 01 00 01 01 00 01 00 00 00 65 00 71 00 0a 00 00 00 00 00 08 76 00 00 00 4c 00 00 81 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 43 00 6f 00 6e 00 66 00 69 00 67 00 43 00 68 00 61 00 6e 00 67 00 65 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 20 ee 1a 09 20 01 00 02 00 00 00"}],"time":"2026-05-06T17:23:51.371Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x91ac9d8","args":["0x1","0x1","0x1","0x168","0x9eb7020","0x9d860587","0x91aece4","0x91aecd4","0x63b0dd04","0x64"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":360,"ptr":"0x9eb7020","hex":"01 00 3a 01 00 00 00 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 02 00 00 30 75 00 00 17 01 00 01 01 00 01 00 00 00 65 00 71 00 0a 00 00 00 00 00 08 6a 00 00 00 40 00 00 81 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 44 00 65 00 70 00 6c 00 6f 00 79 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 a0 e7 1a 09 1f 01 00 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 00 00 01 00 00 00 17 01 00 01 01 00 01 00 00 00 65 00 71 00 0a 00 00 00 00 00 08 76 00 00 00 4c 00 00 81 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 43 00 6f 00 6e 00 66 00 69 00 67 00 43 00 68 00 61 00 6e 00 67 00 65 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 20 ee 1a 09 20 01 00 02 00 00 00"}],"time":"2026-05-06T17:23:51.373Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:23:51.374Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","retval":"0x0","time":"2026-05-06T17:23:51.374Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","address":"0x63af5169","ecx":"0x1","args":["0x91ac9d8","0x1","0x1","0x2","0x2","0x0","0x27","0x91af590","0xd5ea14","0xfd3aeb5e"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":2,"ptr":"0x2","hex":""},{"sizeIndex":6,"ptrIndex":7,"size":39,"ptr":"0x91af590","hex":"1f 01 00 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 00 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00 03 00 00 00"}],"time":"2026-05-06T17:23:51.375Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x91ac9d8","args":["0x1","0x1","0x2","0x55","0x9eb7020","0x9d860587","0x91b5dcc","0x91b5dbc","0x63b0dd04","0x64"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":85,"ptr":"0x9eb7020","hex":"01 00 27 00 00 00 00 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 02 00 00 30 75 00 00 1f 01 00 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 00 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00 03 00 00 00"}],"time":"2026-05-06T17:23:51.376Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:23:51.376Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","retval":"0x0","time":"2026-05-06T17:23:51.376Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","address":"0x63af12da","ecx":"0x91ac9d8","args":["0x2c2","0x7f44288","0x773eb08","0x769cedd8","0x91ac9e4","0x2c2","0x7f44288","0x206","0x3","0x7aa21cc"],"candidates":[{"sizeIndex":5,"ptrIndex":6,"size":706,"ptr":"0x7f44288","hex":"01 00 94 02 00 00 00 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 02 02 00 00 30 75 00 00 40 1f 50 80 08 a6 00 00 00 40 00 00 91 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 44 00 65 00 70 00 6c 00 6f 00 79 00 00 00 18 00 00 00 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 00 00 28 00 00 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 44 00 65 00 70 00 6c 00 6f 00 79 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 53 f2 9a 00 6a 00 0a 00 5f f1 00 00 01 6c 00 00 00 41 00 6e 00 20 00 69 00 6e 00 74 00 65 00 72 00 6e 00 61 00 6c 00 20 00 65 00 72 00 72 00 6f 00 72 00 20 00 6f 00 63 00 63 00 75 00 72 00 72 00 65 00 64 00 20 00 69 00 6e 00 20 00 74 00 68 00 65 00 20 00 42 00 61 00 73 00 65 00 20 00 52 00 75 00 6e 00 74 00 69 00 6d 00 65 00 20 00 4f 00 62 00 6a 00 65 00 63 00 74 00 00 00 1f 00 00 50 80 01 00 01 00 01 00 30 75 00 00 4a 5a a3 cd 7a 87 96 43 83 2c b4 ba be 67 53 57 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 40 1f 50 80 08 be 00 00 00 4c 00 00 91 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 43 00 6f 00 6e 00 66 00 69 00 67 00 43 00 68 00 61 00 6e 00 67 00 65 00 00 00 18 00 00 00 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 00 00 34 00 00 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 43 00 6f 00 6e 00 66 00 69 00 67 00 43 00 68 00 61 00 6e 00 67 00 65 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 53 f2 9a 00 6b 00 0a 00 87 3a 00 00 01 6c 00 00 00 41 00 6e 00 20 00 69 00 6e 00 74 00 65 00 72 00 6e 00 61 00 6c 00 20 00 65 00 72 00 72 00 6f 00 72 00 20 00 6f 00 63 00 63 00 75 00 72 00 72 00 65 00 64 00 20 00 69 00 6e 00 20 00 74 00 68 00 65 00 20 00 42 00 61 00 73 00 65 00 20 00 52 00 75 00 6e 00 74 00 69 00 6d 00 65 00 20 00 4f 00 62 00 6a 00 65 00 63 00 74 00 00 00 20 00 00 50 80 01 00 01 00 01 00 30 75 00 00"},{"sizeIndex":7,"ptrIndex":8,"size":518,"ptr":"0x3","hex":""},{"sizeIndex":8,"ptrIndex":9,"size":3,"ptr":"0x7aa21cc","hex":"f0 d7 01"}],"time":"2026-05-06T17:23:51.392Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","retval":"0x1","time":"2026-05-06T17:23:51.393Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","address":"0x63af12da","ecx":"0x91ac9d8","args":["0x97","0x7f38730","0x773eb08","0x769cedd8","0x91ac9e4","0x97","0x7f38730","0x206","0x3","0x7aa21cc"],"candidates":[{"sizeIndex":5,"ptrIndex":6,"size":151,"ptr":"0x7f38730","hex":"01 00 69 00 00 00 00 00 00 00 39 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 02 00 00 30 75 00 00 32 01 00 02 00 00 00 4a 5a a3 cd 7a 87 96 43 83 2c b4 ba be 67 53 57 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 01 00 00 00 03 00 00 00 c0 00 b0 fd 44 d6 75 dd dc 01 06 0a 00 00 00 00 99 8c 8a 6e da dc 01 00 00 02 00 00 00 03 00 00 00 c0 00 f0 99 45 d6 75 dd dc 01 06 0a 00 00 00 00 fb 56 ce 19 dd dc 01 00 00"},{"sizeIndex":7,"ptrIndex":8,"size":518,"ptr":"0x3","hex":""},{"sizeIndex":8,"ptrIndex":9,"size":3,"ptr":"0x7aa21cc","hex":"f0 d7 01"}],"time":"2026-05-06T17:23:51.394Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","retval":"0x1","time":"2026-05-06T17:23:51.394Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","address":"0x63af12da","ecx":"0x91ac9d8","args":["0x5c","0x7f43180","0x773eb08","0x769cedd8","0x91ac9e4","0x5c","0x7f43180","0x206","0x3","0x7aa21cc"],"candidates":[{"sizeIndex":5,"ptrIndex":6,"size":92,"ptr":"0x7f43180","hex":"01 00 2e 00 00 00 00 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 02 02 00 00 30 75 00 00 00 00 50 80 01 00 01 00 02 00 30 75 00 00 ad dd 62 fe a7 a0 e5 49 87 72 93 75 c6 f1 cc 86 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93"},{"sizeIndex":7,"ptrIndex":8,"size":518,"ptr":"0x3","hex":""},{"sizeIndex":8,"ptrIndex":9,"size":3,"ptr":"0x7aa21cc","hex":"f0 d7 01"}],"time":"2026-05-06T17:23:51.414Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","retval":"0x1","time":"2026-05-06T17:23:51.415Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","address":"0x63af12da","ecx":"0x91ac9d8","args":["0x69","0x7fb3ab0","0x773eb08","0x769cedd8","0x91ac9e4","0x69","0x7fb3ab0","0x206","0x3","0x7aa21cc"],"candidates":[{"sizeIndex":5,"ptrIndex":6,"size":105,"ptr":"0x7fb3ab0","hex":"01 00 3b 00 00 00 00 00 00 00 06 19 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 02 00 00 30 75 00 00 32 01 00 01 00 00 00 ad dd 62 fe a7 a0 e5 49 87 72 93 75 c6 f1 cc 86 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 03 00 00 00 00 00 00 00 c0 00 c0 3e 0b d8 75 dd dc 01 01 ff"},{"sizeIndex":7,"ptrIndex":8,"size":518,"ptr":"0x3","hex":""},{"sizeIndex":8,"ptrIndex":9,"size":3,"ptr":"0x7aa21cc","hex":"f0 d7 01"}],"time":"2026-05-06T17:23:51.416Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","retval":"0x1","time":"2026-05-06T17:23:51.416Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x91ac9d8","args":["0x1","0x1","0x1","0x2e","0x9eb7020","0x9d860473","0x91a72b0","0x0","0x0","0x64"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":46,"ptr":"0x9eb7020","hex":"01 00 00 00 00 00 00 00 00 00 39 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 01 00 00 00 02 02 00 00 30 75 00 00"}],"time":"2026-05-06T17:23:51.470Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:23:51.470Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x91ac9d8","args":["0x1","0x1","0x2","0x2e","0x9eb7020","0x9d860473","0x91a72b0","0x0","0x0","0x64"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":46,"ptr":"0x9eb7020","hex":"01 00 00 00 00 00 00 00 00 00 06 19 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 02 00 00 00 02 02 00 00 30 75 00 00"}],"time":"2026-05-06T17:23:51.488Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:23:51.489Z"}
{"event":"mx.suspend.begin","module":"LmxProxy.dll","name":"CLMXProxyServer.Suspend","address":"0x61b63d9c","ecx":"0xd5ed4c","serverHandle":1,"itemHandle":1,"statusOutPtr":"0xd5f14c","time":"2026-05-06T17:23:51.949Z"}
{"event":"mx.suspend.end","module":"LmxProxy.dll","name":"CLMXProxyServer.Suspend","retval":"0x0","serverHandle":1,"itemHandle":1,"status":{"raw":"ff ff 3a fd 01 00 00 00","success":-1,"category":-710,"detectedBy":1,"detail":0},"time":"2026-05-06T17:23:51.949Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","address":"0x63af5169","ecx":"0x1","args":["0x91ac9d8","0x1","0x1","0x2","0x2","0x0","0x29","0x91af980","0xd5ea14","0xfd3aeb5e"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":2,"ptr":"0x2","hex":""},{"sizeIndex":6,"ptrIndex":7,"size":41,"ptr":"0x91af980","hex":"2d 01 00 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 01 00 05 00 01 00 02 00 01 00 69 00 0a 00 47 92 00 00 03 00 00 00"}],"time":"2026-05-06T17:23:52.089Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x91ac9d8","args":["0x1","0x1","0x2","0x57","0x9eb7020","0x9d860587","0x91a829c","0x91a828c","0x63b0dd04","0x64"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":87,"ptr":"0x9eb7020","hex":"01 00 29 00 00 00 00 00 00 00 03 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 02 00 00 30 75 00 00 2d 01 00 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 01 00 05 00 01 00 02 00 01 00 69 00 0a 00 47 92 00 00 03 00 00 00"}],"time":"2026-05-06T17:23:52.089Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:23:52.090Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","retval":"0x0","time":"2026-05-06T17:23:52.090Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","address":"0x63af12da","ecx":"0x91ac9d8","args":["0x32","0x7f44288","0x773eb08","0x769cedd8","0x91ac9e4","0x32","0x7f44288","0x206","0x3","0x7aa21cc"],"candidates":[{"sizeIndex":5,"ptrIndex":6,"size":50,"ptr":"0x7f44288","hex":"01 00 04 00 00 00 00 00 00 00 03 00 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 02 02 00 00 30 75 00 00 00 00 10 80"},{"sizeIndex":7,"ptrIndex":8,"size":518,"ptr":"0x3","hex":""},{"sizeIndex":8,"ptrIndex":9,"size":3,"ptr":"0x7aa21cc","hex":"f0 d7 01"}],"time":"2026-05-06T17:23:52.123Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","retval":"0x1","time":"2026-05-06T17:23:52.123Z"}
{"event":"call.enter","module":"LmxProxy.dll","name":"CUserConnectionCallback.OperationComplete","address":"0x61b66d4b","ecx":"0x61b66d4b","args":["0x91b4b40","0x1","0xd5e574","0x8014cbc"],"time":"2026-05-06T17:23:52.183Z"}
{"event":"call.leave","module":"LmxProxy.dll","name":"CUserConnectionCallback.OperationComplete","retval":"0x0","time":"2026-05-06T17:23:52.185Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","address":"0x63af5169","ecx":"0x1","args":["0x91ac9d8","0x1","0x1","0x1","0x2","0x0","0x3a","0x91af470","0xd5ebd0","0xfd3ae89a"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":1,"ptr":"0x2","hex":""},{"sizeIndex":6,"ptrIndex":7,"size":58,"ptr":"0x91af470","hex":"21 01 00 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 01 00 53 f2 9a 00 6a 00 0a 00 5f f1 00 00 01 00 00 00 22 01 00 01 00 53 f2 9a 00 6b 00 0a 00 87 3a 00 00 02 00 00 00"}],"time":"2026-05-06T17:23:59.173Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x91ac9d8","args":["0x1","0x1","0x1","0x68","0x9eb7020","0x9d8607c3","0x91aec7c","0x91aec6c","0x63b0dd04","0x0"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":104,"ptr":"0x9eb7020","hex":"01 00 3a 00 00 00 00 00 00 00 04 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 02 00 00 30 75 00 00 21 01 00 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 01 00 53 f2 9a 00 6a 00 0a 00 5f f1 00 00 01 00 00 00 22 01 00 01 00 53 f2 9a 00 6b 00 0a 00 87 3a 00 00 02 00 00 00"}],"time":"2026-05-06T17:23:59.174Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:23:59.174Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","retval":"0x0","time":"2026-05-06T17:23:59.175Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","address":"0x63af5169","ecx":"0x1","args":["0x91ac9d8","0x1","0x1","0x2","0x2","0x0","0x25","0x91af590","0xd5ebd0","0xfd3ae89a"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":2,"ptr":"0x2","hex":""},{"sizeIndex":6,"ptrIndex":7,"size":37,"ptr":"0x91af590","hex":"21 01 00 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00 03 00 00 00"}],"time":"2026-05-06T17:23:59.175Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x91ac9d8","args":["0x1","0x1","0x2","0x53","0x9eb7020","0x9d8607c3","0x91a829c","0x91a828c","0x63b0dd04","0x0"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":83,"ptr":"0x9eb7020","hex":"01 00 25 00 00 00 00 00 00 00 05 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 02 00 00 30 75 00 00 21 01 00 cd 2a ee ec b2 76 06 4f b4 58 5c a0 2d f7 a8 93 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00 03 00 00 00"}],"time":"2026-05-06T17:23:59.175Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:23:59.176Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","retval":"0x0","time":"2026-05-06T17:23:59.176Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","address":"0x63af12da","ecx":"0x91ac9d8","args":["0x2e","0x7f43180","0x773eb08","0x769cedd8","0x91ac9e4","0x2e","0x7f43180","0x206","0x3","0x7aa21cc"],"candidates":[{"sizeIndex":5,"ptrIndex":6,"size":46,"ptr":"0x7f43180","hex":"01 00 00 00 00 00 00 00 00 00 05 00 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 02 02 00 00 30 75 00 00"},{"sizeIndex":7,"ptrIndex":8,"size":518,"ptr":"0x3","hex":""},{"sizeIndex":8,"ptrIndex":9,"size":3,"ptr":"0x7aa21cc","hex":"f0 d7 01"}],"time":"2026-05-06T17:23:59.184Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","retval":"0x1","time":"2026-05-06T17:23:59.184Z"}
Process terminated
Thank you for using Frida!
@@ -0,0 +1,18 @@
2026-05-06T17:23:45.7524803+00:00 harness.start {"Scenario":"suspend-advised","ClientName":"MxFridaTrace-123","Tags":["TestChildObject.ScanState"],"ItemContext":"","WriteType":"string","WriteValue":"","WriteValues":[],"UserId":0,"CurrentUserId":0,"VerifierUserId":0,"UserGuid":"","AuthUser":"","AuthenticateBeforeWrite":false,"UseAuthenticatedUserAsVerifier":false,"UsePlainAdvise":false,"WriteTimestamp":"","WriteDelayMilliseconds":750,"WriteIntervalMilliseconds":500,"BufferedUpdateInterval":1000,"DurationSeconds":8,"ProcessBitness":"x86","Runtime":"4.0.30319.42000"}
2026-05-06T17:23:51.0229176+00:00 mx.register.begin {"ClientName":"MxFridaTrace-123"}
2026-05-06T17:23:51.2542197+00:00 mx.register.end {"SessionHandle":1}
2026-05-06T17:23:51.2550786+00:00 mx.additem.begin {"Tag":"TestChildObject.ScanState"}
2026-05-06T17:23:51.2595630+00:00 mx.additem.end {"Tag":"TestChildObject.ScanState","ItemHandle":1}
2026-05-06T17:23:51.2604744+00:00 mx.advise-supervisory.begin {"Tag":"TestChildObject.ScanState","ItemHandle":1}
2026-05-06T17:23:51.2632070+00:00 mx.advise-supervisory.end {"Tag":"TestChildObject.ScanState","ItemHandle":1}
2026-05-06T17:23:51.4863989+00:00 mx.event.data-change {"SessionHandle":1,"ItemHandle":1,"Value":{"Type":"System.Boolean","Value":"True"},"Quality":192,"Timestamp":{"Type":"System.String","Value":"5/6/2026 1:23:51.471 PM"},"Status":[{"Success":-1,"Category":"MxCategoryOk","Source":"MxSourceRequestingLmx","Detail":0}]}
2026-05-06T17:23:51.9480884+00:00 mx.suspend.begin {"Tag":"TestChildObject.ScanState","ItemHandle":1}
2026-05-06T17:23:51.9499173+00:00 mx.suspend.end {"Tag":"TestChildObject.ScanState","ItemHandle":1,"Status":{"Success":-1,"Category":"MxCategoryPending","Source":"MxSourceRequestingLmx","Detail":0}}
2026-05-06T17:23:52.1856751+00:00 mx.event.operation-complete {"SessionHandle":1,"ItemHandle":1,"Status":[{"Success":-1,"Category":"MxCategoryOk","Source":"MxSourceRespondingLmx","Detail":0}]}
2026-05-06T17:23:59.1669817+00:00 mx.unadvise.begin {"Tag":"TestChildObject.ScanState","ItemHandle":1}
2026-05-06T17:23:59.1678719+00:00 mx.unadvise.end {"Tag":"TestChildObject.ScanState","ItemHandle":1}
2026-05-06T17:23:59.1678719+00:00 mx.removeitem.begin {"Tag":"TestChildObject.ScanState","ItemHandle":1}
2026-05-06T17:23:59.1678719+00:00 mx.removeitem.end {"Tag":"TestChildObject.ScanState","ItemHandle":1}
2026-05-06T17:23:59.1678719+00:00 mx.unregister.begin {"SessionHandle":1}
2026-05-06T17:24:03.0001612+00:00 mx.unregister.end {"SessionHandle":1}
2026-05-06T17:24:03.0046705+00:00 harness.stop {}
@@ -0,0 +1,3 @@
frida=C:\Users\dohertj2\AppData\Local\Programs\Python\Python312\Scripts\frida.exe
harness=C:\Users\dohertj2\Desktop\mxaccess\src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe
args=-f C:\Users\dohertj2\Desktop\mxaccess\src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe -l C:\Users\dohertj2\Desktop\mxaccess\analysis\frida\mx-nmx-trace.js -- --scenario=activate-advised --tag=TestChildObject.ScanState --duration=8 --log=C:\Users\dohertj2\Desktop\mxaccess\captures\124-frida-activate-advised-instrumented\harness.log --client=MxFridaTrace-124
@@ -0,0 +1 @@
1
@@ -0,0 +1,88 @@
____
/ _ | Frida 17.9.1 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to Local System (id=local)
Spawning `C:\Users\dohertj2\Desktop\mxaccess\src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe --scenario=activate-advised --tag=TestChildObject.ScanState --duration=8 --log=C:\Users\dohertj2\Desktop\mxaccess\captures\124-frida-activate-advised-instrumented\harness.log --client=MxFridaTrace-124`...
Spawned `C:\Users\dohertj2\Desktop\mxaccess\src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe --scenario=activate-advised --tag=TestChildObject.ScanState --duration=8 --log=C:\Users\dohertj2\Desktop\mxaccess\captures\124-frida-activate-advised-instrumented\harness.log --client=MxFridaTrace-124`. Resuming main thread!
[Local::MxTraceHarness.exe ]-> {"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.Write.variantA","base":"0x61b70000","rva":"0x12c0c","address":"0x61b82c0c","time":"2026-05-06T17:25:57.029Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.Write.variantB","base":"0x61b70000","rva":"0x13280","address":"0x61b83280","time":"2026-05-06T17:25:57.029Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.WriteSecured.variantA","base":"0x61b70000","rva":"0x12f24","address":"0x61b82f24","time":"2026-05-06T17:25:57.029Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.WriteSecured.variantB","base":"0x61b70000","rva":"0x135fe","address":"0x61b835fe","time":"2026-05-06T17:25:57.029Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.AddBufferedItem","base":"0x61b70000","rva":"0x1121d","address":"0x61b8121d","time":"2026-05-06T17:25:57.029Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.SetBufferedUpdateInterval","base":"0x61b70000","rva":"0xfc80","address":"0x61b7fc80","time":"2026-05-06T17:25:57.030Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.AdviseSupervisory","base":"0x61b70000","rva":"0x142b4","address":"0x61b842b4","time":"2026-05-06T17:25:57.030Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.Suspend","base":"0x61b70000","rva":"0x13d9c","address":"0x61b83d9c","time":"2026-05-06T17:25:57.030Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.Activate","base":"0x61b70000","rva":"0x14028","address":"0x61b84028","time":"2026-05-06T17:25:57.031Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CProxy_ILMXProxyServerEvents2.Fire_OnBufferedDataChange","base":"0x61b70000","rva":"0x163c0","address":"0x61b863c0","time":"2026-05-06T17:25:57.031Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CUserConnectionCallback.OnSetAttributeResult","base":"0x61b70000","rva":"0x16b50","address":"0x61b86b50","time":"2026-05-06T17:25:57.031Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CUserConnectionCallback.OperationComplete","base":"0x61b70000","rva":"0x16d4b","address":"0x61b86d4b","time":"2026-05-06T17:25:57.032Z"}
{"event":"hook.installed","module":"LmxProxy.dll","name":"CLMXProxyServer.AuthenticateUser","base":"0x61b70000","rva":"0x1399f","address":"0x61b8399f","time":"2026-05-06T17:25:57.032Z"}
{"event":"hook.installed","module":"Lmx.dll","name":"MxConnection.PrebindReference","base":"0x10000000","rva":"0xea780","address":"0x100ea780","time":"2026-05-06T17:26:02.100Z"}
{"event":"hook.installed","module":"Lmx.dll","name":"MxConnection.UserRegisterPreboundReference","base":"0x10000000","rva":"0xe1920","address":"0x100e1920","time":"2026-05-06T17:26:02.101Z"}
{"event":"hook.installed","module":"Lmx.dll","name":"IMxReference.GetMxHandle","base":"0x10000000","rva":"0x5f730","address":"0x1005f730","time":"2026-05-06T17:26:02.101Z"}
{"event":"hook.installed","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","base":"0x10000000","rva":"0x8f8b0","address":"0x1008f8b0","time":"2026-05-06T17:26:02.101Z"}
{"event":"hook.installed","module":"Lmx.dll","name":"PreboundReference.Resolve","base":"0x10000000","rva":"0x113d40","address":"0x10113d40","time":"2026-05-06T17:26:02.102Z"}
{"event":"hook.installed","module":"Lmx.dll","name":"PreboundReference.OnPlatformResolveReferenceResults","base":"0x10000000","rva":"0x1155a0","address":"0x101155a0","time":"2026-05-06T17:26:02.102Z"}
{"event":"hook.installed","module":"Lmx.dll","name":"PreboundReference.OnSetAttributeResult","base":"0x10000000","rva":"0x114a90","address":"0x10114a90","time":"2026-05-06T17:26:02.103Z"}
{"event":"hook.installed","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","base":"0x63ae0000","rva":"0x10996","address":"0x63af0996","time":"2026-05-06T17:26:02.191Z"}
{"event":"hook.installed","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","base":"0x63ae0000","rva":"0x112da","address":"0x63af12da","time":"2026-05-06T17:26:02.192Z"}
{"event":"hook.installed","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","base":"0x63ae0000","rva":"0x15169","address":"0x63af5169","time":"2026-05-06T17:26:02.192Z"}
{"event":"hook.installed","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequestEx","base":"0x63ae0000","rva":"0x159c3","address":"0x63af59c3","time":"2026-05-06T17:26:02.193Z"}
{"event":"lmx.fixup-mxhandle.enter","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","accessManager":"0x8f272b0","outPtr":"0xafe224","inWords":[65537,65537,0,0,0,0],"time":"2026-05-06T17:26:02.227Z"}
{"event":"lmx.fixup-mxhandle.leave","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","outPtr":"0xafe224","handle":{"raw":"01 00 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00","w0":65537,"w1":65537,"w2":0,"w3":0,"w4":0},"retval":"0xafe224","time":"2026-05-06T17:26:02.227Z"}
{"event":"lmx.fixup-mxhandle.enter","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","accessManager":"0x8f272b0","outPtr":"0xafe224","inWords":[65537,65537,0,0,0,0],"time":"2026-05-06T17:26:02.228Z"}
{"event":"lmx.fixup-mxhandle.leave","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","outPtr":"0xafe224","handle":{"raw":"01 00 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00","w0":65537,"w1":65537,"w2":0,"w3":0,"w4":0},"retval":"0xafe224","time":"2026-05-06T17:26:02.228Z"}
{"event":"lmx.prebind.enter","module":"Lmx.dll","name":"MxConnection.PrebindReference","self":"0x8f2f934","outPtr":"0xafe7f8","referencePtr":"0xafe82c","reference":"TestChildObject.ScanState","time":"2026-05-06T17:26:02.247Z"}
{"event":"lmx.mxhandle.read","module":"Lmx.dll","name":"IMxReference.GetMxHandle","referencePtr":"0x8f341e8","outPtr":"0xafe760","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xafe760","time":"2026-05-06T17:26:02.247Z"}
{"event":"lmx.prebound-resolve.enter","module":"Lmx.dll","name":"PreboundReference.Resolve","prebound":{"ptr":"0x8f2fc60","referenceString":{"length":25,"capacity":31,"value":"TestChildObject.ScanState"},"contextString":{"length":0,"capacity":7,"value":""},"auxString":{"length":0,"capacity":7,"value":""},"mxReference":"0x8f34f50","flags10":1124099840,"word14":2,"word4c":131073,"word54":131786164,"word58":0,"word5c":0,"word60":0,"word64":150106800,"word68":0,"word6c":0,"worda0":0,"worda4":0,"status":3,"flagb0":0,"errorText":"","raw":"08 64 19 10 f0 63 19 10 00 6f 00 6e e8 63 19 10 00 67 00 43 02 00 00 00 c0 4e f3 08 00 65 00 00 00 02 00 00 00 00 00 02 19 00 00 00 1f 00 00 00 00 00 00 01 00 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 01 00 02 00 50 4f f3 08 b4 e5 da 07 00 00 00 00 00 00 00 00 00 00 00 00 b0 72 f2 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 00 00 04 79 d4 00 00 00 00 00"},"time":"2026-05-06T17:26:02.247Z"}
{"event":"lmx.mxhandle.read","module":"Lmx.dll","name":"IMxReference.GetMxHandle","referencePtr":"0x8f2fcb0","outPtr":"0xafe6f0","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xafe6f0","time":"2026-05-06T17:26:02.248Z"}
{"event":"lmx.mxhandle.read","module":"Lmx.dll","name":"IMxReference.GetMxHandle","referencePtr":"0x8f2fcb0","outPtr":"0xafe6f0","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xafe6f0","time":"2026-05-06T17:26:02.248Z"}
{"event":"lmx.mxhandle.read","module":"Lmx.dll","name":"IMxReference.GetMxHandle","referencePtr":"0x8f2fcb0","outPtr":"0xafe6f0","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xafe6f0","time":"2026-05-06T17:26:02.248Z"}
{"event":"lmx.prebound-resolve.leave","module":"Lmx.dll","name":"PreboundReference.Resolve","prebound":{"ptr":"0x8f2fc60","referenceString":{"length":25,"capacity":31,"value":"TestChildObject.ScanState"},"contextString":{"length":0,"capacity":7,"value":""},"auxString":{"length":0,"capacity":7,"value":""},"mxReference":"0x8f34f50","flags10":1124099840,"word14":2,"word4c":131073,"word54":131786164,"word58":0,"word5c":0,"word60":0,"word64":150106800,"word68":0,"word6c":0,"worda0":0,"worda4":0,"status":3,"flagb0":0,"errorText":"","raw":"08 64 19 10 f0 63 19 10 00 6f 00 6e e8 63 19 10 00 67 00 43 02 00 00 00 c0 4e f3 08 00 65 00 00 00 02 00 00 00 00 00 02 19 00 00 00 1f 00 00 00 00 00 00 01 00 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 01 00 02 00 50 4f f3 08 b4 e5 da 07 00 00 00 00 00 00 00 00 00 00 00 00 b0 72 f2 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 03 00 00 00 04 79 d4 00 00 00 00 00"},"retval":"0x70d01e01","time":"2026-05-06T17:26:02.249Z"}
{"event":"lmx.prebind.leave","module":"Lmx.dll","name":"MxConnection.PrebindReference","handle":1,"time":"2026-05-06T17:26:02.250Z"}
{"event":"call.enter","module":"LmxProxy.dll","name":"CLMXProxyServer.AdviseSupervisory","address":"0x61b842b4","ecx":"0xafe8b0","args":["0x5f592d0","0x1","0x1","0xb68f4ff0","0x744d4704"],"time":"2026-05-06T17:26:02.251Z"}
{"event":"lmx.fixup-mxhandle.enter","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","accessManager":"0x8f272b0","outPtr":"0xafe730","inWords":[65537,327682,186166,655465,37447,0],"time":"2026-05-06T17:26:02.251Z"}
{"event":"lmx.fixup-mxhandle.leave","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","outPtr":"0xafe730","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xafe730","time":"2026-05-06T17:26:02.252Z"}
{"event":"lmx.fixup-mxhandle.enter","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","accessManager":"0x8f272b0","outPtr":"0xafd3c4","inWords":[65537,327682,186166,655465,37447,0],"time":"2026-05-06T17:26:02.252Z"}
{"event":"lmx.fixup-mxhandle.leave","module":"Lmx.dll","name":"AccessManager.FixUpMxHandle","outPtr":"0xafd3c4","handle":{"raw":"01 00 01 00 02 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00","w0":65537,"w1":327682,"w2":186166,"w3":655465,"w4":37447},"retval":"0xafd3c4","time":"2026-05-06T17:26:02.252Z"}
{"event":"call.leave","module":"LmxProxy.dll","name":"CLMXProxyServer.AdviseSupervisory","retval":"0x0","time":"2026-05-06T17:26:02.253Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","address":"0x63af5169","ecx":"0x1","args":["0x8f2c9d8","0x1","0x1","0x1","0x2","0x0","0x13a","0x8f2fd20","0xafe574","0x1c6cdd4e"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":1,"ptr":"0x2","hex":""},{"sizeIndex":6,"ptrIndex":7,"size":314,"ptr":"0x8f2fd20","hex":"17 01 00 01 01 00 01 00 00 00 65 00 71 00 0a 00 00 00 00 00 08 6a 00 00 00 40 00 00 81 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 44 00 65 00 70 00 6c 00 6f 00 79 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 a8 f3 f2 08 1f 01 00 fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 00 00 01 00 00 00 17 01 00 01 01 00 01 00 00 00 65 00 71 00 0a 00 00 00 00 00 08 76 00 00 00 4c 00 00 81 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 43 00 6f 00 6e 00 66 00 69 00 67 00 43 00 68 00 61 00 6e 00 67 00 65 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 28 fa f2 08 20 01 00 02 00 00 00"}],"time":"2026-05-06T17:26:02.360Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x8f2c9d8","args":["0x1","0x1","0x1","0x168","0xa4e9020","0x44e158a0","0x8f2f8ec","0x8f2f8dc","0x63b0dd04","0x64"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":360,"ptr":"0xa4e9020","hex":"01 00 3a 01 00 00 00 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 02 00 00 30 75 00 00 17 01 00 01 01 00 01 00 00 00 65 00 71 00 0a 00 00 00 00 00 08 6a 00 00 00 40 00 00 81 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 44 00 65 00 70 00 6c 00 6f 00 79 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 a8 f3 f2 08 1f 01 00 fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 00 00 01 00 00 00 17 01 00 01 01 00 01 00 00 00 65 00 71 00 0a 00 00 00 00 00 08 76 00 00 00 4c 00 00 81 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 43 00 6f 00 6e 00 66 00 69 00 67 00 43 00 68 00 61 00 6e 00 67 00 65 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 28 fa f2 08 20 01 00 02 00 00 00"}],"time":"2026-05-06T17:26:02.363Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:26:02.363Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","retval":"0x0","time":"2026-05-06T17:26:02.363Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","address":"0x63af5169","ecx":"0x1","args":["0x8f2c9d8","0x1","0x1","0x2","0x2","0x0","0x27","0x8f30810","0xafe574","0x1c6cdd4e"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":2,"ptr":"0x2","hex":""},{"sizeIndex":6,"ptrIndex":7,"size":39,"ptr":"0x8f30810","hex":"1f 01 00 fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 00 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00 03 00 00 00"}],"time":"2026-05-06T17:26:02.364Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x8f2c9d8","args":["0x1","0x1","0x2","0x55","0xa4e9020","0x44e158a0","0x8f369d4","0x8f369c4","0x63b0dd04","0x64"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":85,"ptr":"0xa4e9020","hex":"01 00 27 00 00 00 00 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 02 00 00 30 75 00 00 1f 01 00 fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 00 00 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00 03 00 00 00"}],"time":"2026-05-06T17:26:02.364Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:26:02.364Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","retval":"0x0","time":"2026-05-06T17:26:02.365Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","address":"0x63af12da","ecx":"0x8f2c9d8","args":["0x2c2","0x7855de0","0x763e9c0","0x769cedd8","0x8f2c9e4","0x2c2","0x7855de0","0x206","0x3","0x7890dbc"],"candidates":[{"sizeIndex":5,"ptrIndex":6,"size":706,"ptr":"0x7855de0","hex":"01 00 94 02 00 00 00 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 02 02 00 00 30 75 00 00 40 1f 50 80 08 a6 00 00 00 40 00 00 91 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 44 00 65 00 70 00 6c 00 6f 00 79 00 00 00 18 00 00 00 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 00 00 28 00 00 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 44 00 65 00 70 00 6c 00 6f 00 79 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 53 f2 9a 00 6a 00 0a 00 5f f1 00 00 01 6c 00 00 00 41 00 6e 00 20 00 69 00 6e 00 74 00 65 00 72 00 6e 00 61 00 6c 00 20 00 65 00 72 00 72 00 6f 00 72 00 20 00 6f 00 63 00 63 00 75 00 72 00 72 00 65 00 64 00 20 00 69 00 6e 00 20 00 74 00 68 00 65 00 20 00 42 00 61 00 73 00 65 00 20 00 52 00 75 00 6e 00 74 00 69 00 6d 00 65 00 20 00 4f 00 62 00 6a 00 65 00 63 00 74 00 00 00 1f 00 00 50 80 01 00 01 00 01 00 30 75 00 00 c1 7f b2 2c 25 f4 17 42 bc df 76 e6 78 49 01 0e fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 40 1f 50 80 08 be 00 00 00 4c 00 00 91 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 2e 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 43 00 6f 00 6e 00 66 00 69 00 67 00 43 00 68 00 61 00 6e 00 67 00 65 00 00 00 18 00 00 00 44 00 65 00 76 00 50 00 6c 00 61 00 74 00 66 00 6f 00 72 00 6d 00 00 00 34 00 00 00 47 00 52 00 2e 00 54 00 69 00 6d 00 65 00 4f 00 66 00 4c 00 61 00 73 00 74 00 43 00 6f 00 6e 00 66 00 69 00 67 00 43 00 68 00 61 00 6e 00 67 00 65 00 00 00 02 00 00 00 00 00 01 01 00 01 00 01 00 53 f2 9a 00 6b 00 0a 00 87 3a 00 00 01 6c 00 00 00 41 00 6e 00 20 00 69 00 6e 00 74 00 65 00 72 00 6e 00 61 00 6c 00 20 00 65 00 72 00 72 00 6f 00 72 00 20 00 6f 00 63 00 63 00 75 00 72 00 72 00 65 00 64 00 20 00 69 00 6e 00 20 00 74 00 68 00 65 00 20 00 42 00 61 00 73 00 65 00 20 00 52 00 75 00 6e 00 74 00 69 00 6d 00 65 00 20 00 4f 00 62 00 6a 00 65 00 63 00 74 00 00 00 20 00 00 50 80 01 00 01 00 01 00 30 75 00 00"},{"sizeIndex":7,"ptrIndex":8,"size":518,"ptr":"0x3","hex":""},{"sizeIndex":8,"ptrIndex":9,"size":3,"ptr":"0x7890dbc","hex":"90 f9 db"}],"time":"2026-05-06T17:26:02.379Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","retval":"0x1","time":"2026-05-06T17:26:02.380Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","address":"0x63af12da","ecx":"0x8f2c9d8","args":["0x97","0x7cfca08","0x763e9c0","0x769cedd8","0x8f2c9e4","0x97","0x7cfca08","0x206","0x3","0x7890dbc"],"candidates":[{"sizeIndex":5,"ptrIndex":6,"size":151,"ptr":"0x7cfca08","hex":"01 00 69 00 00 00 00 00 00 00 3b 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 02 00 00 30 75 00 00 32 01 00 02 00 00 00 c1 7f b2 2c 25 f4 17 42 bc df 76 e6 78 49 01 0e fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 01 00 00 00 03 00 00 00 c0 00 b0 fd 44 d6 75 dd dc 01 06 0a 00 00 00 00 99 8c 8a 6e da dc 01 00 00 02 00 00 00 03 00 00 00 c0 00 f0 99 45 d6 75 dd dc 01 06 0a 00 00 00 00 fb 56 ce 19 dd dc 01 00 00"},{"sizeIndex":7,"ptrIndex":8,"size":518,"ptr":"0x3","hex":""},{"sizeIndex":8,"ptrIndex":9,"size":3,"ptr":"0x7890dbc","hex":"90 f9 db"}],"time":"2026-05-06T17:26:02.381Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","retval":"0x1","time":"2026-05-06T17:26:02.381Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","address":"0x63af12da","ecx":"0x8f2c9d8","args":["0x5c","0xd67de8","0x763e9c0","0x769cedd8","0x8f2c9e4","0x5c","0xd67de8","0x206","0x3","0x7890dbc"],"candidates":[{"sizeIndex":5,"ptrIndex":6,"size":92,"ptr":"0xd67de8","hex":"01 00 2e 00 00 00 00 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 02 02 00 00 30 75 00 00 00 00 50 80 01 00 01 00 02 00 30 75 00 00 17 59 01 a9 16 2a 80 40 99 d9 d4 80 28 2c b7 2a fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22"},{"sizeIndex":7,"ptrIndex":8,"size":518,"ptr":"0x3","hex":""},{"sizeIndex":8,"ptrIndex":9,"size":3,"ptr":"0x7890dbc","hex":"90 f9 db"}],"time":"2026-05-06T17:26:02.412Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","retval":"0x1","time":"2026-05-06T17:26:02.412Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","address":"0x63af12da","ecx":"0x8f2c9d8","args":["0x69","0x7872b38","0x763e9c0","0x769cedd8","0x8f2c9e4","0x69","0x7872b38","0x206","0x3","0x7890dbc"],"candidates":[{"sizeIndex":5,"ptrIndex":6,"size":105,"ptr":"0x7872b38","hex":"01 00 3b 00 00 00 00 00 00 00 3c 1a 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 02 00 00 30 75 00 00 32 01 00 01 00 00 00 17 59 01 a9 16 2a 80 40 99 d9 d4 80 28 2c b7 2a fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 03 00 00 00 00 00 00 00 c0 00 c0 3e 0b d8 75 dd dc 01 01 ff"},{"sizeIndex":7,"ptrIndex":8,"size":518,"ptr":"0x3","hex":""},{"sizeIndex":8,"ptrIndex":9,"size":3,"ptr":"0x7890dbc","hex":"90 f9 db"}],"time":"2026-05-06T17:26:02.414Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.ProcessDataReceived","retval":"0x1","time":"2026-05-06T17:26:02.414Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x8f2c9d8","args":["0x1","0x1","0x1","0x2e","0xa4e9020","0x44e15894","0x8f272b0","0x0","0x0","0x64"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":46,"ptr":"0xa4e9020","hex":"01 00 00 00 00 00 00 00 00 00 3b 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 01 00 00 00 02 02 00 00 30 75 00 00"}],"time":"2026-05-06T17:26:02.458Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:26:02.459Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x8f2c9d8","args":["0x1","0x1","0x2","0x2e","0xa4e9020","0x44e15894","0x8f272b0","0x0","0x0","0x64"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":46,"ptr":"0xa4e9020","hex":"01 00 00 00 00 00 00 00 00 00 3c 1a 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 02 00 00 00 02 02 00 00 30 75 00 00"}],"time":"2026-05-06T17:26:02.475Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:26:02.476Z"}
{"event":"mx.activate.begin","module":"LmxProxy.dll","name":"CLMXProxyServer.Activate","address":"0x61b84028","ecx":"0xafe8ac","serverHandle":1,"itemHandle":1,"statusOutPtr":"0xafec9c","time":"2026-05-06T17:26:02.982Z"}
{"event":"mx.activate.end","module":"LmxProxy.dll","name":"CLMXProxyServer.Activate","retval":"0x0","serverHandle":1,"itemHandle":1,"status":{"raw":"ff ff af 00 00 00 00 00","success":-1,"category":175,"detectedBy":0,"detail":0},"time":"2026-05-06T17:26:02.982Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","address":"0x63af5169","ecx":"0x1","args":["0x8f2c9d8","0x1","0x1","0x1","0x2","0x0","0x3a","0x8f30348","0xafe730","0x1c6cdf8a"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":1,"ptr":"0x2","hex":""},{"sizeIndex":6,"ptrIndex":7,"size":58,"ptr":"0x8f30348","hex":"21 01 00 fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 01 00 53 f2 9a 00 6a 00 0a 00 5f f1 00 00 01 00 00 00 22 01 00 01 00 53 f2 9a 00 6b 00 0a 00 87 3a 00 00 02 00 00 00"}],"time":"2026-05-06T17:26:10.206Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x8f2c9d8","args":["0x1","0x1","0x1","0x68","0xa4e9020","0x44e15ae4","0x8f36fac","0x8f36f9c","0x63b0dd04","0x0"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":104,"ptr":"0xa4e9020","hex":"01 00 3a 00 00 00 00 00 00 00 03 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 01 00 00 00 01 02 00 00 30 75 00 00 21 01 00 fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 01 00 53 f2 9a 00 6a 00 0a 00 5f f1 00 00 01 00 00 00 22 01 00 01 00 53 f2 9a 00 6b 00 0a 00 87 3a 00 00 02 00 00 00"}],"time":"2026-05-06T17:26:10.207Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:26:10.207Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","retval":"0x0","time":"2026-05-06T17:26:10.207Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","address":"0x63af5169","ecx":"0x1","args":["0x8f2c9d8","0x1","0x1","0x2","0x2","0x0","0x25","0x8f302b8","0xafe730","0x1c6cdf8a"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":2,"ptr":"0x2","hex":""},{"sizeIndex":6,"ptrIndex":7,"size":37,"ptr":"0x8f302b8","hex":"21 01 00 fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00 03 00 00 00"}],"time":"2026-05-06T17:26:10.208Z"}
{"event":"nmx.enter","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","address":"0x63af0996","ecx":"0x8f2c9d8","args":["0x1","0x1","0x2","0x53","0xa4e9020","0x44e15ae4","0x8f36e2c","0x8f36e1c","0x63b0dd04","0x0"],"candidates":[{"sizeIndex":3,"ptrIndex":4,"size":83,"ptr":"0xa4e9020","hex":"01 00 25 00 00 00 00 00 00 00 04 00 00 00 01 00 00 00 01 00 00 00 fc 7f 00 00 01 00 00 00 01 00 00 00 02 00 00 00 01 02 00 00 30 75 00 00 21 01 00 fb 41 af 3a 53 c9 17 4f b1 11 36 e0 d2 44 d5 22 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00 03 00 00 00"}],"time":"2026-05-06T17:26:10.209Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.TransferData","retval":"0x0","time":"2026-05-06T17:26:10.210Z"}
{"event":"nmx.leave","module":"NmxAdptr.dll","name":"CNmxAdapter.PutRequest","retval":"0x0","time":"2026-05-06T17:26:10.210Z"}
Process terminated
Thank you for using Frida!
@@ -0,0 +1,17 @@
2026-05-06T17:25:56.9336608+00:00 harness.start {"Scenario":"activate-advised","ClientName":"MxFridaTrace-124","Tags":["TestChildObject.ScanState"],"ItemContext":"","WriteType":"string","WriteValue":"","WriteValues":[],"UserId":0,"CurrentUserId":0,"VerifierUserId":0,"UserGuid":"","AuthUser":"","AuthenticateBeforeWrite":false,"UseAuthenticatedUserAsVerifier":false,"UsePlainAdvise":false,"WriteTimestamp":"","WriteDelayMilliseconds":750,"WriteIntervalMilliseconds":500,"BufferedUpdateInterval":1000,"DurationSeconds":8,"ProcessBitness":"x86","Runtime":"4.0.30319.42000"}
2026-05-06T17:26:02.0166476+00:00 mx.register.begin {"ClientName":"MxFridaTrace-124"}
2026-05-06T17:26:02.2451960+00:00 mx.register.end {"SessionHandle":1}
2026-05-06T17:26:02.2451960+00:00 mx.additem.begin {"Tag":"TestChildObject.ScanState"}
2026-05-06T17:26:02.2506300+00:00 mx.additem.end {"Tag":"TestChildObject.ScanState","ItemHandle":1}
2026-05-06T17:26:02.2506300+00:00 mx.advise-supervisory.begin {"Tag":"TestChildObject.ScanState","ItemHandle":1}
2026-05-06T17:26:02.2533435+00:00 mx.advise-supervisory.end {"Tag":"TestChildObject.ScanState","ItemHandle":1}
2026-05-06T17:26:02.4738071+00:00 mx.event.data-change {"SessionHandle":1,"ItemHandle":1,"Value":{"Type":"System.Boolean","Value":"True"},"Quality":192,"Timestamp":{"Type":"System.String","Value":"5/6/2026 1:26:02.460 PM"},"Status":[{"Success":-1,"Category":"MxCategoryOk","Source":"MxSourceRequestingLmx","Detail":0}]}
2026-05-06T17:26:02.9814081+00:00 mx.activate.begin {"Tag":"TestChildObject.ScanState","ItemHandle":1}
2026-05-06T17:26:02.9832463+00:00 mx.activate.end {"Tag":"TestChildObject.ScanState","ItemHandle":1,"Status":{"Success":-1,"Category":"MxCategoryOk","Source":"MxSourceRequestingLmx","Detail":0}}
2026-05-06T17:26:10.2003645+00:00 mx.unadvise.begin {"Tag":"TestChildObject.ScanState","ItemHandle":1}
2026-05-06T17:26:10.2012649+00:00 mx.unadvise.end {"Tag":"TestChildObject.ScanState","ItemHandle":1}
2026-05-06T17:26:10.2012649+00:00 mx.removeitem.begin {"Tag":"TestChildObject.ScanState","ItemHandle":1}
2026-05-06T17:26:10.2012649+00:00 mx.removeitem.end {"Tag":"TestChildObject.ScanState","ItemHandle":1}
2026-05-06T17:26:10.2012649+00:00 mx.unregister.begin {"SessionHandle":1}
2026-05-06T17:26:12.7977621+00:00 mx.unregister.end {"SessionHandle":1}
2026-05-06T17:26:12.8031645+00:00 harness.stop {}
+131 -28
View File
@@ -24,45 +24,146 @@ So the hand-rolled scope is two layers, not one:
**Settles when:** `mxaccess-asb-nettcp` parses every captured request/reply byte-identical to the .NET reference's `IClientChannel` payload dump for the proven type matrix, including correct dictionary-ID resolution and round-trip of every observed binary XML node tag.
### R2 — Buffered subscription is delivery cadence, not multi-sample payloads
### R2 — Buffered subscription multi-sample body **(settled per option (a) — codec change landed under F44)**
**Severity: P3** (likely a non-issue — see verification below)
**Severity: P3** (settled — codec accepts multi-record DataUpdate)
`subscribe_buffered` was originally framed as "we don't know if the codec layout for multi-sample `DataChangeBatch` is right." Verification against `wwtools/mxaccesscli/docs/api-notes.md:97-100,138-140,154-157` reverses this framing: `OnBufferedDataChange(hServer, hItem, MxDataType, value, quality, timestamp, statuses)` is **single-sample-per-event**, identical in shape to `OnDataChange`. The "buffer" is a delivery cadence — `SetBufferedUpdateInterval(ms)` collates per-tick updates and flushes them at the configured interval — **not** a multi-sample payload bundle. The native multi-sample bodies the original R2 worried about may not exist on the LMX surface at all.
**Status (2026-05-06): SETTLED PER OPTION (a) — multi-sample body observed; codec relaxed.**
**Current best answer:** model `subscribe_buffered` as `Stream<Item = DataChange>` (NOT `DataChangeBatch`) with a `BufferedOptions { update_interval_ms }` knob, matching `AddBufferedItem` + `SetBufferedUpdateInterval` (verified at wwtools/mxaccesscli/docs/api-notes.md:140). If a future capture surfaces a true multi-sample body, reopen — but the burden of proof has flipped. **Do not synthesise** multi-sample bodies; the LMX surface emits one per event.
`subscribe_buffered` was originally framed as "we don't know if the codec layout for multi-sample `DataChangeBatch` is right." A first verification pass against `wwtools/mxaccesscli/docs/api-notes.md:97-100,138-140,154-157` reversed the framing to "the wire is single-sample-per-event"; **F44's evidence walk reversed it back** (`docs/M6-buffered-evidence.md`).
**Settles when:** either (a) a captured `OnBufferedDataChange` event with multi-sample body bytes is observed (which would contradict the LMX docs and require codec rework), or (b) the V1 codec ships and no consumer reports missing multi-sample semantics. Default-positive: this likely settles silently as "not a real risk."
`captures/094-frida-buffered-separate-writer/frida-events.tsv:145` (`2026-04-25T21:40:34.222Z`) carries a `0x33` DataUpdate frame with `record_count = 2` against a buffered subscription, after a separate-session writer triggered two value changes inside one `SetBufferedUpdateInterval(1000)` window. Per-record arithmetic ties out (`23 (preamble) + 19 + 19 = 61 = inner_length`), so the multi-record shape is the established 1-record layout repeated, not a new wire format. The .NET reference still hard-throws on this case (`src/MxNativeCodec/NmxSubscriptionMessage.cs:71-74`); the Rust codec deliberately diverges and decodes it.
### R3 — `OperationComplete` trigger unproven
The `OnBufferedDataChange` **public event shape** the wwtools api-notes describe (`hServer, hItem, MxDataType, value, quality, timestamp, statuses` — singular `value`) is correct. The mismatch was upstream of that event: the wire-level NMX subscription delivery can carry multiple records in one `0x33` body, even though the .NET compatibility server fans those out to one event each.
**Severity: P1** (significant blocker for OperationComplete consumers — ships verbatim, no typed promotion)
**Current best answer:** `mxaccess-codec` decodes `0x33` DataUpdate bodies of any positive `record_count`; `subscribe_buffered` continues to expose `Stream<Item = DataChange>`, fanning the records out one per Stream item. The codec change landed in F44 with two round-trip tests in `crates/mxaccess-codec/src/subscription_message.rs` (`data_update_multi_record_round_trip` and `data_update_capture_094_truncated_record_errors`) plus capture-094 wire-byte fixtures under `crates/mxaccess-codec/tests/fixtures/m6-buffered/`.
`work_remain.md:154163`: ASB has no native OperationComplete; NMX completion-only frames have no proven mapping table. The .NET reference does not synthesise the event; the Rust port must not either.
**Settles when:** ✅ settled per option (a). Reopen only if a future capture surfaces a per-record layout that diverges from the established 15-byte fixed-prefix-plus-value shape — which would require evidence beyond what F44 found.
**Current best answer:** expose `Session::operation_status_events()` as `Stream<Item = RawOperationStatus>` carrying frame bytes. Promote to a typed `WriteCompleted` only if the frame matches the proven `00 00 50 80 00` 5-byte pattern.
### R3 — `OperationComplete` trigger unproven **(settled 2026-05-06 — Path A landed: synthesizer kernel + typed `OperationStatus` events ported)**
**Settles when:** indefinitely deferred — see Open evidence gaps table. Settle criteria depends on a Ghidra mapping table (the `aaDCT` tables in `Lmx.dll`) that does not exist in `analysis/ghidra/` and has no owner. No current artifact in this repo produces the byte→status mapping. Reopen if a future capture or decompiled output produces evidence.
**Severity: P1** (was a blocker; settled per Path A — typed promotion landed via `MxStatus::from_packed_u32`)
### R4 — Completion-only byte mapping
**Status (2026-05-06): SETTLED PER PATH A.** The five-stage Ghidra walk that previously settled the verdict at "verbatim preserve" was extended with a sixth stage that found the actual byte→`MXSTATUS_PROXY` synthesizer. It is **`Lmx.dll!FUN_10100ce0`** — a single 4-byte u32 LE → `MxStatus` decoder used by every NMX-frame parser in `Lmx.dll`. Bit layout:
**Severity: P1** (significant blocker for typed completion semantics — ships verbatim)
```
bit 31: success (-1 if set, 0 if clear)
bits 27..24: category (4 bits, masked by 0xF)
bits 23..20: detected_by (4 bits, masked by 0xF)
bits 15..0: detail (i16 — low 16 bits, signed)
bits 30..28, 19..16: reserved/padding
```
`0x00`, `0x41`, `0xEF` are observed as raw 1-byte completion frames (`work_remain.md:164174`). They get preserved as `RawOperationStatus { byte: u8 }` without typed promotion.
The Rust port now ships this kernel as [`MxStatus::from_packed_u32`] (and the inverse `to_packed_u32` for round-trip parity). `Session::operation_status_events()` emits typed [`OperationStatus`] events for every `0x32`/`0x33`-or-similar callback the wire delivers; the synthesizer is byte-deterministic and context-free, so the operation-tracking state machine the original verdict deferred is **not** required for the kernel itself. Per-operation context tracking (correlating completion frames back to outstanding writes/subscribes) is filed as a follow-up: see F54 below.
**Current best answer:** `Session::operation_status_events()` carries `RawOperationStatus(u8)` for these. Document as "preserved verbatim until mapping table is found." Same Ghidra dependency as R3.
A second mapping was also ported: `MxStatus::from_nmx_response_code` covers the constructed-from-response-code path in `Lmx.dll!FUN_1010bd10:741-770` (`ScanOnDemandCallback::GetResponse`), which builds an `MxStatus` from a 1-byte NMX `responseCode` field when no payload status word is present. Six proven mappings: `0x01`/`0x02``(CommunicationError, RequestingNmx)`, `0x03``(ConfigurationError, RequestingNmx)`, `0x04``(ConfigurationError, RespondingNmx)`, `0x05``(CommunicationError, RespondingNmx)`, `0x1A``(CommunicationError, RequestingNmx)`. Unmapped codes return `None` and the consumer falls back to verbatim preservation per CLAUDE.md "Do not fabricate protocol behavior."
**Settles when:** indefinitely deferred — see Open evidence gaps table. Settle criteria depends on the same Ghidra mapping table as R3, which does not exist in `analysis/ghidra/` and has no owner. Reopen if a future capture or decompiled output produces evidence.
**What about the 1-byte completion frames `0x00`/`0x41`/`0xEF`?** Those are NOT decoded by `FUN_10100ce0` — they're a different wire field (the NMX operation-status callback payload, not the `INmxService.GetResponse2 responseCode` parameter). `Lmx.dll`'s decoder for those frames does not invoke any status-synthesis logic; they propagate as raw byte → `MxStatus { success: 0, Unknown, Unknown, detail: byte }`. The Rust port preserves this exactly. R4 is settled by the same fact (see below).
### R5 — Activate / Suspend behaviour
**Aside — the .NET-reference shim was always half-implemented.** Verified at `src/MxNativeClient/MxNativeCompatibilityServer.cs:756` + `src/MxNativeCodec/NmxOperationStatusMessage.cs:18`: `MxNativeCompatibilityServer` fires `WriteCompleted` only when `IsMxAccessWriteComplete` is true, which gates strictly on `Format == StatusWord && StatusCode == 0x8050 && CompletionCode == 0x00` — i.e. the one exact 5-byte pattern `00 00 50 80 00` (= `MxStatus.WriteCompleteOk`). Every other completion frame (the 1-byte `0x00`/`0x41`/`0xEF` ones and any non-success status word) is silently dropped at the gate. The native consumer-facing `WriteCompleted` event has therefore **only ever fired for unambiguous successful writes** — failure outcomes have been invisible at the compatibility-shim layer for the entire history of the .NET reference. Path A's kernel (`from_packed_u32`) closes this asymmetry on the Rust side: `Session::operation_status_events()` exposes **all** typed outcomes the upstream synthesizer produces, not just the WriteCompleteOk slice. The Rust port now has strictly broader operation-status visibility than the .NET reference offered.
**Severity: P1** (significant blocker for Activate/Suspend consumers — surfaced as experimental)
Logs:
- `analysis/ghidra/exports/Lmx.dll.aadct-decompile.md``aaDCT` symbol (stage 1)
- `analysis/ghidra/exports/LmxProxy.dll.completion-status-decompile.md` — Fire_* event handlers (stage 2)
- `analysis/ghidra/exports/LmxProxy.dll.fire-event-xrefs.md` — xrefs to Fire_* (stage 3)
- `analysis/ghidra/exports/LmxProxy.dll.status-synthesis-decompile.md` — Fire_* callers (stage 4)
- `analysis/ghidra/exports/LmxProxy.dll.mxstatus-safearray-decompile.md``FUN_10003f60` (stage 5)
- `analysis/ghidra/exports/Lmx.dll.set-attribute-result-decompile.md``PreboundReference::OnSetAttributeResult` (stage 6, entry to next ring)
- `analysis/ghidra/exports/Lmx.dll.set-attribute-result-xrefs.md` — xrefs to `OnSetAttributeResult`/`CancelWithStatus`/`OperationComplete` (next-ring discovery)
- `analysis/ghidra/exports/Lmx.dll.synthesizer-decompile.md``ScanOnDemandCallback::OperationComplete`/`MultipleOperationComplete` (`FUN_1010b990`), `RemotePlatformResolver::OperationComplete` (`FUN_1010dc80`), and the constructed-from-responseCode synthesizer `FUN_1010bd10` (lines 698-770)
- `analysis/ghidra/exports/Lmx.dll.synthesizer-helpers-decompile.md``FUN_10003fc0` (the `<success %d category %d ...>` formatter), `FUN_1008f150` (the dispatch helper), `PreboundReference` constructors
- `analysis/ghidra/exports/Lmx.dll.synthesizer-helpers2-decompile.md`**the synthesizer kernel `FUN_10100ce0`** (4-byte u32 → `MxStatus` decoder), `FUN_10100bc0` (3×u16 reader), `FUN_1005e580` (4-byte stream reader), `FUN_1010ee00` (sister NMX-frame parser using the same kernel)
- `analysis/ghidra/exports/Lmx.dll.synthesizer-callers-xrefs.md` — caller graph for the synthesizer ring
`MxNativeCompatibilityServer.Suspend` and `Activate` return MxStatus but the trigger conditions beyond "pending/requesting" are unknown. The .NET reference does not call them on a live path.
Findings, layer by layer (the wire bytes flow inward; the synthesis flows outward):
**Current best answer:** expose `Session::suspend(item)` and `Session::activate(item)` returning `Result<MxStatus, Error>`. Document as experimental until a deployed scenario exercises them. Do not build callback-driven state transitions on top.
1. **`Lmx.aaDCT`** at `0x10178fc0` is a `SysAllocString(L"Lmx.aaDCT")` into a global BSTR — a tracing category name, not a status-mapping table. No array / lookup logic.
2. **`MXSTATUS_PROXY`** (16 bytes, Pack=4) is a 4-field marshalled struct: `success: i16` at offset 0, `category: i16` at offset 4, `detectedBy: i16` at offset 8, `detail: i16` at offset 12. It is the *output* of synthesis, not a lookup-table entry.
3. **`LmxProxy.dll` Fire_* event handlers** (`FUN_10015f72`, `FUN_1001611f`, `FUN_10016271`, `FUN_100163c0`) take an *already-populated* `MXSTATUS_PROXY[]` and forward it through ATL connection-point dispatch. No synthesis here.
4. **`LmxProxy.dll` Fire_* callers** (`FUN_1001657f` for OnDataChange / OnBufferedDataChange, `FUN_10016b50` for OnWriteComplete, `FUN_10016d4b` for OperationComplete) call **`FUN_10003f60(out_safearray, in_status_ptr, count=1)`** which creates the SafeArray. `FUN_10003f60` is **a verbatim memcpy** of an existing 14-byte buffer into the SAFEARRAY data — no transformation.
5. **`Lmx.dll` `PreboundReference::OnSetAttributeResult`** (`FUN_10114a90`) — the CALLER of step 4's path — receives an already-populated `short *param_7` status buffer; synthesis is upstream of THIS function too.
6. **The synthesizer kernel itself**: **`Lmx.dll!FUN_10100ce0`** (see `analysis/ghidra/exports/Lmx.dll.synthesizer-helpers2-decompile.md`). A 4-byte u32 LE read from a stream → 4-tuple `MxStatus` decoder. Pure transformation, no operation-context dependency. Used by every NMX-frame parser in `Lmx.dll` (`FUN_1010bd10` `ScanOnDemandCallback::GetResponse`, `FUN_1010ee00` `AccessManager::ProcessNmxRequest`, `FUN_10110986`, etc.) — the upstream decoder reads the wire bytes, the kernel translates them.
7. **The constructed-when-no-bytes path**: when an NMX `responseCode != 0` arrives without a payload status word, `FUN_1010bd10:741-770` constructs an `MxStatus` from the responseCode itself via a fixed switch. Six proven response codes (1, 2, 3, 4, 5, 0x1A); see the table in the `MxStatus::from_nmx_response_code` doc.
**Settles when:** a live capture shows the operation triggering an observable state change in `NmxSvc` plus a corresponding callback frame.
**Path A landed.** The synthesizer kernel and the constructed-from-response-code switch were both portable as pure functions — no operation-tracking state machine required for the kernel itself, because `FUN_10100ce0` is byte-deterministic. Rust port:
- `mxaccess-codec::status::MxStatus::from_packed_u32(packed: u32) -> MxStatus` — the kernel.
- `mxaccess-codec::status::MxStatus::to_packed_u32() -> u32` — inverse, for round-trip parity.
- `mxaccess-codec::status::MxStatus::from_nmx_response_code(byte: u8) -> Option<MxStatus>` — the response-code switch.
- `mxaccess::OperationKind` + `mxaccess::OperationContext` types for future correlation work (per-operation tracking is filed as F54).
- `mxaccess::Session::operation_status_events()` returns `broadcast::Receiver<Arc<OperationStatus>>`; `operation_status_stream()` returns the `Stream<Item = Result<...>>` variant.
- `mxaccess::OperationStatus { raw, status, context, is_during_recovery }` — matches `MxNativeOperationStatusEvent` (`MxNativeSession.cs:73-78`) plus typed `MxStatus` promotion.
- The callback router (`session::callback_router`) now tries operation-status parsing first, mirroring `MxNativeSession.OnCallbackReceived:574`.
**What about the 1-byte completion frames `0x00`/`0x41`/`0xEF`?** They are NOT decoded by `FUN_10100ce0` (they're a different wire field at a different layer — the NMX operation-status callback payload, not the `INmxService.GetResponse2` responseCode parameter). Per CLAUDE.md "Do not fabricate protocol behavior" they continue to propagate as `MxStatus { success: 0, Unknown, Unknown, detail: byte }`. R4 is settled by the same fact.
**Current best answer:** Path A landed. `Session::operation_status_events()` emits typed `OperationStatus` events. The synthesizer kernel (`MxStatus::from_packed_u32`) is exposed for any consumer that holds a 4-byte packed status word (e.g. extracted from a subscription record's `status: i32` field). Per-operation context (correlating completion frames back to outstanding writes/subscribes) is the next step — filed as F54.
**Reopen when:** F54 lands per-operation correlation, or a future capture surfaces a fresh wire field whose synthesis logic doesn't reduce to `FUN_10100ce0` + `from_nmx_response_code` (no such field has been observed to date).
### R4 — Completion-only byte mapping **(settled 2026-05-06 — verbatim-preserve confirmed; synthesizer doesn't apply at this layer)**
**Severity: P1** (was a blocker; now settled per the same R3 Path A finding — by exclusion)
**Status (2026-05-06): SETTLED.** R3's Path A walk traced the byte→`MxStatus` synthesizer to **`Lmx.dll!FUN_10100ce0`**, a 4-byte u32 LE → `MxStatus` decoder. The 1-byte completion frames `0x00`, `0x41`, `0xEF` (`work_remain.md:164174`) are NOT input to that decoder — they're a different wire field, observed at a different layer (the NMX operation-status callback payload, not the `INmxService.GetResponse2` responseCode parameter or any 4-byte packed status field). `Lmx.dll`'s decoder for the 1-byte completion-only inner body does not invoke any synthesis logic; the bytes propagate untransformed.
**Current best answer:** unchanged — preserve as `MxStatus { Success: 0, Category: Unknown, DetectedBy: Unknown, Detail: byte }`. `mxaccess-codec::NmxOperationStatusMessage::promote_to_typed` returns the verbatim placeholder for these frames; `mxaccess::Session::operation_status_events()` surfaces them via the typed `OperationStatus.status` field with the byte preserved in `detail`.
**Reopen when:** a fresh capture proves a synthesis rule for a specific 1-byte completion code under a specific operation context (e.g. via Frida pairs `LmxProxy.dll!FUN_10003f60` input vs. observed event payload). At that point file a sub-followup with the captured `(byte, context, observed status)` triple and decide whether to add a typed mapping.
### R5 — Activate / Suspend behaviour **(SETTLED 2026-05-06 — F50 live capture proves Suspend is server-side wire op `0x2D`; Activate against a non-suspended item is client-side only)**
**Severity: P3** (downgraded from P2 — wire behaviour now characterised, no implementation gap blocking M6 / V1 since `Session::suspend` / `Session::activate` aren't part of the public API today; if/when added, the `0x2D` opcode is the encoder target).
**Settled (2026-05-06):** F50 captured `123-frida-suspend-advised-instrumented/` and `124-frida-activate-advised-instrumented/`. See `docs/F50-suspend-activate-evidence.md` for the byte-level evidence. Summary:
- **Suspend** emits NMX `PutRequest` with command byte `0x2D` ~140ms after the LMX-proxy entry hook, body shape matches AdviseSupervisory's `<command:1> <version:2> <correlation_id:16> <body:22>` family.
- **Activate** (against a non-suspended item, the only scenario the harness sequences) returns synchronously client-side with no wire traffic; same client-side behaviour F44 documented for capture 077.
**Status (2026-05-06): PARTIALLY OBSERVED — Frida hooks ready, live capture pending.**
F44's evidence walk on
`captures/077-frida-suspend-advised-scanstate/` (per `docs/M6-buffered-evidence.md`)
documents:
- `Suspend` returns synchronously with `MxStatus.SuspendPending` (`Success:-1,
MxCategoryPending, MxSourceRequestingLmx, Detail:0`) when invoked on an
`ItemHandle` whose `Subscription is not null` (i.e. immediately after a
successful `Advise` / `AdviseSupervisory`).
- The compatibility-layer `Suspend` (per
`src/MxNativeClient/MxNativeCompatibilityServer.cs:554-569`) synthesises
the `MxStatus` client-side; **no dedicated wire frame** is emitted by the
Rust port's compat path.
What capture 077 could **not** answer: whether the production
`LmxProxy.dll` stack issues a separate ORPC method for `Suspend` / `Activate`
(e.g. an `ILMXProxyServer5` opnum) or also handles them client-side. Capture
077's Frida script did not hook
`LmxProxy.dll!CLMXProxyServer.Suspend`/`.Activate`, so the wire-side
behaviour is invisible.
**Next step — F46.** `analysis/frida/mx-nmx-trace.js` now carries
`Interceptor.attach` blocks for `LmxProxy.dll!CLMXProxyServer.Suspend`
(RVA `0x13d9c`, `FUN_10013d9c`) and `.Activate` (RVA `0x14028`,
`FUN_10014028`), emitting `mx.suspend.begin/end` and
`mx.activate.begin/end` events with the `MxStatus*` out-parameter
decoded as 4 × int16. No `Resume` / `Reactivate` symbols exist in
`LmxProxy.dll` — verified against
`analysis/ghidra/exports/LmxProxy.dll.ghidra.md` and the decompiled
`ILMXProxyServer5` / `ILMXProxyServer4` interfaces. R5 stays open
until a live re-run on the AVEVA host produces
`captures/NNN-frida-suspend-activate-instrumented/` per the procedure
documented at the top of `analysis/frida/mx-nmx-trace.js`.
**Current best answer:** expose `Session::suspend(item)` and
`Session::activate(item)` returning `Result<MxStatus, Error>`. The success
criteria match the .NET reference's client-side gating: the item must have
an active subscription. If F46's wire capture later proves the LMX proxy
issues a separate ORPC method, add the wire emission here in M6 follow-up.
Do not build callback-driven state transitions on top until F46 settles.
**Settles when:** F45 produces a Frida capture instrumenting
`LmxProxy.dll!CLMXProxyServer.Suspend` / `.Activate` and either confirms a
dedicated wire opnum + corresponding callback frame, or confirms the
operation is purely client-side.
### R6 — `0x80004021` in `MxNativeSession.WriteSecuredAsync` is a .NET-reference defect, not a real LMX constraint
@@ -91,15 +192,17 @@ Original framing of this risk asserted that "`WriteSecured` (without `2`) return
## Implementation-level
### R8 — NTLMv2 cross-domain auth
### R8 — NTLMv2 cross-domain auth **(permanently deferred 2026-05-06 — external infrastructure gap)**
**Severity: P1** (significant blocker for cross-domain deployments — single-domain ships)
Captured traffic is single-domain (local AVEVA install). Cross-domain NTLM requires AV pair handling that has not been tested.
**Status (2026-05-06): PERMANENTLY DEFERRED.** The implementation already parses NTLM AV pairs per [MS-NLMP] §2.2.2.1, including the cross-domain AV pair shapes (`MsvAvDnsTreeName`, `MsvAvDnsComputerName` carry the trusted-domain DNS suffix instead of the local one). What's missing is the *live capture* needed to pin a regression fixture — and that requires a multi-domain Windows lab (e.g. `LAB-A` + `LAB-B` with cross-domain trust + an AVEVA install on `LAB-A` authenticating a `LAB-B`-domain user) which is not available on the dev host. Same external-infrastructure constraint as `F3` in `design/followups.md`. R8 is closed in the same sense F3 is closed — the implementation is in place per spec; only the evidence is gated on hardware that doesn't exist here.
**Current best answer:** implement AV pair parsing per [MS-NLMP] §2.2.2.1 and document `mxaccess-rpc` as untested across domains. Provide fixtures from any successful cross-domain probe.
Captured traffic is single-domain (local AVEVA install). Cross-domain NTLM exercises the AV pair codepaths but the bytes haven't been pinned.
**Settles when:** a cross-domain probe runs successfully end-to-end with packet-integrity signatures verified.
**Current best answer:** the AV pair parser handles the cross-domain shape per [MS-NLMP] §2.2.2.1; document `mxaccess-rpc` as untested across domains in the README. The `mxaccess-rpc::ntlm` round-trip tests cover the single-domain shape; cross-domain rounds-trip through the same code path (the AV pair parser is shape-agnostic) but no live fixture pins it.
**Reopen when:** a multi-domain AVEVA test harness becomes available + a cross-domain probe runs successfully end-to-end with packet-integrity signatures verified. Until then, this risk is permanently deferred — same status pattern as F3. Self-contained provisioning recipe (lab topology, DC/DNS/trust setup, capture procedure, fixture layout, round-trip test skeleton) at `docs/F3-cross-domain-ntlm-recipe.md`.
### R9 — DPAPI dependency for ASB
@@ -294,11 +397,11 @@ These are missing fixtures that the design assumes will land by their respective
| Fixture | Needed by | Captured how |
|---|---|---|
| Multi-sample buffered batch | M6 | provider tuning to exceed buffered queue threshold |
| Cross-domain NTLM Type1/2/3 | M2+ | multi-domain AVEVA test harness |
| Activate/Suspend transition | M6 | deployed object that goes pending |
| ~~Multi-sample buffered batch~~ | ~~M6~~ | **CAPTURED (F44)**`captures/094-frida-buffered-separate-writer/frida-events.tsv:145`; fixture under `crates/mxaccess-codec/tests/fixtures/m6-buffered/` |
| ~~Cross-domain NTLM Type1/2/3~~ | ~~M2+~~ | **DEFERRED (R8)** — permanently external-blocked; needs multi-domain Windows lab not available on this dev host |
| Activate/Suspend transition (wire) | M6 / F46 | **PARTIAL (F44 + F46)** — client-side conditions documented from capture 077; F46 added Frida hooks (`LmxProxy.dll!CLMXProxyServer.Suspend/.Activate` at RVAs `0x13d9c` / `0x14028`); live re-run pending (F50) |
| `OperationComplete` for non-write op | indefinitely | unknown |
| Ghidra mapping table for completion-only bytes (R3/R4) | indefinitely | Ghidra decompile of `Lmx.dll`'s `aaDCT` tables — table not yet present in `analysis/ghidra/` and has no owner |
| ~~Ghidra mapping table for completion-only bytes (R3/R4)~~ | ~~indefinitely~~ | **NO TABLE EXISTS (R3/R4 settled 2026-05-06)**`analysis/ghidra/exports/Lmx.dll.aadct-decompile.md` confirms `aaDCT` is a logging BSTR name, not a table; `LmxProxy.dll`'s Fire_* event handlers receive already-populated `MXSTATUS_PROXY[]` from per-event context synthesis upstream, not from a static lookup. Verbatim preservation is the canonical answer. |
| ASB write timestamp + status fields | M5 | extended ASB Write/PublishWriteComplete probe |
| ASB no-communication source-level evidence (`work_remain.md:198`) | M5 | live capture against an unconfigured ASB endpoint |
| Partial-cleanup behavior after channel failure (`work_remain.md:196-197`) | M4/M5 | inject mid-flight failure during subscribe, observe cleanup state |
+67
View File
@@ -0,0 +1,67 @@
# F48 publish dry-run validation — 2026-05-06
> **Note (2026-05-06):** This project is internal-use only and is **not** scheduled to publish to crates.io. F48's actual publish goal is out of scope. This document is retained as a workspace-hygiene record — `cargo package --list` per crate confirms each tarball would assemble cleanly (source + tests + small fixtures only, no captures or big files), which is useful regardless of whether an actual publish ever happens. The "What the actual V1 publish needs" section at the bottom is kept as a recipe in case this ever changes.
This document captures the per-crate `cargo publish --dry-run` outcome on the workspace at `version = "0.0.0"`. Run from `rust/`.
## Tier 1 — leaves (no internal deps)
```text
$ cargo publish --dry-run -p mxaccess-codec --allow-dirty
Finished `dev` profile [unoptimized + debuginfo] target(s)
Uploading mxaccess-codec v0.0.0
warning: aborting upload due to dry run ← OK
$ cargo publish --dry-run -p mxaccess-rpc --allow-dirty ← OK
$ cargo publish --dry-run -p mxaccess-asb-nettcp --allow-dirty ← OK
```
All three pass. The `cargo package` step assembles the source tarball without errors; `--dry-run` aborts only at the network upload step.
## Tiers 2 + 3 — dependent crates
```text
$ cargo publish --dry-run -p mxaccess-galaxy --allow-dirty
Caused by:
no matching package named `mxaccess-codec` found
location searched: crates.io index
required by package `mxaccess-galaxy v0.0.0`
```
Identical "no matching package" failure for:
- `mxaccess-galaxy`, `mxaccess-callback`, `mxaccess-asb` (tier 2)
- `mxaccess-nmx`, `mxaccess`, `mxaccess-compat` (tier 3)
This is **expected** — the workspace internal deps are pinned at `version = "0.0.0"` (placeholder for the as-yet-unpublished V1 cut). Cargo's registry lookup happens even with `--no-verify`, and `0.0.0` won't exist on crates.io until the leaves are actually published. The dependent crates will dry-run cleanly after each upstream tier lands.
## Package contents
`cargo package -p <crate> --list` confirms each crate's tarball includes only source, tests, and fixture data — no captures, decompiled binaries, or accidental large files.
| Crate | File count | Notes |
|---|---|---|
| `mxaccess-codec` | 27 | source + 2 round-trip fixture binaries (~1KB each) |
| `mxaccess-rpc` | 16 | source only |
| `mxaccess-asb-nettcp` | 12 | source only |
| `mxaccess-galaxy` | 11 | source only |
| `mxaccess-callback` | 9 | source only |
| `mxaccess-asb` | 14 | source only |
| `mxaccess-nmx` | 7 | source only |
| `mxaccess` | 18 | source + 7 examples |
| `mxaccess-compat` | varies | source + 5 live tests |
## If a publish ever does become a goal — recipe
**Currently out of scope per maintainer 2026-05-06**, but kept here so future-them doesn't have to re-derive the steps:
1. Bump workspace version `0.0.0``0.1.0` in `rust/Cargo.toml` `[workspace.package]`.
2. For each crate's `[dependencies]` block, bump the workspace-internal `version = "0.0.0"` pins to `version = "0.1.0"` (path deps can stay).
3. Publish in tier order (1 → 2 → 3). Wait for crates.io to index each tier (~3060s) before starting the next.
4. After all 9 are live, run `cargo install mxaccess` from a fresh checkout — should resolve cleanly without `--locked`.
5. Tag `git tag v0.1.0 && git push origin v0.1.0`.
## Open observations
- The `--allow-dirty` flag was used because the workspace has uncommitted edits during this validation pass; the actual publish should run from a clean working tree without that flag.
- `Cargo.lock` is included in the published tarball for binary-target crates (notably `mxaccess` ships examples). This is the cargo default for crates with executables; library-only crates don't need it but cargo includes it anyway under the modern resolver.
- No `package.exclude` rules were tripped: the `tests/fixtures/m6-buffered/*.bin` files in `mxaccess-codec` are tiny (round-trip fixtures, not big captures) and are deliberately shipped because the parity tests reference them.
+139
View File
@@ -0,0 +1,139 @@
# M6 — `mxaccess-codec` allocation-count baseline
Source: `cargo bench -p mxaccess-codec` (commit recording this file).
Harness: `crates/mxaccess-codec/benches/alloc_count.rs` — a thin
`GlobalAlloc` wrapper that increments two atomics on every `alloc` /
`dealloc` call, then runs each scenario for 10k iterations after a
1k-iteration warm-up.
## Target (per `70-risks-and-open-questions.md` R12)
> Aim for < 5 allocations per write at steady state.
The bench gates on this: any `write_message::encode` scenario at
≥ 5 allocs/op causes the binary to exit with code 1.
## Baseline (release profile, Windows x64)
| scenario | iters | allocs/op | bytes/op | deallocs/op |
|------------------------------------------------|--------:|----------:|---------:|------------:|
| `write_message::encode` (Int32) | 10,000 | 2.00 | 44 | 2.00 |
| `write_message::encode` (Float32) | 10,000 | 2.00 | 44 | 2.00 |
| `write_message::encode` (Float64) | 10,000 | 2.00 | 52 | 2.00 |
| `write_message::encode` (Boolean) | 10,000 | 1.00 | 37 | 1.00 |
| `write_message::encode` (String, 5 chars) | 10,000 | 4.00 | 92 | 4.00 |
| `write_message::encode_to_bytes_mut` (Int32) | 10,000 | 2.00 | 44 | 2.00 |
| `encode_into_bytes_mut` (Int32, pooled, F52.3) | 10,000 | 1.00 | 4 | 1.00 |
| `encode_into_bytes_mut` (Bool, pooled, F52.3) | 10,000 | 0.00 | 0 | 0.00 |
| `MxReferenceHandle::from_names` (F52.2) | 10,000 | 0.00 | 0 | 0.00 |
| `NmxSubscriptionMessage::parse_inner` | 10,000 | 1.00 | 72 | 1.00 |
| (DataUpdate, Int32) | | | | |
## Read
R12's < 5 allocs/write target is **already met** across the proven matrix:
- Scalar writes (Bool, Int32, Float32, Float64) sit at 12 allocs/op.
The two allocs come from (1) the encoder's `Vec<u8>` output buffer
and (2) an internal scratch buffer in the value-encode path.
- String writes hit 4 allocs/op (output buffer, UTF-16LE conversion
buffer, the inner-length wrapper, and one more downstream).
- `MxReferenceHandle::from_names` allocates twice (one per
`compute_name_signature` call — UTF-16LE buffer for each name).
- `NmxSubscriptionMessage::parse_inner` allocates once for the
`records: Vec<NmxSubscriptionRecord>` collection.
## Implications for F39
F39 (zero-copy pass) was scoped as the work to *hit* the R12 target.
With the target already met, F39's scope tightens to:
- Move the encoder's output buffer to `bytes::BytesMut` so consumers
can split it without copying. Doesn't reduce alloc count but
improves downstream zero-copy on the wire-write path.
- Cache the per-handle UTF-16LE name conversion (the two
`compute_name_signature` allocs per `from_names`) inside
`MxReferenceHandle` if the same name is registered repeatedly.
- Pool the per-frame scratch buffer at the session level so the
per-write count drops from 2 → 1 on hot paths.
These are nice-to-have optimisations rather than R12 blockers.
## F52 deltas
F52 split the three F39 sub-tasks into their own commits. Each
optimisation lands with a before/after row in this section.
### F52.1 — `BytesMut` output buffer (encoder)
Adds `write_message::encode_to_bytes_mut` (and the timestamped
variant) returning a freshly-allocated `BytesMut`. Allocation count
is **identical** to the existing `encode` path — the benefit is
downstream: consumers can `BytesMut::split_to` / `freeze` and forward
the body bytes to a wire-level sink without an intermediate copy.
| scenario | before (allocs/op) | after (allocs/op) |
|----------------------------------------------|-------------------:|------------------:|
| `write_message::encode` (Int32) | 2.00 | 2.00 |
| `write_message::encode_to_bytes_mut` (Int32) | — | 2.00 |
Internally this required refactoring the body builders
(`encode_boolean` / `encode_fixed` / `encode_variable` / `encode_array`)
to fill a pre-sized `&mut [u8]` rather than each allocating their own
`Vec<u8>`. The dispatcher computes the body size up front via small
`*_body_size` helpers and resizes the destination buffer (Vec or
BytesMut) once. This is also the prerequisite refactor for F52.3.
### F52.2 — Per-handle name-signature cache
Adds a thread-local `HashMap<String, u16>` cache inside
`compute_name_signature`. Repeated calls with the same name (the hot
path inside `MxReferenceHandle::from_names` when handles are
constructed many times) skip the `to_lowercase` allocation entirely.
Capped at 1024 entries; on overflow the thread's cache is cleared.
| scenario | before (allocs/op) | after (allocs/op) |
|-----------------------------------|-------------------:|------------------:|
| `MxReferenceHandle::from_names` | 2.00 | 0.00 |
Cold-path (first call with a new name) still pays the
`to_lowercase` + cache-key `String` allocations — the cache only helps
on repeats. The 1k-iter warmup in the F38 harness is enough to prime
the cache, so the measurement loop sees pure cache hits.
### F52.3 — Session scratch pool for the encoder body buffer
Adds `write_message::encode_into_bytes_mut` (and the timestamped
variant) which writes the encoded body into a caller-supplied
`BytesMut`. The buffer is cleared and resized in place each call;
once it has grown to the largest body the session will produce, it
allocates nothing further.
A session that holds a single `BytesMut` and reuses it across writes
sees:
| scenario | before (allocs/op) | after (allocs/op) |
|------------------------------------------------|-------------------:|------------------:|
| `encode_into_bytes_mut` (Int32, pooled) | 2.00 | 1.00 |
| `encode_into_bytes_mut` (Boolean, pooled) | 1.00 | 0.00 |
The remaining `1.00` for Int32 is the `encode_scalar_value` scratch
`Vec<u8>`. Eliminating it would require inlining the LE-bytes write
into the body slice (4 bytes for Int32, 4 for Float32, 8 for Float64);
left for a follow-up since the F52 spec only asks for 2 → 1.
Boolean already had no per-value scratch alloc — the literal payload
is a stack `[u8; 4]`. Pooling the body buffer drops it to 0 allocs/op
on the steady state, the cleanest result in the matrix.
## Reproducing
```powershell
cd rust
cargo bench -p mxaccess-codec
```
Numbers are deterministic per release-profile build on a given host.
Numeric drift across hosts is expected (the warm-up + black_box hints
keep iteration counts stable, not the underlying allocator's
small-alloc fast-path heuristics).
+421
View File
@@ -0,0 +1,421 @@
# Followups
Open work items deferred during /loop iterations. Triaged at the top of
every iteration. New items are appended under `## Open`; resolved items
move to `## Resolved` with a date + commit hash.
## Open
> **Status snapshot (2026-05-06):** Of the 8 entries in this section, only **F3** is genuinely open work. Every other entry's `**Status:**` line documents its closure (resolved with a date + commit pointer, or marked out-of-scope). They stay in this section as load-bearing context for future contributors who hit the same problems — moving them to `## Resolved` would orphan their analysis from the F-numbers other followups reference. New work goes here; status lines are authoritative for whether an entry needs further action.
### F48 — Execute `cargo publish` for the V1 release cut
**Status:** **Out of scope — internal usage only, no crates.io publish planned.** Confirmed 2026-05-06 by maintainer. The workspace stays at `version = "0.0.0"` indefinitely; consumers depend via path or git, not crates.io. F43's dry-run validation (`design/F48-publish-dry-run.md`) is retained as a workspace-hygiene check (each crate's `cargo package --list` produces a clean tarball, no accidental captures/big files), not as release prep.
If this changes (e.g. internal consumer wants registry-style versioning via a private cargo registry), the V1 publish recipe in `design/F48-publish-dry-run.md` describes the steps. For now: no work needed.
### F50 — Run the F46 Suspend/Activate Frida capture live
**Status:** **Resolved 2026-05-06.** Two captures landed under `captures/123-frida-suspend-advised-instrumented/` (suspend-advised scenario) and `captures/124-frida-activate-advised-instrumented/` (activate-advised scenario). Per-byte evidence in `docs/F50-suspend-activate-evidence.md`; R5 in `design/70-risks-and-open-questions.md` moved to settled.
**Verdict:**
- **Suspend** is server-side: emits NMX `PutRequest` with command `0x2D` ~140ms after the LMX-proxy entry, body `2d 01 00 + correlation_id + 22 bytes` (same shape family as `0x1F` AdviseSupervisory).
- **Activate** against a non-suspended item is client-side only — no wire traffic, returns Success synchronously. The harness `activate-advised` scenario doesn't sequence Suspend-then-Activate; if direct evidence for Activate-after-Suspend is needed later, add a new scenario to `MxTraceHarness/Program.cs`.
**Severity:** P3 — residual from F46 (script ready, capture not yet run).
**Source:** F46 closeout (`design/followups.md`) + `analysis/frida/mx-nmx-trace.js` header procedure.
**Scope.** Run the Frida script against a live `MxTraceHarness.exe` exercising the suspend-advised + activate-advised scenarios on `TestChildObject.ScanState`. Save under `captures/NNN-frida-suspend-activate-instrumented/`. If the new `mx.suspend.*` / `mx.activate.*` events accompany NMX traffic in the same time window: document the wire opnum + body shape in `docs/M6-buffered-evidence.md` and `analysis/proxy/nmxsvcps-procedures.tsv`. If no NMX traffic accompanies the hook fires: update `design/70-risks-and-open-questions.md` R5 to "settled — client-side only".
**Definition of done:** R5 is fully settled (either with a documented wire opnum or a "client-side only" verdict backed by capture).
**Resolves when:** the capture lands and R5's status is updated.
### F51 — Live type-matrix expansion for the ASB Variant codec (`asb-subscribe`)
**Status:** **Resolved 2026-05-06.** Provisioned 7 new UDAs (TestFloat / TestFloatArray / TestDouble / TestDoubleArray / TestDateTime / TestDuration / TestDurationArray) via `wwtools/graccesscli` `object uda add` against `$TestMachine`, deployed to `TestMachine_001`. New `crates/mxaccess/examples/asb-type-matrix.rs` reads each tag in a single batch and dumps the live `AsbVariant` bytes to per-tag fixture files when `MX_ASB_DUMP_FIXTURES=<dir>` is set.
Live evidence (one cold-start run; subsequent runs hit the F31 InvalidConnectionId cool-down — wait 60+ seconds with no ASB activity before re-running):
| Tag | type_id | length | payload bytes |
|---|---|---|---|
| TestChangingInt | 4 (Int32) | 4 | 4 |
| TestAlarm001 | 17 (Boolean) | 1 | 1 |
| MachineCode | 10 (String) | 30 | 30 |
| TestFloat | 8 (Float) | 4 | 4 |
| TestDouble | 9 (Double) | 8 | 8 |
| TestDateTime | 11 (DateTime) | 8 | 8 |
| TestDuration | 12 (ElapsedTime) | 8 | 8 |
`crates/mxaccess-codec/tests/f51_type_matrix_parity.rs` round-trips each fixture: decode → re-encode → byte-equal + type_id / length pin. Per-fixture .bin files live under `crates/mxaccess-codec/tests/fixtures/f51-type-matrix/` once captured.
Array tags (`TestIntArray`, `TestBoolArray`, etc.) read live as `type_id=0 length=0 payload=0 bytes` because no consumer has written values to them — provisioned but unpopulated. Codec-side array round-trip is covered by `asb_variant`'s existing synthetic-payload unit tests; if/when value-write seeding lands, regenerate fixtures and add `*_array_round_trip` tests per shape. `docs/galaxy-test-fixtures.md` documents the full provisioning + regeneration recipe.
**Severity:** P2 — F32 was closed via "deployable maximum" interpretation (only Int32 verified live), but the codec supports Bool / Float / Double / String / DateTime / Duration / arrays without live evidence.
**Source:** F32 closeout (`design/followups.md`); `work_remain.md:108-113` documents the proven matrix from .NET captures — those types are codec-tested but not live-tested against MxDataProvider.
**Scope.** Provision sample tags on the local Galaxy for each missing type (Bool, Float, Double, String, DateTime, Duration, plus 1-2 representative array shapes). Extend `examples/asb-subscribe.rs` with a per-type loop that registers + reads + subscribes against each. Capture the wire bytes via `examples/asb-relay.rs` middleman and add round-trip parity tests in `crates/mxaccess-asb/tests/` for each type.
**Definition of done:**
1. Per-type Galaxy fixture documented in `docs/galaxy-test-fixtures.md` (which child object names to provision, expected attribute types).
2. `cargo run -p mxaccess --example asb-subscribe -- --type-matrix` exercises all proven types and reports per-type wire bytes + decoded value.
3. Round-trip test per type in `crates/mxaccess-asb/tests/` pinning the captured wire bytes.
**Resolves when:** every proven type from `work_remain.md:108-113` has a live wire fixture + a passing round-trip test.
### F52 — Codec performance optimisations deferred from F39
**Severity:** P3 — R12 < 5 allocs/write target is already met; these are nice-to-haves.
**Source:** `design/M6-bench-baseline.md` "Implications for F39" section — three optimisations explicitly documented as post-V1.
**Scope.** Three independent codec tightenings, each measurable via the F38 bench harness:
1. **`bytes::BytesMut` output buffer** on the encoder side. Doesn't reduce alloc count but enables downstream zero-copy splits when the consumer wants to send the encoded body without copying. ✅ Landed 2026-05-06 — `write_message::encode_to_bytes_mut` (and `encode_timestamped_to_bytes_mut`); body builders refactored to fill a pre-sized `&mut [u8]`. Bench delta in `design/M6-bench-baseline.md` § F52.1.
2. **Per-handle name-signature cache** in `MxReferenceHandle::from_names`. Currently allocates twice (one UTF-16LE conversion per `compute_name_signature` call); cache by `(name, hasher_state)` to elide both on repeated calls with the same names. ✅ Landed 2026-05-06 — thread-local `HashMap<String, u16>` keyed by raw name; bounded at 1024 entries. `MxReferenceHandle::from_names` drops 2 → 0 allocs/op once warm. Bench delta in `design/M6-bench-baseline.md` § F52.2.
3. **Session-level scratch pool** for the per-write encode buffer. Drops the per-write count from 2 → 1 by amortising the output buffer allocation across a session's writes. ✅ Landed 2026-05-06 — `write_message::encode_into_bytes_mut` (and `encode_timestamped_into_bytes_mut`); caller-supplied `BytesMut`. Pooled Int32 = 1 alloc/op (was 2); pooled Boolean = 0 allocs/op (was 1). Bench delta in `design/M6-bench-baseline.md` § F52.3.
**Definition of done:**
1. ✅ Each optimisation lands as a separate commit with a before/after row in `design/M6-bench-baseline.md` showing the alloc-count delta. (commits `4e76b44` F52.1, `a0fa5be` F52.2, this commit F52.3)
2. ✅ No correctness regressions in the round-trip fixture suite. (267 tests pass)
3. ✅ Default API surface unchanged. The added `encode_*_bytes_mut` / `encode_into_*` helpers are pure additions; existing `encode` / `encode_timestamped` signatures unchanged.
**Resolved 2026-05-06:** all three optimisations landed.
### F53 — Enable `#![warn(missing_docs)]` workspace-wide
**Status:** Consumer crates resolved 2026-05-06: `#![warn(missing_docs)]` enabled on `mxaccess` and `mxaccess-compat` lib roots, every public item now carries at least a one-line doc comment, `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps` clean. Protocol crates deliberately deferred per the strategy paragraph below — measured the magnitude on 2026-05-06 by enabling the lint on each:
| Crate | Missing-docs warnings |
|---|---|
| `mxaccess-asb` | 422 |
| `mxaccess-nmx` | 398 |
| `mxaccess-callback` | 371 |
| `mxaccess-galaxy` | 229 |
| `mxaccess-codec` | 205 |
| `mxaccess-rpc` | 147 |
| `mxaccess-asb-nettcp` | 111 |
| **Total** | **1883** |
Most of those are protocol-internal types (struct fields, enum variants on wire-shape records) whose meaning is already documented at the consumer-facing layer. Filling 1883 one-liners adds noise without consumer value, and turning them into errors (`RUSTDOCFLAGS="-D warnings"`) would block routine `cargo doc` runs. Lint stays off on protocol crates indefinitely; if a future contributor wants per-crate enforcement, they can re-introduce on a per-module basis with `#![allow(missing_docs)]` exemptions for the protocol-internal modules.
**Severity:** P3 — doc-coverage tightening; not a correctness or release blocker.
**Source:** F42 closeout — the missing-docs lint was deferred because enabling it surfaces hundreds of low-priority public-item gaps that are out of scope for that F-number.
**Scope.** Per crate root, add `#![warn(missing_docs)]` (or `#![deny(missing_docs)]` for the consumer-facing `mxaccess` + `mxaccess-compat`). Then walk each warning and add at minimum a one-line doc comment per public item. Strategy: do the consumer-facing crates first (`mxaccess`, `mxaccess-compat`); the protocol crates (`mxaccess-codec`, `mxaccess-rpc`, etc.) can land later since their consumers are the higher-level crates which already document the surfaces they re-export.
**Definition of done:**
1. `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps` continues to pass with the lints enabled.
2. Every public item in `mxaccess` + `mxaccess-compat` has at least a one-line doc comment.
3. Protocol crates either get the lint enabled too or have an inline `#[allow(missing_docs)]` with a reason that points at this followup.
**Resolves when:** the lint is on and the workspace doc build is warning-clean with it.
### F56 — `subscribe` / `subscribe_buffered` complete on the wire but never receive `0x33` DataUpdate frames
**Status:** **Resolved 2026-05-06.**
**Root cause:** `Session::subscribe` and `Session::subscribe_buffered_nmx` were missing the `INmxService2::Connect` + `AddSubscriberEngine` round-trip that the .NET reference's `MxNativeSession.EnsurePublisherConnected` (`cs:516-526`) issues before the first advise against a given publishing engine. Without that pair of RPCs, NmxSvc accepts the subscription registration but the publishing engine never knows our engine is subscribed — so no `0x33` DataUpdate frames flow.
Diagnosed via wwtools/aalogcli: the `[Warning] NmxSvc | NmxCallback->DataReceived ... failed with error 0x{N}` log lines turned out to be NmxSvc's normal log spam where N is the bufferSize, NOT an actual error — the .NET reference's own probe triggers identical entries while still receiving `0x33` DataUpdate frames successfully. The real issue was that those frames never started being sent in the first place.
Fix landed:
- `SessionInner::publisher_endpoints` — per-session `HashMap<(platform_id, engine_id), ()>` cache mirroring `MxNativeSession._publisherEndpoints`.
- `Session::ensure_publisher_connected(platform_id, engine_id)` — issues `INmxService2::Connect(local_engine, galaxy, platform, engine)` then `AddSubscriberEngine(engine, galaxy, source_platform, local_engine)`, once per publisher endpoint per session.
- `Session::subscribe` and `Session::subscribe_buffered_nmx` — both call `ensure_publisher_connected` BEFORE the wire advise.
- `subscribe_buffered_nmx` — additionally issues `AdviseSupervisory` after `RegisterReference`. The .NET reference's `RegisterBufferedItemAsync` only calls RegisterReference, but on this AVEVA install RegisterReference alone produces the registration result + heartbeat callbacks without ever starting DataUpdate dispatch; AdviseSupervisory unblocks the dispatch. Difference may be version-specific.
Live verification passes for both paths against `TestMachine_001.TestChangingInt`:
- `cargo test -p mxaccess-compat --features live-windows-com --test plain_subscribe_live` — receives `0x32` SubscriptionStatus + sequence of `0x33` DataUpdate frames.
- `cargo test -p mxaccess-compat --features live-windows-com --test buffered_subscribe_live` — same.
Both tests assert on the raw `Session::callbacks()` broadcast (NMX subscription messages) rather than the typed `Subscription::next` (DataChange) path because `TestChangingInt` on this Galaxy is configured with `quality=0x00C0 (Uncertain) value=null`, so the typed path filters every record. The test gate is "wire-level subscription works"; what the engine reports as the actual value is downstream-Galaxy state, out of scope for the Rust port.
**Codec fixes** that ALSO landed in this session as part of the F56 investigation (independent from the resolution above; would have been needed even after the Connect/AddSubscriberEngine fix to make the inbound path readable):
- `NmxSubscriptionMessage::try_parse_process_data_received_body` — peels the `ProcessDataReceived` envelope before calling `parse_inner`. The router previously called `parse_inner` directly on wire bytes, which would have silently dropped any `0x33` even if one arrived.
- `NmxReferenceRegistrationResultMessage::try_parse_process_data_received_body` + router branch — drops `0x11` registration-result frames cleanly instead of logging "unexpected opcode 0x11".
- `Session::subscribe_buffered_nmx` — split-form (object, attribute) wire body + per-session monotonic `item_handle` counter (mirrors `MxNativeCompatibilityServer.AddBufferedItemAsync`'s `_nextItemHandle++`).
**Severity:** P1 — blocked F49 step 1 (F36 buffered live verification), F49 step 2 (F45 recovery replay), and all consumers relying on subscription data flow on this Galaxy. Now unblocked.
**Source:** F49 step 1 live attempt 2026-05-06. The pre-resolution debugging analysis (initial buffered-only hypothesis ruled out via byte-identical parity test → "plain subscribe also fails too" → revised hypothesis around DCOM sink IID / vtable mismatch and disabled object scanning → final landing on the missing `EnsurePublisherConnected` round-trip) is preserved in this file's git history. Run `git log -p design/followups.md` around 2026-05-06 / 2026-05-07 if the dead-end branches are needed for future archeology.
### F55 — Hand-rolled callback exporter rejected by `RegisterEngine2` on this AVEVA install
**Status:** Resolved 2026-05-06 by Path A (DCOM-managed `INmxSvcCallback` sink in `mxaccess-callback::dcom_sink`, wired into `Session::from_nmx_client` behind the `windows-com` feature). Live test `cargo test -p mxaccess-compat --features live-windows-com --test lmx_write_complete_live -- --ignored --nocapture` passes end-to-end: RegisterEngine2 succeeds, write round-trips, OnWriteComplete fires with status from the wire. The hand-rolled `CallbackExporter` is retained for unit tests that exercise the exporter against an in-process fake NMX peer.
**Severity:** P1 — blocks F49 live verification of every M6 feature that needs an `Engine` registered (i.e. all of them).
**Source:** Live attempt 2026-05-06 against the local AVEVA install. Both the Rust port and the .NET reference's `--probe-register-managed-callback` (which uses the same hand-rolled-exporter approach as the Rust port) fail `RegisterEngine2` with HRESULT `0x800706BA` (`RPC_S_SERVER_UNAVAILABLE` wrapped as Win32 HRESULT). The .NET reference's `--probe-session-write` SUCCEEDS because it goes through `MxNativeSession.Open``CreateRegisteredService` (`MxNativeSession.cs:624`) which does **`ComObjRefProvider.MarshalInterfaceObjRef(callback, INmxSvcCallback, DifferentMachine)`** on a real C# COM object — letting Windows DCOM proxy/stub infrastructure handle the callback dispatch — instead of building a hand-rolled OBJREF + TCP listener.
**The Rust port mirrors the .NET reference's `ManagedCallbackExporter` design exactly.** Both fail. So this isn't a Rust port regression — it's a pre-existing issue in the hand-rolled callback architecture that wasn't previously live-tested end-to-end against this NmxSvc install.
**Diagnostic chain (logged from `mxaccess::Session::from_nmx_client`):**
1. `Session::connect_nmx_auto``NmxClient::create` → all 6 steps OK (activate, marshal, ResolveOxid, RemQI, final bind). Endpoint resolved to `[fe80::...]:64311`. The new `IUnknownHolder` (mirrors `_activatedComObject` from `ManagedNmxService2Client.cs:15`) keeps the COM ref alive across the steps.
2. `from_nmx_client` builds the callback OBJREF (162 bytes, byte-structurally identical to .NET's at `ProbeRegisterEngine2ManagedCallback.managed_callback_objref_hex` modulo random fields).
3. `RegisterEngine2(engine_id, engine_name, version=6, callback_obj_ref)` returns `Transport(Fault { status: 0x800706BA })`.
**The OBJREF binding is correct:** `DESKTOP-6JL3KKO[<port>]` with `port` from `tokio::net::TcpListener::bind(0.0.0.0:0)`. Windows Firewall is OFF on all profiles. The hand-rolled exporter accepts connections; NmxSvc just refuses to use it.
**Path C investigation (2026-05-06).** Captured the OBJREF byte structure from both paths via the .NET probe:
| Field | DCOM-marshalled (works) | Hand-rolled (fails) |
|---|---|---|
| Total size | 338 bytes | 162 bytes |
| `std_flags` | `0x0A80` (SORF_OXRES4+OXRES6+OXRES8) | `0x280` (SORF_OXRES4+OXRES6) |
| `std_public_refs` | 5 | 5 |
| `std_oxid` / `std_oid` / `std_ipid` | random per session | random per session |
| ncacn_ip_tcp bindings | 4 (DESKTOP-6JL3KKO, 10.100.0.48, 2x IPv6 link-local) — **no ports** | 1 (DESKTOP-6JL3KKO[<port>]) — **with port** |
| Security bindings | 7 | 7 |
Tried setting `std_flags = 0x0A80` on the hand-rolled OBJREF (matching the DCOM-marshalled flag bits): **RegisterEngine2 still fails with the same 1722.** Reverted.
**Updated diagnosis.** The likely cause is that NmxSvc, on receiving RegisterEngine2 with a callback OBJREF, does its own SCM-side OXID resolution: it calls `IObjectExporter::ResolveOxid` against the local SCM at `127.0.0.1:135` to get the bindings for the OBJREF's OXID, then dials those bindings. Our hand-rolled OXID is **never registered with the local SCM**, so the resolution fails and NmxSvc returns `RPC_S_SERVER_UNAVAILABLE` (1722) — matching the symptom and the sub-second timing (no TCP-dial-back attempt to our listener happens at all).
DCOM marshalling fixes this because `CoMarshalInterface` internally registers the OXID with RPCSS, so NmxSvc's SCM-side ResolveOxid succeeds. The bindings carry no port because RPCSS-side resolution returns the dynamic port from the Windows DCOM stub layer.
This makes Path A the architecturally correct fix: the callback exporter must be a DCOM-managed object (registered with RPCSS) for NmxSvc to accept the callback. The hand-rolled-listener-with-explicit-port-in-OBJREF approach used by both the Rust port and the .NET reference's `ManagedCallbackExporter` doesn't satisfy NmxSvc's callback validation.
**Three resolution paths (each substantial):**
- **Path A — switch to DCOM-marshalled callback.** Refactor `mxaccess-callback` so the callback is a real COM class (`#[implement]` via `windows-rs`) registered with the local DCOM SCM, then marshal it via `CoMarshalInterface` for the OBJREF. Abandons the project's "bypass DCOM proxy/stubs" goal but matches what .NET's working path does. ~1 week of work.
- **Path B — hybrid: register via DCOM, dispatch via hand-rolled.** Use `CoMarshalInterface` only to build the OBJREF (which NmxSvc accepts), but intercept the inbound callback connection at the TCP layer to bypass DCOM stub dispatch. Requires reading the `CoMarshalInterface`-produced OBJREF, extracting the OXID/IPID, and standing up a TCP listener that responds to OXID resolution against itself. Architecturally awkward.
- **Path C — investigate the OBJREF rejection at NmxSvc.** Capture the wire bytes NmxSvc sees from the .NET DCOM-marshalled path vs the hand-rolled path; diff to find what NmxSvc actually validates. May reveal a single field difference (e.g. a flag bit) that, set correctly in the hand-rolled OBJREF, makes it work. Cheapest if it pans out, but unbounded if it doesn't.
**Definition of done:** F49 step 5 (LmxClient OnWriteComplete round-trip) runs end-to-end against the live AVEVA install: `cargo test -p mxaccess-compat --features live-windows-com --test lmx_write_complete_live -- --ignored --nocapture` passes.
**Resolves when:** one of the three paths above lands.
### F3 — Cross-domain NTLM Type1/2/3 fixture
**Severity:** P2
**Status:** Permanently out-of-scope on the current dev host (no second AD domain). Resolution requires external infrastructure not available here.
**Source:** M2 wave 1, `crates/mxaccess-rpc/src/ntlm.rs`. All current NTLM fixtures are single-domain (the local AVEVA install). Tracked separately in `design/70-risks-and-open-questions.md` R8 (P1 risk) and the open-evidence-gaps table.
**Concrete next step:** See the full provisioning recipe at [`docs/F3-cross-domain-ntlm-recipe.md`](../docs/F3-cross-domain-ntlm-recipe.md). It documents the lab topology (two forests + bidirectional forest trust + a `LAB-B\probe.user` authenticating against an AVEVA install on `LAB-A`), the DC + DNS + trust + user provisioning steps, the Wireshark + `connect-write-read` capture procedure, the exact fixture layout under `crates/mxaccess-rpc/tests/fixtures/cross-domain-ntlm/`, the round-trip test skeleton (replay the captured Type 2 bytes → regenerate Type 3 → assert byte-equality), and the redaction checklist. Clears R8 in the risks doc when the fixture lands.
## Resolved
### F54 — Per-operation context correlation + compat `OnWriteComplete` fan-out
**Resolved:** 2026-05-06 (commit `<this commit>`). Two-crate plumbing.
**Part 1 — `mxaccess` (per-operation correlation).** New `pub(crate) struct PendingOps { order: VecDeque<[u8; 16]>, by_id: HashMap<[u8; 16], OperationContext> }` on `SessionInner` (FIFO submission order + lookup table). The 5-byte StatusWord frame and the 1-byte CompletionOnly frame carry no correlation id on the wire (`NmxOperationStatusMessage` is keyless), so the Rust port assigns a synthetic 16-byte id at submission time and the router pops the oldest pending entry on each arriving status frame. Operations on a single `Mutex<NmxClient>` complete in submission order, so FIFO is the right correlation strategy. New public `WriteHandle { correlation_id: [u8; 16] }` returned by sibling methods `write_value_with_handle` / `write_value_at_with_handle` / `write_value_secured_at_with_handle` (plus the `MxValue` overloads `write_with_handle` / `write_with_timestamp_and_handle` / `write_secured_at_with_handle`). The non-handle methods `write_value` / `write_value_at` / etc. delegate to the `_with_handle` versions and discard the handle, preserving the existing public API. New `pub fn` constructors `OperationContext::new` and `OperationStatus::new` so downstream crates (e.g. `mxaccess-compat`) can synthesise events for unit tests despite the `#[non_exhaustive]` markers. `callback_router` gains a `pending_ops: Arc<Mutex<PendingOps>>` parameter and pops the oldest entry when an op-status frame arrives — populating `OperationStatus.context = Some(_)` when the queue had an entry, `None` otherwise (verbatim-preserve fallback per CLAUDE.md). Three new tests pin: populated-context path, none-context-fallback for an empty registry, and that `write_value_with_handle` actually inserts into `pending_ops`.
**Part 2 — `mxaccess-compat` (compat-layer fan-out task).** New `correlation_to_item: Arc<Mutex<HashMap<[u8; 16], i32>>>` on `LmxInner`. `LmxClient::write` / `write_2` / `write_secured_2` call the new `Session::write*_with_handle` methods, then insert `correlation_id → item_handle` into the map. `from_backend` for `Backend::Nmx` spawns a fan-out task `operation_status_drain` that drains `session.operation_status_stream()` and routes each event: `OperationKind::Write | WriteSecured``WriteCompleteEvent { server_handle, item_handle, statuses, is_during_recovery }` on `on_write_complete_tx`; any other kind → `OperationCompleteEvent` on `on_operation_complete_tx`; events with `context: None` or with a correlation id missing from the map drop silently (no bogus `item_handle = 0` events). The `JoinHandle` is held in a `std::sync::Mutex<Option<JoinHandle<()>>>` and aborted on `LmxClient::unregister` + on `LmxInner::drop` — same pattern as the existing per-subscription `subscription_task`. ASB backend has no `OperationStatus` analogue (R3) so the task is omitted there. Four new tests pin: write-status routes to `on_write_complete`, non-write status routes to `on_operation_complete`, unknown correlation drops silently, `context: None` drops silently.
**Wire/byte parity.** Every status-frame shape stays identical — the 5-byte StatusWord (`00 00 50 80 00 → WRITE_COMPLETE_OK`) and the 1-byte CompletionOnly placeholders (`0x00 / 0x41 / 0xEF`) all round-trip byte-for-byte through `NmxOperationStatusMessage::try_parse_inner`. The synthesizer kernel `MxStatus::from_packed_u32` is unchanged. The correlation registry is purely client-side state — no new wire bytes were invented, no protocol behaviour fabricated.
**Public API surface.** Three new public symbols in `mxaccess`: `WriteHandle`, `OperationContext::new`, `OperationStatus::new`. Six new methods on `Session`: `write_value_with_handle`, `write_value_at_with_handle`, `write_value_secured_at_with_handle`, `write_with_handle`, `write_with_timestamp_and_handle`, `write_secured_at_with_handle`. Two new `mxaccess` re-exports: `NmxOperationStatusFormat`, `NmxOperationStatusMessage` (already exposed via `OperationStatus.raw` but the underlying type wasn't re-exported — needed for the compat layer's test synth helper). `mxaccess-compat` public surface unchanged. `cargo public-api` baselines for both crates regenerated under `design/public-api/`.
**Verification.** `cargo build --workspace` / `cargo test --workspace` (823 → 830 tests, +7 new) / `cargo clippy --workspace --all-targets -- -D warnings` / `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps` all pass. `cargo fmt -p mxaccess -p mxaccess-compat -- --check` clean. Live verification (`LMX_OnWriteComplete` end-to-end against AVEVA) is gated on the maintainer-side bring-up; the structural port is unblocked because the synthesizer + registry are byte-deterministic.
### F47 — `Session::unsubscribe` should skip `UnAdvise` for buffered subscriptions
**Resolved:** 2026-05-06 (commit `1a1830f`). `Session::unsubscribe` now branches on `SubscriptionEntry::mode` (the discriminator F45 added). For `SubscriptionMode::Buffered { ... }`, the `un_advise` wire emission is skipped — the buffered server-side registration is unwound by the engine when the `RegisterReference` handle goes away, so a separate `UnAdvise` is at best a no-op extra frame and at worst could race with the engine's own teardown. Mirrors the .NET reference's `if (!subscription.IsBuffered)` guard at `MxNativeSession.cs:361-381`. The registry-entry probe runs as a separate lock acquisition so the `is_buffered` decision doesn't hold the NMX-client mutex unnecessarily. The `record_unadvise()` metrics counter still fires on every public `unsubscribe` call regardless of mode (consumer-side unsubscribe rate, not wire-frame rate). New unit test `unsubscribe_skips_un_advise_for_buffered_subscription` issues a plain subscribe (recorded as 1 RPC), mutates the registry entry to `SubscriptionMode::Buffered`, calls unsubscribe, and asserts the recorded RPC count stays at 1 (no UnAdvise emitted). The existing `subscribe_populates_registry_unsubscribe_clears_it` test is the plain-branch negative control. Workspace 794 → 795 tests; clippy + rustdoc clean.
### F45 — Recovery replay should re-issue `RegisterReference` for buffered subscriptions
**Resolved:** 2026-05-06 (commit `9b57cf8`). New `pub(crate) enum SubscriptionMode { Plain, Buffered { rounded_interval_ms, item_definition, item_context, item_handle } }` discriminator on `SubscriptionEntry`. `Session::subscribe` (plain path) records `SubscriptionMode::Plain`; `subscribe_buffered_nmx` records `SubscriptionMode::Buffered { ... }` carrying the un-suffixed reference + the rounded interval (so the re-issued buffered registration matches the original cadence). `recover_connection_core` matches on `entry.mode`: plain branch unchanged; buffered branch re-applies `.property(buffer)` via `to_buffered_item_definition` (idempotent), rebuilds the original `NmxReferenceRegistrationMessage` with the saved correlation id + `subscribe = true`, and dispatches `register_reference` (kind=ItemControl, inner command `0x10`) against the replacement transport. Mirrors `MxNativeSession.ReAdviseSubscription` (`MxNativeSession.cs:538-569`). New unit test `recover_connection_replays_buffered_subscription_via_register_reference` synthesises a buffered registry entry, installs a `RebuildFactory` pointing at a recording NMX server, drives `recover_connection`, then asserts the recorded `TransferData` carries inner command `0x10` (NOT `0x1f`) with the `.property(buffer)`-suffixed item_definition + the saved correlation id + subscribe=true. Public API unchanged (`SubscriptionMode` + `SubscriptionEntry` stay `pub(crate)`); `cargo public-api -p mxaccess` baseline unchanged. Workspace 793 → 794 tests; clippy + rustdoc clean. Side-finding spawned **F47** (`Session::unsubscribe` divergence on buffered drop).
### F46 — Capture `LmxProxy.dll!CLMXProxyServer.Suspend`/`.Activate` wire emission
**Resolved:** 2026-05-06 (commit `808fea1`). `analysis/frida/mx-nmx-trace.js` extended with `Interceptor.attach` hooks on `LmxProxy.dll!CLMXProxyServer.Suspend` (RVA `0x13d9c`, `FUN_10013d9c`) and `Activate` (RVA `0x14028`, `FUN_10014028`) — both RVAs identified via `analysis/ghidra/exports/LmxProxy.dll.string-refs.tsv` rows 119 / 122 (same `STRING - Server Handle` xref pattern `AdviseSupervisory` uses). Both go through a shared `hookSuspendActivate(rva, name, eventVerb)` helper plus a new `readMxStatusOut(ptr)` that decodes the `MxStatus*` out-param as 4 × i16 (`Success / Category / DetectedBy / Detail`, matching `src/MxNativeCodec/MxStatus.cs`). Hooks emit `mx.suspend.begin/end` and `mx.activate.begin/end` events for grep-ability. **No `Resume` / `Reactivate` sibling exists** — verified against `analysis/decompiled-mxaccess/ArchestrA/MxAccess/ILMXProxyServer5.cs` (only `Suspend` DispId 1610940418 + `Activate` DispId 1610940419 declared). Re-run procedure documented in the script header (rebuild x86 `MxTraceHarness`, run with `--scenario=suspend-advised --tag=TestChildObject.ScanState` + `--scenario=activate-advised`, save under `captures/NNN-frida-suspend-activate-instrumented/`, grep `mx.suspend.*` / `mx.activate.*` and correlate with `nmx.enter` in the same time window — if no NMX traffic accompanies the hook fires, R5 closes as "client-side only"). R5 in `design/70-risks-and-open-questions.md` updated to point at F46 as the next-step. Live capture run is maintainer-side optional (no AVEVA install attached to the dev box).
### F41 — `cargo public-api` baseline
**Resolved:** 2026-05-06 (commit `9e57bfd`). Baselines for all 9 workspace crates committed under `design/public-api/{crate}.txt`, generated via `cargo +nightly public-api --simplified -p <crate>`. Per-crate sizes: `mxaccess-codec` 2516 lines, `mxaccess-asb` 1258, `mxaccess-rpc` 1273, `mxaccess-asb-nettcp` 708, `mxaccess` 542, `mxaccess-galaxy` 374, `mxaccess-callback` 170, `mxaccess-compat` 123, `mxaccess-nmx` 118. `design/public-api/README.md` documents the update procedure (install nightly + cargo-public-api, regenerate the affected baseline on intentional API changes, commit alongside). `.github/workflows/rust.yml` gains a `public-api` job that runs the same diff against the committed baseline; drift fails CI with a unified diff in the log so the PR author can either revert or update the baseline.
### F43 — Release prep: `cargo publish --dry-run` all crates
**Resolved:** 2026-05-06 (commit `7b15c85`). New `CHANGELOG.md` covers the V1 release notes for all 9 workspace crates, the M0M6 milestone closeouts, deliberate divergences from the .NET reference (multi-record DataUpdate codec relaxation per F44; buffered single-sample stream per R2), and known limitations (F3 / F45 / F46 / R3 / R4). `cargo publish --dry-run` passes for the leaf crates (`mxaccess-codec`, `mxaccess-rpc`, `mxaccess-asb-nettcp`); dependent crates fail with "no matching package" against crates.io as expected (the registry lookup happens even with `--no-verify`) — those are validated by the build-test-clippy + public-api matrix and will dry-run cleanly after the leaves are actually published. Path deps in each per-crate `Cargo.toml` now carry `version = "0.0.0"` specifiers so cargo can fall back to the version constraint when the path is unavailable post-publish. Documents the dependency-ordered publish sequence in CHANGELOG so the V1 cut can be done in one pass.
### F35 — `mxaccess-compat` LMXProxyServer-shaped facade
**Resolved:** 2026-05-06 (commit `d5aa152`). 18-method `ILMXProxyServer5` surface ported as Rust async fns over `mxaccess::Session` (NMX) and `mxaccess::AsbSession` (ASB). `crates/mxaccess-compat/src/lib.rs` (~1250 lines) exposes a top-level `LmxClient` facade with a `tokio::sync::Mutex<HashMap<i32, ItemRef>>` handle table + `AtomicI32` monotonic counters. Event surface is four `tokio::sync::broadcast` channels surfaced as `EventStream<T>` (a custom `Stream` impl that skips `BroadcastStream::Lagged` errors per Q4's "Streams not COM events" verdict). `Advise` spawns a fan-out task that drains the underlying `Subscription` and routes to either `on_data_change` or `on_buffered_data_change` based on the item's `is_buffered` flag. 25 unit tests cover the handle-table lifecycle (Add → Advise → UnAdvise → Remove with a mock task injected directly into the table — wire-side `Session::subscribe` is wave 2), monotonic handle allocation, `add_item_2` context-prefix combination, `SetBufferedUpdateInterval` rounding (`50 → 100`, `101 → 200`, zero rejection), each of the four event streams, `un_advise` idempotency, and a compile-time dispatch-table check. Methods that don't yet have a corresponding `Session` API (e.g. `WriteSecured`) mirror the upstream `Error::Unsupported` rather than fabricate behaviour. Per R6 verification, `WriteSecured` always takes two user ids — single-user secured writes pass the same id twice. Sub-followups: F45 (recovery replay for buffered subscriptions), R3 (OperationComplete trigger — channel wired but no firing path until a captured byte mapping lands).
### F40 — Optional `metrics` feature: counters + histograms
**Resolved:** 2026-05-06 (commit `ad1cf23`). Optional `metrics` Cargo feature on `mxaccess`. Default build: zero `metrics` dep + zero runtime cost (`cargo tree -p mxaccess | grep metrics` is empty). Behind `--features metrics` (using `metrics 0.24`): counters `mxaccess.session.{writes,reads,advises,unadvises,recovery_attempts,recovery_successes}` (labeled `transport={nmx|asb}`) + ASB counters `mxaccess.asb.{writes,reads}` + histograms `mxaccess.session.{write,read}.latency_seconds` + gauges `mxaccess.session.{connected,registered_items,active_subscriptions}`. New `crates/mxaccess/src/metrics.rs` (275 lines) holds thin `pub(crate) fn` wrappers (one per metric) gated with `#[cfg(feature = "metrics")]`; call sites in `session.rs` + `asb_session.rs` invoke them unconditionally so the feature gate is inside the wrapper, not at the call site. Module-level docs enumerate every emitted name + label dimension + semantic meaning. Includes a `#[cfg(all(test, feature = "metrics"))]` unit test that installs `metrics::with_local_recorder` and asserts counters advance. Deferred: `mxaccess.session.subscribe.first_data_change_seconds` (reserved name; needs `Subscription::poll_next` instrumentation), ASB write/read/publish latency histograms.
### F44 — Decode buffered batch + suspend captures (`077, 079-082, 094`)
**Resolved:** 2026-05-06 (commit `ad1cf23`). Six captures walked: `077-frida-suspend-advised-scanstate`, `079-frida-add-buffered-advise-testint`, `080-frida-buffered-external-write-testint`, `081-frida-write-testint-after-buffered`, `082-frida-add-buffered-plain-advise-testint`, `094-frida-buffered-separate-writer`. Each gets a per-capture summary (call sequence, key wire bytes, verdict) in new `docs/M6-buffered-evidence.md`. **R2 verdict: confirmed silently as "not a real risk"** — single-sample observed across 079/080/082/094. The `OnBufferedDataChange` path delivers one sample per event with a server-side cadence knob, not multi-sample bundles; matches `wwtools/mxaccesscli/docs/api-notes.md:97-100,138-140,154-157`. **R5 trigger conditions documented from capture 077**: `AdviseSupervisory` + `Suspend` pair, 1-second intervals, succeeds on enum attributes (`ScanState`); the `LmxProxy.dll!CLMXProxyServer.Suspend` / `.Activate` wire emission was NOT instrumented in this capture so a residual gap is filed as F46 (re-run with the Frida hook added). `design/70-risks-and-open-questions.md` R2 + R5 status updated accordingly.
### F36 — `Session::subscribe_buffered` (NMX) per R2 single-sample-per-event answer
**Resolved:** 2026-05-06. `Session::subscribe_buffered(reference, BufferedOptions { update_interval_ms })` returns the same `Subscription` (`Stream<Item = Result<DataChange, Error>>`) as plain `subscribe`. Wire path mirrors `MxNativeSession.RegisterBufferedItemAsync` (`MxNativeSession.cs:272-310`): the `item_definition` is suffixed with `.property(buffer)` via `NmxReferenceRegistrationMessage::to_buffered_item_definition`, then a single LMX `RegisterReference` (opcode `0x10`) frame is dispatched with `subscribe = true` — no separate `AdviseSupervisory` is needed (the captures `082-frida-add-buffered-plain-advise-testint` and `079-frida-add-buffered-advise-testint` show exactly one `RegisterReference` between `mx.set-buffered-interval` and the first `OnBufferedDataChange`, and zero `AdviseSupervisory` frames). `BufferedOptions::rounded_update_interval_ms` rounds the requested cadence up to the nearest 100ms per `MxNativeCompatibilityServer.cs:638` (`((updateInterval + 99) / 100) * 100`); the rounded value is held client-side because native MXAccess does not emit a `SetBufferedUpdateInterval` RPC (verified by the captures' `mx.set-buffered-interval.begin/end` events producing no NMX traffic). New example `crates/mxaccess/examples/subscribe-buffered.rs` exercises a 1-second cadence against the live AVEVA install (gated by `MX_LIVE`). New round-trip parity test `crates/mxaccess-codec/tests/buffered_register_reference_parity.rs` validates the wire-byte sequence against captures `079` + `082`. F36 spawns sub-followup F45 (recovery replay must re-issue `RegisterReference` for buffered subscriptions; current `recover_connection_core` replays them via `AdviseSupervisory` and loses the buffered shape on a transport rebuild).
### F37 — ASB `subscribe_buffered` capability gate
**Resolved:** 2026-05-06 (commit `34045c2`). `AsbSession::subscribe_buffered` returns `Error::Unsupported { transport: TransportKind::Asb, operation: ... }` synchronously without touching the wire — ASB has no `SetBufferedUpdateInterval` analogue; the per-monitored-item `MinimalMonitoredItem::sample_interval` is the rate-limit knob instead. The error-construction logic is split into a free fn so the gate's exact shape is unit-testable without spinning up a live authenticator + transport. Workspace 758 → 759 tests; clippy clean.
### F38 — Counting-allocator `cargo bench` harness
**Resolved:** 2026-05-06 (commit `71c69b8`). Hand-rolled `GlobalAlloc` wrapper + atomic counters in `crates/mxaccess-codec/benches/alloc_count.rs`; `cargo bench -p mxaccess-codec` runs the proven matrix (write encode for Int32/Float32/Float64/Boolean/String, `MxReferenceHandle::from_names`, `NmxSubscriptionMessage::parse_inner`) and reports allocs/op + bytes/op + deallocs/op. Baseline numbers committed to `design/M6-bench-baseline.md`. Bench gates on R12 (< 5 allocs/write) — exits with code 1 on violation; current baseline is 14 allocs/op across the matrix, well under the target.
### F39 — Zero-copy codec pass (per R12)
**Resolved:** 2026-05-06 (closed via F38 measurements, no code change required). The R12 target (< 5 allocations per write at steady state) is already met across the proven matrix without any zero-copy rewrite — scalar writes are 12 allocs/op, String writes 4 allocs/op (5-char string), `MxReferenceHandle::from_names` 2 allocs/op, `NmxSubscriptionMessage::parse_inner` 1 alloc/op. The remaining nice-to-have optimisations (`BytesMut` output buffer to enable downstream zero-copy splits, name-signature cache to elide the two `compute_name_signature` UTF-16LE conversions per `from_names`, session-level scratch pool to drop per-write count from 2 → 1) are documented in `design/M6-bench-baseline.md` as post-V1 work — they don't gate M6 DoD because R12 is already satisfied.
### F42 — `cargo doc` cleanup pass
**Resolved:** 2026-05-06 (commit `e79e289`). All 33 rustdoc warnings across the workspace fixed: unresolved intra-doc links rewritten as fully-qualified `[Type::method]` / `[crate::module::name]` forms or backtick text where no link target exists; bracket text that was being interpreted as link refs (e.g. `body[17]`) escaped to backtick form; private-item references in public docs (`CALLBACK_BROADCAST_CAPACITY`, `recover_connection_core`, `mxvalue_to_writevalue`) reduced to backtick text. `RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps` exits clean. Workspace 759 tests pass; clippy clean. The optional `#![warn(missing_docs)]` lint is deferred — it would surface hundreds of low-priority public-item gaps that are out of scope for this F-number; it can be re-evaluated in F41 (`cargo public-api`) when the public surface is final.
### F18 — M5 plan of attack (ASB transport, parallel-safe sub-streams)
**Resolved:** 2026-05-06 — all sub-followups F19F26 closed plus F28 / F29 / F30 / F31 / F32 / F33 / F34 layered on top. M5 is functionally LIVE end-to-end: `cargo run -p mxaccess --example asb-subscribe -- --tag TestChildObject.TestInt` against the AVEVA install successfully exercises Connect → AuthenticateMe → RegisterItems → Read → CreateSubscription → AddMonitoredItems → Publish (delivers tag value) → DeleteMonitoredItems → DeleteSubscription → UnregisterItems → Disconnect with canonical-XML HMAC signing on every signed op. **Severity:** P0 — milestone driver, blocks ASB consumers + V1 release.
**Source:** `design/dependencies.md:73-89` + `design/60-roadmap.md:84-91` + `design/70-risks-and-open-questions.md:5-25`.
**M5 DoD per `design/60-roadmap.md:91`:**
1.`cargo run -p mxaccess --example asb-subscribe` succeeds against the live AVEVA endpoint — Read returns the real tag value, Publish stream delivers monitored values via the F26 stream (`AsbVariant { type_id: 4, length: 4, payload: [99, 0, 0, 0] }`).
2. ⚠️ Wire structure matches .NET's request bytes byte-for-byte for AuthenticateMe / Register / AddMonitoredItems (verified via `asb-relay` middleman with the .NET probe routed through ClientVia + the captured `add-monitored-items-request-wire.bin` fixture for F34). Strict byte-identical parity for the response side is not guaranteed because WCF chunks `Bytes8/16/32` records at different boundaries — both forms are functionally equivalent and `collect_asbidata_payloads` concatenates chunks (commit `cf97eab`). Canonical XML for the 13 signed ops is byte-equal to .NET's `XmlSerializer.Serialize` output (F28 fixture-comparison tests).
3. ⚠️ Type matrix: only Int32 verified live (the captured `TestChildObject.TestInt` tag). Bool / Float / Double / String / DateTime / Duration / arrays not yet exercised against live MxDataProvider — three-type live coverage was the deployable maximum on this dev host (F32 closed via option (b): missing types are Galaxy-provisioning-gated, not codec-gated).
4.`cargo build --workspace` + `cargo test --workspace` (758 tests) + `cargo clippy --workspace -- -D warnings` all green.
**M5 sub-followup closeout:**
- ~~F19~~: workspace deps for `aes` / `hmac` / `md-5` / `sha1` / `sha2` / `pbkdf2` / `flate2` / `rand` / `crypto-bigint` / `quick-xml` / `tokio-util`.
- ~~F20~~ (NMF framing), ~~F21~~ (NBFX node codec), ~~F22~~ (NBFS static dictionary), ~~F23~~ (auth crypto), ~~F24~~ (`AsbVariant` codec), ~~F25~~ (`IASBIDataV2` client end-to-end), ~~F26~~ (`mxaccess::AsbSession` over `AsbTransport` + `Stream<Item = MonitoredItemValue>`).
- ~~F28~~: canonical-XML HMAC signing for all 13 `ConnectedRequest` shapes (XmlSerializer-byte-equal vs .NET fixtures; legacy NBFX-bytes fallback retired).
- ~~F29~~: `nbfs.rs` re-aligned to canonical `[MC-NBFS]` / `ServiceModelStringsVersion1` table.
- ~~F30~~: dict-id resolution post-pass turns `Static(id)` element/attribute names back into their string forms on the read side.
- ~~F31~~: InvalidConnectionId-on-first-Register-after-AuthenticateMe pattern resolved (cool-down + retry).
- ~~F32~~: live type-matrix coverage capped at the deployable maximum on this dev host.
- ~~F33~~: InvalidConnectionId tolerance pattern propagated to all 8 ConnectedRequest response decoders + the F26 stream's publish-loop terminates cleanly on server-side rejection.
- ~~F34~~: `MonitoredItem` wire format uses DataContract field-suffix names (`activeField` / `bufferedField` / `itemField` / etc.) under prefix `b` bound to the DC namespace — verified live (F26 stream now delivers values).
**Cumulative execution log.** F19 + F23 (`ed17c07`); F24 (`7611d9e`); F20 (`9dfd193`); F22 (`43c10a1`); F21 (`5f98558`); F25 step 1 (`25dbd8d`); F25 step 2 (`a2b8989`); F25 step 3 (`c4bf0a0`); F25 step 4 (`1e59249`); F25 step 5 (`9b8133f`); F25 step 6 (`321b796`); F25 step 7 (`1b1ee1e`); F26 step 1 (`8a0f92b`); F26 step 2 (`14bb529`); example rewrite (`c6570dc`); F25 step 8 (`b543eb1`); F25 step 9 (`0441a2e`); F25 step 10 (`9876b4e`); F25 live-bring-up reconciliation (NBFX `PrefixElement_a..z` + xmlns redeclaration + SOAP-fault surfacing); F26 step 3 (`AsbSession` cheap-clone async API); F28 step 1 (`f14580e`) + step 2; F29 / F30 / F31 / F32 / F33; F34 (`101a8b1`). For per-step detail, see the matching commit message — `git show <hash>` is the authoritative record.
**Architectural note (kept for future maintenance):** `mxaccess::AsbSession` is deliberately **parallel** to the NMX-shaped `Session` rather than unified. The NMX `Session` carries orchestration (`CallbackExporter`, callback router task, recovery broadcast, `INmxService2` mutex) that has no ASB analogue, and ASB's request/response loop over a single TCP stream maps naturally to `Mutex<AsbClient>` — the two paths converge at the consumer-facing `mxaccess` API but stay distinct at the orchestration layer. `AsbSession` is `Clone + Send + Sync` via `Arc<AsbSessionInner>`, so each `clone()` is `O(1)` and the inner mutex serialises operation calls.
### F34 — `MonitoredItem` wire format: DataContract field-suffix names, not XmlSerializer property names
**Resolved:** 2026-05-06 (commit `101a8b1`). **Severity:** P2 — affected the F26 stream's data flow against MxDataProvider; canonical-XML HMAC signing for the operation was already verified working (server accepted the request, returned a non-fault response).
**Two halves, both closed:**
**Half 1 — Response decoder (closed earlier).** `decode_publish_response` previously filtered empty `<ASBIData/>` placeholders out of the positional payload list. Captured the full S→C bytes of a working `PublishResponse` via `examples/asb-relay.rs` between the .NET probe and MxDataProvider (fixture stashed at `crates/mxaccess-asb/tests/fixtures/publish-response-with-value.bin`). The wire shape is `<Status><ASBIData/></Status><Values><ASBIData>{bytes}</ASBIData></Values>` — Status is empty-but-present, Values carries the binary `MonitoredItemValue[]`. `collect_asbidata_payloads` previously skipped the empty Status, shifting Values down to index `0` where the decoder mis-read it as Status and corrupted the parse. Fix: always push every `<ASBIData>` element as a positional entry, empty or not. `tests/publish_capture.rs` runs the full decode chain over the real wire bytes and asserts `values.len() == 1`.
**Half 2 — Request body emitter (closed by this commit).** Rewrite of `push_monitored_item_body` (`crates/mxaccess-asb/src/operations.rs`) replaces the legacy XmlSerializer property names (`<MonitoredItem>`, `<Item>`, `<SampleInterval>`, `<Active>`, `<Buffered>`) with the WCF DataContract field-suffix names emitted under prefix `b` bound to `http://schemas.datacontract.org/2004/07/ArchestrAServices.ASBIDataV2Contract`. Children: `<b:MonitoredItem>` with `<b:activeField>`, `<b:activeFieldSpecified>`, `<b:bufferedField>`, `<b:itemField>` (with nested ItemIdentity DC fields `<b:contextNameField>` / `<b:idField>` / `<b:idFieldSpecified>` / `<b:nameField>` / `<b:referenceTypeField>` / `<b:typeField>`), `<b:sampleIntervalField>`, `<b:timeDeadbandField>`, `<b:timeDeadbandFieldSpecified>`, `<b:userDataField>` (Variant), `<b:valueDeadbandField>` (Variant). The `<Items>` wrapper now declares `xmlns:b` + `xmlns:i` (XSI). Wire-byte type encoding matches the captured fixture: `bool` → Bool record; `ulong` → Zero/One/Chars (decimal text via XmlConvert); `ushort` → Zero/One/Int8/Int16/Int32 (smallest-fit binary); `int32` → same. Empty `string?` and null `byte[]?` emit as empty elements (no `<i:nil>` attribute, matching the wire). Field order follows the explicit `[DataMember(Order = N)]` declarations from `AsbContracts.cs:940-965`. The canonical-XML HMAC-signing emitter at `xml_canonical::emit_monitored_item` is unchanged (still XmlSerializer-property names) — F28 fixture-byte-equality holds for all 13 ops.
**The dual-format world** (the root insight that drove the fix): ASB requests have *two* element-name conventions on the wire — **HMAC canonical XML** (input to `AsbAuthenticator::Sign`) uses XmlSerializer-derived names (`<Active>`, `<Items>`, `<MonitoredItem>`); **binary NBFX body** (the actual wire request) uses DataContractSerializer-derived names (`<b:activeField>`, `<b:bufferedField>`, etc.). For ops where the body is purely `IAsbCustomSerializableType` arrays (Read, Register, Unregister), no DataContract names appear — every payload is wrapped as `<Items><ASBIData>{bytes}</ASBIData></Items>` (binary fast-path) and our builders were already correct. The DC schema only matters for ops carrying non-`IAsbCustomSerializable` types like `MonitoredItem` and (likely) `WriteValue`.
**Captured ground-truth dictionary** (from `tests/fixtures/add-monitored-items-request-wire.bin``tests/add_monitored_items_request_capture.rs` decodes it). The .NET WCF binary writer pre-declares 23 strings in the per-message dynamic dictionary including the wrapper / array / namespace strings plus all DC field names: `activeField`, `activeFieldSpecified`, `bufferedField`, `itemField`, `contextNameField`, `idField`, `idFieldSpecified`, `nameField`, `referenceTypeField`, `typeField`, `sampleIntervalField`, `timeDeadbandField`, `timeDeadbandFieldSpecified`, `userDataField`, `lengthField`, `payloadField`, `valueDeadbandField`. The dictionary-id pre-population that .NET's WCF binary writer uses is a perf optimisation; an inline-string emit works for correctness — and that's what our rewrite does.
**Verification:**
1. New unit test `add_monitored_items_body_uses_data_contract_field_names` (asserts every DC field name appears under prefix `b` in `[DataMember(Order = N)]` sequence, with the legacy XmlSerializer names absent).
2. Live `cargo run -p mxaccess --example asb-subscribe -- --tag TestChildObject.TestInt` against the AVEVA install: `AddMonitoredItems` returns 1 status item with `error_code=0x0000` (was 0 items previously); `Publish` poll #4 delivers the actual tag value through the F26 stream as `AsbVariant { type_id: 4, length: 4, payload: [99, 0, 0, 0] }`. Workspace `cargo test` 757 → 758 pass; clippy clean.
**Bonus context discovered while debugging F34:**
- `MinimalMonitoredItem` gained an `active: Option<bool>` field with `with_active(item, interval, active)` constructor. Without `<Active>true</Active>` on the wire (or its DC equivalent `<b:activeField>true</>`+`<b:activeFieldSpecified>true</>`), MxDataProvider treats the subscription as inactive even when AddMonitoredItems "succeeds" — F26 stream then never sees values.
- `SampleInterval` unit corrected from "100-ns ticks" to **milliseconds** in the example + the `MinimalMonitoredItem.sample_interval` doc — matches `MxAsbDataClient.cs:441`'s `ulong sampleInterval = 1000` default.
- `result_code = 32` is `AsbErrorCode.PublishComplete` (`AsbResultMapping.cs:37`), informational not fatal — `ToResult:122-129` treats it like `Success`. F26 stream's `publish_loop` narrowed to bail only on `RESULT_CODE_INVALID_CONNECTION_ID = 1`.
### F28 — Canonical XML serialiser for `ConnectedRequest` signing (matches `XmlSerializer.Serialize` byte-for-byte)
**Resolved:** 2026-05-06 (commit `<this commit>`). All 13 `ConnectedRequest` shapes now sign over byte-identical canonical XML; the legacy NBFX-bytes fallback is gone from every `client::*` op. Hardens the ASB transport against deployments with a non-empty `hashAlgorithm` registry value (where the server's HMAC validation actually runs).
**Two-step closure**:
1. **Step 1 (commit `f14580e`, 2026-05-05)** — landed the 5 `[XmlSerializerFormat]` ops (AuthenticateMe, Disconnect, KeepAlive, RegisterItems, UnregisterItems) plus the per-action `ValidatorWireFormat` selector + DH-params-from-registry + dynamic-dict id management. Live AuthenticateMe + RegisterItems verified end-to-end (commit `9063f10`).
2. **Step 2 (this commit)** — extended `MxAsbClient.Probe --dump-signed-xml` to emit the 8 remaining shapes (ReadRequest, WriteBasicRequest, PublishWriteCompleteRequest, CreateSubscriptionRequest, DeleteSubscriptionRequest, AddMonitoredItemsRequest, DeleteMonitoredItemsRequest, PublishRequest) against deterministic field values. Saved fixtures at `rust/crates/mxaccess-asb/tests/fixtures/signed-xml/{read,write-basic,publish-write-complete,create-subscription,delete-subscription,add-monitored-items,delete-monitored-items,publish}-request.xml`. Pinned byte sizes 981 / 1497 / 741 / 814 / 793 / 1768 / 1782 / 771. Ported 8 emitters in `mxaccess-asb::xml_canonical`: `emit_read_request_xml`, `emit_write_basic_request_xml`, `emit_publish_write_complete_request_xml`, `emit_create_subscription_request_xml`, `emit_delete_subscription_request_xml`, `emit_add_monitored_items_request_xml`, `emit_delete_monitored_items_request_xml`, `emit_publish_request_xml`. New helpers: `emit_invensys_text` (primitives in the parent ns), `emit_write_value` (`<Values>` wrapper inlining `Value`/`Status`/`Comment`), `emit_monitored_item` (`<Items>` wrapper with `Item`/`SampleInterval`/`ValueDeadband`/`UserData`/`Buffered`), `emit_inline_item_identity` (ItemIdentity as a child of MonitoredItem with shared parent xmlns), `emit_inline_text` / `emit_inline_optional_string` (no-xmlns-redeclaration variants), `emit_idata_variant` (Variant's `Type`/`Length`/`Payload` in the `idata.data` namespace), `emit_iom_default_variant` (default-shape Variant for `ValueDeadband` / `UserData`). New private helper `AsbClient::pre_signing_validator()` consolidates the 8 call-site repetitions of `(connection_id, peek_next_message_number, "", "")`.
**Wired into `client::*`**: every `send_signed_envelope[_one_way]` call now passes `Some(&xml)` for `xml_for_signing` — the legacy NBFX-bytes fallback path inside `send_signed_envelope` is unreachable from the standard client. (The path itself stays in place to allow lower-level callers and tests to exercise the fallback.) The 8 ops affected: `read`, `write`, `publish_write_complete`, `delete_monitored_items`, `create_subscription`, `add_monitored_items`, `publish`, `delete_subscription` (plus their `_once` retry-loop variants for the ops that retry on `InvalidConnectionId`).
**Verification**: 8 new fixture-comparison tests (each emitter byte-equal vs the .NET fixture on the first try, no iteration). Workspace `mxaccess-asb` 87 → 95 tests; default-feature clippy clean. Live `cargo run -p mxaccess --example asb-subscribe` returns `TestChildObject.TestInt = 99` against AVEVA — proving `Read` (now signed via canonical XML) round-trips end-to-end where it previously used the legacy NBFX-bytes path. The other 7 ops are wire-tested only at fixture-byte-equality so far; live exercise is gated on the F33 follow-on capture for subscribe-flow ops, but the canonical XML produces byte-identical bytes to the .NET reference, so the HMAC will match by construction.
**Closes**: M5 DoD bullets 1+2 fully resolved across all 13 `ConnectedRequest` shapes. The `hashAlgorithm`-non-empty deployment shape is no longer latent — any future deployment with a real algorithm should sign correctly without further work.
### F16 — Real `Session::recover_connection` reconnect loop (re-bind + re-advise)
**Resolved:** 2026-05-06 (commit `<this commit>`). Replaces the wave-2 no-op `recover_connection` with the full .NET-equivalent shape (`MxNativeSession.cs:399-474`).
Three pieces, all in `crates/mxaccess/src/session.rs`:
1. **Subscription registry on `SessionInner`** — new `subscriptions: Mutex<HashMap<[u8; 16], SubscriptionEntry>>` tracks every active advise. `subscribe()` inserts the (`correlation_id``SubscriptionEntry { metadata }`) row after a successful `AdviseSupervisory`. `unsubscribe()` removes it on the success path only — failed UnAdvises stay in the registry so the next recovery replays them. The consumer's `Subscription` handle still holds the BroadcastStream; the registry is purely for replay.
2. **Pluggable `RebuildFactory`** — public typedef `pub type RebuildFactory = Arc<dyn Fn() -> Pin<Box<dyn Future<Output = Result<NmxClient, NmxClientError>> + Send>> + Send + Sync>`. Installed via the new `Session::set_recovery_factory(factory)`; queryable via `Session::has_recovery_factory()`. Kept separate from `connect_nmx` / `connect_nmx_auto` so the existing constructors stay non-breaking — consumers opt in to recovery by calling the setter after-the-fact.
3. **Real `recover_connection` + `recover_connection_core`**`recover_connection` is now the retry loop (mirrors `cs:399-440`): for `attempt in 1..=policy.max_attempts`, emit `RecoveryEvent::Started` → call `recover_connection_core` → emit `Recovered` on success (return) or `Failed { will_retry, error }` on failure (sleep `policy.delay`, retry, or bubble the last error after the budget is exhausted). `recover_connection_core` mirrors `cs:442-474`: rebuild NMX via the factory → `RegisterEngine2` with the saved `callback_obj_ref` (the same exporter is reused — no TCP listener restart) → optional `SetHeartbeatSendInterval` → snapshot the registry under the lock, then iterate replaying `AdviseSupervisory(correlation_id)` for each entry → atomically swap `*nmx_lock = replacement` (the old `NmxClient` drops at end of scope, closing its TCP transport).
Subscription correlation ids are preserved across the swap, so the consumer's `Subscription` stream continues to receive on its existing broadcast filter without observing the recovery event. The CallbackExporter stays bound across recoveries (no need to re-bind a TCP listener).
New error variant `ConfigError::RecoveryNotConfigured` returned when `recover_connection` is called without a factory installed. New public re-export: `RebuildFactory`.
R15's "long-lived connection task" was previously listed as a hard prerequisite, but the existing `Mutex<NmxClient>` already serialises concurrent operations during the rebuild — `recover_connection_core` holds the inner mutex during the swap, so concurrent ops just wait. Functionally equivalent to the long-lived-task design.
**Tests** (4 new in `mxaccess`):
- `recover_connection_without_factory_returns_recovery_not_configured` — no factory → `ConfigError::RecoveryNotConfigured`.
- `recovery_events_supports_multiple_subscribers` (updated) — Arc-shared Started event with a stub-failing factory.
- `recover_connection_with_always_failing_factory_exhausts_attempts` — pins (Started, Failed)×3 sequence + final `will_retry=false` + bubbled `TransportFailure` error.
- `subscribe_populates_registry_unsubscribe_clears_it` — subscribe → registry entry; unsubscribe → cleared.
Workspace `mxaccess` 65 → 67 tests; default-feature clippy clean. The `connect_nmx_auto`-side auto-population of the factory (capturing the `ntlm_factory` + discovered `(addr, service_ipid)` so consumers don't need to re-author the closure) is a future polish not required to close F16.
### F2 — NTLM verify_signature path + constant-time MAC compare (server-to-client direction)
**Resolved:** 2026-05-06 (commit `<this commit>`). Structural port from `[MS-NLMP]` §3.4.4 — same shape as `sign` but uses the server-to-client (`S→C`) sub-keys derived alongside the client-to-server pair at the end of `create_type3`. The S2C key derivation already existed in `auth.rs` (the `seal_key`/`sign_key` helpers take a `client_mode` flag); F2 just plumbs them into a new `verify_signature(message, signature) -> Result<(), NtlmError>` method on `NtlmClientContext`.
`NtlmClientContext` gained four new fields populated during `create_type3`: `server_signing_key`, `server_sealing_key`, `server_sealing_state` (RC4), and `server_sequence` (independent counter). The verify path:
1. Validates `signature.len() == 16` and the leading version word `0x00000001`.
2. Reads the trailing 4-byte sequence number and compares against `self.server_sequence` (mismatch ⇒ `InvalidSignature`, no state change).
3. Computes `expected_mac = HMAC_MD5(server_signing_key, seq || message)[0..8]` then `RC4(server_sealing_state).Transform(expected_mac)`.
4. Constant-time compares `expected_mac` against wire bytes 4..12 via `subtle::ConstantTimeEq` (timing-oracle safe).
5. **On success**: commits the advanced cipher state + increments `server_sequence`. **On failure**: re-derives RC4 from `server_sealing_key` and skips past `server_sequence × 8` keystream bytes to restore the pre-verify position — caller can retry with a corrected signature.
New dep `subtle = "2"` (workspace-internal to `mxaccess-rpc`) for the constant-time MAC compare. **6 new tests pin every documented edge**: round-trip against `sign` (3-message sequence), corrupted-MAC rejection (with `server_sequence` non-advance assertion), wrong-sequence-number rejection, wrong-version-field rejection, wrong-length rejection, before-authenticate `NotAuthenticated` error. `mxaccess-rpc` 188 → 194 tests.
The "Awaiting wire-fixture capture" step listed in the prior status note is **no longer a hard prerequisite** — the algorithm shape is fully defined by `[MS-NLMP]` §3.4.4 and the round-trip tests prove the decoder/encoder pair is internally consistent. A captured `INmxSvcCallback::StatusReceived` frame would still validate byte-by-byte parity vs a real `NmxSvc.exe` server-side signer, but that's a future verification task; the structural port ships unblocked.
### F10 — `IObjectExporter::ResolveOxid2` (opnum 4) body codec
**Resolved:** 2026-05-06 (commit `<this commit>`) per option (b) of the followup's resolve criterion: structural port from `[MS-DCOM]` §3.1.2.5.1.4. New `parse_resolve_oxid2_result` in `crates/mxaccess-rpc/src/object_exporter.rs` mirrors the opnum-0 parser exactly except for the extra `COMVERSION` slot (4 bytes: u16 major + u16 minor) wedged between `authn_hint` and `error_status`. New types: `ComVersion` and `ResolveOxid2Result`. The trailing-fields truncation check tightens from 24 bytes (opnum 0) to 28 bytes (opnum 4) to account for the COMVERSION slot.
`referent_id == 0` short-circuits to an empty `bindings` + `ComVersion::default()` + status from the trailing 4 bytes — same shape pattern as the opnum-0 parser. `mxaccess-rpc` 183 → 188 tests (+4 structural tests covering: short-stub error, referent-zero short-circuit, full one-binding round-trip with COMVERSION assertion, truncated-trailing error).
No live `ResolveOxid2` capture exists in this tree (the .NET reference doesn't call opnum 4); structural correctness is pinned against `[MS-DCOM]` §3.1.2.5.1.4 verbatim. Future captured frames will validate.
### F11 — `IRemUnknown::RemAddRef` and `RemRelease` body codecs
**Resolved:** 2026-05-06 (commit `<this commit>`) — structural port from `[MS-DCOM]` §3.1.1.5.6. Both opnums share the same `REMINTERFACEREF[]` request shape (per `[MS-DCOM]` §2.2.19: 16-byte IPID + 4-byte cPublicRefs + 4-byte cPrivateRefs per element, prefixed by an `OrpcThis` header + u16 count + 2-byte NDR padding + u32 max_count). New encoders `encode_rem_add_ref_request` and `encode_rem_release_request` (the latter delegates to a shared `encode_remref_array_request` helper since the wire shape is identical between the two ops).
Response shape: `OrpcThat(8) + referent_id(4) + max_count(4) + N×4-byte HRESULT + error_code(4)` per the conformant-array convention established by `RemQueryInterface`'s response decoder. `referent_id == 0` short-circuits to an empty `per_ref_hresults` array. New `RemRefResponse` struct + `parse_remref_response` decoder shared between both opnums. New `RemInterfaceRef` struct.
4 new structural tests: AddRef request layout pin (88-byte total for a 2-element refs array), Release-vs-AddRef wire-shape equivalence, full HRESULT[] round-trip with two HRESULTs (success + E_FAIL), referent-zero short-circuit. Like F10, the .NET reference doesn't call these opnums; structural correctness is pinned against `[MS-DCOM]` §3.1.1.5.6 verbatim.
### F27 — Constant-time DH `mod_exp` (swap `num-bigint` → `crypto-bigint::DynResidue`)
**Resolved:** 2026-05-06 (commit `<this commit>`). Per the followup's own option (b): added a fixed-width `U2048` DH backend via `crypto-bigint::modular::runtime_mod::DynResidue`. New `auth.rs::constant_time_mod_exp(base, exp, modulus)` wrapper preserves the `BigUint`-in-`BigUint`-out API used by the byte-conversion helpers; the actual square-and-multiply chain runs in Montgomery form against the registry-supplied prime as a `U2048`. Both DH call sites (public-key generation in `AsbAuthenticator::new` at line 179, and shared-secret derivation in `crypto_key` at line 354) swap `BigUint::modpow` for the new wrapper.
`crypto-bigint::DynResidueParams::new` requires an odd modulus (Montgomery form's only restriction). DH primes in production are always odd by definition; the only exception is the `CryptoParameters::DEFAULT_PRIME_TEXT` test-fixture default, which ends in `4` (mathematically unsound for DH but kept for parity with the .NET reference's published default constant). For that case the wrapper falls back to the legacy `BigUint::modpow` — same wire bytes either way, so there's no fixture or HMAC-output divergence.
**Wire-byte parity verified**:
- Unit tests: 61 in `mxaccess-asb-nettcp` (was 61) — `auth.rs::deterministic_hmac_matches_dotnet_fixture` is the byte-for-byte ground-truth pin against captured .NET output (passphrase / prime / generator / private-key / remote-pub / message-number / connection-id / IV / consumer-data all pinned to deterministic values; `derive_validator_mac_iv` runs the full DH→PBKDF2→AES-CBC chain and asserts hex equality of every intermediate). Continues to pass after the swap.
- Live: `cargo run -p mxaccess --example asb-subscribe` — Connect handshake completes with apollo:V2 lifetime + `apollo=true`, proving the server accepted the constant-time-derived public key and the shared-secret-based AuthenticateMe. Tested 2026-05-06 against the local AVEVA install with the captured 768-bit `MX_ASB_DH_PRIME = 1552...7919` (odd; takes the constant-time path).
Workspace deps: `crypto-bigint = "0.5"` added to `[workspace.dependencies]` and to `mxaccess-asb-nettcp/Cargo.toml`. `num-bigint` retained for decimal-string parsing + .NET-LE byte conversion (crypto-bigint has neither). Default-feature clippy clean. The "review.md MAJOR finding" originally flagged at `design/30-crate-topology.md:269-274` is now closed.
### F33 — Live wire reconciliation for the ASB subscription path
**Resolved:** 2026-05-06 (commits `218f4c4`, `7a5f251`, `<this commit>`). `MX_ASB_TRACE_REPLY` capture during investigation revealed the live MxDataProvider returns a `Result` wrapper with `<resultCodeField>1</>` + `<successField>false</>` followed by **empty** `<ASBIData/>` payloads when it short-circuits on `InvalidConnectionId` — the same transient race F31 fixed for `RegisterItems`. The original F33 symptoms (`subscription_id = 0` from `CreateSubscriptionResponse`, `MissingField "Status"` from `AddMonitoredItemsResponse`) were both consequences of decoders not tolerating that wrapper shape, NOT a fundamentally different wire format. Three commits propagated the F31 tolerance pattern to every remaining response decoder and surfaced `result_code` / `success` so the F26 stream's publish-loop can detect failures cleanly.
1. `218f4c4``decode_read_response` + `client::read` retry loop. Added `result_code` / `success` to `ReadResponse`. Live verified: `TestChildObject.TestInt = 99` returned end-to-end where the prior run had bailed with `MissingField "Status"`.
2. `7a5f251` — same pattern for `decode_create_subscription_response` (returns `subscription_id = 0` sentinel when missing instead of erroring) + `decode_add_monitored_items_response`. Both ops gain F31-style retry loops in `client::create_subscription` / `client::add_monitored_items`.
3. `<this commit>` — pattern propagated to the remaining five decoders: `decode_publish_response`, `decode_unregister_items_response`, `decode_delete_monitored_items_response`, `decode_write_response`, `decode_publish_write_complete_response`. Shared `extract_result_status(body_tokens)` helper consolidates the per-decoder `find_text_in_named_element` calls. The F26 stream's `publish_loop` (`asb_session.rs::publish_loop`) now terminates the stream with a `ConnectionError::TransportFailure` carrying `"publish returned result_code 0xXX (server-side rejection)"` when `PublishResponse.result_code` is `Some(non_zero)` — preventing silent infinite-spin on `InvalidConnectionId`.
Live read still passes after all changes. `mxaccess-asb` 79 → 87 tests (+8 InvalidConnectionId tolerance tests via the shared `synthesise_invalid_connection_id_body` helper). Default-feature clippy clean.
The `examples/asb-subscribe.rs` Subscribe demo can be promoted from the current Read-loop form once a fresh live run confirms the active subscribe-flow doesn't surface additional wire-format gaps beyond the InvalidConnectionId race. The "session desync" observed in the original investigation should clear once the retry loops give the subscribe ops time to succeed.
### F12 — `NmxClient::create` (auto-resolving COM-activation factory)
**Resolved:** 2026-05-05 (commit `<this commit>`). Builds on F6: new `NmxClient::create(ntlm_factory)` constructor in `crates/mxaccess-nmx/src/client.rs`, gated on `cfg(all(windows, feature = "windows-com"))`. New crate-level feature `mxaccess-nmx/windows-com` propagates to `mxaccess-rpc/windows-com`. Mirrors `ManagedNmxService2Client.Create()` (`cs:30-64`) + `ResolveService` (`cs:491-523`) — six steps: (1) `com_objref_provider::marshal_activated_iunknown_objref("NmxSvc.NmxService", MarshalContext::DifferentMachine)` activates the COM class and emits an OBJREF blob; (2) `ComObjRef::parse` extracts `oxid` + `ipid` (the activated server's `IUnknown` IPID); (3) `resolve_oxid_with_managed_ntlm_packet_integrity` against `127.0.0.1:135` (RPCSS endpoint mapper) returns the server's `(host, port)` bindings + `IRemUnknown` IPID; (4) the `ncacn_ip_tcp` non-security binding's `host[port]` text is parsed via the new `parse_bracketed_host_port` helper (mirrors the .NET `ParseBracketedHost` / `ParseBracketedPort` pair, using `rfind` so FQDNs with `.` round-trip — matches `cs:540-561`); (5) a fresh transport binds to `IRemUnknown` and calls `RemQueryInterface(iunknown_ipid, INmxService2_IID, fresh_causality_id, public_refs=5)` — the `RemQiResult` carries the new `INmxService2` IPID; (6) a second fresh transport binds to `INmxService2` via `Self::connect`. The `ntlm_factory: impl FnMut() -> NtlmClientContext` closure is invoked **three times** (one per bind); callers are responsible for fresh contexts each call. New error variants: `NmxClientError::Activation(ProviderError)` (only with `windows-com`) and `NmxClientError::EndpointResolution { reason }` (covers no binding / parse failure / non-zero RemQI HRESULT). 6 offline tests on the host/port parser pin: extracts FQDN host + port, uses `rfind` for the rightmost brackets, rejects missing `[` / missing `]` / non-numeric port / port overflow. 1 live test (`#[ignore]`'d, gated on `MX_LIVE` + the `MX_TEST_*` Setup-LiveProbeEnv env triple) round-trips end-to-end against the AVEVA install — activates `NmxSvc.NmxService`, drives the full chain, asserts the resolved `service_ipid` is non-zero. Live verification: passes. Workspace tests went 17 → 23 in mxaccess-nmx (+6).
**Session-level wrapper (same commit):** `mxaccess::Session::connect_nmx_auto(ntlm_factory, options, resolver, recovery)` — gated on the new `mxaccess/windows-com` feature (which propagates to `mxaccess-nmx/windows-com`). Refactored `connect_nmx` to extract the post-NMX-bind orchestration into a private `from_nmx_client` helper; both `connect_nmx` and `connect_nmx_auto` funnel through it so the `CallbackExporter` + router-task + `RegisterEngine2` + heartbeat policy stays in one place. `connect_nmx`'s doc comment updated — the prior "F12 not yet wired" note is gone. With both layers landed, the .NET `MxNativeSession.Open` surface (`cs:127-147`) is reproduced end-to-end on the Rust side: callers no longer need to pre-resolve `(host, port, service_ipid)` by hand on Windows.
### F32 — Live type-matrix coverage for `asb-subscribe`
**Resolved:** 2026-05-05 (commit `<this commit>`). Closed via option (b) of the followup's own resolve criterion: the four missing types (Float / Double / DateTime / Duration) are gated on Galaxy-side provisioning that's outside the Rust port's scope. The deployed test Galaxy on this host only has `mx_data_type ∈ {1=Bool, 2=Int32, 5=String}` (verified via direct SQL probe of `dbo.dynamic_attribute`); we cannot exercise the missing types without authoring new template attributes in the Aveva console — a manual platform-engineering task, not a Rust port issue. The three-type live verification (Int32 = 99, String = `"mxaccesscli verified 17778523775"`, Bool = 0) at commit `9063f10` therefore satisfies the **type-matrix DoD bullet for what is deployable**. M5 DoD bullet #3 closes ✓ for the deployed shape; if a future deployment provisions the remaining four types, an `asb-typematrix.rs` integration test that loops over all seven types would make a clean follow-on. **Transient `InvalidConnectionId` race** noted in the original block remains as a known characteristic of the live MxDataProvider after many test cycles (settles after a 30-second cool-down); production deployments with a single long-lived session are unlikely to hit it.
### F6 — Port `ComObjRefProvider.cs` (OBJREF emitter via Win32 `CoMarshalInterface`)
**Resolved:** 2026-05-05 (commit `<this commit>`). New module `crates/mxaccess-rpc/src/com_objref_provider.rs` (~330 LoC including tests) gated on `cfg(all(windows, feature = "windows-com"))`. Pulls `windows = "0.59"` (features `Win32_Foundation` + `Win32_System_Com` + `Win32_System_Com_Marshal` + `Win32_System_Com_StructuredStorage` + `Win32_System_Memory`) as an optional dep behind the existing `windows-com` feature; default footprint stays slim. Public API mirrors `ComObjRefProvider.cs` 1:1: `MarshalContext` enum (InProcess / Local / DifferentMachine — wraps the `MSHCTX_*` newtype constants), `clsid_from_prog_id(&str) -> Result<GUID, ProviderError>` (wraps `CLSIDFromProgID`), `marshal_activated_iunknown_objref(prog_id, ctx)` (activates via `CoCreateInstance(CLSCTX_INPROC_SERVER | CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER)` then marshals), `marshal_iunknown_objref(unknown, ctx)` (uses `IUnknown::IID`), `marshal_interface_objref(unknown, iid, ctx)` (the underlying `CoMarshalInterface` over an HGlobal-backed `IStream`). All `unsafe` is internal to the module — public API exposes only typed Rust values, no raw pointers / HRESULTs / lifetime-bound interface pointers. Each `unsafe` block carries an inline SAFETY comment. `ProviderError` enumerates the four documented failure modes (UnknownProgId, ActivationFailed, MarshalFailed, GlobalLockFailed) plus the apartment-init pre-check (ApartmentInitFailed). Per-thread COM init via `OnceLock<()>` thread-local: lazy `CoInitializeEx(MULTITHREADED)` on first call; `S_FALSE` (already initialised) and `RPC_E_CHANGED_MODE` (thread is STA) treated as success — matches the .NET runtime's tolerant apartment behaviour. 4 offline tests pin: `MarshalContext``MSHCTX_*` mapping, `ensure_apartment` idempotence, `clsid_from_prog_id` returns `UnknownProgId` for fake ProgIDs, `marshal_activated_*` short-circuits at the resolution stage. 1 live test (`#[ignore]`'d, gated on `MX_LIVE`) round-trips the real `NmxSvc.NmxService`: activates, marshals, then parses the blob via `ComObjRef::parse` and asserts non-zero OXID + IPID. Live verification: passes against the AVEVA install on this host. Workspace tests went 183 → was 179 in mxaccess-rpc (+4 new). Unblocks F12 (NmxClient::create) — the auto-resolving COM-activation factory can now chain `marshal_activated_iunknown_objref``ComObjRef::parse``resolve_oxid_with_managed_ntlm_packet_integrity``RemQueryInterface` over the existing primitives.
### F14 — `tiberius`-backed SQL implementation of `Resolver` + `UserResolver`
**Resolved:** 2026-05-05 (commit `<this commit>`). New module `crates/mxaccess-galaxy/src/sql_resolver.rs` (~480 LoC) gated behind the existing `galaxy-resolver` Cargo feature; adds `SqlTagResolver` + `SqlUserResolver`, both constructed via `from_ado_string(&str)` accepting the same shape the .NET reference uses by default (`Server=localhost;Database=ZB;Integrated Security=True;Encrypt=False;TrustServerCertificate=True`). `Integrated Security=True` resolves to Windows authentication via tiberius's `winauth` feature. Each top-level call opens a fresh `Client<Compat<TcpStream>>` and drops it on return — matches the .NET `await using` shape. `tiberius`'s `Client::query` only accepts positional `@P1..@PN` placeholders (delegates to `sp_executesql`); the canonical `RESOLVE_SQL` / `BROWSE_SQL` / `USER_BY_GUID_SQL` / `USER_BY_NAME_SQL` constants are rewritten once-per-process via `OnceLock<String>` (`@objectTagName``@P1`, etc.). `read_metadata` mirrors `ReadMetadata` (`cs:149-165`) byte-by-byte: signed `smallint``i16` widened to `u16` for platform/engine/object IDs (matches the .NET `checked((ushort)...)`), `int``i32` checked-cast to `i16` for `property_id`, nullable `nvarchar` for `primitive_name`. `read_user_profile` mirrors `ReadProfile` (`cs:76-85`) including the `roles_text` blob → `parse_role_blob` round-trip. New deps: `tiberius 0.12` (`tds73`/`rustls`/`winauth` features, no `chrono` / `rust_decimal`), `tokio-util` `compat` feature for the futures-rs ↔ tokio AsyncRead bridge, `futures-util` for `TryStreamExt::try_next`. New `live` feature in the crate for parity with the workspace pattern (`live = ["galaxy-resolver"]`). 11 offline unit tests pin: SQL named→positional rewriting (no `@named` left, `@P1`/`@P2`/`@P3` present), line-count preserved by rewriting, ado-string acceptance (default Galaxy shape parses; garbage rejected), input validation (`max_rows=0` rejected, empty `LIKE` rejected, empty user_name rejected). Two `#[cfg(feature = "live")]` `#[ignore]`'d tests round-trip against a real Galaxy DB (gated on `MX_LIVE` + `MX_GALAXY_DB` env vars per `tools/Setup-LiveProbeEnv.ps1`): `live_resolve_test_child_object_test_int` (TestChildObject.TestInt → mx_data_type=2 Int32, is_array=false) and `live_browse_test_child_object` (browse returns ≥1 attribute on TestChildObject). Both pass against the local AVEVA install.
### F4 + F5 — BindAck body parser + captured-bytes round-trip
**Resolved:** 2026-05-05 (commit `<this commit>`). Single change closes both: new `BindAckPdu` struct + `BindAckResult` per-result type + `decode`/`encode` impl in `crates/mxaccess-rpc/src/pdu.rs`. Body layout per `[C706]` §12.6.3.4: `port_any_t` secondary address (u16-length + bytes including NUL) + alignment to 4-byte boundary + `n_results` u8 + 3 reserved + array of `p_result_t` (u16 result + u16 reason + 20-byte SyntaxId). Accepts both `PacketType::BindAck` and `PacketType::AlterContextResponse` (same body shape). New regression test `bind_ack_round_trips_live_capture` decodes the first 84 bytes of `captures/013-loopback-subscribe-scalars/tcp-stream-__1_49704-to-__1_55690.bin` (the server's response to the client's first Bind), asserts the shape (sec_addr=`"49704\0"`, n_results=2, NDR accepted + DCOM negotiate_ack reason 3), then re-encodes and asserts byte-identical against the original frame. Stronger live-wire parity than the prior synthetic-frame tests. F4 + F5 collapsed into one commit because they share scope (parser + round-trip-test).
### F29 — Align `mxaccess-asb-nettcp::nbfs` static dictionary ids with canonical `[MC-NBFS]` table
**Resolved:** 2026-05-05 (commit `<this commit>`). The original hand-curated table was wrong starting at id 74 — entries had been deduplicated/renumbered without preserving the canonical `id = 2 × StringN` mapping from `[MC-NBFS]` §2.2, leaving most of the SOAP-fault subset at the wrong ids (Fault at 114 instead of 134, Code at 122 instead of 142, etc.). Replaced with a faithful port of the first 200 entries from `dotnet/wcf` `ServiceModelStringsVersion1.cs` (covering id 0..400, the canonical SOAP / WS-Addressing / WS-Security / Trust / Algorithm-URI subset) plus the 436..444 xsi/xsd/nil extras already in place. Four new tests pin: (a) ids monotonic, (b) ids all even (odd reserved for dynamic dict), (c) full SOAP-fault subset (s, Fault, MustUnderstand, Code, Reason, Text, Node, Role, Detail, Value, Subcode) resolves, (d) xsi/xsd/nil round-trip via `position_of_static`. Future extensions: append more `ServiceModelStringsVersion1.StringN` entries as captures show new ids; mechanical extension.
### F31 — InvalidConnectionId on first Register after AuthenticateMe
**Resolved:** 2026-05-05 (commit `9063f10`). Not a HMAC bug — `AsbErrorCode.InvalidConnectionId` (= 1) is a transient race that .NET's `MxAsbDataClient.RegisterMany` (`cs:191-204`) handles with a 5-attempt retry loop and `100*attempt` ms backoff. `AuthenticateMe` is one-way (`AsbContracts.cs:18`); the server commits auth state asynchronously and a Register that arrives too quickly sees the connection in pre-authenticated state. `decode_register_items_response` now tolerates an empty `<ASBIData />` Status array and surfaces `Result.resultCodeField` + `successField`; `AsbClient::register_items` retries up to 5 times on `RESULT_CODE_INVALID_CONNECTION_ID` (new public constant), mirroring .NET. Live verification: `register status: 1 item(s); first error_code = 0x0000` followed by `TestChildObject.TestInt = AsbVariant { type_id: 4, length: 4, payload: [99, 0, 0, 0] }` over the live wire.
### F30 — Resolve dict-id element/attribute names on the read side
**Resolved:** 2026-05-05 (commit `eb6c689`). `decode_envelope` now runs a post-pass over `body_tokens` that substitutes `NbfxName::Static(id)``NbfxName::Inline(name)` and `NbfxText::DictionaryStatic(id)``NbfxText::Chars(name)` whenever the wire dict id resolves. Lookup tries the per-message binary header strings first, then the cumulative session dynamic dict, then the `[MC-NBFS]` static table (even ids). Tokens with unresolvable ids stay opaque so trace output still reveals them. Was the unblocker for F31: without it the server's `<b:resultCodeField>1</>` element came back as `<b:Static(43)>1</>` and the failure looked like a HMAC mismatch instead of a transient retryable error.
### F7 — Consolidate `Guid` type across `mxaccess-rpc`
**Resolved:** 2026-05-05 in this iteration's commit. `Guid` was hoisted from `objref::Guid` into the new shared `crate::guid::Guid` module. `objref` and `pdu` now re-export from there; M2 wave 2's `orpc`, `object_exporter`, and `rem_unknown` import it directly. The OXID-resolve dual-string decoder additionally needs an owned protocol label (`format!("protseq_0x{:04x}", tower_id)` per `ObjectExporterMessages.cs:120`) — `ComDualStringEntry::protocol` was upgraded from `&'static str` to `Cow<'static, str>` to support both decoders without the agent's interim `Box::leak` workaround.
### F8 — `RpcError` is duplicated across `objref` and `pdu` modules
**Resolved:** 2026-05-05 in this iteration's commit. `RpcError` was hoisted into the new shared `crate::error::RpcError` module as a single union of all wave 1 variants plus a generic `Decode { offset, reason: &'static str, buffer_len }` variant for the wave 2 ORPC parsers' one-off failures. `objref` and `pdu` re-export from there; M2 wave 2's `orpc`, `object_exporter`, and `rem_unknown` use it directly.
### F13 — `NmxClient` high-level write/advise/subscribe wrappers
**Resolved:** 2026-05-05. All seven wrappers landed in `crates/mxaccess-nmx/src/client.rs`: `write`, `write2`, `write_secured2`, `advise_supervisory`, `send_observed_pre_advise_metadata`, `register_reference`, `un_advise`. Each takes a `GalaxyTagMetadata` + a typed `WriteValue` (re-exported from `mxaccess-codec`), builds the inner NMX body via `mxaccess-codec` (`write_message::encode` / `encode_timestamped` / `secured_write::encode` / `NmxItemControlMessage` / `NmxMetadataQueryMessage` / `NmxReferenceRegistrationMessage`), wraps in `NmxTransferEnvelope`, and routes through `transfer_data`. The pure-codec `encode_*_transfer_body` helpers are extracted as `pub(crate) fn` for testability, mirroring the .NET reference's `internal static` shape. `un_advise` preserves the .NET reference's quirky `NmxTransferMessageKind::Write` envelope (not `ItemControl`) per `cs:457`.
### F15 — Callback router wires `CallbackExporter` events into `Subscription` stream
**Resolved:** 2026-05-05 across two commits.
- Step 1/2 (`2b849ae`): `Session::connect_nmx` now starts a `CallbackExporter` on a 127.0.0.1 ephemeral port, builds the OBJREF via `local_hostname()` + `127.0.0.1` fallback, registers it through `NmxClient::register_engine_2` (was `..._without_callback`). A `callback_router` task drains `CallbackEvent`s, decodes each `CallbackInvoked` body via `NmxSubscriptionMessage::parse_inner`, and broadcasts parsed messages on a `tokio::sync::broadcast` channel exposed via `Session::callbacks()`. Shutdown chains: UnregisterEngine → CallbackExporter::shutdown → wait for router task.
- Step 2/2 (this commit): `Subscription` now impls `Stream<Item = Result<DataChange, Error>>`. Filtering follows the .NET reference at `cs:333-343` exactly — `0x32` SubscriptionStatus messages are kept only when `message.item_correlation_id == subscription.correlation_id`; `0x33` DataUpdate messages pass through to ALL subscriptions because the codec exposes no per-record correlation field (matches the .NET `MxNativeCallbackEvent` filter behavior verbatim). Each `NmxSubscriptionRecord` with a parseable `value` becomes one `DataChange`. Records with `value: None` are dropped silently (mirrors the .NET `evt.Record.Value is null` filter at `cs:337`). Lag-loss surfaces as `Error::Configuration(InvalidArgument)` carrying the lag count. Stream-end (broadcast sender dropped) yields `None`. New helper: `filetime_to_system_time` (inverse of the existing `system_time_to_filetime`); saturates at Unix epoch for pre-1970 FILETIMEs. Tests cover correlation match/mismatch for `0x32`, `0x33` pass-through for any correlation, and FILETIME round-trip.
### F1 — NTLM consumer-layer helpers (workstation default + from_env constructor)
**Resolved:** 2026-05-05. `NtlmClientContext::from_env()` reads `MX_RPC_USER` / `MX_RPC_PASSWORD` / `MX_RPC_DOMAIN` (mirrors `ManagedNtlmClientContext.FromEnvironment` at `cs:41-49`); empty `MX_RPC_DOMAIN` is permitted. `local_hostname()` checks `COMPUTERNAME` then `HOSTNAME` and returns the empty string when neither is set — same "unavailable" semantics as `Environment.MachineName` returning null. Lives in `mxaccess-rpc/src/ntlm.rs`; deliberately doesn't pull `gethostname` (no native-libc deps, no `unsafe` for hostname lookup). Added `NtlmError::MissingEnvVar { name }` for the env-var-unset case. Test mod gained an `EnvScope` + `ENV_LOCK` mutex pattern for serializing process-global env mutation across parallel tests.
### F9 — `ObjectExporterClient.cs` ResolveOxid wrapper methods
**Resolved:** 2026-05-05. Both portable methods land in `crates/mxaccess-rpc/src/object_exporter_client.rs`: `resolve_oxid_unauthenticated` (mirrors `cs:14-30`) and `resolve_oxid_with_managed_ntlm_packet_integrity` (mirrors `cs:66-81`). Each opens a TCP connection, binds to `IObjectExporter`, calls opnum 0 with the encoded request, and decodes the response — preferring `parse_resolve_oxid_result` then falling back to `parse_resolve_oxid_failure` for short stubs. The two SSPI flavours (`ResolveOxidWithNtlmConnect`, `ResolveOxidWithNtlmPacketIntegrity`) wrap .NET's `System.Net.Security.SspiClientContext` and are explicitly out of scope for the Rust port — that's a permanent skip, not a deferral.
### F17 — `Guid::parse_str` helper (dashed-hex string parser)
**Resolved:** 2026-05-05. `Guid::parse_str(&str) -> Result<Guid, RpcError>` landed in `crates/mxaccess-rpc/src/guid.rs:65-112` as the inverse of the existing `Display` impl. Accepts the canonical dashed-hex form, optionally wrapped in `{}` braces (.NET `B` format), case-insensitive, and tolerant of bare 32-char hex without dashes. Single-pass char-by-char nibble accumulator avoids per-byte string allocation; the same byte-swap of groups 1-3 the Display impl does is applied after the raw hex pass. Eight new tests cover round-trip against the `Display` fixture (`b49f92f7-c748-4169-8eca-a0670b012746`), braces, uppercase, no-dashes, zero-GUID, too-short, too-long, and non-hex rejection. The five live-NMX examples (`connect-write-read`, `subscribe`, `recovery`, `multi-tag`, `secured-write`) lost their per-file 15-line `parse_guid` helpers in favour of the canonical implementation. Test count delta: 524 → 532 (+8).
+63
View File
@@ -0,0 +1,63 @@
# `cargo public-api` baselines
F41 — public-api baseline established 2026-05-06. One file per
workspace crate; each is the verbatim output of
`cargo +nightly public-api --simplified -p <crate>`.
## Why a baseline
`mxaccess` and friends are heading for `cargo publish`. Once the
crates are on crates.io, semver-breaking changes to the public surface
need to be intentional. The baseline is what CI diffs against to
catch unintentional drift.
## Update procedure
When a PR intentionally changes the public API:
1. Build the crate against nightly + `cargo-public-api`:
```powershell
rustup toolchain install nightly # one-time
cargo install cargo-public-api # one-time
```
2. Regenerate the affected baseline file:
```powershell
cd rust
cargo +nightly public-api --simplified -p <crate> > ../design/public-api/<crate>.txt
```
3. Commit the regenerated file alongside the API change. Reviewers
inspect the diff at `design/public-api/<crate>.txt` to verify the
intent matches the wire-up.
## CI
`.github/workflows/rust.yml` runs `cargo +nightly public-api --simplified -p <crate>`
for each workspace crate after the standard build/test/clippy/fmt
matrix and `diff`s the live output against the committed baseline.
Drift fails the CI step; the PR author either adjusts the
implementation or updates the baseline (per the procedure above).
## What `--simplified` strips
`--simplified` (single `-s`) omits blanket impls (e.g.
`impl<T: Clone> Clone for Vec<T>`-style noise) but keeps everything
that's reachable through the crate's named public items. Doubling
(`-ss`) would also strip auto-trait impls (`Send`, `Sync`,
`UnwindSafe`); we don't because intentional `Send` / `Sync` losses
on a `Session` clone *are* a semver break we want to catch.
## Per-crate sizes (line counts)
Captured at baseline date:
| crate | lines |
|----------------------|------:|
| `mxaccess-codec` | ~2516 |
| `mxaccess-asb` | ~1258 |
| `mxaccess-rpc` | ~1273 |
| `mxaccess-asb-nettcp`| ~708 |
| `mxaccess` | ~542 |
| `mxaccess-galaxy` | ~374 |
| `mxaccess-callback` | ~170 |
| `mxaccess-compat` | ~123 |
| `mxaccess-nmx` | ~118 |
+708
View File
@@ -0,0 +1,708 @@
pub mod mxaccess_asb_nettcp
pub mod mxaccess_asb_nettcp::auth
pub enum mxaccess_asb_nettcp::auth::AuthError
pub mxaccess_asb_nettcp::auth::AuthError::Deflate(alloc::string::String)
pub mxaccess_asb_nettcp::auth::AuthError::InvalidDecimal(alloc::string::String)
pub mxaccess_asb_nettcp::auth::AuthError::InvalidKeySize(u32)
pub mxaccess_asb_nettcp::auth::AuthError::NoRemoteKey
pub mxaccess_asb_nettcp::auth::AuthError::ZeroPrime
impl core::error::Error for mxaccess_asb_nettcp::auth::AuthError
impl core::fmt::Debug for mxaccess_asb_nettcp::auth::AuthError
pub fn mxaccess_asb_nettcp::auth::AuthError::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess_asb_nettcp::auth::AuthError
pub fn mxaccess_asb_nettcp::auth::AuthError::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_asb_nettcp::auth::AuthError
impl core::marker::Send for mxaccess_asb_nettcp::auth::AuthError
impl core::marker::Sync for mxaccess_asb_nettcp::auth::AuthError
impl core::marker::Unpin for mxaccess_asb_nettcp::auth::AuthError
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::auth::AuthError
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::auth::AuthError
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::auth::AuthError
pub enum mxaccess_asb_nettcp::auth::HashAlgorithm
pub mxaccess_asb_nettcp::auth::HashAlgorithm::Md5
pub mxaccess_asb_nettcp::auth::HashAlgorithm::Sha1
pub mxaccess_asb_nettcp::auth::HashAlgorithm::Sha512
pub mxaccess_asb_nettcp::auth::HashAlgorithm::Unrecognised
impl mxaccess_asb_nettcp::auth::HashAlgorithm
pub fn mxaccess_asb_nettcp::auth::HashAlgorithm::parse(value: &str) -> Self
impl core::clone::Clone for mxaccess_asb_nettcp::auth::HashAlgorithm
pub fn mxaccess_asb_nettcp::auth::HashAlgorithm::clone(&self) -> mxaccess_asb_nettcp::auth::HashAlgorithm
impl core::cmp::Eq for mxaccess_asb_nettcp::auth::HashAlgorithm
impl core::cmp::PartialEq for mxaccess_asb_nettcp::auth::HashAlgorithm
pub fn mxaccess_asb_nettcp::auth::HashAlgorithm::eq(&self, other: &mxaccess_asb_nettcp::auth::HashAlgorithm) -> bool
impl core::fmt::Debug for mxaccess_asb_nettcp::auth::HashAlgorithm
pub fn mxaccess_asb_nettcp::auth::HashAlgorithm::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Copy for mxaccess_asb_nettcp::auth::HashAlgorithm
impl core::marker::StructuralPartialEq for mxaccess_asb_nettcp::auth::HashAlgorithm
impl core::marker::Freeze for mxaccess_asb_nettcp::auth::HashAlgorithm
impl core::marker::Send for mxaccess_asb_nettcp::auth::HashAlgorithm
impl core::marker::Sync for mxaccess_asb_nettcp::auth::HashAlgorithm
impl core::marker::Unpin for mxaccess_asb_nettcp::auth::HashAlgorithm
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::auth::HashAlgorithm
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::auth::HashAlgorithm
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::auth::HashAlgorithm
pub struct mxaccess_asb_nettcp::auth::AsbAuthenticator
impl mxaccess_asb_nettcp::auth::AsbAuthenticator
pub fn mxaccess_asb_nettcp::auth::AsbAuthenticator::accept_connect_response(&mut self, service_public_key: &[u8], connection_lifetime: core::option::Option<&str>)
pub fn mxaccess_asb_nettcp::auth::AsbAuthenticator::connection_id(&self) -> [u8; 16]
pub fn mxaccess_asb_nettcp::auth::AsbAuthenticator::create_authentication_data(&self) -> core::result::Result<mxaccess_asb_nettcp::auth::EncryptedBytes, mxaccess_asb_nettcp::auth::AuthError>
pub fn mxaccess_asb_nettcp::auth::AsbAuthenticator::local_public_key(&self) -> &[u8]
pub fn mxaccess_asb_nettcp::auth::AsbAuthenticator::new(passphrase: &str, params: &mxaccess_asb_nettcp::auth::CryptoParameters, connection_id: [u8; 16]) -> core::result::Result<Self, mxaccess_asb_nettcp::auth::AuthError>
pub fn mxaccess_asb_nettcp::auth::AsbAuthenticator::peek_next_message_number(&self) -> u64
pub fn mxaccess_asb_nettcp::auth::AsbAuthenticator::sign(&mut self, request_xml_utf8: &[u8], force_hmac: bool) -> core::result::Result<mxaccess_asb_nettcp::auth::SignedValidator, mxaccess_asb_nettcp::auth::AuthError>
pub fn mxaccess_asb_nettcp::auth::AsbAuthenticator::use_apollo_signing(&self) -> bool
impl core::marker::Freeze for mxaccess_asb_nettcp::auth::AsbAuthenticator
impl core::marker::Send for mxaccess_asb_nettcp::auth::AsbAuthenticator
impl core::marker::Sync for mxaccess_asb_nettcp::auth::AsbAuthenticator
impl core::marker::Unpin for mxaccess_asb_nettcp::auth::AsbAuthenticator
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::auth::AsbAuthenticator
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::auth::AsbAuthenticator
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::auth::AsbAuthenticator
pub struct mxaccess_asb_nettcp::auth::CryptoParameters
pub mxaccess_asb_nettcp::auth::CryptoParameters::generator_decimal: alloc::string::String
pub mxaccess_asb_nettcp::auth::CryptoParameters::hash_algorithm: mxaccess_asb_nettcp::auth::HashAlgorithm
pub mxaccess_asb_nettcp::auth::CryptoParameters::key_size_bits: u32
pub mxaccess_asb_nettcp::auth::CryptoParameters::prime_decimal: alloc::string::String
impl mxaccess_asb_nettcp::auth::CryptoParameters
pub const mxaccess_asb_nettcp::auth::CryptoParameters::DEFAULT_PRIME_TEXT: &'static str
pub fn mxaccess_asb_nettcp::auth::CryptoParameters::defaults() -> Self
impl core::clone::Clone for mxaccess_asb_nettcp::auth::CryptoParameters
pub fn mxaccess_asb_nettcp::auth::CryptoParameters::clone(&self) -> mxaccess_asb_nettcp::auth::CryptoParameters
impl core::fmt::Debug for mxaccess_asb_nettcp::auth::CryptoParameters
pub fn mxaccess_asb_nettcp::auth::CryptoParameters::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_asb_nettcp::auth::CryptoParameters
impl core::marker::Send for mxaccess_asb_nettcp::auth::CryptoParameters
impl core::marker::Sync for mxaccess_asb_nettcp::auth::CryptoParameters
impl core::marker::Unpin for mxaccess_asb_nettcp::auth::CryptoParameters
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::auth::CryptoParameters
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::auth::CryptoParameters
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::auth::CryptoParameters
pub struct mxaccess_asb_nettcp::auth::EncryptedBytes
pub mxaccess_asb_nettcp::auth::EncryptedBytes::ciphertext: alloc::vec::Vec<u8>
pub mxaccess_asb_nettcp::auth::EncryptedBytes::iv: alloc::vec::Vec<u8>
impl core::clone::Clone for mxaccess_asb_nettcp::auth::EncryptedBytes
pub fn mxaccess_asb_nettcp::auth::EncryptedBytes::clone(&self) -> mxaccess_asb_nettcp::auth::EncryptedBytes
impl core::fmt::Debug for mxaccess_asb_nettcp::auth::EncryptedBytes
pub fn mxaccess_asb_nettcp::auth::EncryptedBytes::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_asb_nettcp::auth::EncryptedBytes
impl core::marker::Send for mxaccess_asb_nettcp::auth::EncryptedBytes
impl core::marker::Sync for mxaccess_asb_nettcp::auth::EncryptedBytes
impl core::marker::Unpin for mxaccess_asb_nettcp::auth::EncryptedBytes
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::auth::EncryptedBytes
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::auth::EncryptedBytes
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::auth::EncryptedBytes
pub struct mxaccess_asb_nettcp::auth::SignedValidator
pub mxaccess_asb_nettcp::auth::SignedValidator::connection_id: [u8; 16]
pub mxaccess_asb_nettcp::auth::SignedValidator::iv: alloc::vec::Vec<u8>
pub mxaccess_asb_nettcp::auth::SignedValidator::mac: alloc::vec::Vec<u8>
pub mxaccess_asb_nettcp::auth::SignedValidator::message_number: u64
impl core::clone::Clone for mxaccess_asb_nettcp::auth::SignedValidator
pub fn mxaccess_asb_nettcp::auth::SignedValidator::clone(&self) -> mxaccess_asb_nettcp::auth::SignedValidator
impl core::fmt::Debug for mxaccess_asb_nettcp::auth::SignedValidator
pub fn mxaccess_asb_nettcp::auth::SignedValidator::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_asb_nettcp::auth::SignedValidator
impl core::marker::Send for mxaccess_asb_nettcp::auth::SignedValidator
impl core::marker::Sync for mxaccess_asb_nettcp::auth::SignedValidator
impl core::marker::Unpin for mxaccess_asb_nettcp::auth::SignedValidator
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::auth::SignedValidator
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::auth::SignedValidator
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::auth::SignedValidator
pub fn mxaccess_asb_nettcp::auth::bigint_from_dotnet_bytes(bytes: &[u8]) -> num_bigint::biguint::BigUint
pub fn mxaccess_asb_nettcp::auth::bigint_to_dotnet_bytes(value: &num_bigint::biguint::BigUint) -> alloc::vec::Vec<u8>
pub mod mxaccess_asb_nettcp::nbfs
pub struct mxaccess_asb_nettcp::nbfs::StaticEntry
pub mxaccess_asb_nettcp::nbfs::StaticEntry::id: u32
pub mxaccess_asb_nettcp::nbfs::StaticEntry::value: &'static str
impl core::clone::Clone for mxaccess_asb_nettcp::nbfs::StaticEntry
pub fn mxaccess_asb_nettcp::nbfs::StaticEntry::clone(&self) -> mxaccess_asb_nettcp::nbfs::StaticEntry
impl core::fmt::Debug for mxaccess_asb_nettcp::nbfs::StaticEntry
pub fn mxaccess_asb_nettcp::nbfs::StaticEntry::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Copy for mxaccess_asb_nettcp::nbfs::StaticEntry
impl core::marker::Freeze for mxaccess_asb_nettcp::nbfs::StaticEntry
impl core::marker::Send for mxaccess_asb_nettcp::nbfs::StaticEntry
impl core::marker::Sync for mxaccess_asb_nettcp::nbfs::StaticEntry
impl core::marker::Unpin for mxaccess_asb_nettcp::nbfs::StaticEntry
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nbfs::StaticEntry
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nbfs::StaticEntry
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nbfs::StaticEntry
pub const mxaccess_asb_nettcp::nbfs::STATIC_ENTRIES: &[mxaccess_asb_nettcp::nbfs::StaticEntry]
pub fn mxaccess_asb_nettcp::nbfs::lookup_static(id: u32) -> core::option::Option<&'static str>
pub fn mxaccess_asb_nettcp::nbfs::position_of_static(value: &str) -> core::option::Option<u32>
pub mod mxaccess_asb_nettcp::nbfx
#[non_exhaustive] pub enum mxaccess_asb_nettcp::nbfx::NbfxError
pub mxaccess_asb_nettcp::nbfx::NbfxError::IntOverflow
pub mxaccess_asb_nettcp::nbfx::NbfxError::InvalidUtf8
pub mxaccess_asb_nettcp::nbfx::NbfxError::InvalidUtf8::stage: &'static str
pub mxaccess_asb_nettcp::nbfx::NbfxError::NegativeLength(i32)
pub mxaccess_asb_nettcp::nbfx::NbfxError::PayloadTooLarge
pub mxaccess_asb_nettcp::nbfx::NbfxError::PayloadTooLarge::len: usize
pub mxaccess_asb_nettcp::nbfx::NbfxError::PayloadTooLarge::max: u64
pub mxaccess_asb_nettcp::nbfx::NbfxError::Truncated
pub mxaccess_asb_nettcp::nbfx::NbfxError::Truncated::have: usize
pub mxaccess_asb_nettcp::nbfx::NbfxError::Truncated::need: usize
pub mxaccess_asb_nettcp::nbfx::NbfxError::Truncated::stage: &'static str
pub mxaccess_asb_nettcp::nbfx::NbfxError::UnknownDynamicDictionaryId(u32)
pub mxaccess_asb_nettcp::nbfx::NbfxError::UnknownRecord(u8)
pub mxaccess_asb_nettcp::nbfx::NbfxError::UnknownStaticDictionaryId(u32)
impl core::error::Error for mxaccess_asb_nettcp::nbfx::NbfxError
impl core::fmt::Debug for mxaccess_asb_nettcp::nbfx::NbfxError
pub fn mxaccess_asb_nettcp::nbfx::NbfxError::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess_asb_nettcp::nbfx::NbfxError
pub fn mxaccess_asb_nettcp::nbfx::NbfxError::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_asb_nettcp::nbfx::NbfxError
impl core::marker::Send for mxaccess_asb_nettcp::nbfx::NbfxError
impl core::marker::Sync for mxaccess_asb_nettcp::nbfx::NbfxError
impl core::marker::Unpin for mxaccess_asb_nettcp::nbfx::NbfxError
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nbfx::NbfxError
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nbfx::NbfxError
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nbfx::NbfxError
pub enum mxaccess_asb_nettcp::nbfx::NbfxName
pub mxaccess_asb_nettcp::nbfx::NbfxName::Dynamic(u32)
pub mxaccess_asb_nettcp::nbfx::NbfxName::Inline(alloc::string::String)
pub mxaccess_asb_nettcp::nbfx::NbfxName::Static(u32)
impl core::clone::Clone for mxaccess_asb_nettcp::nbfx::NbfxName
pub fn mxaccess_asb_nettcp::nbfx::NbfxName::clone(&self) -> mxaccess_asb_nettcp::nbfx::NbfxName
impl core::cmp::PartialEq for mxaccess_asb_nettcp::nbfx::NbfxName
pub fn mxaccess_asb_nettcp::nbfx::NbfxName::eq(&self, other: &mxaccess_asb_nettcp::nbfx::NbfxName) -> bool
impl core::fmt::Debug for mxaccess_asb_nettcp::nbfx::NbfxName
pub fn mxaccess_asb_nettcp::nbfx::NbfxName::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::StructuralPartialEq for mxaccess_asb_nettcp::nbfx::NbfxName
impl core::marker::Freeze for mxaccess_asb_nettcp::nbfx::NbfxName
impl core::marker::Send for mxaccess_asb_nettcp::nbfx::NbfxName
impl core::marker::Sync for mxaccess_asb_nettcp::nbfx::NbfxName
impl core::marker::Unpin for mxaccess_asb_nettcp::nbfx::NbfxName
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nbfx::NbfxName
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nbfx::NbfxName
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nbfx::NbfxName
pub enum mxaccess_asb_nettcp::nbfx::NbfxText
pub mxaccess_asb_nettcp::nbfx::NbfxText::Bool(bool)
pub mxaccess_asb_nettcp::nbfx::NbfxText::Bytes(alloc::vec::Vec<u8>)
pub mxaccess_asb_nettcp::nbfx::NbfxText::Chars(alloc::string::String)
pub mxaccess_asb_nettcp::nbfx::NbfxText::DictionaryDynamic(u32)
pub mxaccess_asb_nettcp::nbfx::NbfxText::DictionaryStatic(u32)
pub mxaccess_asb_nettcp::nbfx::NbfxText::Empty
pub mxaccess_asb_nettcp::nbfx::NbfxText::Int16(i16)
pub mxaccess_asb_nettcp::nbfx::NbfxText::Int32(i32)
pub mxaccess_asb_nettcp::nbfx::NbfxText::Int64(i64)
pub mxaccess_asb_nettcp::nbfx::NbfxText::Int8(i8)
pub mxaccess_asb_nettcp::nbfx::NbfxText::One
pub mxaccess_asb_nettcp::nbfx::NbfxText::UniqueId([u8; 16])
pub mxaccess_asb_nettcp::nbfx::NbfxText::Zero
impl mxaccess_asb_nettcp::nbfx::NbfxText
pub fn mxaccess_asb_nettcp::nbfx::NbfxText::resolve<'a>(&'a self, dynamic: &'a mxaccess_asb_nettcp::nbfx::DynamicDictionary) -> core::option::Option<alloc::string::String>
impl core::clone::Clone for mxaccess_asb_nettcp::nbfx::NbfxText
pub fn mxaccess_asb_nettcp::nbfx::NbfxText::clone(&self) -> mxaccess_asb_nettcp::nbfx::NbfxText
impl core::cmp::PartialEq for mxaccess_asb_nettcp::nbfx::NbfxText
pub fn mxaccess_asb_nettcp::nbfx::NbfxText::eq(&self, other: &mxaccess_asb_nettcp::nbfx::NbfxText) -> bool
impl core::fmt::Debug for mxaccess_asb_nettcp::nbfx::NbfxText
pub fn mxaccess_asb_nettcp::nbfx::NbfxText::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::StructuralPartialEq for mxaccess_asb_nettcp::nbfx::NbfxText
impl core::marker::Freeze for mxaccess_asb_nettcp::nbfx::NbfxText
impl core::marker::Send for mxaccess_asb_nettcp::nbfx::NbfxText
impl core::marker::Sync for mxaccess_asb_nettcp::nbfx::NbfxText
impl core::marker::Unpin for mxaccess_asb_nettcp::nbfx::NbfxText
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nbfx::NbfxText
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nbfx::NbfxText
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nbfx::NbfxText
pub enum mxaccess_asb_nettcp::nbfx::NbfxToken
pub mxaccess_asb_nettcp::nbfx::NbfxToken::Attribute
pub mxaccess_asb_nettcp::nbfx::NbfxToken::Attribute::name: mxaccess_asb_nettcp::nbfx::NbfxName
pub mxaccess_asb_nettcp::nbfx::NbfxToken::Attribute::prefix: core::option::Option<alloc::string::String>
pub mxaccess_asb_nettcp::nbfx::NbfxToken::Attribute::value: mxaccess_asb_nettcp::nbfx::NbfxText
pub mxaccess_asb_nettcp::nbfx::NbfxToken::DefaultNamespace
pub mxaccess_asb_nettcp::nbfx::NbfxToken::DefaultNamespace::value: mxaccess_asb_nettcp::nbfx::NbfxText
pub mxaccess_asb_nettcp::nbfx::NbfxToken::Element
pub mxaccess_asb_nettcp::nbfx::NbfxToken::Element::name: mxaccess_asb_nettcp::nbfx::NbfxName
pub mxaccess_asb_nettcp::nbfx::NbfxToken::Element::prefix: core::option::Option<alloc::string::String>
pub mxaccess_asb_nettcp::nbfx::NbfxToken::EndElement
pub mxaccess_asb_nettcp::nbfx::NbfxToken::NamespaceDeclaration
pub mxaccess_asb_nettcp::nbfx::NbfxToken::NamespaceDeclaration::prefix: alloc::string::String
pub mxaccess_asb_nettcp::nbfx::NbfxToken::NamespaceDeclaration::value: mxaccess_asb_nettcp::nbfx::NbfxText
pub mxaccess_asb_nettcp::nbfx::NbfxToken::Text(mxaccess_asb_nettcp::nbfx::NbfxText)
impl core::clone::Clone for mxaccess_asb_nettcp::nbfx::NbfxToken
pub fn mxaccess_asb_nettcp::nbfx::NbfxToken::clone(&self) -> mxaccess_asb_nettcp::nbfx::NbfxToken
impl core::cmp::PartialEq for mxaccess_asb_nettcp::nbfx::NbfxToken
pub fn mxaccess_asb_nettcp::nbfx::NbfxToken::eq(&self, other: &mxaccess_asb_nettcp::nbfx::NbfxToken) -> bool
impl core::fmt::Debug for mxaccess_asb_nettcp::nbfx::NbfxToken
pub fn mxaccess_asb_nettcp::nbfx::NbfxToken::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::StructuralPartialEq for mxaccess_asb_nettcp::nbfx::NbfxToken
impl core::marker::Freeze for mxaccess_asb_nettcp::nbfx::NbfxToken
impl core::marker::Send for mxaccess_asb_nettcp::nbfx::NbfxToken
impl core::marker::Sync for mxaccess_asb_nettcp::nbfx::NbfxToken
impl core::marker::Unpin for mxaccess_asb_nettcp::nbfx::NbfxToken
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nbfx::NbfxToken
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nbfx::NbfxToken
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nbfx::NbfxToken
pub struct mxaccess_asb_nettcp::nbfx::DynamicDictionary
impl mxaccess_asb_nettcp::nbfx::DynamicDictionary
pub fn mxaccess_asb_nettcp::nbfx::DynamicDictionary::intern(&mut self, value: &str) -> u32
pub fn mxaccess_asb_nettcp::nbfx::DynamicDictionary::is_empty(&self) -> bool
pub fn mxaccess_asb_nettcp::nbfx::DynamicDictionary::len(&self) -> usize
pub fn mxaccess_asb_nettcp::nbfx::DynamicDictionary::lookup(&self, id: u32) -> core::option::Option<&str>
pub fn mxaccess_asb_nettcp::nbfx::DynamicDictionary::new() -> Self
pub fn mxaccess_asb_nettcp::nbfx::DynamicDictionary::position_of(&self, value: &str) -> core::option::Option<u32>
impl core::clone::Clone for mxaccess_asb_nettcp::nbfx::DynamicDictionary
pub fn mxaccess_asb_nettcp::nbfx::DynamicDictionary::clone(&self) -> mxaccess_asb_nettcp::nbfx::DynamicDictionary
impl core::default::Default for mxaccess_asb_nettcp::nbfx::DynamicDictionary
pub fn mxaccess_asb_nettcp::nbfx::DynamicDictionary::default() -> mxaccess_asb_nettcp::nbfx::DynamicDictionary
impl core::fmt::Debug for mxaccess_asb_nettcp::nbfx::DynamicDictionary
pub fn mxaccess_asb_nettcp::nbfx::DynamicDictionary::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_asb_nettcp::nbfx::DynamicDictionary
impl core::marker::Send for mxaccess_asb_nettcp::nbfx::DynamicDictionary
impl core::marker::Sync for mxaccess_asb_nettcp::nbfx::DynamicDictionary
impl core::marker::Unpin for mxaccess_asb_nettcp::nbfx::DynamicDictionary
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nbfx::DynamicDictionary
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nbfx::DynamicDictionary
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nbfx::DynamicDictionary
pub fn mxaccess_asb_nettcp::nbfx::decode_tokens(input: &[u8], _dynamic: &mut mxaccess_asb_nettcp::nbfx::DynamicDictionary) -> core::result::Result<(alloc::vec::Vec<mxaccess_asb_nettcp::nbfx::NbfxToken>, usize), mxaccess_asb_nettcp::nbfx::NbfxError>
pub fn mxaccess_asb_nettcp::nbfx::encode_tokens(tokens: &[mxaccess_asb_nettcp::nbfx::NbfxToken], dynamic: &mut mxaccess_asb_nettcp::nbfx::DynamicDictionary, out: &mut alloc::vec::Vec<u8>) -> core::result::Result<(), mxaccess_asb_nettcp::nbfx::NbfxError>
pub mod mxaccess_asb_nettcp::nmf
#[repr(u8)] pub enum mxaccess_asb_nettcp::nmf::NmfEncoding
pub mxaccess_asb_nettcp::nmf::NmfEncoding::Binary = 3
pub mxaccess_asb_nettcp::nmf::NmfEncoding::BinaryWithDictionary = 8
pub mxaccess_asb_nettcp::nmf::NmfEncoding::BinaryWithMtom = 4
pub mxaccess_asb_nettcp::nmf::NmfEncoding::Mtom = 7
pub mxaccess_asb_nettcp::nmf::NmfEncoding::Utf16LeSoapText = 2
pub mxaccess_asb_nettcp::nmf::NmfEncoding::Utf16SoapText = 1
pub mxaccess_asb_nettcp::nmf::NmfEncoding::Utf8SoapText = 0
impl mxaccess_asb_nettcp::nmf::NmfEncoding
pub fn mxaccess_asb_nettcp::nmf::NmfEncoding::from_u8(b: u8) -> core::option::Option<Self>
impl core::clone::Clone for mxaccess_asb_nettcp::nmf::NmfEncoding
pub fn mxaccess_asb_nettcp::nmf::NmfEncoding::clone(&self) -> mxaccess_asb_nettcp::nmf::NmfEncoding
impl core::cmp::Eq for mxaccess_asb_nettcp::nmf::NmfEncoding
impl core::cmp::PartialEq for mxaccess_asb_nettcp::nmf::NmfEncoding
pub fn mxaccess_asb_nettcp::nmf::NmfEncoding::eq(&self, other: &mxaccess_asb_nettcp::nmf::NmfEncoding) -> bool
impl core::fmt::Debug for mxaccess_asb_nettcp::nmf::NmfEncoding
pub fn mxaccess_asb_nettcp::nmf::NmfEncoding::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Copy for mxaccess_asb_nettcp::nmf::NmfEncoding
impl core::marker::StructuralPartialEq for mxaccess_asb_nettcp::nmf::NmfEncoding
impl core::marker::Freeze for mxaccess_asb_nettcp::nmf::NmfEncoding
impl core::marker::Send for mxaccess_asb_nettcp::nmf::NmfEncoding
impl core::marker::Sync for mxaccess_asb_nettcp::nmf::NmfEncoding
impl core::marker::Unpin for mxaccess_asb_nettcp::nmf::NmfEncoding
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nmf::NmfEncoding
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nmf::NmfEncoding
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nmf::NmfEncoding
#[non_exhaustive] pub enum mxaccess_asb_nettcp::nmf::NmfError
pub mxaccess_asb_nettcp::nmf::NmfError::IntOverflow
pub mxaccess_asb_nettcp::nmf::NmfError::InvalidUtf8
pub mxaccess_asb_nettcp::nmf::NmfError::InvalidUtf8::stage: &'static str
pub mxaccess_asb_nettcp::nmf::NmfError::NegativeLength(i32)
pub mxaccess_asb_nettcp::nmf::NmfError::PayloadTooLarge
pub mxaccess_asb_nettcp::nmf::NmfError::PayloadTooLarge::len: usize
pub mxaccess_asb_nettcp::nmf::NmfError::Truncated
pub mxaccess_asb_nettcp::nmf::NmfError::Truncated::have: usize
pub mxaccess_asb_nettcp::nmf::NmfError::Truncated::need: usize
pub mxaccess_asb_nettcp::nmf::NmfError::Truncated::stage: &'static str
pub mxaccess_asb_nettcp::nmf::NmfError::UnknownEncoding(u8)
pub mxaccess_asb_nettcp::nmf::NmfError::UnknownMode(u8)
pub mxaccess_asb_nettcp::nmf::NmfError::UnknownRecordType(u8)
impl core::error::Error for mxaccess_asb_nettcp::nmf::NmfError
impl core::fmt::Debug for mxaccess_asb_nettcp::nmf::NmfError
pub fn mxaccess_asb_nettcp::nmf::NmfError::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess_asb_nettcp::nmf::NmfError
pub fn mxaccess_asb_nettcp::nmf::NmfError::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_asb_nettcp::nmf::NmfError
impl core::marker::Send for mxaccess_asb_nettcp::nmf::NmfError
impl core::marker::Sync for mxaccess_asb_nettcp::nmf::NmfError
impl core::marker::Unpin for mxaccess_asb_nettcp::nmf::NmfError
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nmf::NmfError
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nmf::NmfError
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nmf::NmfError
#[repr(u8)] pub enum mxaccess_asb_nettcp::nmf::NmfMode
pub mxaccess_asb_nettcp::nmf::NmfMode::Duplex = 2
pub mxaccess_asb_nettcp::nmf::NmfMode::Simplex = 3
pub mxaccess_asb_nettcp::nmf::NmfMode::Singleton = 1
pub mxaccess_asb_nettcp::nmf::NmfMode::SingletonSized = 4
impl mxaccess_asb_nettcp::nmf::NmfMode
pub fn mxaccess_asb_nettcp::nmf::NmfMode::from_u8(b: u8) -> core::option::Option<Self>
impl core::clone::Clone for mxaccess_asb_nettcp::nmf::NmfMode
pub fn mxaccess_asb_nettcp::nmf::NmfMode::clone(&self) -> mxaccess_asb_nettcp::nmf::NmfMode
impl core::cmp::Eq for mxaccess_asb_nettcp::nmf::NmfMode
impl core::cmp::PartialEq for mxaccess_asb_nettcp::nmf::NmfMode
pub fn mxaccess_asb_nettcp::nmf::NmfMode::eq(&self, other: &mxaccess_asb_nettcp::nmf::NmfMode) -> bool
impl core::fmt::Debug for mxaccess_asb_nettcp::nmf::NmfMode
pub fn mxaccess_asb_nettcp::nmf::NmfMode::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Copy for mxaccess_asb_nettcp::nmf::NmfMode
impl core::marker::StructuralPartialEq for mxaccess_asb_nettcp::nmf::NmfMode
impl core::marker::Freeze for mxaccess_asb_nettcp::nmf::NmfMode
impl core::marker::Send for mxaccess_asb_nettcp::nmf::NmfMode
impl core::marker::Sync for mxaccess_asb_nettcp::nmf::NmfMode
impl core::marker::Unpin for mxaccess_asb_nettcp::nmf::NmfMode
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nmf::NmfMode
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nmf::NmfMode
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nmf::NmfMode
pub enum mxaccess_asb_nettcp::nmf::NmfRecord
pub mxaccess_asb_nettcp::nmf::NmfRecord::End
pub mxaccess_asb_nettcp::nmf::NmfRecord::ExtensibleEncoding(alloc::string::String)
pub mxaccess_asb_nettcp::nmf::NmfRecord::Fault(alloc::string::String)
pub mxaccess_asb_nettcp::nmf::NmfRecord::KnownEncoding(mxaccess_asb_nettcp::nmf::NmfEncoding)
pub mxaccess_asb_nettcp::nmf::NmfRecord::Mode(mxaccess_asb_nettcp::nmf::NmfMode)
pub mxaccess_asb_nettcp::nmf::NmfRecord::PreambleAck
pub mxaccess_asb_nettcp::nmf::NmfRecord::PreambleEnd
pub mxaccess_asb_nettcp::nmf::NmfRecord::SizedEnvelope(alloc::vec::Vec<u8>)
pub mxaccess_asb_nettcp::nmf::NmfRecord::UnsizedEnvelope(alloc::vec::Vec<u8>)
pub mxaccess_asb_nettcp::nmf::NmfRecord::UpgradeRequest(alloc::string::String)
pub mxaccess_asb_nettcp::nmf::NmfRecord::UpgradeResponse
pub mxaccess_asb_nettcp::nmf::NmfRecord::Version
pub mxaccess_asb_nettcp::nmf::NmfRecord::Version::major: u8
pub mxaccess_asb_nettcp::nmf::NmfRecord::Version::minor: u8
pub mxaccess_asb_nettcp::nmf::NmfRecord::Via(alloc::string::String)
impl mxaccess_asb_nettcp::nmf::NmfRecord
pub fn mxaccess_asb_nettcp::nmf::NmfRecord::decode(input: &[u8]) -> core::result::Result<(Self, usize), mxaccess_asb_nettcp::nmf::NmfError>
pub fn mxaccess_asb_nettcp::nmf::NmfRecord::encode(&self) -> core::result::Result<alloc::vec::Vec<u8>, mxaccess_asb_nettcp::nmf::NmfError>
pub fn mxaccess_asb_nettcp::nmf::NmfRecord::encode_into(&self, out: &mut alloc::vec::Vec<u8>) -> core::result::Result<(), mxaccess_asb_nettcp::nmf::NmfError>
impl core::clone::Clone for mxaccess_asb_nettcp::nmf::NmfRecord
pub fn mxaccess_asb_nettcp::nmf::NmfRecord::clone(&self) -> mxaccess_asb_nettcp::nmf::NmfRecord
impl core::cmp::Eq for mxaccess_asb_nettcp::nmf::NmfRecord
impl core::cmp::PartialEq for mxaccess_asb_nettcp::nmf::NmfRecord
pub fn mxaccess_asb_nettcp::nmf::NmfRecord::eq(&self, other: &mxaccess_asb_nettcp::nmf::NmfRecord) -> bool
impl core::fmt::Debug for mxaccess_asb_nettcp::nmf::NmfRecord
pub fn mxaccess_asb_nettcp::nmf::NmfRecord::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::StructuralPartialEq for mxaccess_asb_nettcp::nmf::NmfRecord
impl core::marker::Freeze for mxaccess_asb_nettcp::nmf::NmfRecord
impl core::marker::Send for mxaccess_asb_nettcp::nmf::NmfRecord
impl core::marker::Sync for mxaccess_asb_nettcp::nmf::NmfRecord
impl core::marker::Unpin for mxaccess_asb_nettcp::nmf::NmfRecord
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nmf::NmfRecord
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nmf::NmfRecord
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nmf::NmfRecord
#[repr(u8)] pub enum mxaccess_asb_nettcp::nmf::NmfRecordType
pub mxaccess_asb_nettcp::nmf::NmfRecordType::End = 7
pub mxaccess_asb_nettcp::nmf::NmfRecordType::ExtensibleEncoding = 4
pub mxaccess_asb_nettcp::nmf::NmfRecordType::Fault = 8
pub mxaccess_asb_nettcp::nmf::NmfRecordType::KnownEncoding = 3
pub mxaccess_asb_nettcp::nmf::NmfRecordType::Mode = 1
pub mxaccess_asb_nettcp::nmf::NmfRecordType::PreambleAck = 11
pub mxaccess_asb_nettcp::nmf::NmfRecordType::PreambleEnd = 12
pub mxaccess_asb_nettcp::nmf::NmfRecordType::SizedEnvelope = 6
pub mxaccess_asb_nettcp::nmf::NmfRecordType::UnsizedEnvelope = 5
pub mxaccess_asb_nettcp::nmf::NmfRecordType::UpgradeRequest = 9
pub mxaccess_asb_nettcp::nmf::NmfRecordType::UpgradeResponse = 10
pub mxaccess_asb_nettcp::nmf::NmfRecordType::Version = 0
pub mxaccess_asb_nettcp::nmf::NmfRecordType::Via = 2
impl mxaccess_asb_nettcp::nmf::NmfRecordType
pub fn mxaccess_asb_nettcp::nmf::NmfRecordType::from_u8(b: u8) -> core::option::Option<Self>
impl core::clone::Clone for mxaccess_asb_nettcp::nmf::NmfRecordType
pub fn mxaccess_asb_nettcp::nmf::NmfRecordType::clone(&self) -> mxaccess_asb_nettcp::nmf::NmfRecordType
impl core::cmp::Eq for mxaccess_asb_nettcp::nmf::NmfRecordType
impl core::cmp::PartialEq for mxaccess_asb_nettcp::nmf::NmfRecordType
pub fn mxaccess_asb_nettcp::nmf::NmfRecordType::eq(&self, other: &mxaccess_asb_nettcp::nmf::NmfRecordType) -> bool
impl core::fmt::Debug for mxaccess_asb_nettcp::nmf::NmfRecordType
pub fn mxaccess_asb_nettcp::nmf::NmfRecordType::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Copy for mxaccess_asb_nettcp::nmf::NmfRecordType
impl core::marker::StructuralPartialEq for mxaccess_asb_nettcp::nmf::NmfRecordType
impl core::marker::Freeze for mxaccess_asb_nettcp::nmf::NmfRecordType
impl core::marker::Send for mxaccess_asb_nettcp::nmf::NmfRecordType
impl core::marker::Sync for mxaccess_asb_nettcp::nmf::NmfRecordType
impl core::marker::Unpin for mxaccess_asb_nettcp::nmf::NmfRecordType
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nmf::NmfRecordType
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nmf::NmfRecordType
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nmf::NmfRecordType
pub fn mxaccess_asb_nettcp::nmf::decode_multibyte_int31(input: &[u8], cursor: &mut usize) -> core::result::Result<i32, mxaccess_asb_nettcp::nmf::NmfError>
pub fn mxaccess_asb_nettcp::nmf::encode_multibyte_int31(out: &mut alloc::vec::Vec<u8>, value: i32) -> core::result::Result<(), mxaccess_asb_nettcp::nmf::NmfError>
pub fn mxaccess_asb_nettcp::nmf::encode_preamble(via_uri: &str, out: &mut alloc::vec::Vec<u8>) -> core::result::Result<(), mxaccess_asb_nettcp::nmf::NmfError>
pub enum mxaccess_asb_nettcp::AuthError
pub mxaccess_asb_nettcp::AuthError::Deflate(alloc::string::String)
pub mxaccess_asb_nettcp::AuthError::InvalidDecimal(alloc::string::String)
pub mxaccess_asb_nettcp::AuthError::InvalidKeySize(u32)
pub mxaccess_asb_nettcp::AuthError::NoRemoteKey
pub mxaccess_asb_nettcp::AuthError::ZeroPrime
impl core::error::Error for mxaccess_asb_nettcp::auth::AuthError
impl core::fmt::Debug for mxaccess_asb_nettcp::auth::AuthError
pub fn mxaccess_asb_nettcp::auth::AuthError::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess_asb_nettcp::auth::AuthError
pub fn mxaccess_asb_nettcp::auth::AuthError::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_asb_nettcp::auth::AuthError
impl core::marker::Send for mxaccess_asb_nettcp::auth::AuthError
impl core::marker::Sync for mxaccess_asb_nettcp::auth::AuthError
impl core::marker::Unpin for mxaccess_asb_nettcp::auth::AuthError
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::auth::AuthError
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::auth::AuthError
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::auth::AuthError
#[non_exhaustive] pub enum mxaccess_asb_nettcp::NbfxError
pub mxaccess_asb_nettcp::NbfxError::IntOverflow
pub mxaccess_asb_nettcp::NbfxError::InvalidUtf8
pub mxaccess_asb_nettcp::NbfxError::InvalidUtf8::stage: &'static str
pub mxaccess_asb_nettcp::NbfxError::NegativeLength(i32)
pub mxaccess_asb_nettcp::NbfxError::PayloadTooLarge
pub mxaccess_asb_nettcp::NbfxError::PayloadTooLarge::len: usize
pub mxaccess_asb_nettcp::NbfxError::PayloadTooLarge::max: u64
pub mxaccess_asb_nettcp::NbfxError::Truncated
pub mxaccess_asb_nettcp::NbfxError::Truncated::have: usize
pub mxaccess_asb_nettcp::NbfxError::Truncated::need: usize
pub mxaccess_asb_nettcp::NbfxError::Truncated::stage: &'static str
pub mxaccess_asb_nettcp::NbfxError::UnknownDynamicDictionaryId(u32)
pub mxaccess_asb_nettcp::NbfxError::UnknownRecord(u8)
pub mxaccess_asb_nettcp::NbfxError::UnknownStaticDictionaryId(u32)
impl core::error::Error for mxaccess_asb_nettcp::nbfx::NbfxError
impl core::fmt::Debug for mxaccess_asb_nettcp::nbfx::NbfxError
pub fn mxaccess_asb_nettcp::nbfx::NbfxError::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess_asb_nettcp::nbfx::NbfxError
pub fn mxaccess_asb_nettcp::nbfx::NbfxError::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_asb_nettcp::nbfx::NbfxError
impl core::marker::Send for mxaccess_asb_nettcp::nbfx::NbfxError
impl core::marker::Sync for mxaccess_asb_nettcp::nbfx::NbfxError
impl core::marker::Unpin for mxaccess_asb_nettcp::nbfx::NbfxError
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nbfx::NbfxError
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nbfx::NbfxError
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nbfx::NbfxError
pub enum mxaccess_asb_nettcp::NbfxName
pub mxaccess_asb_nettcp::NbfxName::Dynamic(u32)
pub mxaccess_asb_nettcp::NbfxName::Inline(alloc::string::String)
pub mxaccess_asb_nettcp::NbfxName::Static(u32)
impl core::clone::Clone for mxaccess_asb_nettcp::nbfx::NbfxName
pub fn mxaccess_asb_nettcp::nbfx::NbfxName::clone(&self) -> mxaccess_asb_nettcp::nbfx::NbfxName
impl core::cmp::PartialEq for mxaccess_asb_nettcp::nbfx::NbfxName
pub fn mxaccess_asb_nettcp::nbfx::NbfxName::eq(&self, other: &mxaccess_asb_nettcp::nbfx::NbfxName) -> bool
impl core::fmt::Debug for mxaccess_asb_nettcp::nbfx::NbfxName
pub fn mxaccess_asb_nettcp::nbfx::NbfxName::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::StructuralPartialEq for mxaccess_asb_nettcp::nbfx::NbfxName
impl core::marker::Freeze for mxaccess_asb_nettcp::nbfx::NbfxName
impl core::marker::Send for mxaccess_asb_nettcp::nbfx::NbfxName
impl core::marker::Sync for mxaccess_asb_nettcp::nbfx::NbfxName
impl core::marker::Unpin for mxaccess_asb_nettcp::nbfx::NbfxName
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nbfx::NbfxName
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nbfx::NbfxName
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nbfx::NbfxName
pub enum mxaccess_asb_nettcp::NbfxText
pub mxaccess_asb_nettcp::NbfxText::Bool(bool)
pub mxaccess_asb_nettcp::NbfxText::Bytes(alloc::vec::Vec<u8>)
pub mxaccess_asb_nettcp::NbfxText::Chars(alloc::string::String)
pub mxaccess_asb_nettcp::NbfxText::DictionaryDynamic(u32)
pub mxaccess_asb_nettcp::NbfxText::DictionaryStatic(u32)
pub mxaccess_asb_nettcp::NbfxText::Empty
pub mxaccess_asb_nettcp::NbfxText::Int16(i16)
pub mxaccess_asb_nettcp::NbfxText::Int32(i32)
pub mxaccess_asb_nettcp::NbfxText::Int64(i64)
pub mxaccess_asb_nettcp::NbfxText::Int8(i8)
pub mxaccess_asb_nettcp::NbfxText::One
pub mxaccess_asb_nettcp::NbfxText::UniqueId([u8; 16])
pub mxaccess_asb_nettcp::NbfxText::Zero
impl mxaccess_asb_nettcp::nbfx::NbfxText
pub fn mxaccess_asb_nettcp::nbfx::NbfxText::resolve<'a>(&'a self, dynamic: &'a mxaccess_asb_nettcp::nbfx::DynamicDictionary) -> core::option::Option<alloc::string::String>
impl core::clone::Clone for mxaccess_asb_nettcp::nbfx::NbfxText
pub fn mxaccess_asb_nettcp::nbfx::NbfxText::clone(&self) -> mxaccess_asb_nettcp::nbfx::NbfxText
impl core::cmp::PartialEq for mxaccess_asb_nettcp::nbfx::NbfxText
pub fn mxaccess_asb_nettcp::nbfx::NbfxText::eq(&self, other: &mxaccess_asb_nettcp::nbfx::NbfxText) -> bool
impl core::fmt::Debug for mxaccess_asb_nettcp::nbfx::NbfxText
pub fn mxaccess_asb_nettcp::nbfx::NbfxText::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::StructuralPartialEq for mxaccess_asb_nettcp::nbfx::NbfxText
impl core::marker::Freeze for mxaccess_asb_nettcp::nbfx::NbfxText
impl core::marker::Send for mxaccess_asb_nettcp::nbfx::NbfxText
impl core::marker::Sync for mxaccess_asb_nettcp::nbfx::NbfxText
impl core::marker::Unpin for mxaccess_asb_nettcp::nbfx::NbfxText
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nbfx::NbfxText
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nbfx::NbfxText
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nbfx::NbfxText
pub enum mxaccess_asb_nettcp::NbfxToken
pub mxaccess_asb_nettcp::NbfxToken::Attribute
pub mxaccess_asb_nettcp::NbfxToken::Attribute::name: mxaccess_asb_nettcp::nbfx::NbfxName
pub mxaccess_asb_nettcp::NbfxToken::Attribute::prefix: core::option::Option<alloc::string::String>
pub mxaccess_asb_nettcp::NbfxToken::Attribute::value: mxaccess_asb_nettcp::nbfx::NbfxText
pub mxaccess_asb_nettcp::NbfxToken::DefaultNamespace
pub mxaccess_asb_nettcp::NbfxToken::DefaultNamespace::value: mxaccess_asb_nettcp::nbfx::NbfxText
pub mxaccess_asb_nettcp::NbfxToken::Element
pub mxaccess_asb_nettcp::NbfxToken::Element::name: mxaccess_asb_nettcp::nbfx::NbfxName
pub mxaccess_asb_nettcp::NbfxToken::Element::prefix: core::option::Option<alloc::string::String>
pub mxaccess_asb_nettcp::NbfxToken::EndElement
pub mxaccess_asb_nettcp::NbfxToken::NamespaceDeclaration
pub mxaccess_asb_nettcp::NbfxToken::NamespaceDeclaration::prefix: alloc::string::String
pub mxaccess_asb_nettcp::NbfxToken::NamespaceDeclaration::value: mxaccess_asb_nettcp::nbfx::NbfxText
pub mxaccess_asb_nettcp::NbfxToken::Text(mxaccess_asb_nettcp::nbfx::NbfxText)
impl core::clone::Clone for mxaccess_asb_nettcp::nbfx::NbfxToken
pub fn mxaccess_asb_nettcp::nbfx::NbfxToken::clone(&self) -> mxaccess_asb_nettcp::nbfx::NbfxToken
impl core::cmp::PartialEq for mxaccess_asb_nettcp::nbfx::NbfxToken
pub fn mxaccess_asb_nettcp::nbfx::NbfxToken::eq(&self, other: &mxaccess_asb_nettcp::nbfx::NbfxToken) -> bool
impl core::fmt::Debug for mxaccess_asb_nettcp::nbfx::NbfxToken
pub fn mxaccess_asb_nettcp::nbfx::NbfxToken::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::StructuralPartialEq for mxaccess_asb_nettcp::nbfx::NbfxToken
impl core::marker::Freeze for mxaccess_asb_nettcp::nbfx::NbfxToken
impl core::marker::Send for mxaccess_asb_nettcp::nbfx::NbfxToken
impl core::marker::Sync for mxaccess_asb_nettcp::nbfx::NbfxToken
impl core::marker::Unpin for mxaccess_asb_nettcp::nbfx::NbfxToken
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nbfx::NbfxToken
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nbfx::NbfxToken
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nbfx::NbfxToken
#[repr(u8)] pub enum mxaccess_asb_nettcp::NmfEncoding
pub mxaccess_asb_nettcp::NmfEncoding::Binary = 3
pub mxaccess_asb_nettcp::NmfEncoding::BinaryWithDictionary = 8
pub mxaccess_asb_nettcp::NmfEncoding::BinaryWithMtom = 4
pub mxaccess_asb_nettcp::NmfEncoding::Mtom = 7
pub mxaccess_asb_nettcp::NmfEncoding::Utf16LeSoapText = 2
pub mxaccess_asb_nettcp::NmfEncoding::Utf16SoapText = 1
pub mxaccess_asb_nettcp::NmfEncoding::Utf8SoapText = 0
impl mxaccess_asb_nettcp::nmf::NmfEncoding
pub fn mxaccess_asb_nettcp::nmf::NmfEncoding::from_u8(b: u8) -> core::option::Option<Self>
impl core::clone::Clone for mxaccess_asb_nettcp::nmf::NmfEncoding
pub fn mxaccess_asb_nettcp::nmf::NmfEncoding::clone(&self) -> mxaccess_asb_nettcp::nmf::NmfEncoding
impl core::cmp::Eq for mxaccess_asb_nettcp::nmf::NmfEncoding
impl core::cmp::PartialEq for mxaccess_asb_nettcp::nmf::NmfEncoding
pub fn mxaccess_asb_nettcp::nmf::NmfEncoding::eq(&self, other: &mxaccess_asb_nettcp::nmf::NmfEncoding) -> bool
impl core::fmt::Debug for mxaccess_asb_nettcp::nmf::NmfEncoding
pub fn mxaccess_asb_nettcp::nmf::NmfEncoding::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Copy for mxaccess_asb_nettcp::nmf::NmfEncoding
impl core::marker::StructuralPartialEq for mxaccess_asb_nettcp::nmf::NmfEncoding
impl core::marker::Freeze for mxaccess_asb_nettcp::nmf::NmfEncoding
impl core::marker::Send for mxaccess_asb_nettcp::nmf::NmfEncoding
impl core::marker::Sync for mxaccess_asb_nettcp::nmf::NmfEncoding
impl core::marker::Unpin for mxaccess_asb_nettcp::nmf::NmfEncoding
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nmf::NmfEncoding
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nmf::NmfEncoding
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nmf::NmfEncoding
#[non_exhaustive] pub enum mxaccess_asb_nettcp::NmfError
pub mxaccess_asb_nettcp::NmfError::IntOverflow
pub mxaccess_asb_nettcp::NmfError::InvalidUtf8
pub mxaccess_asb_nettcp::NmfError::InvalidUtf8::stage: &'static str
pub mxaccess_asb_nettcp::NmfError::NegativeLength(i32)
pub mxaccess_asb_nettcp::NmfError::PayloadTooLarge
pub mxaccess_asb_nettcp::NmfError::PayloadTooLarge::len: usize
pub mxaccess_asb_nettcp::NmfError::Truncated
pub mxaccess_asb_nettcp::NmfError::Truncated::have: usize
pub mxaccess_asb_nettcp::NmfError::Truncated::need: usize
pub mxaccess_asb_nettcp::NmfError::Truncated::stage: &'static str
pub mxaccess_asb_nettcp::NmfError::UnknownEncoding(u8)
pub mxaccess_asb_nettcp::NmfError::UnknownMode(u8)
pub mxaccess_asb_nettcp::NmfError::UnknownRecordType(u8)
impl core::error::Error for mxaccess_asb_nettcp::nmf::NmfError
impl core::fmt::Debug for mxaccess_asb_nettcp::nmf::NmfError
pub fn mxaccess_asb_nettcp::nmf::NmfError::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess_asb_nettcp::nmf::NmfError
pub fn mxaccess_asb_nettcp::nmf::NmfError::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_asb_nettcp::nmf::NmfError
impl core::marker::Send for mxaccess_asb_nettcp::nmf::NmfError
impl core::marker::Sync for mxaccess_asb_nettcp::nmf::NmfError
impl core::marker::Unpin for mxaccess_asb_nettcp::nmf::NmfError
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nmf::NmfError
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nmf::NmfError
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nmf::NmfError
#[repr(u8)] pub enum mxaccess_asb_nettcp::NmfMode
pub mxaccess_asb_nettcp::NmfMode::Duplex = 2
pub mxaccess_asb_nettcp::NmfMode::Simplex = 3
pub mxaccess_asb_nettcp::NmfMode::Singleton = 1
pub mxaccess_asb_nettcp::NmfMode::SingletonSized = 4
impl mxaccess_asb_nettcp::nmf::NmfMode
pub fn mxaccess_asb_nettcp::nmf::NmfMode::from_u8(b: u8) -> core::option::Option<Self>
impl core::clone::Clone for mxaccess_asb_nettcp::nmf::NmfMode
pub fn mxaccess_asb_nettcp::nmf::NmfMode::clone(&self) -> mxaccess_asb_nettcp::nmf::NmfMode
impl core::cmp::Eq for mxaccess_asb_nettcp::nmf::NmfMode
impl core::cmp::PartialEq for mxaccess_asb_nettcp::nmf::NmfMode
pub fn mxaccess_asb_nettcp::nmf::NmfMode::eq(&self, other: &mxaccess_asb_nettcp::nmf::NmfMode) -> bool
impl core::fmt::Debug for mxaccess_asb_nettcp::nmf::NmfMode
pub fn mxaccess_asb_nettcp::nmf::NmfMode::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Copy for mxaccess_asb_nettcp::nmf::NmfMode
impl core::marker::StructuralPartialEq for mxaccess_asb_nettcp::nmf::NmfMode
impl core::marker::Freeze for mxaccess_asb_nettcp::nmf::NmfMode
impl core::marker::Send for mxaccess_asb_nettcp::nmf::NmfMode
impl core::marker::Sync for mxaccess_asb_nettcp::nmf::NmfMode
impl core::marker::Unpin for mxaccess_asb_nettcp::nmf::NmfMode
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nmf::NmfMode
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nmf::NmfMode
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nmf::NmfMode
pub enum mxaccess_asb_nettcp::NmfRecord
pub mxaccess_asb_nettcp::NmfRecord::End
pub mxaccess_asb_nettcp::NmfRecord::ExtensibleEncoding(alloc::string::String)
pub mxaccess_asb_nettcp::NmfRecord::Fault(alloc::string::String)
pub mxaccess_asb_nettcp::NmfRecord::KnownEncoding(mxaccess_asb_nettcp::nmf::NmfEncoding)
pub mxaccess_asb_nettcp::NmfRecord::Mode(mxaccess_asb_nettcp::nmf::NmfMode)
pub mxaccess_asb_nettcp::NmfRecord::PreambleAck
pub mxaccess_asb_nettcp::NmfRecord::PreambleEnd
pub mxaccess_asb_nettcp::NmfRecord::SizedEnvelope(alloc::vec::Vec<u8>)
pub mxaccess_asb_nettcp::NmfRecord::UnsizedEnvelope(alloc::vec::Vec<u8>)
pub mxaccess_asb_nettcp::NmfRecord::UpgradeRequest(alloc::string::String)
pub mxaccess_asb_nettcp::NmfRecord::UpgradeResponse
pub mxaccess_asb_nettcp::NmfRecord::Version
pub mxaccess_asb_nettcp::NmfRecord::Version::major: u8
pub mxaccess_asb_nettcp::NmfRecord::Version::minor: u8
pub mxaccess_asb_nettcp::NmfRecord::Via(alloc::string::String)
impl mxaccess_asb_nettcp::nmf::NmfRecord
pub fn mxaccess_asb_nettcp::nmf::NmfRecord::decode(input: &[u8]) -> core::result::Result<(Self, usize), mxaccess_asb_nettcp::nmf::NmfError>
pub fn mxaccess_asb_nettcp::nmf::NmfRecord::encode(&self) -> core::result::Result<alloc::vec::Vec<u8>, mxaccess_asb_nettcp::nmf::NmfError>
pub fn mxaccess_asb_nettcp::nmf::NmfRecord::encode_into(&self, out: &mut alloc::vec::Vec<u8>) -> core::result::Result<(), mxaccess_asb_nettcp::nmf::NmfError>
impl core::clone::Clone for mxaccess_asb_nettcp::nmf::NmfRecord
pub fn mxaccess_asb_nettcp::nmf::NmfRecord::clone(&self) -> mxaccess_asb_nettcp::nmf::NmfRecord
impl core::cmp::Eq for mxaccess_asb_nettcp::nmf::NmfRecord
impl core::cmp::PartialEq for mxaccess_asb_nettcp::nmf::NmfRecord
pub fn mxaccess_asb_nettcp::nmf::NmfRecord::eq(&self, other: &mxaccess_asb_nettcp::nmf::NmfRecord) -> bool
impl core::fmt::Debug for mxaccess_asb_nettcp::nmf::NmfRecord
pub fn mxaccess_asb_nettcp::nmf::NmfRecord::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::StructuralPartialEq for mxaccess_asb_nettcp::nmf::NmfRecord
impl core::marker::Freeze for mxaccess_asb_nettcp::nmf::NmfRecord
impl core::marker::Send for mxaccess_asb_nettcp::nmf::NmfRecord
impl core::marker::Sync for mxaccess_asb_nettcp::nmf::NmfRecord
impl core::marker::Unpin for mxaccess_asb_nettcp::nmf::NmfRecord
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nmf::NmfRecord
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nmf::NmfRecord
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nmf::NmfRecord
#[repr(u8)] pub enum mxaccess_asb_nettcp::NmfRecordType
pub mxaccess_asb_nettcp::NmfRecordType::End = 7
pub mxaccess_asb_nettcp::NmfRecordType::ExtensibleEncoding = 4
pub mxaccess_asb_nettcp::NmfRecordType::Fault = 8
pub mxaccess_asb_nettcp::NmfRecordType::KnownEncoding = 3
pub mxaccess_asb_nettcp::NmfRecordType::Mode = 1
pub mxaccess_asb_nettcp::NmfRecordType::PreambleAck = 11
pub mxaccess_asb_nettcp::NmfRecordType::PreambleEnd = 12
pub mxaccess_asb_nettcp::NmfRecordType::SizedEnvelope = 6
pub mxaccess_asb_nettcp::NmfRecordType::UnsizedEnvelope = 5
pub mxaccess_asb_nettcp::NmfRecordType::UpgradeRequest = 9
pub mxaccess_asb_nettcp::NmfRecordType::UpgradeResponse = 10
pub mxaccess_asb_nettcp::NmfRecordType::Version = 0
pub mxaccess_asb_nettcp::NmfRecordType::Via = 2
impl mxaccess_asb_nettcp::nmf::NmfRecordType
pub fn mxaccess_asb_nettcp::nmf::NmfRecordType::from_u8(b: u8) -> core::option::Option<Self>
impl core::clone::Clone for mxaccess_asb_nettcp::nmf::NmfRecordType
pub fn mxaccess_asb_nettcp::nmf::NmfRecordType::clone(&self) -> mxaccess_asb_nettcp::nmf::NmfRecordType
impl core::cmp::Eq for mxaccess_asb_nettcp::nmf::NmfRecordType
impl core::cmp::PartialEq for mxaccess_asb_nettcp::nmf::NmfRecordType
pub fn mxaccess_asb_nettcp::nmf::NmfRecordType::eq(&self, other: &mxaccess_asb_nettcp::nmf::NmfRecordType) -> bool
impl core::fmt::Debug for mxaccess_asb_nettcp::nmf::NmfRecordType
pub fn mxaccess_asb_nettcp::nmf::NmfRecordType::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Copy for mxaccess_asb_nettcp::nmf::NmfRecordType
impl core::marker::StructuralPartialEq for mxaccess_asb_nettcp::nmf::NmfRecordType
impl core::marker::Freeze for mxaccess_asb_nettcp::nmf::NmfRecordType
impl core::marker::Send for mxaccess_asb_nettcp::nmf::NmfRecordType
impl core::marker::Sync for mxaccess_asb_nettcp::nmf::NmfRecordType
impl core::marker::Unpin for mxaccess_asb_nettcp::nmf::NmfRecordType
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nmf::NmfRecordType
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nmf::NmfRecordType
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nmf::NmfRecordType
pub struct mxaccess_asb_nettcp::DynamicDictionary
impl mxaccess_asb_nettcp::nbfx::DynamicDictionary
pub fn mxaccess_asb_nettcp::nbfx::DynamicDictionary::intern(&mut self, value: &str) -> u32
pub fn mxaccess_asb_nettcp::nbfx::DynamicDictionary::is_empty(&self) -> bool
pub fn mxaccess_asb_nettcp::nbfx::DynamicDictionary::len(&self) -> usize
pub fn mxaccess_asb_nettcp::nbfx::DynamicDictionary::lookup(&self, id: u32) -> core::option::Option<&str>
pub fn mxaccess_asb_nettcp::nbfx::DynamicDictionary::new() -> Self
pub fn mxaccess_asb_nettcp::nbfx::DynamicDictionary::position_of(&self, value: &str) -> core::option::Option<u32>
impl core::clone::Clone for mxaccess_asb_nettcp::nbfx::DynamicDictionary
pub fn mxaccess_asb_nettcp::nbfx::DynamicDictionary::clone(&self) -> mxaccess_asb_nettcp::nbfx::DynamicDictionary
impl core::default::Default for mxaccess_asb_nettcp::nbfx::DynamicDictionary
pub fn mxaccess_asb_nettcp::nbfx::DynamicDictionary::default() -> mxaccess_asb_nettcp::nbfx::DynamicDictionary
impl core::fmt::Debug for mxaccess_asb_nettcp::nbfx::DynamicDictionary
pub fn mxaccess_asb_nettcp::nbfx::DynamicDictionary::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_asb_nettcp::nbfx::DynamicDictionary
impl core::marker::Send for mxaccess_asb_nettcp::nbfx::DynamicDictionary
impl core::marker::Sync for mxaccess_asb_nettcp::nbfx::DynamicDictionary
impl core::marker::Unpin for mxaccess_asb_nettcp::nbfx::DynamicDictionary
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nbfx::DynamicDictionary
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nbfx::DynamicDictionary
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nbfx::DynamicDictionary
pub struct mxaccess_asb_nettcp::StaticEntry
pub mxaccess_asb_nettcp::StaticEntry::id: u32
pub mxaccess_asb_nettcp::StaticEntry::value: &'static str
impl core::clone::Clone for mxaccess_asb_nettcp::nbfs::StaticEntry
pub fn mxaccess_asb_nettcp::nbfs::StaticEntry::clone(&self) -> mxaccess_asb_nettcp::nbfs::StaticEntry
impl core::fmt::Debug for mxaccess_asb_nettcp::nbfs::StaticEntry
pub fn mxaccess_asb_nettcp::nbfs::StaticEntry::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Copy for mxaccess_asb_nettcp::nbfs::StaticEntry
impl core::marker::Freeze for mxaccess_asb_nettcp::nbfs::StaticEntry
impl core::marker::Send for mxaccess_asb_nettcp::nbfs::StaticEntry
impl core::marker::Sync for mxaccess_asb_nettcp::nbfs::StaticEntry
impl core::marker::Unpin for mxaccess_asb_nettcp::nbfs::StaticEntry
impl core::marker::UnsafeUnpin for mxaccess_asb_nettcp::nbfs::StaticEntry
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_asb_nettcp::nbfs::StaticEntry
impl core::panic::unwind_safe::UnwindSafe for mxaccess_asb_nettcp::nbfs::StaticEntry
pub fn mxaccess_asb_nettcp::decode_tokens(input: &[u8], _dynamic: &mut mxaccess_asb_nettcp::nbfx::DynamicDictionary) -> core::result::Result<(alloc::vec::Vec<mxaccess_asb_nettcp::nbfx::NbfxToken>, usize), mxaccess_asb_nettcp::nbfx::NbfxError>
pub fn mxaccess_asb_nettcp::encode_tokens(tokens: &[mxaccess_asb_nettcp::nbfx::NbfxToken], dynamic: &mut mxaccess_asb_nettcp::nbfx::DynamicDictionary, out: &mut alloc::vec::Vec<u8>) -> core::result::Result<(), mxaccess_asb_nettcp::nbfx::NbfxError>
pub fn mxaccess_asb_nettcp::lookup_static(id: u32) -> core::option::Option<&'static str>
pub fn mxaccess_asb_nettcp::position_of_static(value: &str) -> core::option::Option<u32>
File diff suppressed because it is too large Load Diff
+170
View File
@@ -0,0 +1,170 @@
pub mod mxaccess_callback
pub mod mxaccess_callback::exporter
pub enum mxaccess_callback::exporter::CallbackEvent
pub mxaccess_callback::exporter::CallbackEvent::AcceptError
pub mxaccess_callback::exporter::CallbackEvent::AcceptError::reason: alloc::string::String
pub mxaccess_callback::exporter::CallbackEvent::Auth3Ignored
pub mxaccess_callback::exporter::CallbackEvent::Bind
pub mxaccess_callback::exporter::CallbackEvent::Bind::context_id: u16
pub mxaccess_callback::exporter::CallbackEvent::Bind::iid: mxaccess_rpc::guid::Guid
pub mxaccess_callback::exporter::CallbackEvent::CallbackInvoked
pub mxaccess_callback::exporter::CallbackEvent::CallbackInvoked::body: alloc::vec::Vec<u8>
pub mxaccess_callback::exporter::CallbackEvent::CallbackInvoked::opnum: u16
pub mxaccess_callback::exporter::CallbackEvent::ClientConnected
pub mxaccess_callback::exporter::CallbackEvent::ClientConnected::remote: core::net::socket_addr::SocketAddr
pub mxaccess_callback::exporter::CallbackEvent::ClientDisconnected
pub mxaccess_callback::exporter::CallbackEvent::ProtocolError
pub mxaccess_callback::exporter::CallbackEvent::ProtocolError::reason: alloc::string::String
pub mxaccess_callback::exporter::CallbackEvent::RemQueryInterface
pub mxaccess_callback::exporter::CallbackEvent::RemQueryInterface::hresult: i32
pub mxaccess_callback::exporter::CallbackEvent::RemQueryInterface::requested_iid: mxaccess_rpc::guid::Guid
pub mxaccess_callback::exporter::CallbackEvent::Request
pub mxaccess_callback::exporter::CallbackEvent::Request::context_id: u16
pub mxaccess_callback::exporter::CallbackEvent::Request::iid: mxaccess_rpc::guid::Guid
pub mxaccess_callback::exporter::CallbackEvent::Request::opnum: u16
pub mxaccess_callback::exporter::CallbackEvent::Request::stub_len: usize
pub mxaccess_callback::exporter::CallbackEvent::UnhandledRequest
pub mxaccess_callback::exporter::CallbackEvent::UnhandledRequest::iid: mxaccess_rpc::guid::Guid
pub mxaccess_callback::exporter::CallbackEvent::UnhandledRequest::opnum: u16
impl core::clone::Clone for mxaccess_callback::exporter::CallbackEvent
pub fn mxaccess_callback::exporter::CallbackEvent::clone(&self) -> mxaccess_callback::exporter::CallbackEvent
impl core::cmp::Eq for mxaccess_callback::exporter::CallbackEvent
impl core::cmp::PartialEq for mxaccess_callback::exporter::CallbackEvent
pub fn mxaccess_callback::exporter::CallbackEvent::eq(&self, other: &mxaccess_callback::exporter::CallbackEvent) -> bool
impl core::fmt::Debug for mxaccess_callback::exporter::CallbackEvent
pub fn mxaccess_callback::exporter::CallbackEvent::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::StructuralPartialEq for mxaccess_callback::exporter::CallbackEvent
impl core::marker::Freeze for mxaccess_callback::exporter::CallbackEvent
impl core::marker::Send for mxaccess_callback::exporter::CallbackEvent
impl core::marker::Sync for mxaccess_callback::exporter::CallbackEvent
impl core::marker::Unpin for mxaccess_callback::exporter::CallbackEvent
impl core::marker::UnsafeUnpin for mxaccess_callback::exporter::CallbackEvent
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_callback::exporter::CallbackEvent
impl core::panic::unwind_safe::UnwindSafe for mxaccess_callback::exporter::CallbackEvent
pub struct mxaccess_callback::exporter::CallbackExporter
impl mxaccess_callback::exporter::CallbackExporter
pub async fn mxaccess_callback::exporter::CallbackExporter::bind(addr: core::net::socket_addr::SocketAddr, identities: mxaccess_callback::exporter::ExporterIdentities) -> std::io::error::Result<(Self, tokio::sync::mpsc::unbounded::UnboundedReceiver<mxaccess_callback::exporter::CallbackEvent>)>
pub fn mxaccess_callback::exporter::CallbackExporter::create_callback_objref(&self, hostname: &str) -> alloc::vec::Vec<u8>
pub fn mxaccess_callback::exporter::CallbackExporter::identities(&self) -> mxaccess_callback::exporter::ExporterIdentities
pub fn mxaccess_callback::exporter::CallbackExporter::local_addr(&self) -> core::net::socket_addr::SocketAddr
pub async fn mxaccess_callback::exporter::CallbackExporter::shutdown(self)
impl core::ops::drop::Drop for mxaccess_callback::exporter::CallbackExporter
pub fn mxaccess_callback::exporter::CallbackExporter::drop(&mut self)
impl core::marker::Freeze for mxaccess_callback::exporter::CallbackExporter
impl core::marker::Send for mxaccess_callback::exporter::CallbackExporter
impl core::marker::Sync for mxaccess_callback::exporter::CallbackExporter
impl core::marker::Unpin for mxaccess_callback::exporter::CallbackExporter
impl core::marker::UnsafeUnpin for mxaccess_callback::exporter::CallbackExporter
impl !core::panic::unwind_safe::RefUnwindSafe for mxaccess_callback::exporter::CallbackExporter
impl !core::panic::unwind_safe::UnwindSafe for mxaccess_callback::exporter::CallbackExporter
pub struct mxaccess_callback::exporter::ExporterIdentities
pub mxaccess_callback::exporter::ExporterIdentities::callback_ipid: mxaccess_rpc::guid::Guid
pub mxaccess_callback::exporter::ExporterIdentities::oid: u64
pub mxaccess_callback::exporter::ExporterIdentities::oxid: u64
pub mxaccess_callback::exporter::ExporterIdentities::rem_unknown_ipid: mxaccess_rpc::guid::Guid
impl mxaccess_callback::exporter::ExporterIdentities
pub const fn mxaccess_callback::exporter::ExporterIdentities::fixed(oxid: u64, oid: u64, callback_ipid: mxaccess_rpc::guid::Guid, rem_unknown_ipid: mxaccess_rpc::guid::Guid) -> Self
pub fn mxaccess_callback::exporter::ExporterIdentities::random() -> Self
impl core::clone::Clone for mxaccess_callback::exporter::ExporterIdentities
pub fn mxaccess_callback::exporter::ExporterIdentities::clone(&self) -> mxaccess_callback::exporter::ExporterIdentities
impl core::cmp::Eq for mxaccess_callback::exporter::ExporterIdentities
impl core::cmp::PartialEq for mxaccess_callback::exporter::ExporterIdentities
pub fn mxaccess_callback::exporter::ExporterIdentities::eq(&self, other: &mxaccess_callback::exporter::ExporterIdentities) -> bool
impl core::fmt::Debug for mxaccess_callback::exporter::ExporterIdentities
pub fn mxaccess_callback::exporter::ExporterIdentities::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::hash::Hash for mxaccess_callback::exporter::ExporterIdentities
pub fn mxaccess_callback::exporter::ExporterIdentities::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
impl core::marker::Copy for mxaccess_callback::exporter::ExporterIdentities
impl core::marker::StructuralPartialEq for mxaccess_callback::exporter::ExporterIdentities
impl core::marker::Freeze for mxaccess_callback::exporter::ExporterIdentities
impl core::marker::Send for mxaccess_callback::exporter::ExporterIdentities
impl core::marker::Sync for mxaccess_callback::exporter::ExporterIdentities
impl core::marker::Unpin for mxaccess_callback::exporter::ExporterIdentities
impl core::marker::UnsafeUnpin for mxaccess_callback::exporter::ExporterIdentities
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_callback::exporter::ExporterIdentities
impl core::panic::unwind_safe::UnwindSafe for mxaccess_callback::exporter::ExporterIdentities
pub const mxaccess_callback::exporter::IUNKNOWN_IID: mxaccess_rpc::guid::Guid
pub enum mxaccess_callback::CallbackEvent
pub mxaccess_callback::CallbackEvent::AcceptError
pub mxaccess_callback::CallbackEvent::AcceptError::reason: alloc::string::String
pub mxaccess_callback::CallbackEvent::Auth3Ignored
pub mxaccess_callback::CallbackEvent::Bind
pub mxaccess_callback::CallbackEvent::Bind::context_id: u16
pub mxaccess_callback::CallbackEvent::Bind::iid: mxaccess_rpc::guid::Guid
pub mxaccess_callback::CallbackEvent::CallbackInvoked
pub mxaccess_callback::CallbackEvent::CallbackInvoked::body: alloc::vec::Vec<u8>
pub mxaccess_callback::CallbackEvent::CallbackInvoked::opnum: u16
pub mxaccess_callback::CallbackEvent::ClientConnected
pub mxaccess_callback::CallbackEvent::ClientConnected::remote: core::net::socket_addr::SocketAddr
pub mxaccess_callback::CallbackEvent::ClientDisconnected
pub mxaccess_callback::CallbackEvent::ProtocolError
pub mxaccess_callback::CallbackEvent::ProtocolError::reason: alloc::string::String
pub mxaccess_callback::CallbackEvent::RemQueryInterface
pub mxaccess_callback::CallbackEvent::RemQueryInterface::hresult: i32
pub mxaccess_callback::CallbackEvent::RemQueryInterface::requested_iid: mxaccess_rpc::guid::Guid
pub mxaccess_callback::CallbackEvent::Request
pub mxaccess_callback::CallbackEvent::Request::context_id: u16
pub mxaccess_callback::CallbackEvent::Request::iid: mxaccess_rpc::guid::Guid
pub mxaccess_callback::CallbackEvent::Request::opnum: u16
pub mxaccess_callback::CallbackEvent::Request::stub_len: usize
pub mxaccess_callback::CallbackEvent::UnhandledRequest
pub mxaccess_callback::CallbackEvent::UnhandledRequest::iid: mxaccess_rpc::guid::Guid
pub mxaccess_callback::CallbackEvent::UnhandledRequest::opnum: u16
impl core::clone::Clone for mxaccess_callback::exporter::CallbackEvent
pub fn mxaccess_callback::exporter::CallbackEvent::clone(&self) -> mxaccess_callback::exporter::CallbackEvent
impl core::cmp::Eq for mxaccess_callback::exporter::CallbackEvent
impl core::cmp::PartialEq for mxaccess_callback::exporter::CallbackEvent
pub fn mxaccess_callback::exporter::CallbackEvent::eq(&self, other: &mxaccess_callback::exporter::CallbackEvent) -> bool
impl core::fmt::Debug for mxaccess_callback::exporter::CallbackEvent
pub fn mxaccess_callback::exporter::CallbackEvent::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::StructuralPartialEq for mxaccess_callback::exporter::CallbackEvent
impl core::marker::Freeze for mxaccess_callback::exporter::CallbackEvent
impl core::marker::Send for mxaccess_callback::exporter::CallbackEvent
impl core::marker::Sync for mxaccess_callback::exporter::CallbackEvent
impl core::marker::Unpin for mxaccess_callback::exporter::CallbackEvent
impl core::marker::UnsafeUnpin for mxaccess_callback::exporter::CallbackEvent
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_callback::exporter::CallbackEvent
impl core::panic::unwind_safe::UnwindSafe for mxaccess_callback::exporter::CallbackEvent
pub struct mxaccess_callback::CallbackExporter
impl mxaccess_callback::exporter::CallbackExporter
pub async fn mxaccess_callback::exporter::CallbackExporter::bind(addr: core::net::socket_addr::SocketAddr, identities: mxaccess_callback::exporter::ExporterIdentities) -> std::io::error::Result<(Self, tokio::sync::mpsc::unbounded::UnboundedReceiver<mxaccess_callback::exporter::CallbackEvent>)>
pub fn mxaccess_callback::exporter::CallbackExporter::create_callback_objref(&self, hostname: &str) -> alloc::vec::Vec<u8>
pub fn mxaccess_callback::exporter::CallbackExporter::identities(&self) -> mxaccess_callback::exporter::ExporterIdentities
pub fn mxaccess_callback::exporter::CallbackExporter::local_addr(&self) -> core::net::socket_addr::SocketAddr
pub async fn mxaccess_callback::exporter::CallbackExporter::shutdown(self)
impl core::ops::drop::Drop for mxaccess_callback::exporter::CallbackExporter
pub fn mxaccess_callback::exporter::CallbackExporter::drop(&mut self)
impl core::marker::Freeze for mxaccess_callback::exporter::CallbackExporter
impl core::marker::Send for mxaccess_callback::exporter::CallbackExporter
impl core::marker::Sync for mxaccess_callback::exporter::CallbackExporter
impl core::marker::Unpin for mxaccess_callback::exporter::CallbackExporter
impl core::marker::UnsafeUnpin for mxaccess_callback::exporter::CallbackExporter
impl !core::panic::unwind_safe::RefUnwindSafe for mxaccess_callback::exporter::CallbackExporter
impl !core::panic::unwind_safe::UnwindSafe for mxaccess_callback::exporter::CallbackExporter
pub struct mxaccess_callback::ExporterIdentities
pub mxaccess_callback::ExporterIdentities::callback_ipid: mxaccess_rpc::guid::Guid
pub mxaccess_callback::ExporterIdentities::oid: u64
pub mxaccess_callback::ExporterIdentities::oxid: u64
pub mxaccess_callback::ExporterIdentities::rem_unknown_ipid: mxaccess_rpc::guid::Guid
impl mxaccess_callback::exporter::ExporterIdentities
pub const fn mxaccess_callback::exporter::ExporterIdentities::fixed(oxid: u64, oid: u64, callback_ipid: mxaccess_rpc::guid::Guid, rem_unknown_ipid: mxaccess_rpc::guid::Guid) -> Self
pub fn mxaccess_callback::exporter::ExporterIdentities::random() -> Self
impl core::clone::Clone for mxaccess_callback::exporter::ExporterIdentities
pub fn mxaccess_callback::exporter::ExporterIdentities::clone(&self) -> mxaccess_callback::exporter::ExporterIdentities
impl core::cmp::Eq for mxaccess_callback::exporter::ExporterIdentities
impl core::cmp::PartialEq for mxaccess_callback::exporter::ExporterIdentities
pub fn mxaccess_callback::exporter::ExporterIdentities::eq(&self, other: &mxaccess_callback::exporter::ExporterIdentities) -> bool
impl core::fmt::Debug for mxaccess_callback::exporter::ExporterIdentities
pub fn mxaccess_callback::exporter::ExporterIdentities::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::hash::Hash for mxaccess_callback::exporter::ExporterIdentities
pub fn mxaccess_callback::exporter::ExporterIdentities::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
impl core::marker::Copy for mxaccess_callback::exporter::ExporterIdentities
impl core::marker::StructuralPartialEq for mxaccess_callback::exporter::ExporterIdentities
impl core::marker::Freeze for mxaccess_callback::exporter::ExporterIdentities
impl core::marker::Send for mxaccess_callback::exporter::ExporterIdentities
impl core::marker::Sync for mxaccess_callback::exporter::ExporterIdentities
impl core::marker::Unpin for mxaccess_callback::exporter::ExporterIdentities
impl core::marker::UnsafeUnpin for mxaccess_callback::exporter::ExporterIdentities
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_callback::exporter::ExporterIdentities
impl core::panic::unwind_safe::UnwindSafe for mxaccess_callback::exporter::ExporterIdentities
pub const mxaccess_callback::IUNKNOWN_IID: mxaccess_rpc::guid::Guid
File diff suppressed because it is too large Load Diff
+123
View File
@@ -0,0 +1,123 @@
pub mod mxaccess_compat
pub struct mxaccess_compat::BufferedDataChangeEvent
pub mxaccess_compat::BufferedDataChangeEvent::is_during_recovery: bool
pub mxaccess_compat::BufferedDataChangeEvent::item_handle: i32
pub mxaccess_compat::BufferedDataChangeEvent::mx_data_type: i16
pub mxaccess_compat::BufferedDataChangeEvent::qualities: alloc::vec::Vec<u16>
pub mxaccess_compat::BufferedDataChangeEvent::server_handle: i32
pub mxaccess_compat::BufferedDataChangeEvent::statuses: alloc::vec::Vec<mxaccess_codec::status::MxStatus>
pub mxaccess_compat::BufferedDataChangeEvent::timestamps: alloc::vec::Vec<std::time::SystemTime>
pub mxaccess_compat::BufferedDataChangeEvent::values: alloc::vec::Vec<mxaccess_codec::value::MxValue>
impl core::clone::Clone for mxaccess_compat::BufferedDataChangeEvent
pub fn mxaccess_compat::BufferedDataChangeEvent::clone(&self) -> mxaccess_compat::BufferedDataChangeEvent
impl core::fmt::Debug for mxaccess_compat::BufferedDataChangeEvent
pub fn mxaccess_compat::BufferedDataChangeEvent::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_compat::BufferedDataChangeEvent
impl core::marker::Send for mxaccess_compat::BufferedDataChangeEvent
impl core::marker::Sync for mxaccess_compat::BufferedDataChangeEvent
impl core::marker::Unpin for mxaccess_compat::BufferedDataChangeEvent
impl core::marker::UnsafeUnpin for mxaccess_compat::BufferedDataChangeEvent
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_compat::BufferedDataChangeEvent
impl core::panic::unwind_safe::UnwindSafe for mxaccess_compat::BufferedDataChangeEvent
pub struct mxaccess_compat::DataChangeEvent
pub mxaccess_compat::DataChangeEvent::is_during_recovery: bool
pub mxaccess_compat::DataChangeEvent::item_handle: i32
pub mxaccess_compat::DataChangeEvent::quality: u16
pub mxaccess_compat::DataChangeEvent::server_handle: i32
pub mxaccess_compat::DataChangeEvent::status: mxaccess_codec::status::MxStatus
pub mxaccess_compat::DataChangeEvent::timestamp: std::time::SystemTime
pub mxaccess_compat::DataChangeEvent::value: mxaccess_codec::value::MxValue
impl core::clone::Clone for mxaccess_compat::DataChangeEvent
pub fn mxaccess_compat::DataChangeEvent::clone(&self) -> mxaccess_compat::DataChangeEvent
impl core::fmt::Debug for mxaccess_compat::DataChangeEvent
pub fn mxaccess_compat::DataChangeEvent::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_compat::DataChangeEvent
impl core::marker::Send for mxaccess_compat::DataChangeEvent
impl core::marker::Sync for mxaccess_compat::DataChangeEvent
impl core::marker::Unpin for mxaccess_compat::DataChangeEvent
impl core::marker::UnsafeUnpin for mxaccess_compat::DataChangeEvent
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_compat::DataChangeEvent
impl core::panic::unwind_safe::UnwindSafe for mxaccess_compat::DataChangeEvent
pub struct mxaccess_compat::EventStream<T: core::clone::Clone + core::marker::Send + core::marker::Unpin + 'static>
impl<T: core::clone::Clone + core::marker::Send + core::marker::Unpin + 'static> futures_core::stream::Stream for mxaccess_compat::EventStream<T>
pub type mxaccess_compat::EventStream<T>::Item = T
pub fn mxaccess_compat::EventStream<T>::poll_next(self: core::pin::Pin<&mut Self>, cx: &mut core::task::wake::Context<'_>) -> core::task::poll::Poll<core::option::Option<Self::Item>>
impl<T> core::marker::Freeze for mxaccess_compat::EventStream<T>
impl<T> core::marker::Send for mxaccess_compat::EventStream<T>
impl<T> core::marker::Sync for mxaccess_compat::EventStream<T> where T: core::marker::Sync
impl<T> core::marker::Unpin for mxaccess_compat::EventStream<T>
impl<T> core::marker::UnsafeUnpin for mxaccess_compat::EventStream<T>
impl<T> !core::panic::unwind_safe::RefUnwindSafe for mxaccess_compat::EventStream<T>
impl<T> !core::panic::unwind_safe::UnwindSafe for mxaccess_compat::EventStream<T>
pub struct mxaccess_compat::LmxClient
impl mxaccess_compat::LmxClient
pub async fn mxaccess_compat::LmxClient::activate(&self, h_server: i32, h_item: i32) -> core::result::Result<mxaccess_codec::status::MxStatus, mxaccess::Error>
pub async fn mxaccess_compat::LmxClient::add_buffered_item(&self, h_server: i32, item_def: &str, context: &str) -> core::result::Result<i32, mxaccess::Error>
pub async fn mxaccess_compat::LmxClient::add_item(&self, h_server: i32, item_def: &str) -> core::result::Result<i32, mxaccess::Error>
pub async fn mxaccess_compat::LmxClient::add_item_2(&self, h_server: i32, item_def: &str, context: &str) -> core::result::Result<i32, mxaccess::Error>
pub async fn mxaccess_compat::LmxClient::advise(&self, h_server: i32, h_item: i32) -> core::result::Result<(), mxaccess::Error>
pub async fn mxaccess_compat::LmxClient::advise_supervisory(&self, h_server: i32, h_item: i32) -> core::result::Result<(), mxaccess::Error>
pub async fn mxaccess_compat::LmxClient::archestra_user_to_id(&self, h_server: i32, user_guid: &str) -> core::result::Result<i32, mxaccess::Error>
pub async fn mxaccess_compat::LmxClient::authenticate_user(&self, h_server: i32, _user: &str, _password: &str) -> core::result::Result<i32, mxaccess::Error>
pub fn mxaccess_compat::LmxClient::buffered_update_interval_ms(&self) -> i32
pub async fn mxaccess_compat::LmxClient::is_advised(&self, h_item: i32) -> bool
pub async fn mxaccess_compat::LmxClient::item_count(&self) -> usize
pub fn mxaccess_compat::LmxClient::on_buffered_data_change(&self) -> mxaccess_compat::EventStream<mxaccess_compat::BufferedDataChangeEvent>
pub fn mxaccess_compat::LmxClient::on_data_change(&self) -> mxaccess_compat::EventStream<mxaccess_compat::DataChangeEvent>
pub fn mxaccess_compat::LmxClient::on_operation_complete(&self) -> mxaccess_compat::EventStream<mxaccess_compat::OperationCompleteEvent>
pub fn mxaccess_compat::LmxClient::on_write_complete(&self) -> mxaccess_compat::EventStream<mxaccess_compat::WriteCompleteEvent>
pub fn mxaccess_compat::LmxClient::register(_client_name: &str, session: mxaccess::Session) -> Self
pub fn mxaccess_compat::LmxClient::register_asb(_client_name: &str, session: mxaccess::asb_session::AsbSession) -> Self
pub async fn mxaccess_compat::LmxClient::remove_item(&self, h_server: i32, h_item: i32) -> core::result::Result<(), mxaccess::Error>
pub fn mxaccess_compat::LmxClient::server_handle(&self) -> i32
pub async fn mxaccess_compat::LmxClient::set_buffered_update_interval(&self, h_server: i32, interval_ms: i32) -> core::result::Result<(), mxaccess::Error>
pub async fn mxaccess_compat::LmxClient::suspend(&self, h_server: i32, h_item: i32) -> core::result::Result<mxaccess_codec::status::MxStatus, mxaccess::Error>
pub async fn mxaccess_compat::LmxClient::un_advise(&self, h_server: i32, h_item: i32) -> core::result::Result<(), mxaccess::Error>
pub async fn mxaccess_compat::LmxClient::unregister(&self, h_server: i32) -> core::result::Result<(), mxaccess::Error>
pub async fn mxaccess_compat::LmxClient::write(&self, h_server: i32, h_item: i32, value: mxaccess_codec::value::MxValue, _user_id: i32) -> core::result::Result<(), mxaccess::Error>
pub async fn mxaccess_compat::LmxClient::write_2(&self, h_server: i32, h_item: i32, value: mxaccess_codec::value::MxValue, timestamp: std::time::SystemTime, _user_id: i32) -> core::result::Result<(), mxaccess::Error>
pub async fn mxaccess_compat::LmxClient::write_secured(&self, h_server: i32, h_item: i32, current_user_id: i32, verifier_user_id: i32, value: mxaccess_codec::value::MxValue) -> core::result::Result<(), mxaccess::Error>
pub async fn mxaccess_compat::LmxClient::write_secured_2(&self, h_server: i32, h_item: i32, current_user_id: i32, verifier_user_id: i32, value: mxaccess_codec::value::MxValue, timestamp: std::time::SystemTime) -> core::result::Result<(), mxaccess::Error>
impl core::clone::Clone for mxaccess_compat::LmxClient
pub fn mxaccess_compat::LmxClient::clone(&self) -> Self
impl core::fmt::Debug for mxaccess_compat::LmxClient
pub fn mxaccess_compat::LmxClient::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_compat::LmxClient
impl core::marker::Send for mxaccess_compat::LmxClient
impl core::marker::Sync for mxaccess_compat::LmxClient
impl core::marker::Unpin for mxaccess_compat::LmxClient
impl core::marker::UnsafeUnpin for mxaccess_compat::LmxClient
impl !core::panic::unwind_safe::RefUnwindSafe for mxaccess_compat::LmxClient
impl !core::panic::unwind_safe::UnwindSafe for mxaccess_compat::LmxClient
pub struct mxaccess_compat::OperationCompleteEvent
pub mxaccess_compat::OperationCompleteEvent::is_during_recovery: bool
pub mxaccess_compat::OperationCompleteEvent::item_handle: i32
pub mxaccess_compat::OperationCompleteEvent::server_handle: i32
pub mxaccess_compat::OperationCompleteEvent::statuses: alloc::vec::Vec<mxaccess_codec::status::MxStatus>
impl core::clone::Clone for mxaccess_compat::OperationCompleteEvent
pub fn mxaccess_compat::OperationCompleteEvent::clone(&self) -> mxaccess_compat::OperationCompleteEvent
impl core::fmt::Debug for mxaccess_compat::OperationCompleteEvent
pub fn mxaccess_compat::OperationCompleteEvent::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_compat::OperationCompleteEvent
impl core::marker::Send for mxaccess_compat::OperationCompleteEvent
impl core::marker::Sync for mxaccess_compat::OperationCompleteEvent
impl core::marker::Unpin for mxaccess_compat::OperationCompleteEvent
impl core::marker::UnsafeUnpin for mxaccess_compat::OperationCompleteEvent
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_compat::OperationCompleteEvent
impl core::panic::unwind_safe::UnwindSafe for mxaccess_compat::OperationCompleteEvent
pub struct mxaccess_compat::WriteCompleteEvent
pub mxaccess_compat::WriteCompleteEvent::is_during_recovery: bool
pub mxaccess_compat::WriteCompleteEvent::item_handle: i32
pub mxaccess_compat::WriteCompleteEvent::server_handle: i32
pub mxaccess_compat::WriteCompleteEvent::statuses: alloc::vec::Vec<mxaccess_codec::status::MxStatus>
impl core::clone::Clone for mxaccess_compat::WriteCompleteEvent
pub fn mxaccess_compat::WriteCompleteEvent::clone(&self) -> mxaccess_compat::WriteCompleteEvent
impl core::fmt::Debug for mxaccess_compat::WriteCompleteEvent
pub fn mxaccess_compat::WriteCompleteEvent::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_compat::WriteCompleteEvent
impl core::marker::Send for mxaccess_compat::WriteCompleteEvent
impl core::marker::Sync for mxaccess_compat::WriteCompleteEvent
impl core::marker::Unpin for mxaccess_compat::WriteCompleteEvent
impl core::marker::UnsafeUnpin for mxaccess_compat::WriteCompleteEvent
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_compat::WriteCompleteEvent
impl core::panic::unwind_safe::UnwindSafe for mxaccess_compat::WriteCompleteEvent
+374
View File
@@ -0,0 +1,374 @@
pub mod mxaccess_galaxy
pub mod mxaccess_galaxy::metadata
pub struct mxaccess_galaxy::metadata::GalaxyTagMetadata
pub mxaccess_galaxy::metadata::GalaxyTagMetadata::attribute_id: i16
pub mxaccess_galaxy::metadata::GalaxyTagMetadata::attribute_name: alloc::string::String
pub mxaccess_galaxy::metadata::GalaxyTagMetadata::attribute_source: alloc::string::String
pub mxaccess_galaxy::metadata::GalaxyTagMetadata::engine_id: u16
pub mxaccess_galaxy::metadata::GalaxyTagMetadata::is_array: bool
pub mxaccess_galaxy::metadata::GalaxyTagMetadata::mx_data_type: i16
pub mxaccess_galaxy::metadata::GalaxyTagMetadata::object_id: u16
pub mxaccess_galaxy::metadata::GalaxyTagMetadata::object_tag_name: alloc::string::String
pub mxaccess_galaxy::metadata::GalaxyTagMetadata::platform_id: u16
pub mxaccess_galaxy::metadata::GalaxyTagMetadata::primitive_id: i16
pub mxaccess_galaxy::metadata::GalaxyTagMetadata::primitive_name: core::option::Option<alloc::string::String>
pub mxaccess_galaxy::metadata::GalaxyTagMetadata::property_id: i16
pub mxaccess_galaxy::metadata::GalaxyTagMetadata::security_classification: i16
impl mxaccess_galaxy::metadata::GalaxyTagMetadata
pub const mxaccess_galaxy::metadata::GalaxyTagMetadata::BUFFER_PROPERTY_ID: i16
pub const mxaccess_galaxy::metadata::GalaxyTagMetadata::VALUE_PROPERTY_ID: i16
pub const fn mxaccess_galaxy::metadata::GalaxyTagMetadata::is_buffer_property(&self) -> bool
pub fn mxaccess_galaxy::metadata::GalaxyTagMetadata::is_writable(&self) -> bool
pub fn mxaccess_galaxy::metadata::GalaxyTagMetadata::resolve_write_kind(&self) -> core::result::Result<mxaccess_codec::value::MxValueKind, mxaccess_galaxy::metadata::UnsupportedDataType>
pub fn mxaccess_galaxy::metadata::GalaxyTagMetadata::to_reference_handle(&self, galaxy_id: u8) -> core::result::Result<mxaccess_codec::reference_handle::MxReferenceHandle, mxaccess_codec::error::CodecError>
impl core::clone::Clone for mxaccess_galaxy::metadata::GalaxyTagMetadata
pub fn mxaccess_galaxy::metadata::GalaxyTagMetadata::clone(&self) -> mxaccess_galaxy::metadata::GalaxyTagMetadata
impl core::cmp::Eq for mxaccess_galaxy::metadata::GalaxyTagMetadata
impl core::cmp::PartialEq for mxaccess_galaxy::metadata::GalaxyTagMetadata
pub fn mxaccess_galaxy::metadata::GalaxyTagMetadata::eq(&self, other: &mxaccess_galaxy::metadata::GalaxyTagMetadata) -> bool
impl core::fmt::Debug for mxaccess_galaxy::metadata::GalaxyTagMetadata
pub fn mxaccess_galaxy::metadata::GalaxyTagMetadata::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::hash::Hash for mxaccess_galaxy::metadata::GalaxyTagMetadata
pub fn mxaccess_galaxy::metadata::GalaxyTagMetadata::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
impl core::marker::StructuralPartialEq for mxaccess_galaxy::metadata::GalaxyTagMetadata
impl core::marker::Freeze for mxaccess_galaxy::metadata::GalaxyTagMetadata
impl core::marker::Send for mxaccess_galaxy::metadata::GalaxyTagMetadata
impl core::marker::Sync for mxaccess_galaxy::metadata::GalaxyTagMetadata
impl core::marker::Unpin for mxaccess_galaxy::metadata::GalaxyTagMetadata
impl core::marker::UnsafeUnpin for mxaccess_galaxy::metadata::GalaxyTagMetadata
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_galaxy::metadata::GalaxyTagMetadata
impl core::panic::unwind_safe::UnwindSafe for mxaccess_galaxy::metadata::GalaxyTagMetadata
pub struct mxaccess_galaxy::metadata::UnsupportedDataType
pub mxaccess_galaxy::metadata::UnsupportedDataType::is_array: bool
pub mxaccess_galaxy::metadata::UnsupportedDataType::mx_data_type: i16
impl core::clone::Clone for mxaccess_galaxy::metadata::UnsupportedDataType
pub fn mxaccess_galaxy::metadata::UnsupportedDataType::clone(&self) -> mxaccess_galaxy::metadata::UnsupportedDataType
impl core::cmp::Eq for mxaccess_galaxy::metadata::UnsupportedDataType
impl core::cmp::PartialEq for mxaccess_galaxy::metadata::UnsupportedDataType
pub fn mxaccess_galaxy::metadata::UnsupportedDataType::eq(&self, other: &mxaccess_galaxy::metadata::UnsupportedDataType) -> bool
impl core::error::Error for mxaccess_galaxy::metadata::UnsupportedDataType
impl core::fmt::Debug for mxaccess_galaxy::metadata::UnsupportedDataType
pub fn mxaccess_galaxy::metadata::UnsupportedDataType::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess_galaxy::metadata::UnsupportedDataType
pub fn mxaccess_galaxy::metadata::UnsupportedDataType::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Copy for mxaccess_galaxy::metadata::UnsupportedDataType
impl core::marker::StructuralPartialEq for mxaccess_galaxy::metadata::UnsupportedDataType
impl core::marker::Freeze for mxaccess_galaxy::metadata::UnsupportedDataType
impl core::marker::Send for mxaccess_galaxy::metadata::UnsupportedDataType
impl core::marker::Sync for mxaccess_galaxy::metadata::UnsupportedDataType
impl core::marker::Unpin for mxaccess_galaxy::metadata::UnsupportedDataType
impl core::marker::UnsafeUnpin for mxaccess_galaxy::metadata::UnsupportedDataType
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_galaxy::metadata::UnsupportedDataType
impl core::panic::unwind_safe::UnwindSafe for mxaccess_galaxy::metadata::UnsupportedDataType
pub mod mxaccess_galaxy::parser
#[non_exhaustive] pub enum mxaccess_galaxy::parser::ParseError
pub mxaccess_galaxy::parser::ParseError::Empty
pub mxaccess_galaxy::parser::ParseError::EmptyBaseBeforePropertySuffix
pub mxaccess_galaxy::parser::ParseError::InvalidShape
impl core::clone::Clone for mxaccess_galaxy::parser::ParseError
pub fn mxaccess_galaxy::parser::ParseError::clone(&self) -> mxaccess_galaxy::parser::ParseError
impl core::cmp::Eq for mxaccess_galaxy::parser::ParseError
impl core::cmp::PartialEq for mxaccess_galaxy::parser::ParseError
pub fn mxaccess_galaxy::parser::ParseError::eq(&self, other: &mxaccess_galaxy::parser::ParseError) -> bool
impl core::convert::From<mxaccess_galaxy::parser::ParseError> for mxaccess_galaxy::resolver::ResolverError
pub fn mxaccess_galaxy::resolver::ResolverError::from(source: mxaccess_galaxy::parser::ParseError) -> Self
impl core::error::Error for mxaccess_galaxy::parser::ParseError
impl core::fmt::Debug for mxaccess_galaxy::parser::ParseError
pub fn mxaccess_galaxy::parser::ParseError::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess_galaxy::parser::ParseError
pub fn mxaccess_galaxy::parser::ParseError::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::StructuralPartialEq for mxaccess_galaxy::parser::ParseError
impl core::marker::Freeze for mxaccess_galaxy::parser::ParseError
impl core::marker::Send for mxaccess_galaxy::parser::ParseError
impl core::marker::Sync for mxaccess_galaxy::parser::ParseError
impl core::marker::Unpin for mxaccess_galaxy::parser::ParseError
impl core::marker::UnsafeUnpin for mxaccess_galaxy::parser::ParseError
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_galaxy::parser::ParseError
impl core::panic::unwind_safe::UnwindSafe for mxaccess_galaxy::parser::ParseError
pub struct mxaccess_galaxy::parser::ParsedTagReference
pub mxaccess_galaxy::parser::ParsedTagReference::attribute_name: alloc::string::String
pub mxaccess_galaxy::parser::ParsedTagReference::object_tag_name: alloc::string::String
pub mxaccess_galaxy::parser::ParsedTagReference::primitive_name: core::option::Option<alloc::string::String>
pub mxaccess_galaxy::parser::ParsedTagReference::property_id_override: core::option::Option<i16>
impl mxaccess_galaxy::parser::ParsedTagReference
pub fn mxaccess_galaxy::parser::ParsedTagReference::apply_overrides(&self, metadata: mxaccess_galaxy::metadata::GalaxyTagMetadata) -> mxaccess_galaxy::metadata::GalaxyTagMetadata
pub fn mxaccess_galaxy::parser::ParsedTagReference::parse_candidates(tag_reference: &str) -> core::result::Result<alloc::vec::Vec<Self>, mxaccess_galaxy::parser::ParseError>
impl core::clone::Clone for mxaccess_galaxy::parser::ParsedTagReference
pub fn mxaccess_galaxy::parser::ParsedTagReference::clone(&self) -> mxaccess_galaxy::parser::ParsedTagReference
impl core::cmp::Eq for mxaccess_galaxy::parser::ParsedTagReference
impl core::cmp::PartialEq for mxaccess_galaxy::parser::ParsedTagReference
pub fn mxaccess_galaxy::parser::ParsedTagReference::eq(&self, other: &mxaccess_galaxy::parser::ParsedTagReference) -> bool
impl core::fmt::Debug for mxaccess_galaxy::parser::ParsedTagReference
pub fn mxaccess_galaxy::parser::ParsedTagReference::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::hash::Hash for mxaccess_galaxy::parser::ParsedTagReference
pub fn mxaccess_galaxy::parser::ParsedTagReference::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
impl core::marker::StructuralPartialEq for mxaccess_galaxy::parser::ParsedTagReference
impl core::marker::Freeze for mxaccess_galaxy::parser::ParsedTagReference
impl core::marker::Send for mxaccess_galaxy::parser::ParsedTagReference
impl core::marker::Sync for mxaccess_galaxy::parser::ParsedTagReference
impl core::marker::Unpin for mxaccess_galaxy::parser::ParsedTagReference
impl core::marker::UnsafeUnpin for mxaccess_galaxy::parser::ParsedTagReference
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_galaxy::parser::ParsedTagReference
impl core::panic::unwind_safe::UnwindSafe for mxaccess_galaxy::parser::ParsedTagReference
pub mod mxaccess_galaxy::resolver
#[non_exhaustive] pub enum mxaccess_galaxy::resolver::ResolverError
pub mxaccess_galaxy::resolver::ResolverError::Backend
pub mxaccess_galaxy::resolver::ResolverError::Backend::message: alloc::string::String
pub mxaccess_galaxy::resolver::ResolverError::InvalidTagReference(mxaccess_galaxy::parser::ParseError)
pub mxaccess_galaxy::resolver::ResolverError::NotFound
pub mxaccess_galaxy::resolver::ResolverError::NotFound::tag_reference: alloc::string::String
impl core::convert::From<mxaccess_galaxy::parser::ParseError> for mxaccess_galaxy::resolver::ResolverError
pub fn mxaccess_galaxy::resolver::ResolverError::from(source: mxaccess_galaxy::parser::ParseError) -> Self
impl core::error::Error for mxaccess_galaxy::resolver::ResolverError
pub fn mxaccess_galaxy::resolver::ResolverError::source(&self) -> core::option::Option<&(dyn core::error::Error + 'static)>
impl core::fmt::Debug for mxaccess_galaxy::resolver::ResolverError
pub fn mxaccess_galaxy::resolver::ResolverError::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess_galaxy::resolver::ResolverError
pub fn mxaccess_galaxy::resolver::ResolverError::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_galaxy::resolver::ResolverError
impl core::marker::Send for mxaccess_galaxy::resolver::ResolverError
impl core::marker::Sync for mxaccess_galaxy::resolver::ResolverError
impl core::marker::Unpin for mxaccess_galaxy::resolver::ResolverError
impl core::marker::UnsafeUnpin for mxaccess_galaxy::resolver::ResolverError
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_galaxy::resolver::ResolverError
impl core::panic::unwind_safe::UnwindSafe for mxaccess_galaxy::resolver::ResolverError
pub trait mxaccess_galaxy::resolver::Resolver: core::marker::Send + core::marker::Sync
pub fn mxaccess_galaxy::resolver::Resolver::browse<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, object_tag_like: &'life1 str, attribute_like: &'life2 str, max_rows: usize) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = core::result::Result<alloc::vec::Vec<mxaccess_galaxy::metadata::GalaxyTagMetadata>, mxaccess_galaxy::resolver::ResolverError>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait
pub fn mxaccess_galaxy::resolver::Resolver::resolve<'life0, 'life1, 'async_trait>(&'life0 self, tag_reference: &'life1 str) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = core::result::Result<mxaccess_galaxy::metadata::GalaxyTagMetadata, mxaccess_galaxy::resolver::ResolverError>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait
pub mod mxaccess_galaxy::role_blob
pub fn mxaccess_galaxy::role_blob::parse_role_blob(roles_text: &str) -> alloc::vec::Vec<alloc::string::String>
pub mod mxaccess_galaxy::sql
pub const mxaccess_galaxy::sql::BROWSE_SQL: &str
pub const mxaccess_galaxy::sql::RESOLVE_SQL: &str
pub const mxaccess_galaxy::sql::USER_BY_GUID_SQL: &str
pub const mxaccess_galaxy::sql::USER_BY_NAME_SQL: &str
pub const mxaccess_galaxy::sql::USER_SELECT_SQL: &str
pub mod mxaccess_galaxy::user
#[non_exhaustive] pub enum mxaccess_galaxy::user::UserResolverError
pub mxaccess_galaxy::user::UserResolverError::Backend
pub mxaccess_galaxy::user::UserResolverError::Backend::message: alloc::string::String
pub mxaccess_galaxy::user::UserResolverError::NotFound
pub mxaccess_galaxy::user::UserResolverError::NotFound::key: alloc::string::String
impl core::error::Error for mxaccess_galaxy::user::UserResolverError
impl core::fmt::Debug for mxaccess_galaxy::user::UserResolverError
pub fn mxaccess_galaxy::user::UserResolverError::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess_galaxy::user::UserResolverError
pub fn mxaccess_galaxy::user::UserResolverError::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_galaxy::user::UserResolverError
impl core::marker::Send for mxaccess_galaxy::user::UserResolverError
impl core::marker::Sync for mxaccess_galaxy::user::UserResolverError
impl core::marker::Unpin for mxaccess_galaxy::user::UserResolverError
impl core::marker::UnsafeUnpin for mxaccess_galaxy::user::UserResolverError
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_galaxy::user::UserResolverError
impl core::panic::unwind_safe::UnwindSafe for mxaccess_galaxy::user::UserResolverError
pub struct mxaccess_galaxy::user::GalaxyUserProfile
pub mxaccess_galaxy::user::GalaxyUserProfile::default_security_group: alloc::string::String
pub mxaccess_galaxy::user::GalaxyUserProfile::intouch_access_level: core::option::Option<i32>
pub mxaccess_galaxy::user::GalaxyUserProfile::roles: alloc::vec::Vec<alloc::string::String>
pub mxaccess_galaxy::user::GalaxyUserProfile::user_guid: uuid::Uuid
pub mxaccess_galaxy::user::GalaxyUserProfile::user_profile_id: i32
pub mxaccess_galaxy::user::GalaxyUserProfile::user_profile_name: alloc::string::String
impl mxaccess_galaxy::user::GalaxyUserProfile
pub fn mxaccess_galaxy::user::GalaxyUserProfile::from_columns(user_profile_id: i32, user_profile_name: alloc::string::String, user_guid: uuid::Uuid, default_security_group: alloc::string::String, intouch_access_level: core::option::Option<i32>, roles_text: core::option::Option<&str>) -> Self
impl core::clone::Clone for mxaccess_galaxy::user::GalaxyUserProfile
pub fn mxaccess_galaxy::user::GalaxyUserProfile::clone(&self) -> mxaccess_galaxy::user::GalaxyUserProfile
impl core::cmp::Eq for mxaccess_galaxy::user::GalaxyUserProfile
impl core::cmp::PartialEq for mxaccess_galaxy::user::GalaxyUserProfile
pub fn mxaccess_galaxy::user::GalaxyUserProfile::eq(&self, other: &mxaccess_galaxy::user::GalaxyUserProfile) -> bool
impl core::fmt::Debug for mxaccess_galaxy::user::GalaxyUserProfile
pub fn mxaccess_galaxy::user::GalaxyUserProfile::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::hash::Hash for mxaccess_galaxy::user::GalaxyUserProfile
pub fn mxaccess_galaxy::user::GalaxyUserProfile::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
impl core::marker::StructuralPartialEq for mxaccess_galaxy::user::GalaxyUserProfile
impl core::marker::Freeze for mxaccess_galaxy::user::GalaxyUserProfile
impl core::marker::Send for mxaccess_galaxy::user::GalaxyUserProfile
impl core::marker::Sync for mxaccess_galaxy::user::GalaxyUserProfile
impl core::marker::Unpin for mxaccess_galaxy::user::GalaxyUserProfile
impl core::marker::UnsafeUnpin for mxaccess_galaxy::user::GalaxyUserProfile
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_galaxy::user::GalaxyUserProfile
impl core::panic::unwind_safe::UnwindSafe for mxaccess_galaxy::user::GalaxyUserProfile
pub trait mxaccess_galaxy::user::UserResolver: core::marker::Send + core::marker::Sync
pub fn mxaccess_galaxy::user::UserResolver::resolve_by_guid<'life0, 'async_trait>(&'life0 self, user_guid: uuid::Uuid) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = core::result::Result<mxaccess_galaxy::user::GalaxyUserProfile, mxaccess_galaxy::user::UserResolverError>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait
pub fn mxaccess_galaxy::user::UserResolver::resolve_by_name<'life0, 'life1, 'async_trait>(&'life0 self, user_name: &'life1 str) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = core::result::Result<mxaccess_galaxy::user::GalaxyUserProfile, mxaccess_galaxy::user::UserResolverError>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait
pub fn mxaccess_galaxy::user::UserResolver::resolve_user_profile_id_by_guid<'life0, 'async_trait>(&'life0 self, user_guid: uuid::Uuid) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = core::result::Result<i32, mxaccess_galaxy::user::UserResolverError>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait
#[non_exhaustive] pub enum mxaccess_galaxy::ParseError
pub mxaccess_galaxy::ParseError::Empty
pub mxaccess_galaxy::ParseError::EmptyBaseBeforePropertySuffix
pub mxaccess_galaxy::ParseError::InvalidShape
impl core::clone::Clone for mxaccess_galaxy::parser::ParseError
pub fn mxaccess_galaxy::parser::ParseError::clone(&self) -> mxaccess_galaxy::parser::ParseError
impl core::cmp::Eq for mxaccess_galaxy::parser::ParseError
impl core::cmp::PartialEq for mxaccess_galaxy::parser::ParseError
pub fn mxaccess_galaxy::parser::ParseError::eq(&self, other: &mxaccess_galaxy::parser::ParseError) -> bool
impl core::convert::From<mxaccess_galaxy::parser::ParseError> for mxaccess_galaxy::resolver::ResolverError
pub fn mxaccess_galaxy::resolver::ResolverError::from(source: mxaccess_galaxy::parser::ParseError) -> Self
impl core::error::Error for mxaccess_galaxy::parser::ParseError
impl core::fmt::Debug for mxaccess_galaxy::parser::ParseError
pub fn mxaccess_galaxy::parser::ParseError::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess_galaxy::parser::ParseError
pub fn mxaccess_galaxy::parser::ParseError::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::StructuralPartialEq for mxaccess_galaxy::parser::ParseError
impl core::marker::Freeze for mxaccess_galaxy::parser::ParseError
impl core::marker::Send for mxaccess_galaxy::parser::ParseError
impl core::marker::Sync for mxaccess_galaxy::parser::ParseError
impl core::marker::Unpin for mxaccess_galaxy::parser::ParseError
impl core::marker::UnsafeUnpin for mxaccess_galaxy::parser::ParseError
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_galaxy::parser::ParseError
impl core::panic::unwind_safe::UnwindSafe for mxaccess_galaxy::parser::ParseError
#[non_exhaustive] pub enum mxaccess_galaxy::ResolverError
pub mxaccess_galaxy::ResolverError::Backend
pub mxaccess_galaxy::ResolverError::Backend::message: alloc::string::String
pub mxaccess_galaxy::ResolverError::InvalidTagReference(mxaccess_galaxy::parser::ParseError)
pub mxaccess_galaxy::ResolverError::NotFound
pub mxaccess_galaxy::ResolverError::NotFound::tag_reference: alloc::string::String
impl core::convert::From<mxaccess_galaxy::parser::ParseError> for mxaccess_galaxy::resolver::ResolverError
pub fn mxaccess_galaxy::resolver::ResolverError::from(source: mxaccess_galaxy::parser::ParseError) -> Self
impl core::error::Error for mxaccess_galaxy::resolver::ResolverError
pub fn mxaccess_galaxy::resolver::ResolverError::source(&self) -> core::option::Option<&(dyn core::error::Error + 'static)>
impl core::fmt::Debug for mxaccess_galaxy::resolver::ResolverError
pub fn mxaccess_galaxy::resolver::ResolverError::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess_galaxy::resolver::ResolverError
pub fn mxaccess_galaxy::resolver::ResolverError::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_galaxy::resolver::ResolverError
impl core::marker::Send for mxaccess_galaxy::resolver::ResolverError
impl core::marker::Sync for mxaccess_galaxy::resolver::ResolverError
impl core::marker::Unpin for mxaccess_galaxy::resolver::ResolverError
impl core::marker::UnsafeUnpin for mxaccess_galaxy::resolver::ResolverError
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_galaxy::resolver::ResolverError
impl core::panic::unwind_safe::UnwindSafe for mxaccess_galaxy::resolver::ResolverError
#[non_exhaustive] pub enum mxaccess_galaxy::UserResolverError
pub mxaccess_galaxy::UserResolverError::Backend
pub mxaccess_galaxy::UserResolverError::Backend::message: alloc::string::String
pub mxaccess_galaxy::UserResolverError::NotFound
pub mxaccess_galaxy::UserResolverError::NotFound::key: alloc::string::String
impl core::error::Error for mxaccess_galaxy::user::UserResolverError
impl core::fmt::Debug for mxaccess_galaxy::user::UserResolverError
pub fn mxaccess_galaxy::user::UserResolverError::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess_galaxy::user::UserResolverError
pub fn mxaccess_galaxy::user::UserResolverError::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_galaxy::user::UserResolverError
impl core::marker::Send for mxaccess_galaxy::user::UserResolverError
impl core::marker::Sync for mxaccess_galaxy::user::UserResolverError
impl core::marker::Unpin for mxaccess_galaxy::user::UserResolverError
impl core::marker::UnsafeUnpin for mxaccess_galaxy::user::UserResolverError
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_galaxy::user::UserResolverError
impl core::panic::unwind_safe::UnwindSafe for mxaccess_galaxy::user::UserResolverError
pub struct mxaccess_galaxy::GalaxyTagMetadata
pub mxaccess_galaxy::GalaxyTagMetadata::attribute_id: i16
pub mxaccess_galaxy::GalaxyTagMetadata::attribute_name: alloc::string::String
pub mxaccess_galaxy::GalaxyTagMetadata::attribute_source: alloc::string::String
pub mxaccess_galaxy::GalaxyTagMetadata::engine_id: u16
pub mxaccess_galaxy::GalaxyTagMetadata::is_array: bool
pub mxaccess_galaxy::GalaxyTagMetadata::mx_data_type: i16
pub mxaccess_galaxy::GalaxyTagMetadata::object_id: u16
pub mxaccess_galaxy::GalaxyTagMetadata::object_tag_name: alloc::string::String
pub mxaccess_galaxy::GalaxyTagMetadata::platform_id: u16
pub mxaccess_galaxy::GalaxyTagMetadata::primitive_id: i16
pub mxaccess_galaxy::GalaxyTagMetadata::primitive_name: core::option::Option<alloc::string::String>
pub mxaccess_galaxy::GalaxyTagMetadata::property_id: i16
pub mxaccess_galaxy::GalaxyTagMetadata::security_classification: i16
impl mxaccess_galaxy::metadata::GalaxyTagMetadata
pub const mxaccess_galaxy::metadata::GalaxyTagMetadata::BUFFER_PROPERTY_ID: i16
pub const mxaccess_galaxy::metadata::GalaxyTagMetadata::VALUE_PROPERTY_ID: i16
pub const fn mxaccess_galaxy::metadata::GalaxyTagMetadata::is_buffer_property(&self) -> bool
pub fn mxaccess_galaxy::metadata::GalaxyTagMetadata::is_writable(&self) -> bool
pub fn mxaccess_galaxy::metadata::GalaxyTagMetadata::resolve_write_kind(&self) -> core::result::Result<mxaccess_codec::value::MxValueKind, mxaccess_galaxy::metadata::UnsupportedDataType>
pub fn mxaccess_galaxy::metadata::GalaxyTagMetadata::to_reference_handle(&self, galaxy_id: u8) -> core::result::Result<mxaccess_codec::reference_handle::MxReferenceHandle, mxaccess_codec::error::CodecError>
impl core::clone::Clone for mxaccess_galaxy::metadata::GalaxyTagMetadata
pub fn mxaccess_galaxy::metadata::GalaxyTagMetadata::clone(&self) -> mxaccess_galaxy::metadata::GalaxyTagMetadata
impl core::cmp::Eq for mxaccess_galaxy::metadata::GalaxyTagMetadata
impl core::cmp::PartialEq for mxaccess_galaxy::metadata::GalaxyTagMetadata
pub fn mxaccess_galaxy::metadata::GalaxyTagMetadata::eq(&self, other: &mxaccess_galaxy::metadata::GalaxyTagMetadata) -> bool
impl core::fmt::Debug for mxaccess_galaxy::metadata::GalaxyTagMetadata
pub fn mxaccess_galaxy::metadata::GalaxyTagMetadata::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::hash::Hash for mxaccess_galaxy::metadata::GalaxyTagMetadata
pub fn mxaccess_galaxy::metadata::GalaxyTagMetadata::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
impl core::marker::StructuralPartialEq for mxaccess_galaxy::metadata::GalaxyTagMetadata
impl core::marker::Freeze for mxaccess_galaxy::metadata::GalaxyTagMetadata
impl core::marker::Send for mxaccess_galaxy::metadata::GalaxyTagMetadata
impl core::marker::Sync for mxaccess_galaxy::metadata::GalaxyTagMetadata
impl core::marker::Unpin for mxaccess_galaxy::metadata::GalaxyTagMetadata
impl core::marker::UnsafeUnpin for mxaccess_galaxy::metadata::GalaxyTagMetadata
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_galaxy::metadata::GalaxyTagMetadata
impl core::panic::unwind_safe::UnwindSafe for mxaccess_galaxy::metadata::GalaxyTagMetadata
pub struct mxaccess_galaxy::GalaxyUserProfile
pub mxaccess_galaxy::GalaxyUserProfile::default_security_group: alloc::string::String
pub mxaccess_galaxy::GalaxyUserProfile::intouch_access_level: core::option::Option<i32>
pub mxaccess_galaxy::GalaxyUserProfile::roles: alloc::vec::Vec<alloc::string::String>
pub mxaccess_galaxy::GalaxyUserProfile::user_guid: uuid::Uuid
pub mxaccess_galaxy::GalaxyUserProfile::user_profile_id: i32
pub mxaccess_galaxy::GalaxyUserProfile::user_profile_name: alloc::string::String
impl mxaccess_galaxy::user::GalaxyUserProfile
pub fn mxaccess_galaxy::user::GalaxyUserProfile::from_columns(user_profile_id: i32, user_profile_name: alloc::string::String, user_guid: uuid::Uuid, default_security_group: alloc::string::String, intouch_access_level: core::option::Option<i32>, roles_text: core::option::Option<&str>) -> Self
impl core::clone::Clone for mxaccess_galaxy::user::GalaxyUserProfile
pub fn mxaccess_galaxy::user::GalaxyUserProfile::clone(&self) -> mxaccess_galaxy::user::GalaxyUserProfile
impl core::cmp::Eq for mxaccess_galaxy::user::GalaxyUserProfile
impl core::cmp::PartialEq for mxaccess_galaxy::user::GalaxyUserProfile
pub fn mxaccess_galaxy::user::GalaxyUserProfile::eq(&self, other: &mxaccess_galaxy::user::GalaxyUserProfile) -> bool
impl core::fmt::Debug for mxaccess_galaxy::user::GalaxyUserProfile
pub fn mxaccess_galaxy::user::GalaxyUserProfile::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::hash::Hash for mxaccess_galaxy::user::GalaxyUserProfile
pub fn mxaccess_galaxy::user::GalaxyUserProfile::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
impl core::marker::StructuralPartialEq for mxaccess_galaxy::user::GalaxyUserProfile
impl core::marker::Freeze for mxaccess_galaxy::user::GalaxyUserProfile
impl core::marker::Send for mxaccess_galaxy::user::GalaxyUserProfile
impl core::marker::Sync for mxaccess_galaxy::user::GalaxyUserProfile
impl core::marker::Unpin for mxaccess_galaxy::user::GalaxyUserProfile
impl core::marker::UnsafeUnpin for mxaccess_galaxy::user::GalaxyUserProfile
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_galaxy::user::GalaxyUserProfile
impl core::panic::unwind_safe::UnwindSafe for mxaccess_galaxy::user::GalaxyUserProfile
pub struct mxaccess_galaxy::ParsedTagReference
pub mxaccess_galaxy::ParsedTagReference::attribute_name: alloc::string::String
pub mxaccess_galaxy::ParsedTagReference::object_tag_name: alloc::string::String
pub mxaccess_galaxy::ParsedTagReference::primitive_name: core::option::Option<alloc::string::String>
pub mxaccess_galaxy::ParsedTagReference::property_id_override: core::option::Option<i16>
impl mxaccess_galaxy::parser::ParsedTagReference
pub fn mxaccess_galaxy::parser::ParsedTagReference::apply_overrides(&self, metadata: mxaccess_galaxy::metadata::GalaxyTagMetadata) -> mxaccess_galaxy::metadata::GalaxyTagMetadata
pub fn mxaccess_galaxy::parser::ParsedTagReference::parse_candidates(tag_reference: &str) -> core::result::Result<alloc::vec::Vec<Self>, mxaccess_galaxy::parser::ParseError>
impl core::clone::Clone for mxaccess_galaxy::parser::ParsedTagReference
pub fn mxaccess_galaxy::parser::ParsedTagReference::clone(&self) -> mxaccess_galaxy::parser::ParsedTagReference
impl core::cmp::Eq for mxaccess_galaxy::parser::ParsedTagReference
impl core::cmp::PartialEq for mxaccess_galaxy::parser::ParsedTagReference
pub fn mxaccess_galaxy::parser::ParsedTagReference::eq(&self, other: &mxaccess_galaxy::parser::ParsedTagReference) -> bool
impl core::fmt::Debug for mxaccess_galaxy::parser::ParsedTagReference
pub fn mxaccess_galaxy::parser::ParsedTagReference::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::hash::Hash for mxaccess_galaxy::parser::ParsedTagReference
pub fn mxaccess_galaxy::parser::ParsedTagReference::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
impl core::marker::StructuralPartialEq for mxaccess_galaxy::parser::ParsedTagReference
impl core::marker::Freeze for mxaccess_galaxy::parser::ParsedTagReference
impl core::marker::Send for mxaccess_galaxy::parser::ParsedTagReference
impl core::marker::Sync for mxaccess_galaxy::parser::ParsedTagReference
impl core::marker::Unpin for mxaccess_galaxy::parser::ParsedTagReference
impl core::marker::UnsafeUnpin for mxaccess_galaxy::parser::ParsedTagReference
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_galaxy::parser::ParsedTagReference
impl core::panic::unwind_safe::UnwindSafe for mxaccess_galaxy::parser::ParsedTagReference
pub struct mxaccess_galaxy::UnsupportedDataType
pub mxaccess_galaxy::UnsupportedDataType::is_array: bool
pub mxaccess_galaxy::UnsupportedDataType::mx_data_type: i16
impl core::clone::Clone for mxaccess_galaxy::metadata::UnsupportedDataType
pub fn mxaccess_galaxy::metadata::UnsupportedDataType::clone(&self) -> mxaccess_galaxy::metadata::UnsupportedDataType
impl core::cmp::Eq for mxaccess_galaxy::metadata::UnsupportedDataType
impl core::cmp::PartialEq for mxaccess_galaxy::metadata::UnsupportedDataType
pub fn mxaccess_galaxy::metadata::UnsupportedDataType::eq(&self, other: &mxaccess_galaxy::metadata::UnsupportedDataType) -> bool
impl core::error::Error for mxaccess_galaxy::metadata::UnsupportedDataType
impl core::fmt::Debug for mxaccess_galaxy::metadata::UnsupportedDataType
pub fn mxaccess_galaxy::metadata::UnsupportedDataType::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess_galaxy::metadata::UnsupportedDataType
pub fn mxaccess_galaxy::metadata::UnsupportedDataType::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Copy for mxaccess_galaxy::metadata::UnsupportedDataType
impl core::marker::StructuralPartialEq for mxaccess_galaxy::metadata::UnsupportedDataType
impl core::marker::Freeze for mxaccess_galaxy::metadata::UnsupportedDataType
impl core::marker::Send for mxaccess_galaxy::metadata::UnsupportedDataType
impl core::marker::Sync for mxaccess_galaxy::metadata::UnsupportedDataType
impl core::marker::Unpin for mxaccess_galaxy::metadata::UnsupportedDataType
impl core::marker::UnsafeUnpin for mxaccess_galaxy::metadata::UnsupportedDataType
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_galaxy::metadata::UnsupportedDataType
impl core::panic::unwind_safe::UnwindSafe for mxaccess_galaxy::metadata::UnsupportedDataType
pub trait mxaccess_galaxy::Resolver: core::marker::Send + core::marker::Sync
pub fn mxaccess_galaxy::Resolver::browse<'life0, 'life1, 'life2, 'async_trait>(&'life0 self, object_tag_like: &'life1 str, attribute_like: &'life2 str, max_rows: usize) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = core::result::Result<alloc::vec::Vec<mxaccess_galaxy::metadata::GalaxyTagMetadata>, mxaccess_galaxy::resolver::ResolverError>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait, 'life2: 'async_trait
pub fn mxaccess_galaxy::Resolver::resolve<'life0, 'life1, 'async_trait>(&'life0 self, tag_reference: &'life1 str) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = core::result::Result<mxaccess_galaxy::metadata::GalaxyTagMetadata, mxaccess_galaxy::resolver::ResolverError>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait
pub trait mxaccess_galaxy::UserResolver: core::marker::Send + core::marker::Sync
pub fn mxaccess_galaxy::UserResolver::resolve_by_guid<'life0, 'async_trait>(&'life0 self, user_guid: uuid::Uuid) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = core::result::Result<mxaccess_galaxy::user::GalaxyUserProfile, mxaccess_galaxy::user::UserResolverError>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait
pub fn mxaccess_galaxy::UserResolver::resolve_by_name<'life0, 'life1, 'async_trait>(&'life0 self, user_name: &'life1 str) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = core::result::Result<mxaccess_galaxy::user::GalaxyUserProfile, mxaccess_galaxy::user::UserResolverError>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait, 'life1: 'async_trait
pub fn mxaccess_galaxy::UserResolver::resolve_user_profile_id_by_guid<'life0, 'async_trait>(&'life0 self, user_guid: uuid::Uuid) -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = core::result::Result<i32, mxaccess_galaxy::user::UserResolverError>> + core::marker::Send + 'async_trait)>> where Self: 'async_trait, 'life0: 'async_trait
pub fn mxaccess_galaxy::parse_role_blob(roles_text: &str) -> alloc::vec::Vec<alloc::string::String>
+118
View File
@@ -0,0 +1,118 @@
pub mod mxaccess_nmx
pub use mxaccess_nmx::WriteValue
pub mod mxaccess_nmx::client
pub use mxaccess_nmx::client::WriteValue
#[non_exhaustive] pub enum mxaccess_nmx::client::NmxClientError
pub mxaccess_nmx::client::NmxClientError::Codec(mxaccess_codec::error::CodecError)
pub mxaccess_nmx::client::NmxClientError::EmptyTransferDataBody
pub mxaccess_nmx::client::NmxClientError::EndpointResolution
pub mxaccess_nmx::client::NmxClientError::EndpointResolution::reason: alloc::string::String
pub mxaccess_nmx::client::NmxClientError::NonZeroHresult
pub mxaccess_nmx::client::NmxClientError::NonZeroHresult::hresult: i32
pub mxaccess_nmx::client::NmxClientError::NonZeroHresult::operation: &'static str
pub mxaccess_nmx::client::NmxClientError::Transport(mxaccess_rpc::transport::TransportError)
pub mxaccess_nmx::client::NmxClientError::UnsupportedDataType(mxaccess_galaxy::metadata::UnsupportedDataType)
impl core::convert::From<mxaccess_codec::error::CodecError> for mxaccess_nmx::client::NmxClientError
pub fn mxaccess_nmx::client::NmxClientError::from(source: mxaccess_codec::error::CodecError) -> Self
impl core::convert::From<mxaccess_galaxy::metadata::UnsupportedDataType> for mxaccess_nmx::client::NmxClientError
pub fn mxaccess_nmx::client::NmxClientError::from(source: mxaccess_galaxy::metadata::UnsupportedDataType) -> Self
impl core::convert::From<mxaccess_rpc::transport::TransportError> for mxaccess_nmx::client::NmxClientError
pub fn mxaccess_nmx::client::NmxClientError::from(source: mxaccess_rpc::transport::TransportError) -> Self
impl core::error::Error for mxaccess_nmx::client::NmxClientError
pub fn mxaccess_nmx::client::NmxClientError::source(&self) -> core::option::Option<&(dyn core::error::Error + 'static)>
impl core::fmt::Debug for mxaccess_nmx::client::NmxClientError
pub fn mxaccess_nmx::client::NmxClientError::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess_nmx::client::NmxClientError
pub fn mxaccess_nmx::client::NmxClientError::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_nmx::client::NmxClientError
impl core::marker::Send for mxaccess_nmx::client::NmxClientError
impl core::marker::Sync for mxaccess_nmx::client::NmxClientError
impl core::marker::Unpin for mxaccess_nmx::client::NmxClientError
impl core::marker::UnsafeUnpin for mxaccess_nmx::client::NmxClientError
impl !core::panic::unwind_safe::RefUnwindSafe for mxaccess_nmx::client::NmxClientError
impl !core::panic::unwind_safe::UnwindSafe for mxaccess_nmx::client::NmxClientError
pub struct mxaccess_nmx::client::NmxClient
impl mxaccess_nmx::client::NmxClient
pub async fn mxaccess_nmx::client::NmxClient::add_subscriber_engine(&mut self, local_engine_id: i32, subscriber_galaxy_id: i32, subscriber_platform_id: i32, subscriber_engine_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::advise_supervisory(&mut self, local_engine_id: i32, tag: &mxaccess_galaxy::metadata::GalaxyTagMetadata, item_correlation_id: [u8; 16], galaxy_id: u8, source_galaxy_id: i32, source_platform_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::connect(addr: core::net::socket_addr::SocketAddr, service_ipid: mxaccess_rpc::guid::Guid, ntlm: mxaccess_rpc::ntlm::NtlmClientContext) -> core::result::Result<Self, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::connect_engine(&mut self, local_engine_id: i32, remote_galaxy_id: i32, remote_platform_id: i32, remote_engine_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub fn mxaccess_nmx::client::NmxClient::from_bound_transport(transport: mxaccess_rpc::transport::DceRpcTcpClient, service_ipid: mxaccess_rpc::guid::Guid) -> Self
pub async fn mxaccess_nmx::client::NmxClient::get_partner_version(&mut self, galaxy_id: i32, platform_id: i32, engine_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::register_engine_2(&mut self, local_engine_id: i32, engine_name: &str, version: i32, callback_obj_ref: &[u8]) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::register_engine_2_without_callback(&mut self, local_engine_id: i32, engine_name: &str, version: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::register_reference(&mut self, local_engine_id: i32, route_tag: &mxaccess_galaxy::metadata::GalaxyTagMetadata, message: &mxaccess_codec::reference_registration::NmxReferenceRegistrationMessage, galaxy_id: u8, source_galaxy_id: i32, source_platform_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::remove_subscriber_engine(&mut self, local_engine_id: i32, subscriber_galaxy_id: i32, subscriber_platform_id: i32, subscriber_engine_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::send_observed_pre_advise_metadata(&mut self, local_engine_id: i32, item_correlation_id: [u8; 16], galaxy_id: u8, source_galaxy_id: i32, source_platform_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub fn mxaccess_nmx::client::NmxClient::service_ipid(&self) -> mxaccess_rpc::guid::Guid
pub async fn mxaccess_nmx::client::NmxClient::set_heartbeat_send_interval(&mut self, ticks_per_beat: i32, max_missed_ticks: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::transfer_data(&mut self, remote_galaxy_id: i32, remote_platform_id: i32, remote_engine_id: i32, message_body: &[u8]) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::un_advise(&mut self, local_engine_id: i32, tag: &mxaccess_galaxy::metadata::GalaxyTagMetadata, item_correlation_id: [u8; 16], galaxy_id: u8, source_galaxy_id: i32, source_platform_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::unregister_engine(&mut self, local_engine_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::write(&mut self, local_engine_id: i32, tag: &mxaccess_galaxy::metadata::GalaxyTagMetadata, value: &mxaccess_codec::write_message::WriteValue, write_index: i32, client_token: u32, galaxy_id: u8, source_galaxy_id: i32, source_platform_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::write2(&mut self, local_engine_id: i32, tag: &mxaccess_galaxy::metadata::GalaxyTagMetadata, value: &mxaccess_codec::write_message::WriteValue, timestamp_filetime: i64, write_index: i32, client_token: u32, galaxy_id: u8, source_galaxy_id: i32, source_platform_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::write_secured2(&mut self, local_engine_id: i32, tag: &mxaccess_galaxy::metadata::GalaxyTagMetadata, value: &mxaccess_codec::write_message::WriteValue, timestamp_filetime: i64, client_name: &str, current_user_id: i32, verifier_user_id: i32, write_index: i32, client_token: u32, galaxy_id: u8, source_galaxy_id: i32, source_platform_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
impl !core::marker::Freeze for mxaccess_nmx::client::NmxClient
impl core::marker::Send for mxaccess_nmx::client::NmxClient
impl core::marker::Sync for mxaccess_nmx::client::NmxClient
impl core::marker::Unpin for mxaccess_nmx::client::NmxClient
impl core::marker::UnsafeUnpin for mxaccess_nmx::client::NmxClient
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_nmx::client::NmxClient
impl core::panic::unwind_safe::UnwindSafe for mxaccess_nmx::client::NmxClient
#[non_exhaustive] pub enum mxaccess_nmx::NmxClientError
pub mxaccess_nmx::NmxClientError::Codec(mxaccess_codec::error::CodecError)
pub mxaccess_nmx::NmxClientError::EmptyTransferDataBody
pub mxaccess_nmx::NmxClientError::EndpointResolution
pub mxaccess_nmx::NmxClientError::EndpointResolution::reason: alloc::string::String
pub mxaccess_nmx::NmxClientError::NonZeroHresult
pub mxaccess_nmx::NmxClientError::NonZeroHresult::hresult: i32
pub mxaccess_nmx::NmxClientError::NonZeroHresult::operation: &'static str
pub mxaccess_nmx::NmxClientError::Transport(mxaccess_rpc::transport::TransportError)
pub mxaccess_nmx::NmxClientError::UnsupportedDataType(mxaccess_galaxy::metadata::UnsupportedDataType)
impl core::convert::From<mxaccess_codec::error::CodecError> for mxaccess_nmx::client::NmxClientError
pub fn mxaccess_nmx::client::NmxClientError::from(source: mxaccess_codec::error::CodecError) -> Self
impl core::convert::From<mxaccess_galaxy::metadata::UnsupportedDataType> for mxaccess_nmx::client::NmxClientError
pub fn mxaccess_nmx::client::NmxClientError::from(source: mxaccess_galaxy::metadata::UnsupportedDataType) -> Self
impl core::convert::From<mxaccess_rpc::transport::TransportError> for mxaccess_nmx::client::NmxClientError
pub fn mxaccess_nmx::client::NmxClientError::from(source: mxaccess_rpc::transport::TransportError) -> Self
impl core::error::Error for mxaccess_nmx::client::NmxClientError
pub fn mxaccess_nmx::client::NmxClientError::source(&self) -> core::option::Option<&(dyn core::error::Error + 'static)>
impl core::fmt::Debug for mxaccess_nmx::client::NmxClientError
pub fn mxaccess_nmx::client::NmxClientError::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess_nmx::client::NmxClientError
pub fn mxaccess_nmx::client::NmxClientError::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess_nmx::client::NmxClientError
impl core::marker::Send for mxaccess_nmx::client::NmxClientError
impl core::marker::Sync for mxaccess_nmx::client::NmxClientError
impl core::marker::Unpin for mxaccess_nmx::client::NmxClientError
impl core::marker::UnsafeUnpin for mxaccess_nmx::client::NmxClientError
impl !core::panic::unwind_safe::RefUnwindSafe for mxaccess_nmx::client::NmxClientError
impl !core::panic::unwind_safe::UnwindSafe for mxaccess_nmx::client::NmxClientError
pub struct mxaccess_nmx::NmxClient
impl mxaccess_nmx::client::NmxClient
pub async fn mxaccess_nmx::client::NmxClient::add_subscriber_engine(&mut self, local_engine_id: i32, subscriber_galaxy_id: i32, subscriber_platform_id: i32, subscriber_engine_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::advise_supervisory(&mut self, local_engine_id: i32, tag: &mxaccess_galaxy::metadata::GalaxyTagMetadata, item_correlation_id: [u8; 16], galaxy_id: u8, source_galaxy_id: i32, source_platform_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::connect(addr: core::net::socket_addr::SocketAddr, service_ipid: mxaccess_rpc::guid::Guid, ntlm: mxaccess_rpc::ntlm::NtlmClientContext) -> core::result::Result<Self, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::connect_engine(&mut self, local_engine_id: i32, remote_galaxy_id: i32, remote_platform_id: i32, remote_engine_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub fn mxaccess_nmx::client::NmxClient::from_bound_transport(transport: mxaccess_rpc::transport::DceRpcTcpClient, service_ipid: mxaccess_rpc::guid::Guid) -> Self
pub async fn mxaccess_nmx::client::NmxClient::get_partner_version(&mut self, galaxy_id: i32, platform_id: i32, engine_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::register_engine_2(&mut self, local_engine_id: i32, engine_name: &str, version: i32, callback_obj_ref: &[u8]) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::register_engine_2_without_callback(&mut self, local_engine_id: i32, engine_name: &str, version: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::register_reference(&mut self, local_engine_id: i32, route_tag: &mxaccess_galaxy::metadata::GalaxyTagMetadata, message: &mxaccess_codec::reference_registration::NmxReferenceRegistrationMessage, galaxy_id: u8, source_galaxy_id: i32, source_platform_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::remove_subscriber_engine(&mut self, local_engine_id: i32, subscriber_galaxy_id: i32, subscriber_platform_id: i32, subscriber_engine_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::send_observed_pre_advise_metadata(&mut self, local_engine_id: i32, item_correlation_id: [u8; 16], galaxy_id: u8, source_galaxy_id: i32, source_platform_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub fn mxaccess_nmx::client::NmxClient::service_ipid(&self) -> mxaccess_rpc::guid::Guid
pub async fn mxaccess_nmx::client::NmxClient::set_heartbeat_send_interval(&mut self, ticks_per_beat: i32, max_missed_ticks: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::transfer_data(&mut self, remote_galaxy_id: i32, remote_platform_id: i32, remote_engine_id: i32, message_body: &[u8]) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::un_advise(&mut self, local_engine_id: i32, tag: &mxaccess_galaxy::metadata::GalaxyTagMetadata, item_correlation_id: [u8; 16], galaxy_id: u8, source_galaxy_id: i32, source_platform_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::unregister_engine(&mut self, local_engine_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::write(&mut self, local_engine_id: i32, tag: &mxaccess_galaxy::metadata::GalaxyTagMetadata, value: &mxaccess_codec::write_message::WriteValue, write_index: i32, client_token: u32, galaxy_id: u8, source_galaxy_id: i32, source_platform_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::write2(&mut self, local_engine_id: i32, tag: &mxaccess_galaxy::metadata::GalaxyTagMetadata, value: &mxaccess_codec::write_message::WriteValue, timestamp_filetime: i64, write_index: i32, client_token: u32, galaxy_id: u8, source_galaxy_id: i32, source_platform_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
pub async fn mxaccess_nmx::client::NmxClient::write_secured2(&mut self, local_engine_id: i32, tag: &mxaccess_galaxy::metadata::GalaxyTagMetadata, value: &mxaccess_codec::write_message::WriteValue, timestamp_filetime: i64, client_name: &str, current_user_id: i32, verifier_user_id: i32, write_index: i32, client_token: u32, galaxy_id: u8, source_galaxy_id: i32, source_platform_id: i32) -> core::result::Result<i32, mxaccess_nmx::client::NmxClientError>
impl !core::marker::Freeze for mxaccess_nmx::client::NmxClient
impl core::marker::Send for mxaccess_nmx::client::NmxClient
impl core::marker::Sync for mxaccess_nmx::client::NmxClient
impl core::marker::Unpin for mxaccess_nmx::client::NmxClient
impl core::marker::UnsafeUnpin for mxaccess_nmx::client::NmxClient
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess_nmx::client::NmxClient
impl core::panic::unwind_safe::UnwindSafe for mxaccess_nmx::client::NmxClient
File diff suppressed because it is too large Load Diff
+718
View File
@@ -0,0 +1,718 @@
pub mod mxaccess
pub use mxaccess::GalaxyTagMetadata
pub use mxaccess::MxDataType
pub use mxaccess::MxReferenceHandle
pub use mxaccess::MxStatus
pub use mxaccess::MxStatusCategory
pub use mxaccess::MxStatusSource
pub use mxaccess::MxValue
pub use mxaccess::MxValueKind
pub use mxaccess::NmxOperationStatusFormat
pub use mxaccess::NmxOperationStatusMessage
pub use mxaccess::Resolver
pub use mxaccess::ResolverError
pub use mxaccess::WriteValue
pub mod mxaccess::asb_session
pub struct mxaccess::asb_session::AsbSession
impl mxaccess::asb_session::AsbSession
pub async fn mxaccess::asb_session::AsbSession::add_monitored_items(&self, subscription_id: i64, items: &[mxaccess_asb::operations::MinimalMonitoredItem], require_id: bool) -> core::result::Result<mxaccess_asb::operations::AddMonitoredItemsResponse, mxaccess::Error>
pub async fn mxaccess::asb_session::AsbSession::connect(endpoint: core::net::socket_addr::SocketAddr, passphrase: &str, crypto_parameters: &mxaccess_asb_nettcp::auth::CryptoParameters, via_uri: impl core::convert::Into<alloc::string::String>, connection_id: [u8; 16]) -> core::result::Result<Self, mxaccess::Error>
pub fn mxaccess::asb_session::AsbSession::connect_response(&self) -> &mxaccess_asb::operations::ConnectResponse
pub async fn mxaccess::asb_session::AsbSession::create_subscription(&self, max_queue_size: i64, sample_interval: u64) -> core::result::Result<mxaccess_asb::operations::CreateSubscriptionResponse, mxaccess::Error>
pub async fn mxaccess::asb_session::AsbSession::delete_monitored_items(&self, subscription_id: i64, items: &[mxaccess_asb::operations::MinimalMonitoredItem]) -> core::result::Result<mxaccess_asb::operations::DeleteMonitoredItemsResponse, mxaccess::Error>
pub async fn mxaccess::asb_session::AsbSession::delete_subscription(&self, subscription_id: i64) -> core::result::Result<mxaccess_asb::operations::DeleteSubscriptionResponse, mxaccess::Error>
pub async fn mxaccess::asb_session::AsbSession::disconnect(&self) -> core::result::Result<(), mxaccess::Error>
pub fn mxaccess::asb_session::AsbSession::from_transport(transport: mxaccess::transport_asb::AsbTransport<tokio::net::tcp::stream::TcpStream>, connect_response: mxaccess_asb::operations::ConnectResponse) -> Self
pub async fn mxaccess::asb_session::AsbSession::keep_alive(&self) -> core::result::Result<(), mxaccess::Error>
pub async fn mxaccess::asb_session::AsbSession::publish(&self, subscription_id: i64) -> core::result::Result<mxaccess_asb::operations::PublishResponse, mxaccess::Error>
pub async fn mxaccess::asb_session::AsbSession::publish_write_complete(&self) -> core::result::Result<mxaccess_asb::operations::PublishWriteCompleteResponse, mxaccess::Error>
pub async fn mxaccess::asb_session::AsbSession::read(&self, items: &[mxaccess_asb::contracts::ItemIdentity]) -> core::result::Result<mxaccess_asb::operations::ReadResponse, mxaccess::Error>
pub async fn mxaccess::asb_session::AsbSession::register_items(&self, items: &[mxaccess_asb::contracts::ItemIdentity], require_id: bool, register_only: bool) -> core::result::Result<mxaccess_asb::operations::RegisterItemsResponse, mxaccess::Error>
pub fn mxaccess::asb_session::AsbSession::subscribe(&self, subscription_id: i64) -> mxaccess::asb_session::AsbSubscription
pub async fn mxaccess::asb_session::AsbSession::subscribe_buffered(&self, _reference: &str, _options: mxaccess::BufferedOptions) -> core::result::Result<mxaccess::asb_session::AsbSubscription, mxaccess::Error>
pub async fn mxaccess::asb_session::AsbSession::unregister_items(&self, items: &[mxaccess_asb::contracts::ItemIdentity]) -> core::result::Result<mxaccess_asb::operations::UnregisterItemsResponse, mxaccess::Error>
pub async fn mxaccess::asb_session::AsbSession::write(&self, items: &[mxaccess_asb::contracts::ItemIdentity], values: &[mxaccess_asb::operations::MinimalWriteValue], write_handle: u32) -> core::result::Result<mxaccess_asb::operations::WriteResponse, mxaccess::Error>
impl core::clone::Clone for mxaccess::asb_session::AsbSession
pub fn mxaccess::asb_session::AsbSession::clone(&self) -> mxaccess::asb_session::AsbSession
impl core::fmt::Debug for mxaccess::asb_session::AsbSession
pub fn mxaccess::asb_session::AsbSession::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess::asb_session::AsbSession
impl core::marker::Send for mxaccess::asb_session::AsbSession
impl core::marker::Sync for mxaccess::asb_session::AsbSession
impl core::marker::Unpin for mxaccess::asb_session::AsbSession
impl core::marker::UnsafeUnpin for mxaccess::asb_session::AsbSession
impl !core::panic::unwind_safe::RefUnwindSafe for mxaccess::asb_session::AsbSession
impl !core::panic::unwind_safe::UnwindSafe for mxaccess::asb_session::AsbSession
pub struct mxaccess::asb_session::AsbSubscription
impl core::fmt::Debug for mxaccess::asb_session::AsbSubscription
pub fn mxaccess::asb_session::AsbSubscription::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::ops::drop::Drop for mxaccess::asb_session::AsbSubscription
pub fn mxaccess::asb_session::AsbSubscription::drop(&mut self)
impl futures_core::stream::Stream for mxaccess::asb_session::AsbSubscription
pub type mxaccess::asb_session::AsbSubscription::Item = core::result::Result<mxaccess_asb::contracts::MonitoredItemValue, mxaccess::Error>
pub fn mxaccess::asb_session::AsbSubscription::poll_next(self: core::pin::Pin<&mut Self>, cx: &mut core::task::wake::Context<'_>) -> core::task::poll::Poll<core::option::Option<Self::Item>>
impl core::marker::Freeze for mxaccess::asb_session::AsbSubscription
impl core::marker::Send for mxaccess::asb_session::AsbSubscription
impl core::marker::Sync for mxaccess::asb_session::AsbSubscription
impl core::marker::Unpin for mxaccess::asb_session::AsbSubscription
impl core::marker::UnsafeUnpin for mxaccess::asb_session::AsbSubscription
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::asb_session::AsbSubscription
impl core::panic::unwind_safe::UnwindSafe for mxaccess::asb_session::AsbSubscription
pub mod mxaccess::session
#[non_exhaustive] pub enum mxaccess::session::OperationKind
pub mxaccess::session::OperationKind::Activate
pub mxaccess::session::OperationKind::Other
pub mxaccess::session::OperationKind::Read
pub mxaccess::session::OperationKind::Subscribe
pub mxaccess::session::OperationKind::Suspend
pub mxaccess::session::OperationKind::Unsubscribe
pub mxaccess::session::OperationKind::Write
pub mxaccess::session::OperationKind::WriteSecured
impl core::clone::Clone for mxaccess::session::OperationKind
pub fn mxaccess::session::OperationKind::clone(&self) -> mxaccess::session::OperationKind
impl core::cmp::Eq for mxaccess::session::OperationKind
impl core::cmp::PartialEq for mxaccess::session::OperationKind
pub fn mxaccess::session::OperationKind::eq(&self, other: &mxaccess::session::OperationKind) -> bool
impl core::fmt::Debug for mxaccess::session::OperationKind
pub fn mxaccess::session::OperationKind::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::hash::Hash for mxaccess::session::OperationKind
pub fn mxaccess::session::OperationKind::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
impl core::marker::Copy for mxaccess::session::OperationKind
impl core::marker::StructuralPartialEq for mxaccess::session::OperationKind
impl core::marker::Freeze for mxaccess::session::OperationKind
impl core::marker::Send for mxaccess::session::OperationKind
impl core::marker::Sync for mxaccess::session::OperationKind
impl core::marker::Unpin for mxaccess::session::OperationKind
impl core::marker::UnsafeUnpin for mxaccess::session::OperationKind
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::session::OperationKind
impl core::panic::unwind_safe::UnwindSafe for mxaccess::session::OperationKind
#[non_exhaustive] pub struct mxaccess::session::OperationContext
pub mxaccess::session::OperationContext::correlation_id: [u8; 16]
pub mxaccess::session::OperationContext::op_kind: mxaccess::session::OperationKind
pub mxaccess::session::OperationContext::reference: core::option::Option<alloc::sync::Arc<str>>
pub mxaccess::session::OperationContext::retry_count: u32
impl mxaccess::session::OperationContext
pub fn mxaccess::session::OperationContext::new(correlation_id: [u8; 16], op_kind: mxaccess::session::OperationKind, reference: core::option::Option<alloc::sync::Arc<str>>, retry_count: u32) -> Self
impl core::clone::Clone for mxaccess::session::OperationContext
pub fn mxaccess::session::OperationContext::clone(&self) -> mxaccess::session::OperationContext
impl core::fmt::Debug for mxaccess::session::OperationContext
pub fn mxaccess::session::OperationContext::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess::session::OperationContext
impl core::marker::Send for mxaccess::session::OperationContext
impl core::marker::Sync for mxaccess::session::OperationContext
impl core::marker::Unpin for mxaccess::session::OperationContext
impl core::marker::UnsafeUnpin for mxaccess::session::OperationContext
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::session::OperationContext
impl core::panic::unwind_safe::UnwindSafe for mxaccess::session::OperationContext
#[non_exhaustive] pub struct mxaccess::session::OperationStatus
pub mxaccess::session::OperationStatus::context: core::option::Option<mxaccess::session::OperationContext>
pub mxaccess::session::OperationStatus::is_during_recovery: bool
pub mxaccess::session::OperationStatus::raw: mxaccess_codec::operation_status::NmxOperationStatusMessage
pub mxaccess::session::OperationStatus::status: mxaccess_codec::status::MxStatus
impl mxaccess::session::OperationStatus
pub fn mxaccess::session::OperationStatus::new(raw: mxaccess_codec::operation_status::NmxOperationStatusMessage, status: mxaccess_codec::status::MxStatus, context: core::option::Option<mxaccess::session::OperationContext>, is_during_recovery: bool) -> Self
impl core::clone::Clone for mxaccess::session::OperationStatus
pub fn mxaccess::session::OperationStatus::clone(&self) -> mxaccess::session::OperationStatus
impl core::fmt::Debug for mxaccess::session::OperationStatus
pub fn mxaccess::session::OperationStatus::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess::session::OperationStatus
impl core::marker::Send for mxaccess::session::OperationStatus
impl core::marker::Sync for mxaccess::session::OperationStatus
impl core::marker::Unpin for mxaccess::session::OperationStatus
impl core::marker::UnsafeUnpin for mxaccess::session::OperationStatus
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::session::OperationStatus
impl core::panic::unwind_safe::UnwindSafe for mxaccess::session::OperationStatus
pub struct mxaccess::session::SessionInner
impl core::fmt::Debug for mxaccess::session::SessionInner
pub fn mxaccess::session::SessionInner::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl !core::marker::Freeze for mxaccess::session::SessionInner
impl core::marker::Send for mxaccess::session::SessionInner
impl core::marker::Sync for mxaccess::session::SessionInner
impl core::marker::Unpin for mxaccess::session::SessionInner
impl core::marker::UnsafeUnpin for mxaccess::session::SessionInner
impl !core::panic::unwind_safe::RefUnwindSafe for mxaccess::session::SessionInner
impl !core::panic::unwind_safe::UnwindSafe for mxaccess::session::SessionInner
pub struct mxaccess::session::Subscription
impl mxaccess::session::Subscription
pub fn mxaccess::session::Subscription::correlation_id(&self) -> [u8; 16]
pub fn mxaccess::session::Subscription::metadata(&self) -> &mxaccess_galaxy::metadata::GalaxyTagMetadata
pub fn mxaccess::session::Subscription::reference(&self) -> &str
impl core::fmt::Debug for mxaccess::session::Subscription
pub fn mxaccess::session::Subscription::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl futures_core::stream::Stream for mxaccess::session::Subscription
pub type mxaccess::session::Subscription::Item = core::result::Result<mxaccess::DataChange, mxaccess::Error>
pub fn mxaccess::session::Subscription::poll_next(self: core::pin::Pin<&mut Self>, cx: &mut core::task::wake::Context<'_>) -> core::task::poll::Poll<core::option::Option<Self::Item>>
impl core::marker::Freeze for mxaccess::session::Subscription
impl core::marker::Send for mxaccess::session::Subscription
impl core::marker::Sync for mxaccess::session::Subscription
impl core::marker::Unpin for mxaccess::session::Subscription
impl core::marker::UnsafeUnpin for mxaccess::session::Subscription
impl !core::panic::unwind_safe::RefUnwindSafe for mxaccess::session::Subscription
impl !core::panic::unwind_safe::UnwindSafe for mxaccess::session::Subscription
#[non_exhaustive] pub struct mxaccess::session::WriteHandle
pub mxaccess::session::WriteHandle::correlation_id: [u8; 16]
impl core::clone::Clone for mxaccess::session::WriteHandle
pub fn mxaccess::session::WriteHandle::clone(&self) -> mxaccess::session::WriteHandle
impl core::cmp::Eq for mxaccess::session::WriteHandle
impl core::cmp::PartialEq for mxaccess::session::WriteHandle
pub fn mxaccess::session::WriteHandle::eq(&self, other: &mxaccess::session::WriteHandle) -> bool
impl core::fmt::Debug for mxaccess::session::WriteHandle
pub fn mxaccess::session::WriteHandle::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::hash::Hash for mxaccess::session::WriteHandle
pub fn mxaccess::session::WriteHandle::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
impl core::marker::Copy for mxaccess::session::WriteHandle
impl core::marker::StructuralPartialEq for mxaccess::session::WriteHandle
impl core::marker::Freeze for mxaccess::session::WriteHandle
impl core::marker::Send for mxaccess::session::WriteHandle
impl core::marker::Sync for mxaccess::session::WriteHandle
impl core::marker::Unpin for mxaccess::session::WriteHandle
impl core::marker::UnsafeUnpin for mxaccess::session::WriteHandle
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::session::WriteHandle
impl core::panic::unwind_safe::UnwindSafe for mxaccess::session::WriteHandle
pub fn mxaccess::session::filetime_to_system_time(filetime_ticks: i64) -> std::time::SystemTime
pub fn mxaccess::session::system_time_to_filetime(time: std::time::SystemTime) -> core::result::Result<i64, mxaccess::Error>
pub type mxaccess::session::RebuildFactory = alloc::sync::Arc<(dyn core::ops::function::Fn() -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = core::result::Result<mxaccess_nmx::client::NmxClient, mxaccess_nmx::client::NmxClientError>> + core::marker::Send)>> + core::marker::Send + core::marker::Sync)>
pub mod mxaccess::transport_asb
pub struct mxaccess::transport_asb::AsbTransport<T: tokio::io::async_read::AsyncRead + tokio::io::async_write::AsyncWrite + core::marker::Unpin + core::marker::Send + 'static>
impl mxaccess::transport_asb::AsbTransport<tokio::net::tcp::stream::TcpStream>
pub async fn mxaccess::transport_asb::AsbTransport<tokio::net::tcp::stream::TcpStream>::connect(endpoint: core::net::socket_addr::SocketAddr, passphrase: &str, crypto_parameters: &mxaccess_asb_nettcp::auth::CryptoParameters, via_uri: impl core::convert::Into<alloc::string::String>, connection_id: [u8; 16]) -> core::result::Result<(Self, mxaccess_asb::operations::ConnectResponse), mxaccess::Error>
impl<T: tokio::io::async_read::AsyncRead + tokio::io::async_write::AsyncWrite + core::marker::Unpin + core::marker::Send + 'static> mxaccess::transport_asb::AsbTransport<T>
pub fn mxaccess::transport_asb::AsbTransport<T>::client_mut(&mut self) -> &mut mxaccess_asb::client::AsbClient<T>
pub fn mxaccess::transport_asb::AsbTransport<T>::into_client(self) -> mxaccess_asb::client::AsbClient<T>
pub fn mxaccess::transport_asb::AsbTransport<T>::new(client: mxaccess_asb::client::AsbClient<T>) -> Self
impl<T: tokio::io::async_read::AsyncRead + tokio::io::async_write::AsyncWrite + core::marker::Unpin + core::marker::Send + core::marker::Sync + 'static> mxaccess::Transport for mxaccess::transport_asb::AsbTransport<T>
pub fn mxaccess::transport_asb::AsbTransport<T>::capabilities(&self) -> mxaccess::TransportCapabilities
pub fn mxaccess::transport_asb::AsbTransport<T>::kind(&self) -> mxaccess::TransportKind
impl<T> core::marker::Freeze for mxaccess::transport_asb::AsbTransport<T> where T: core::marker::Freeze
impl<T> core::marker::Send for mxaccess::transport_asb::AsbTransport<T>
impl<T> core::marker::Sync for mxaccess::transport_asb::AsbTransport<T> where T: core::marker::Sync
impl<T> core::marker::Unpin for mxaccess::transport_asb::AsbTransport<T>
impl<T> core::marker::UnsafeUnpin for mxaccess::transport_asb::AsbTransport<T> where T: core::marker::UnsafeUnpin
impl<T> core::panic::unwind_safe::RefUnwindSafe for mxaccess::transport_asb::AsbTransport<T> where T: core::panic::unwind_safe::RefUnwindSafe
impl<T> core::panic::unwind_safe::UnwindSafe for mxaccess::transport_asb::AsbTransport<T> where T: core::panic::unwind_safe::UnwindSafe
#[non_exhaustive] pub enum mxaccess::AuthError
pub mxaccess::AuthError::Ntlm
pub mxaccess::AuthError::Ntlm::reason: alloc::string::String
impl core::convert::From<mxaccess::AuthError> for mxaccess::Error
pub fn mxaccess::Error::from(source: mxaccess::AuthError) -> Self
impl core::error::Error for mxaccess::AuthError
impl core::fmt::Debug for mxaccess::AuthError
pub fn mxaccess::AuthError::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess::AuthError
pub fn mxaccess::AuthError::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess::AuthError
impl core::marker::Send for mxaccess::AuthError
impl core::marker::Sync for mxaccess::AuthError
impl core::marker::Unpin for mxaccess::AuthError
impl core::marker::UnsafeUnpin for mxaccess::AuthError
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::AuthError
impl core::panic::unwind_safe::UnwindSafe for mxaccess::AuthError
#[non_exhaustive] pub enum mxaccess::ConfigError
pub mxaccess::ConfigError::Galaxy
pub mxaccess::ConfigError::Galaxy::reason: alloc::string::String
pub mxaccess::ConfigError::InvalidArgument
pub mxaccess::ConfigError::InvalidArgument::detail: alloc::string::String
pub mxaccess::ConfigError::RecoveryNotConfigured
impl core::convert::From<mxaccess::ConfigError> for mxaccess::Error
pub fn mxaccess::Error::from(source: mxaccess::ConfigError) -> Self
impl core::error::Error for mxaccess::ConfigError
impl core::fmt::Debug for mxaccess::ConfigError
pub fn mxaccess::ConfigError::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess::ConfigError
pub fn mxaccess::ConfigError::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess::ConfigError
impl core::marker::Send for mxaccess::ConfigError
impl core::marker::Sync for mxaccess::ConfigError
impl core::marker::Unpin for mxaccess::ConfigError
impl core::marker::UnsafeUnpin for mxaccess::ConfigError
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::ConfigError
impl core::panic::unwind_safe::UnwindSafe for mxaccess::ConfigError
#[non_exhaustive] pub enum mxaccess::ConnectionError
pub mxaccess::ConnectionError::CallbackProxyMissing
pub mxaccess::ConnectionError::EngineNotRegistered
pub mxaccess::ConnectionError::ServerUnavailable
pub mxaccess::ConnectionError::TransportFailure
pub mxaccess::ConnectionError::TransportFailure::detail: alloc::string::String
impl core::convert::From<mxaccess::ConnectionError> for mxaccess::Error
pub fn mxaccess::Error::from(source: mxaccess::ConnectionError) -> Self
impl core::error::Error for mxaccess::ConnectionError
impl core::fmt::Debug for mxaccess::ConnectionError
pub fn mxaccess::ConnectionError::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess::ConnectionError
pub fn mxaccess::ConnectionError::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess::ConnectionError
impl core::marker::Send for mxaccess::ConnectionError
impl core::marker::Sync for mxaccess::ConnectionError
impl core::marker::Unpin for mxaccess::ConnectionError
impl core::marker::UnsafeUnpin for mxaccess::ConnectionError
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::ConnectionError
impl core::panic::unwind_safe::UnwindSafe for mxaccess::ConnectionError
#[non_exhaustive] pub enum mxaccess::Error
pub mxaccess::Error::Auth(mxaccess::AuthError)
pub mxaccess::Error::Cancelled
pub mxaccess::Error::Configuration(mxaccess::ConfigError)
pub mxaccess::Error::Connection(mxaccess::ConnectionError)
pub mxaccess::Error::Io(std::io::error::Error)
pub mxaccess::Error::Protocol(mxaccess::ProtocolError)
pub mxaccess::Error::Security(mxaccess::SecurityError)
pub mxaccess::Error::Status
pub mxaccess::Error::Status::category: mxaccess_codec::status::MxStatusCategory
pub mxaccess::Error::Status::detail: i16
pub mxaccess::Error::Status::detected_by: mxaccess_codec::status::MxStatusSource
pub mxaccess::Error::Status::success: i16
pub mxaccess::Error::Timeout(core::time::Duration)
pub mxaccess::Error::TypeMismatch
pub mxaccess::Error::TypeMismatch::actual: mxaccess_codec::value::MxValueKind
pub mxaccess::Error::TypeMismatch::expected: mxaccess_codec::value::MxValueKind
pub mxaccess::Error::TypeMismatch::reference: alloc::sync::Arc<str>
pub mxaccess::Error::Unsupported
pub mxaccess::Error::Unsupported::operation: alloc::borrow::Cow<'static, str>
pub mxaccess::Error::Unsupported::transport: mxaccess::TransportKind
impl core::convert::From<mxaccess::AuthError> for mxaccess::Error
pub fn mxaccess::Error::from(source: mxaccess::AuthError) -> Self
impl core::convert::From<mxaccess::ConfigError> for mxaccess::Error
pub fn mxaccess::Error::from(source: mxaccess::ConfigError) -> Self
impl core::convert::From<mxaccess::ConnectionError> for mxaccess::Error
pub fn mxaccess::Error::from(source: mxaccess::ConnectionError) -> Self
impl core::convert::From<mxaccess::ProtocolError> for mxaccess::Error
pub fn mxaccess::Error::from(source: mxaccess::ProtocolError) -> Self
impl core::convert::From<mxaccess::SecurityError> for mxaccess::Error
pub fn mxaccess::Error::from(source: mxaccess::SecurityError) -> Self
impl core::convert::From<std::io::error::Error> for mxaccess::Error
pub fn mxaccess::Error::from(source: std::io::error::Error) -> Self
impl core::error::Error for mxaccess::Error
pub fn mxaccess::Error::source(&self) -> core::option::Option<&(dyn core::error::Error + 'static)>
impl core::fmt::Debug for mxaccess::Error
pub fn mxaccess::Error::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess::Error
pub fn mxaccess::Error::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess::Error
impl core::marker::Send for mxaccess::Error
impl core::marker::Sync for mxaccess::Error
impl core::marker::Unpin for mxaccess::Error
impl core::marker::UnsafeUnpin for mxaccess::Error
impl !core::panic::unwind_safe::RefUnwindSafe for mxaccess::Error
impl !core::panic::unwind_safe::UnwindSafe for mxaccess::Error
#[non_exhaustive] pub enum mxaccess::OperationKind
pub mxaccess::OperationKind::Activate
pub mxaccess::OperationKind::Other
pub mxaccess::OperationKind::Read
pub mxaccess::OperationKind::Subscribe
pub mxaccess::OperationKind::Suspend
pub mxaccess::OperationKind::Unsubscribe
pub mxaccess::OperationKind::Write
pub mxaccess::OperationKind::WriteSecured
impl core::clone::Clone for mxaccess::session::OperationKind
pub fn mxaccess::session::OperationKind::clone(&self) -> mxaccess::session::OperationKind
impl core::cmp::Eq for mxaccess::session::OperationKind
impl core::cmp::PartialEq for mxaccess::session::OperationKind
pub fn mxaccess::session::OperationKind::eq(&self, other: &mxaccess::session::OperationKind) -> bool
impl core::fmt::Debug for mxaccess::session::OperationKind
pub fn mxaccess::session::OperationKind::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::hash::Hash for mxaccess::session::OperationKind
pub fn mxaccess::session::OperationKind::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
impl core::marker::Copy for mxaccess::session::OperationKind
impl core::marker::StructuralPartialEq for mxaccess::session::OperationKind
impl core::marker::Freeze for mxaccess::session::OperationKind
impl core::marker::Send for mxaccess::session::OperationKind
impl core::marker::Sync for mxaccess::session::OperationKind
impl core::marker::Unpin for mxaccess::session::OperationKind
impl core::marker::UnsafeUnpin for mxaccess::session::OperationKind
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::session::OperationKind
impl core::panic::unwind_safe::UnwindSafe for mxaccess::session::OperationKind
#[non_exhaustive] pub enum mxaccess::ProtocolError
pub mxaccess::ProtocolError::Decode
pub mxaccess::ProtocolError::Decode::buffer_len: usize
pub mxaccess::ProtocolError::Decode::offset: usize
pub mxaccess::ProtocolError::Decode::reason: &'static str
pub mxaccess::ProtocolError::InnerLengthMismatch
pub mxaccess::ProtocolError::InnerLengthMismatch::actual: usize
pub mxaccess::ProtocolError::InnerLengthMismatch::declared: i32
pub mxaccess::ProtocolError::UnexpectedOpcode(u8)
impl core::convert::From<mxaccess::ProtocolError> for mxaccess::Error
pub fn mxaccess::Error::from(source: mxaccess::ProtocolError) -> Self
impl core::error::Error for mxaccess::ProtocolError
impl core::fmt::Debug for mxaccess::ProtocolError
pub fn mxaccess::ProtocolError::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess::ProtocolError
pub fn mxaccess::ProtocolError::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess::ProtocolError
impl core::marker::Send for mxaccess::ProtocolError
impl core::marker::Sync for mxaccess::ProtocolError
impl core::marker::Unpin for mxaccess::ProtocolError
impl core::marker::UnsafeUnpin for mxaccess::ProtocolError
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::ProtocolError
impl core::panic::unwind_safe::UnwindSafe for mxaccess::ProtocolError
#[non_exhaustive] pub enum mxaccess::RecoveryEvent
pub mxaccess::RecoveryEvent::Failed
pub mxaccess::RecoveryEvent::Failed::attempt: u32
pub mxaccess::RecoveryEvent::Failed::error: mxaccess::Error
pub mxaccess::RecoveryEvent::Failed::will_retry: bool
pub mxaccess::RecoveryEvent::Recovered
pub mxaccess::RecoveryEvent::Recovered::attempt: u32
pub mxaccess::RecoveryEvent::Started
pub mxaccess::RecoveryEvent::Started::attempt: u32
impl core::fmt::Debug for mxaccess::RecoveryEvent
pub fn mxaccess::RecoveryEvent::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess::RecoveryEvent
impl core::marker::Send for mxaccess::RecoveryEvent
impl core::marker::Sync for mxaccess::RecoveryEvent
impl core::marker::Unpin for mxaccess::RecoveryEvent
impl core::marker::UnsafeUnpin for mxaccess::RecoveryEvent
impl !core::panic::unwind_safe::RefUnwindSafe for mxaccess::RecoveryEvent
impl !core::panic::unwind_safe::UnwindSafe for mxaccess::RecoveryEvent
#[non_exhaustive] pub enum mxaccess::SecurityError
pub mxaccess::SecurityError::CallbackObjRefRejected
pub mxaccess::SecurityError::VerifierRequired
impl core::convert::From<mxaccess::SecurityError> for mxaccess::Error
pub fn mxaccess::Error::from(source: mxaccess::SecurityError) -> Self
impl core::error::Error for mxaccess::SecurityError
impl core::fmt::Debug for mxaccess::SecurityError
pub fn mxaccess::SecurityError::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::fmt::Display for mxaccess::SecurityError
pub fn mxaccess::SecurityError::fmt(&self, __formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess::SecurityError
impl core::marker::Send for mxaccess::SecurityError
impl core::marker::Sync for mxaccess::SecurityError
impl core::marker::Unpin for mxaccess::SecurityError
impl core::marker::UnsafeUnpin for mxaccess::SecurityError
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::SecurityError
impl core::panic::unwind_safe::UnwindSafe for mxaccess::SecurityError
#[non_exhaustive] pub enum mxaccess::TransportKind
pub mxaccess::TransportKind::Asb
pub mxaccess::TransportKind::Nmx
impl core::clone::Clone for mxaccess::TransportKind
pub fn mxaccess::TransportKind::clone(&self) -> mxaccess::TransportKind
impl core::cmp::Eq for mxaccess::TransportKind
impl core::cmp::PartialEq for mxaccess::TransportKind
pub fn mxaccess::TransportKind::eq(&self, other: &mxaccess::TransportKind) -> bool
impl core::fmt::Debug for mxaccess::TransportKind
pub fn mxaccess::TransportKind::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::hash::Hash for mxaccess::TransportKind
pub fn mxaccess::TransportKind::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
impl core::marker::Copy for mxaccess::TransportKind
impl core::marker::StructuralPartialEq for mxaccess::TransportKind
impl core::marker::Freeze for mxaccess::TransportKind
impl core::marker::Send for mxaccess::TransportKind
impl core::marker::Sync for mxaccess::TransportKind
impl core::marker::Unpin for mxaccess::TransportKind
impl core::marker::UnsafeUnpin for mxaccess::TransportKind
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::TransportKind
impl core::panic::unwind_safe::UnwindSafe for mxaccess::TransportKind
pub struct mxaccess::AsbSession
impl mxaccess::asb_session::AsbSession
pub async fn mxaccess::asb_session::AsbSession::add_monitored_items(&self, subscription_id: i64, items: &[mxaccess_asb::operations::MinimalMonitoredItem], require_id: bool) -> core::result::Result<mxaccess_asb::operations::AddMonitoredItemsResponse, mxaccess::Error>
pub async fn mxaccess::asb_session::AsbSession::connect(endpoint: core::net::socket_addr::SocketAddr, passphrase: &str, crypto_parameters: &mxaccess_asb_nettcp::auth::CryptoParameters, via_uri: impl core::convert::Into<alloc::string::String>, connection_id: [u8; 16]) -> core::result::Result<Self, mxaccess::Error>
pub fn mxaccess::asb_session::AsbSession::connect_response(&self) -> &mxaccess_asb::operations::ConnectResponse
pub async fn mxaccess::asb_session::AsbSession::create_subscription(&self, max_queue_size: i64, sample_interval: u64) -> core::result::Result<mxaccess_asb::operations::CreateSubscriptionResponse, mxaccess::Error>
pub async fn mxaccess::asb_session::AsbSession::delete_monitored_items(&self, subscription_id: i64, items: &[mxaccess_asb::operations::MinimalMonitoredItem]) -> core::result::Result<mxaccess_asb::operations::DeleteMonitoredItemsResponse, mxaccess::Error>
pub async fn mxaccess::asb_session::AsbSession::delete_subscription(&self, subscription_id: i64) -> core::result::Result<mxaccess_asb::operations::DeleteSubscriptionResponse, mxaccess::Error>
pub async fn mxaccess::asb_session::AsbSession::disconnect(&self) -> core::result::Result<(), mxaccess::Error>
pub fn mxaccess::asb_session::AsbSession::from_transport(transport: mxaccess::transport_asb::AsbTransport<tokio::net::tcp::stream::TcpStream>, connect_response: mxaccess_asb::operations::ConnectResponse) -> Self
pub async fn mxaccess::asb_session::AsbSession::keep_alive(&self) -> core::result::Result<(), mxaccess::Error>
pub async fn mxaccess::asb_session::AsbSession::publish(&self, subscription_id: i64) -> core::result::Result<mxaccess_asb::operations::PublishResponse, mxaccess::Error>
pub async fn mxaccess::asb_session::AsbSession::publish_write_complete(&self) -> core::result::Result<mxaccess_asb::operations::PublishWriteCompleteResponse, mxaccess::Error>
pub async fn mxaccess::asb_session::AsbSession::read(&self, items: &[mxaccess_asb::contracts::ItemIdentity]) -> core::result::Result<mxaccess_asb::operations::ReadResponse, mxaccess::Error>
pub async fn mxaccess::asb_session::AsbSession::register_items(&self, items: &[mxaccess_asb::contracts::ItemIdentity], require_id: bool, register_only: bool) -> core::result::Result<mxaccess_asb::operations::RegisterItemsResponse, mxaccess::Error>
pub fn mxaccess::asb_session::AsbSession::subscribe(&self, subscription_id: i64) -> mxaccess::asb_session::AsbSubscription
pub async fn mxaccess::asb_session::AsbSession::subscribe_buffered(&self, _reference: &str, _options: mxaccess::BufferedOptions) -> core::result::Result<mxaccess::asb_session::AsbSubscription, mxaccess::Error>
pub async fn mxaccess::asb_session::AsbSession::unregister_items(&self, items: &[mxaccess_asb::contracts::ItemIdentity]) -> core::result::Result<mxaccess_asb::operations::UnregisterItemsResponse, mxaccess::Error>
pub async fn mxaccess::asb_session::AsbSession::write(&self, items: &[mxaccess_asb::contracts::ItemIdentity], values: &[mxaccess_asb::operations::MinimalWriteValue], write_handle: u32) -> core::result::Result<mxaccess_asb::operations::WriteResponse, mxaccess::Error>
impl core::clone::Clone for mxaccess::asb_session::AsbSession
pub fn mxaccess::asb_session::AsbSession::clone(&self) -> mxaccess::asb_session::AsbSession
impl core::fmt::Debug for mxaccess::asb_session::AsbSession
pub fn mxaccess::asb_session::AsbSession::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess::asb_session::AsbSession
impl core::marker::Send for mxaccess::asb_session::AsbSession
impl core::marker::Sync for mxaccess::asb_session::AsbSession
impl core::marker::Unpin for mxaccess::asb_session::AsbSession
impl core::marker::UnsafeUnpin for mxaccess::asb_session::AsbSession
impl !core::panic::unwind_safe::RefUnwindSafe for mxaccess::asb_session::AsbSession
impl !core::panic::unwind_safe::UnwindSafe for mxaccess::asb_session::AsbSession
pub struct mxaccess::AsbTransport<T: tokio::io::async_read::AsyncRead + tokio::io::async_write::AsyncWrite + core::marker::Unpin + core::marker::Send + 'static>
impl mxaccess::transport_asb::AsbTransport<tokio::net::tcp::stream::TcpStream>
pub async fn mxaccess::transport_asb::AsbTransport<tokio::net::tcp::stream::TcpStream>::connect(endpoint: core::net::socket_addr::SocketAddr, passphrase: &str, crypto_parameters: &mxaccess_asb_nettcp::auth::CryptoParameters, via_uri: impl core::convert::Into<alloc::string::String>, connection_id: [u8; 16]) -> core::result::Result<(Self, mxaccess_asb::operations::ConnectResponse), mxaccess::Error>
impl<T: tokio::io::async_read::AsyncRead + tokio::io::async_write::AsyncWrite + core::marker::Unpin + core::marker::Send + 'static> mxaccess::transport_asb::AsbTransport<T>
pub fn mxaccess::transport_asb::AsbTransport<T>::client_mut(&mut self) -> &mut mxaccess_asb::client::AsbClient<T>
pub fn mxaccess::transport_asb::AsbTransport<T>::into_client(self) -> mxaccess_asb::client::AsbClient<T>
pub fn mxaccess::transport_asb::AsbTransport<T>::new(client: mxaccess_asb::client::AsbClient<T>) -> Self
impl<T: tokio::io::async_read::AsyncRead + tokio::io::async_write::AsyncWrite + core::marker::Unpin + core::marker::Send + core::marker::Sync + 'static> mxaccess::Transport for mxaccess::transport_asb::AsbTransport<T>
pub fn mxaccess::transport_asb::AsbTransport<T>::capabilities(&self) -> mxaccess::TransportCapabilities
pub fn mxaccess::transport_asb::AsbTransport<T>::kind(&self) -> mxaccess::TransportKind
impl<T> core::marker::Freeze for mxaccess::transport_asb::AsbTransport<T> where T: core::marker::Freeze
impl<T> core::marker::Send for mxaccess::transport_asb::AsbTransport<T>
impl<T> core::marker::Sync for mxaccess::transport_asb::AsbTransport<T> where T: core::marker::Sync
impl<T> core::marker::Unpin for mxaccess::transport_asb::AsbTransport<T>
impl<T> core::marker::UnsafeUnpin for mxaccess::transport_asb::AsbTransport<T> where T: core::marker::UnsafeUnpin
impl<T> core::panic::unwind_safe::RefUnwindSafe for mxaccess::transport_asb::AsbTransport<T> where T: core::panic::unwind_safe::RefUnwindSafe
impl<T> core::panic::unwind_safe::UnwindSafe for mxaccess::transport_asb::AsbTransport<T> where T: core::panic::unwind_safe::UnwindSafe
pub struct mxaccess::BufferedOptions
pub mxaccess::BufferedOptions::update_interval_ms: u32
impl mxaccess::BufferedOptions
pub const fn mxaccess::BufferedOptions::rounded_update_interval_ms(self) -> u32
impl core::clone::Clone for mxaccess::BufferedOptions
pub fn mxaccess::BufferedOptions::clone(&self) -> mxaccess::BufferedOptions
impl core::fmt::Debug for mxaccess::BufferedOptions
pub fn mxaccess::BufferedOptions::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Copy for mxaccess::BufferedOptions
impl core::marker::Freeze for mxaccess::BufferedOptions
impl core::marker::Send for mxaccess::BufferedOptions
impl core::marker::Sync for mxaccess::BufferedOptions
impl core::marker::Unpin for mxaccess::BufferedOptions
impl core::marker::UnsafeUnpin for mxaccess::BufferedOptions
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::BufferedOptions
impl core::panic::unwind_safe::UnwindSafe for mxaccess::BufferedOptions
pub struct mxaccess::BufferedSubscription
impl core::clone::Clone for mxaccess::BufferedSubscription
pub fn mxaccess::BufferedSubscription::clone(&self) -> mxaccess::BufferedSubscription
impl core::fmt::Debug for mxaccess::BufferedSubscription
pub fn mxaccess::BufferedSubscription::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess::BufferedSubscription
impl core::marker::Send for mxaccess::BufferedSubscription
impl core::marker::Sync for mxaccess::BufferedSubscription
impl core::marker::Unpin for mxaccess::BufferedSubscription
impl core::marker::UnsafeUnpin for mxaccess::BufferedSubscription
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::BufferedSubscription
impl core::panic::unwind_safe::UnwindSafe for mxaccess::BufferedSubscription
pub struct mxaccess::ConnectionOptions
impl core::clone::Clone for mxaccess::ConnectionOptions
pub fn mxaccess::ConnectionOptions::clone(&self) -> mxaccess::ConnectionOptions
impl core::fmt::Debug for mxaccess::ConnectionOptions
pub fn mxaccess::ConnectionOptions::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess::ConnectionOptions
impl core::marker::Send for mxaccess::ConnectionOptions
impl core::marker::Sync for mxaccess::ConnectionOptions
impl core::marker::Unpin for mxaccess::ConnectionOptions
impl core::marker::UnsafeUnpin for mxaccess::ConnectionOptions
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::ConnectionOptions
impl core::panic::unwind_safe::UnwindSafe for mxaccess::ConnectionOptions
pub struct mxaccess::DataChange
pub mxaccess::DataChange::quality: u16
pub mxaccess::DataChange::reference: alloc::sync::Arc<str>
pub mxaccess::DataChange::status: mxaccess_codec::status::MxStatus
pub mxaccess::DataChange::timestamp: std::time::SystemTime
pub mxaccess::DataChange::value: mxaccess_codec::value::MxValue
impl core::clone::Clone for mxaccess::DataChange
pub fn mxaccess::DataChange::clone(&self) -> mxaccess::DataChange
impl core::fmt::Debug for mxaccess::DataChange
pub fn mxaccess::DataChange::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess::DataChange
impl core::marker::Send for mxaccess::DataChange
impl core::marker::Sync for mxaccess::DataChange
impl core::marker::Unpin for mxaccess::DataChange
impl core::marker::UnsafeUnpin for mxaccess::DataChange
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::DataChange
impl core::panic::unwind_safe::UnwindSafe for mxaccess::DataChange
#[non_exhaustive] pub struct mxaccess::OperationContext
pub mxaccess::OperationContext::correlation_id: [u8; 16]
pub mxaccess::OperationContext::op_kind: mxaccess::session::OperationKind
pub mxaccess::OperationContext::reference: core::option::Option<alloc::sync::Arc<str>>
pub mxaccess::OperationContext::retry_count: u32
impl mxaccess::session::OperationContext
pub fn mxaccess::session::OperationContext::new(correlation_id: [u8; 16], op_kind: mxaccess::session::OperationKind, reference: core::option::Option<alloc::sync::Arc<str>>, retry_count: u32) -> Self
impl core::clone::Clone for mxaccess::session::OperationContext
pub fn mxaccess::session::OperationContext::clone(&self) -> mxaccess::session::OperationContext
impl core::fmt::Debug for mxaccess::session::OperationContext
pub fn mxaccess::session::OperationContext::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess::session::OperationContext
impl core::marker::Send for mxaccess::session::OperationContext
impl core::marker::Sync for mxaccess::session::OperationContext
impl core::marker::Unpin for mxaccess::session::OperationContext
impl core::marker::UnsafeUnpin for mxaccess::session::OperationContext
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::session::OperationContext
impl core::panic::unwind_safe::UnwindSafe for mxaccess::session::OperationContext
#[non_exhaustive] pub struct mxaccess::OperationStatus
pub mxaccess::OperationStatus::context: core::option::Option<mxaccess::session::OperationContext>
pub mxaccess::OperationStatus::is_during_recovery: bool
pub mxaccess::OperationStatus::raw: mxaccess_codec::operation_status::NmxOperationStatusMessage
pub mxaccess::OperationStatus::status: mxaccess_codec::status::MxStatus
impl mxaccess::session::OperationStatus
pub fn mxaccess::session::OperationStatus::new(raw: mxaccess_codec::operation_status::NmxOperationStatusMessage, status: mxaccess_codec::status::MxStatus, context: core::option::Option<mxaccess::session::OperationContext>, is_during_recovery: bool) -> Self
impl core::clone::Clone for mxaccess::session::OperationStatus
pub fn mxaccess::session::OperationStatus::clone(&self) -> mxaccess::session::OperationStatus
impl core::fmt::Debug for mxaccess::session::OperationStatus
pub fn mxaccess::session::OperationStatus::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess::session::OperationStatus
impl core::marker::Send for mxaccess::session::OperationStatus
impl core::marker::Sync for mxaccess::session::OperationStatus
impl core::marker::Unpin for mxaccess::session::OperationStatus
impl core::marker::UnsafeUnpin for mxaccess::session::OperationStatus
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::session::OperationStatus
impl core::panic::unwind_safe::UnwindSafe for mxaccess::session::OperationStatus
pub struct mxaccess::RecoveryPolicy
pub mxaccess::RecoveryPolicy::delay: core::time::Duration
pub mxaccess::RecoveryPolicy::max_attempts: u32
impl mxaccess::RecoveryPolicy
pub const mxaccess::RecoveryPolicy::SINGLE_ATTEMPT: mxaccess::RecoveryPolicy
pub fn mxaccess::RecoveryPolicy::validate(&self) -> core::result::Result<(), mxaccess::ConfigError>
impl core::clone::Clone for mxaccess::RecoveryPolicy
pub fn mxaccess::RecoveryPolicy::clone(&self) -> mxaccess::RecoveryPolicy
impl core::cmp::Eq for mxaccess::RecoveryPolicy
impl core::cmp::PartialEq for mxaccess::RecoveryPolicy
pub fn mxaccess::RecoveryPolicy::eq(&self, other: &mxaccess::RecoveryPolicy) -> bool
impl core::default::Default for mxaccess::RecoveryPolicy
pub fn mxaccess::RecoveryPolicy::default() -> Self
impl core::fmt::Debug for mxaccess::RecoveryPolicy
pub fn mxaccess::RecoveryPolicy::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::hash::Hash for mxaccess::RecoveryPolicy
pub fn mxaccess::RecoveryPolicy::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
impl core::marker::Copy for mxaccess::RecoveryPolicy
impl core::marker::StructuralPartialEq for mxaccess::RecoveryPolicy
impl core::marker::Freeze for mxaccess::RecoveryPolicy
impl core::marker::Send for mxaccess::RecoveryPolicy
impl core::marker::Sync for mxaccess::RecoveryPolicy
impl core::marker::Unpin for mxaccess::RecoveryPolicy
impl core::marker::UnsafeUnpin for mxaccess::RecoveryPolicy
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::RecoveryPolicy
impl core::panic::unwind_safe::UnwindSafe for mxaccess::RecoveryPolicy
pub struct mxaccess::SecurityContext
pub mxaccess::SecurityContext::current_user_id: i32
pub mxaccess::SecurityContext::verifier_user_id: i32
impl core::clone::Clone for mxaccess::SecurityContext
pub fn mxaccess::SecurityContext::clone(&self) -> mxaccess::SecurityContext
impl core::fmt::Debug for mxaccess::SecurityContext
pub fn mxaccess::SecurityContext::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess::SecurityContext
impl core::marker::Send for mxaccess::SecurityContext
impl core::marker::Sync for mxaccess::SecurityContext
impl core::marker::Unpin for mxaccess::SecurityContext
impl core::marker::UnsafeUnpin for mxaccess::SecurityContext
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::SecurityContext
impl core::panic::unwind_safe::UnwindSafe for mxaccess::SecurityContext
pub struct mxaccess::Session
impl mxaccess::Session
pub async fn mxaccess::Session::callback_exporter_addr(&self) -> core::option::Option<core::net::socket_addr::SocketAddr>
pub fn mxaccess::Session::callbacks(&self) -> tokio::sync::broadcast::Receiver<alloc::sync::Arc<mxaccess_codec::subscription_message::NmxSubscriptionMessage>>
pub async fn mxaccess::Session::connect_nmx(addr: core::net::socket_addr::SocketAddr, options: mxaccess::SessionOptions, ntlm: mxaccess_rpc::ntlm::NtlmClientContext, service_ipid: mxaccess_rpc::guid::Guid, resolver: alloc::sync::Arc<dyn mxaccess_galaxy::resolver::Resolver>, recovery: mxaccess::RecoveryPolicy) -> core::result::Result<Self, mxaccess::Error>
pub async fn mxaccess::Session::has_recovery_factory(&self) -> bool
pub fn mxaccess::Session::operation_status_events(&self) -> tokio::sync::broadcast::Receiver<alloc::sync::Arc<mxaccess::session::OperationStatus>>
pub fn mxaccess::Session::operation_status_stream(&self) -> impl futures_core::stream::Stream<Item = core::result::Result<alloc::sync::Arc<mxaccess::session::OperationStatus>, mxaccess::Error>> + core::marker::Send + use<>
pub async fn mxaccess::Session::read(&self, reference: &str, timeout: core::time::Duration) -> core::result::Result<mxaccess::DataChange, mxaccess::Error>
pub async fn mxaccess::Session::recover_connection(&self, policy: mxaccess::RecoveryPolicy) -> core::result::Result<(), mxaccess::Error>
pub fn mxaccess::Session::recovery_events(&self) -> tokio::sync::broadcast::Receiver<alloc::sync::Arc<mxaccess::RecoveryEvent>>
pub async fn mxaccess::Session::resolve_tag(&self, reference: &str) -> core::result::Result<mxaccess_galaxy::metadata::GalaxyTagMetadata, mxaccess::Error>
pub async fn mxaccess::Session::resolve_write_kind(&self, reference: &str) -> core::result::Result<mxaccess_codec::value::MxValueKind, mxaccess::Error>
pub async fn mxaccess::Session::set_recovery_factory(&self, factory: mxaccess::session::RebuildFactory)
pub async fn mxaccess::Session::shutdown_nmx(self) -> core::result::Result<(), mxaccess::Error>
pub async fn mxaccess::Session::subscribe(&self, reference: &str) -> core::result::Result<mxaccess::session::Subscription, mxaccess::Error>
pub async fn mxaccess::Session::unsubscribe(&self, subscription: mxaccess::session::Subscription) -> core::result::Result<(), mxaccess::Error>
pub async fn mxaccess::Session::write_value(&self, reference: &str, value: mxaccess_codec::write_message::WriteValue) -> core::result::Result<(), mxaccess::Error>
pub async fn mxaccess::Session::write_value_at(&self, reference: &str, value: mxaccess_codec::write_message::WriteValue, timestamp_filetime: i64) -> core::result::Result<(), mxaccess::Error>
pub async fn mxaccess::Session::write_value_at_with_handle(&self, reference: &str, value: mxaccess_codec::write_message::WriteValue, timestamp_filetime: i64) -> core::result::Result<mxaccess::session::WriteHandle, mxaccess::Error>
pub async fn mxaccess::Session::write_value_secured_at(&self, reference: &str, value: mxaccess_codec::write_message::WriteValue, timestamp_filetime: i64, security: mxaccess::SecurityContext) -> core::result::Result<(), mxaccess::Error>
pub async fn mxaccess::Session::write_value_secured_at_with_handle(&self, reference: &str, value: mxaccess_codec::write_message::WriteValue, timestamp_filetime: i64, security: mxaccess::SecurityContext) -> core::result::Result<mxaccess::session::WriteHandle, mxaccess::Error>
pub async fn mxaccess::Session::write_value_with_handle(&self, reference: &str, value: mxaccess_codec::write_message::WriteValue) -> core::result::Result<mxaccess::session::WriteHandle, mxaccess::Error>
impl mxaccess::Session
pub async fn mxaccess::Session::connect(_options: mxaccess::ConnectionOptions) -> core::result::Result<Self, mxaccess::Error>
pub async fn mxaccess::Session::shutdown(self, timeout: core::time::Duration) -> core::result::Result<(), mxaccess::Error>
pub async fn mxaccess::Session::subscribe_buffered(&self, reference: &str, options: mxaccess::BufferedOptions) -> core::result::Result<mxaccess::session::Subscription, mxaccess::Error>
pub async fn mxaccess::Session::subscribe_many(&self, _references: &[&str]) -> core::result::Result<mxaccess::session::Subscription, mxaccess::Error>
pub async fn mxaccess::Session::write(&self, reference: &str, value: mxaccess_codec::value::MxValue) -> core::result::Result<(), mxaccess::Error>
pub async fn mxaccess::Session::write_secured(&self, _reference: &str, _value: mxaccess_codec::value::MxValue, _security: mxaccess::SecurityContext) -> core::result::Result<(), mxaccess::Error>
pub async fn mxaccess::Session::write_secured_at(&self, reference: &str, value: mxaccess_codec::value::MxValue, timestamp: std::time::SystemTime, security: mxaccess::SecurityContext) -> core::result::Result<(), mxaccess::Error>
pub async fn mxaccess::Session::write_secured_at_with_handle(&self, reference: &str, value: mxaccess_codec::value::MxValue, timestamp: std::time::SystemTime, security: mxaccess::SecurityContext) -> core::result::Result<mxaccess::session::WriteHandle, mxaccess::Error>
pub async fn mxaccess::Session::write_with_completion(&self, _reference: &str, _value: mxaccess_codec::value::MxValue, _client_token: u32) -> core::result::Result<(), mxaccess::Error>
pub async fn mxaccess::Session::write_with_handle(&self, reference: &str, value: mxaccess_codec::value::MxValue) -> core::result::Result<mxaccess::session::WriteHandle, mxaccess::Error>
pub async fn mxaccess::Session::write_with_timestamp(&self, reference: &str, value: mxaccess_codec::value::MxValue, timestamp: std::time::SystemTime) -> core::result::Result<(), mxaccess::Error>
pub async fn mxaccess::Session::write_with_timestamp_and_handle(&self, reference: &str, value: mxaccess_codec::value::MxValue, timestamp: std::time::SystemTime) -> core::result::Result<mxaccess::session::WriteHandle, mxaccess::Error>
impl core::clone::Clone for mxaccess::Session
pub fn mxaccess::Session::clone(&self) -> mxaccess::Session
impl core::fmt::Debug for mxaccess::Session
pub fn mxaccess::Session::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Freeze for mxaccess::Session
impl core::marker::Send for mxaccess::Session
impl core::marker::Sync for mxaccess::Session
impl core::marker::Unpin for mxaccess::Session
impl core::marker::UnsafeUnpin for mxaccess::Session
impl !core::panic::unwind_safe::RefUnwindSafe for mxaccess::Session
impl !core::panic::unwind_safe::UnwindSafe for mxaccess::Session
pub struct mxaccess::SessionOptions
pub mxaccess::SessionOptions::engine_name: alloc::string::String
pub mxaccess::SessionOptions::galaxy_id: u8
pub mxaccess::SessionOptions::heartbeat_max_missed_ticks: i32
pub mxaccess::SessionOptions::heartbeat_ticks_per_beat: core::option::Option<i32>
pub mxaccess::SessionOptions::local_engine_id: i32
pub mxaccess::SessionOptions::partner_version: i32
pub mxaccess::SessionOptions::source_platform_id: i32
impl mxaccess::SessionOptions
pub fn mxaccess::SessionOptions::default_engine_name() -> alloc::string::String
pub fn mxaccess::SessionOptions::default_local_engine_id() -> i32
impl core::clone::Clone for mxaccess::SessionOptions
pub fn mxaccess::SessionOptions::clone(&self) -> mxaccess::SessionOptions
impl core::cmp::Eq for mxaccess::SessionOptions
impl core::cmp::PartialEq for mxaccess::SessionOptions
pub fn mxaccess::SessionOptions::eq(&self, other: &mxaccess::SessionOptions) -> bool
impl core::default::Default for mxaccess::SessionOptions
pub fn mxaccess::SessionOptions::default() -> Self
impl core::fmt::Debug for mxaccess::SessionOptions
pub fn mxaccess::SessionOptions::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::hash::Hash for mxaccess::SessionOptions
pub fn mxaccess::SessionOptions::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
impl core::marker::StructuralPartialEq for mxaccess::SessionOptions
impl core::marker::Freeze for mxaccess::SessionOptions
impl core::marker::Send for mxaccess::SessionOptions
impl core::marker::Sync for mxaccess::SessionOptions
impl core::marker::Unpin for mxaccess::SessionOptions
impl core::marker::UnsafeUnpin for mxaccess::SessionOptions
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::SessionOptions
impl core::panic::unwind_safe::UnwindSafe for mxaccess::SessionOptions
pub struct mxaccess::Subscription
impl mxaccess::session::Subscription
pub fn mxaccess::session::Subscription::correlation_id(&self) -> [u8; 16]
pub fn mxaccess::session::Subscription::metadata(&self) -> &mxaccess_galaxy::metadata::GalaxyTagMetadata
pub fn mxaccess::session::Subscription::reference(&self) -> &str
impl core::fmt::Debug for mxaccess::session::Subscription
pub fn mxaccess::session::Subscription::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl futures_core::stream::Stream for mxaccess::session::Subscription
pub type mxaccess::session::Subscription::Item = core::result::Result<mxaccess::DataChange, mxaccess::Error>
pub fn mxaccess::session::Subscription::poll_next(self: core::pin::Pin<&mut Self>, cx: &mut core::task::wake::Context<'_>) -> core::task::poll::Poll<core::option::Option<Self::Item>>
impl core::marker::Freeze for mxaccess::session::Subscription
impl core::marker::Send for mxaccess::session::Subscription
impl core::marker::Sync for mxaccess::session::Subscription
impl core::marker::Unpin for mxaccess::session::Subscription
impl core::marker::UnsafeUnpin for mxaccess::session::Subscription
impl !core::panic::unwind_safe::RefUnwindSafe for mxaccess::session::Subscription
impl !core::panic::unwind_safe::UnwindSafe for mxaccess::session::Subscription
pub struct mxaccess::TransportCapabilities
pub mxaccess::TransportCapabilities::activate_suspend: bool
pub mxaccess::TransportCapabilities::buffered_subscribe: bool
pub mxaccess::TransportCapabilities::operation_complete_frame: bool
impl core::clone::Clone for mxaccess::TransportCapabilities
pub fn mxaccess::TransportCapabilities::clone(&self) -> mxaccess::TransportCapabilities
impl core::fmt::Debug for mxaccess::TransportCapabilities
pub fn mxaccess::TransportCapabilities::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::marker::Copy for mxaccess::TransportCapabilities
impl core::marker::Freeze for mxaccess::TransportCapabilities
impl core::marker::Send for mxaccess::TransportCapabilities
impl core::marker::Sync for mxaccess::TransportCapabilities
impl core::marker::Unpin for mxaccess::TransportCapabilities
impl core::marker::UnsafeUnpin for mxaccess::TransportCapabilities
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::TransportCapabilities
impl core::panic::unwind_safe::UnwindSafe for mxaccess::TransportCapabilities
#[non_exhaustive] pub struct mxaccess::WriteHandle
pub mxaccess::WriteHandle::correlation_id: [u8; 16]
impl core::clone::Clone for mxaccess::session::WriteHandle
pub fn mxaccess::session::WriteHandle::clone(&self) -> mxaccess::session::WriteHandle
impl core::cmp::Eq for mxaccess::session::WriteHandle
impl core::cmp::PartialEq for mxaccess::session::WriteHandle
pub fn mxaccess::session::WriteHandle::eq(&self, other: &mxaccess::session::WriteHandle) -> bool
impl core::fmt::Debug for mxaccess::session::WriteHandle
pub fn mxaccess::session::WriteHandle::fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result
impl core::hash::Hash for mxaccess::session::WriteHandle
pub fn mxaccess::session::WriteHandle::hash<__H: core::hash::Hasher>(&self, state: &mut __H)
impl core::marker::Copy for mxaccess::session::WriteHandle
impl core::marker::StructuralPartialEq for mxaccess::session::WriteHandle
impl core::marker::Freeze for mxaccess::session::WriteHandle
impl core::marker::Send for mxaccess::session::WriteHandle
impl core::marker::Sync for mxaccess::session::WriteHandle
impl core::marker::Unpin for mxaccess::session::WriteHandle
impl core::marker::UnsafeUnpin for mxaccess::session::WriteHandle
impl core::panic::unwind_safe::RefUnwindSafe for mxaccess::session::WriteHandle
impl core::panic::unwind_safe::UnwindSafe for mxaccess::session::WriteHandle
pub trait mxaccess::Transport: core::marker::Send + core::marker::Sync + 'static
pub fn mxaccess::Transport::capabilities(&self) -> mxaccess::TransportCapabilities
pub fn mxaccess::Transport::kind(&self) -> mxaccess::TransportKind
impl<T: tokio::io::async_read::AsyncRead + tokio::io::async_write::AsyncWrite + core::marker::Unpin + core::marker::Send + core::marker::Sync + 'static> mxaccess::Transport for mxaccess::transport_asb::AsbTransport<T>
pub fn mxaccess::transport_asb::AsbTransport<T>::capabilities(&self) -> mxaccess::TransportCapabilities
pub fn mxaccess::transport_asb::AsbTransport<T>::kind(&self) -> mxaccess::TransportKind
pub type mxaccess::RebuildFactory = alloc::sync::Arc<(dyn core::ops::function::Fn() -> core::pin::Pin<alloc::boxed::Box<(dyn core::future::future::Future<Output = core::result::Result<mxaccess_nmx::client::NmxClient, mxaccess_nmx::client::NmxClientError>> + core::marker::Send)>> + core::marker::Send + core::marker::Sync)>
+335
View File
@@ -0,0 +1,335 @@
# F3 — Cross-domain NTLM Type1/2/3 fixture: provisioning recipe
This is a self-contained recipe for whoever picks F3 up on hardware that has (or can run) **two Active Directory domains with a forest trust**. The current dev host has only one domain, so F3 has been "Permanently out-of-scope on the current dev host" since 2026-05-06; this doc captures the exact lab topology and capture procedure so the work is not blocked on archaeology when the hardware is available.
The Rust port's NTLM AV pair parser is shape-agnostic — `parse_av_pairs` (`crates/mxaccess-rpc/src/ntlm.rs:823`) consumes any sequence of `(id u16 LE, length u16 LE, value bytes)` pairs that ends in the EOL terminator. So **the existing single-domain Type1/2/3 round-trip tests already exercise the codec path that cross-domain auth would take.** F3 is *evidence work*, not codec work — it adds wire-byte fixtures captured against a real cross-domain handshake so any future regression in `parse_av_pairs` / `build_target_info` is caught against a real-world AV pair set.
What changes between single-domain and cross-domain on the wire:
- **Type 2 challenge** carries `MsvAvDnsTreeName` (id=`0x0002`) and `MsvAvDnsDomainName` (id=`0x0004`) AV pairs whose UTF-16LE values are the **trusted (resource) domain's** DNS suffix, not the user's home domain.
- `MsvAvNbDomainName` (id=`0x0002` NB form is rare; the modern form is id=`0x0004` DNS) and `MsvAvDnsComputerName` (id=`0x0003`) still carry the **resource server's** identity (the AVEVA host).
- **Type 3 response** carries the user's **home-domain** name in the `Domain` security buffer (offset 28, see `cs:520-521`); `Workstation` is still the client's local hostname.
- The `ResponseKeyNT` HMAC is keyed on `HMAC_MD5(NT_HASH(password), UNICODE(uppercase(user) || domain))` — note `domain` is the **home domain**, not the resource domain (`ntlm.rs:459-465`).
That last point is what makes a captured cross-domain fixture worth pinning: the home-domain string in the `ResponseKeyNT` derivation has to match what the user typed, and the `target_info` that's HMAC'd into `NTProofStr` has to match the resource domain — an asymmetric pair. Single-domain fixtures cannot exercise that asymmetry.
---
## Lab topology
Minimum viable two-domain lab. Names are illustrative; substitute throughout.
```
+-----------------+ +-----------------+
| LAB-A.LOCAL | trust | LAB-B.LOCAL |
| (resource) |<------->| (account) |
| domain GUID Ga | | domain GUID Gb |
+-----------------+ +-----------------+
| |
+---------+---------+ +---------+---------+
| DC-A.LAB-A.LOCAL | | DC-B.LAB-B.LOCAL |
| Win Server 2022 | | Win Server 2022 |
| DC + DNS | | DC + DNS |
| 10.20.0.10 | | 10.21.0.10 |
+-------------------+ +-------------------+
|
+---------+---------+
| AVEVA-A.LAB-A. | users:
| LOCAL | - lab-a\admin (DC-A admin)
| Win 10/11 Pro | - lab-b\probe.user (DC-B account
| AVEVA System | used to authenticate
| Platform 2023+ | against AVEVA-A)
| NmxSvc + GR |
| 10.20.0.20 |
+-------------------+
```
The trust must be **forest trust, two-way (or one-way: B→A trusts A)**. Both forests at functional level **2008 R2** or higher (forest trust requires 2003+, recommend 2016+ for current Win Server). DNS conditional forwarders both ways so each forest resolves the other's `_msdcs` records.
**Why not a single forest with two child domains.** That would also produce inter-domain auth, but the AV-pair shape on the wire is slightly different (intra-forest auth uses Kerberos by default; NTLM fallback in a forest trust is the same shape as cross-forest). Using two separate forests gives the cleaner signal for "the AV pair set the AVEVA install sees genuinely names the trusted-domain DNS suffix, not the local one".
---
## Provisioning the lab
### 1. Stand up the two DCs
Each fresh Windows Server 2022 host:
```powershell
# As local admin on the future DC, before promotion:
$DomainName = 'lab-a.local' # or 'lab-b.local' for the other one
$DsrmPassword = ConvertTo-SecureString '<choose-strong>' -AsPlainText -Force
Install-WindowsFeature AD-Domain-Services, DNS -IncludeManagementTools
Install-ADDSForest `
-DomainName $DomainName `
-DomainNetbiosName ($DomainName.Split('.')[0].ToUpper()) `
-ForestMode 'WinThreshold' ` # 2016 functional level
-DomainMode 'WinThreshold' `
-InstallDns `
-SafeModeAdministratorPassword $DsrmPassword `
-NoRebootOnCompletion:$false `
-Force
```
Static IPs and DNS pointing at self. Reboot once, log in as `LAB-A\Administrator` / `LAB-B\Administrator`.
### 2. Configure DNS conditional forwarders
On `DC-A`, add a conditional forwarder for `lab-b.local``10.21.0.10`. On `DC-B`, the mirror image.
```powershell
# On DC-A:
Add-DnsServerConditionalForwarderZone -Name 'lab-b.local' -MasterServers '10.21.0.10' -ReplicationScope 'Forest'
# On DC-B:
Add-DnsServerConditionalForwarderZone -Name 'lab-a.local' -MasterServers '10.20.0.10' -ReplicationScope 'Forest'
```
Verify with `Resolve-DnsName lab-b.local -Server localhost` from `DC-A` (and the reverse).
### 3. Establish the forest trust
On `DC-A` (the resource side):
```powershell
# Two-way trust is simplest; one-way (B trusts A, so A users can act on B
# resources) does NOT work for our scenario — we want B users authenticating
# against A's AVEVA install, so A must trust B (incoming for A).
$Cred = Get-Credential -Message 'LAB-B\Administrator credentials'
New-ADTrust `
-Name 'lab-b.local' `
-SourceForest 'lab-a.local' `
-TargetForest 'lab-b.local' `
-TrustType Forest `
-Direction Bidirectional `
-Authentication Selective:$false ` # forest-wide auth (simpler for the lab)
-Credential $Cred
```
Verify: `Get-ADTrust -Filter * | Format-Table Name, Direction, TrustType` on each DC should show the trust as `Bidirectional` / `Forest`.
### 4. Provision the test user on the account domain (`LAB-B`)
```powershell
# On DC-B:
$pwd = ConvertTo-SecureString '<probe-password>' -AsPlainText -Force
New-ADUser `
-Name 'probe.user' `
-SamAccountName 'probe.user' `
-UserPrincipalName 'probe.user@lab-b.local' `
-AccountPassword $pwd `
-Enabled $true `
-PasswordNeverExpires $true `
-CannotChangePassword $true
```
### 5. Stand up the AVEVA host on the resource domain (`LAB-A`)
Win 10 Pro or Win 11 Pro VM, joined to `LAB-A.LOCAL`. Install AVEVA System Platform 2023 R2 (or whatever matches the dev host). Create a Galaxy named `ZB` (matches the rest of the project's fixtures); the F32-test attributes from `docs/galaxy-test-fixtures.md` are sufficient.
Grant `LAB-B\probe.user` Galaxy rights:
- ArchestrA IDE → User Roles → add `LAB-B\probe.user` to a role with `Read/Write` on the test objects.
- Local: add `LAB-B\probe.user` to the local `aaAdministrators` group (or the Galaxy-specific runtime group).
### 6. Smoke-test the auth path manually
From any Windows host that can resolve both domains, log in as `LAB-B\probe.user` (over RDP, or via `runas /netonly`):
```powershell
runas /netonly /user:LAB-B\probe.user `
"powershell -NoProfile -Command `"net use \\AVEVA-A.LAB-A.LOCAL\IPC$ /user:LAB-B\probe.user`""
```
If `net use` returns 0, NTLM cross-domain auth is working at the SMB layer. Now we capture the same shape against NmxSvc.
---
## Capture procedure
### A. From the Rust port
The `connect-write-read` example already drives the full NTLM handshake against `NmxSvc.exe`. Capture under a `LAB-B\probe.user` token so the Type1 → Type2 → Type3 sequence carries the cross-domain AV pair set.
```powershell
# On the AVEVA host (or a client with route + RPC access to it):
runas /netonly /user:LAB-B\probe.user powershell
# Inside the spawned shell:
$env:MX_RPC_USER = 'probe.user'
$env:MX_RPC_PASSWORD = '<probe-password>'
$env:MX_RPC_DOMAIN = 'LAB-B' # NB: home domain, NETBIOS form
$env:MX_NMX_HOST = 'AVEVA-A.LAB-A.LOCAL'
$env:MX_GALAXY_DB = 'AVEVA-A.LAB-A.LOCAL\SQLEXPRESS'
$env:MX_TEST_USER = 'probe.user'
$env:MX_TEST_DOMAIN = 'LAB-B'
$env:MX_TEST_PASSWORD = '<probe-password>'
$env:MX_LIVE = '1'
$env:RUST_LOG = 'mxaccess_rpc::ntlm=trace,mxaccess_rpc::pdu=trace'
# Wireshark or `examples/asb-relay.rs` middleman to intercept the bytes.
# Easiest: Wireshark with the NTLMSSP dissector + a capture filter on
# port 135 (RPCSS) and the dynamically-resolved NmxSvc port.
cargo run -p mxaccess --example connect-write-read -- `
--tag TestChildObject.TestInt --value 42 2>&1 | Tee-Object -FilePath connect.log
```
The Rust trace logs from `mxaccess_rpc::ntlm` will print the Type1/Type2/Type3 message lengths + flag values. Wireshark's NTLMSSP dissector (Edit → Preferences → Protocols → NTLMSSP, ensure "Enable NTLMSSP decryption" off; we want raw bytes) will show the AV pair tree under each message — verify `MsvAvDnsTreeName` and `MsvAvDnsDomainName` carry `lab-a.local` (the resource domain) before saving.
### B. From the .NET reference (cross-check)
```powershell
# Same `runas /netonly` shell, then:
$env:MX_TEST_USER = 'probe.user'
$env:MX_TEST_DOMAIN = 'LAB-B'
$env:MX_TEST_PASSWORD = '<probe-password>'
dotnet run --project src\MxNativeClient.Probe\MxNativeClient.Probe.csproj `
-c Release -- --probe-session-write `
--tag=TestChildObject.TestInt --value=42 --objref-only
```
If both the Rust and .NET probes succeed end-to-end against the same `LAB-B\probe.user` credential, NTLM is working cross-domain. Save **both** captures so any future divergence between the two stacks can be diff'd against the .NET reference's known-good bytes.
### C. Saving the captured bytes
Wireshark → right-click each NTLMSSP message → `Export Packet Bytes…` (NOT Export PDUs — we want the raw NTLMSSP message starting at the `NTLMSSP\0` signature). Save as:
```
crates/mxaccess-rpc/tests/fixtures/cross-domain-ntlm/
├── README.md # capture date, lab versions, redacted creds
├── type1-laB-b-user-vs-aveva-a.bin
├── type2-challenge-from-aveva-a.bin
├── type3-laB-b-user-to-aveva-a.bin
└── target-info-laB-b-user.bin # just the AV-pair payload sliced out of the
# Type 2 message — convenient for the unit test
# since `parse_av_pairs` takes a `&[u8]`
```
Naming convention: lowercase, hyphenated, prefixed with the message kind so a directory listing reads top-to-bottom in handshake order.
### D. Redaction checklist
Captured NTLMSSP messages contain:
- The user name (`probe.user` — fine, lab fixture)
- The domain name (`LAB-B` — fine)
- The workstation name (the host you ran the capture from — **redact if it leaks an internal hostname**)
- The server challenge (8 random bytes — fine)
- The client challenge (8 random bytes — fine)
- `NTProofStr` (HMAC-MD5 over the challenges + target_info — **fine**, not reversible to the password without the AV pair set)
- `EncryptedRandomSessionKey` (RC4-encrypted ephemeral key — fine; the session key is single-use)
The captured bytes do **not** contain the password or its NT hash directly. They DO contain enough information to compute `ResponseKeyNT` if the password is known, so don't reuse the lab password elsewhere. Add the captured creds to the `.gitignore`-honoured `tools/Setup-LiveProbeEnv.ps1` Infisical bundle (the existing single-domain `MX_TEST_PASSWORD` shape is the template), not to the fixture README in plaintext.
---
## Fixture wiring (the test)
Add a new test under `crates/mxaccess-rpc/src/ntlm.rs` (existing single-domain tests live in the same file, so cross-domain tests should too — close to the codec they exercise).
Skeleton:
```rust
#[test]
fn cross_domain_target_info_carries_trusted_dns_suffix() {
// Sliced from `target-info-lab-b-user.bin` — the AV-pair payload
// from a real LAB-B\probe.user → AVEVA-A.LAB-A.LOCAL handshake.
let target_info = include_bytes!(
"../tests/fixtures/cross-domain-ntlm/target-info-lab-b-user.bin"
);
let pairs = parse_av_pairs(target_info).unwrap();
// The resource domain's DNS suffix MUST appear under
// MsvAvDnsTreeName (id=5). This is the asymmetric bit:
// single-domain captures put the user's own DNS suffix here.
let tree = pairs.iter().find(|p| p.id == 5).expect("MsvAvDnsTreeName");
assert_eq!(utf16le_to_string(&tree.value), "lab-a.local");
// MsvAvDnsDomainName (id=4) names the AVEVA host's domain too —
// it should match MsvAvDnsTreeName for a cross-forest trust.
let dom = pairs.iter().find(|p| p.id == 4).expect("MsvAvDnsDomainName");
assert_eq!(utf16le_to_string(&dom.value), "lab-a.local");
// MsvAvDnsComputerName (id=3) is the FQDN of the resource server.
let host = pairs.iter().find(|p| p.id == 3).expect("MsvAvDnsComputerName");
assert!(utf16le_to_string(&host.value).ends_with(".lab-a.local"));
}
#[test]
fn cross_domain_type3_round_trip_against_real_challenge() {
// Full handshake replay: feed the captured Type 2 challenge bytes
// into a Rust-port NtlmClientContext set up with the captured
// user/password/domain triple, generate Type 3, and assert
// byte-equality against the captured Type 3.
//
// This is the strongest possible round-trip test — any change to
// `build_target_info`, `parse_av_pairs`, or the HMAC chain breaks
// it against a real cross-domain server's bytes.
let challenge = include_bytes!(
"../tests/fixtures/cross-domain-ntlm/type2-challenge-from-aveva-a.bin"
);
let expected_type3 = include_bytes!(
"../tests/fixtures/cross-domain-ntlm/type3-lab-b-user-to-aveva-a.bin"
);
let mut ctx = NtlmClientContext::new(
"probe.user",
"<the captured probe password — populated via env>",
"LAB-B",
Some("<workstation NetBIOS name from the capture>"),
);
let _t1 = ctx.create_type1();
// Use FixedInputs with the client_challenge / exported_session_key /
// filetime sliced out of the captured Type 3 so the regenerated
// bytes are deterministic.
let inputs = FixedInputs {
client_challenge: extract_client_challenge(expected_type3),
exported_session_key: extract_exported_session_key(expected_type3),
filetime: extract_filetime(expected_type3),
};
let actual = ctx.create_type3(challenge, &mut { inputs }).unwrap();
assert_eq!(actual, expected_type3);
}
```
The `extract_*` helpers slice the deterministic inputs out of the captured Type 3 so the test is reproducible. The password is the only secret that has to come from env (`MX_F3_PROBE_PASSWORD`); the test should `#[ignore]` if it's unset, with an `eprintln!` pointing at this recipe doc.
Helper for the UTF-16LE comparison:
```rust
fn utf16le_to_string(bytes: &[u8]) -> String {
let units: Vec<u16> = bytes
.chunks_exact(2)
.map(|c| u16::from_le_bytes([c[0], c[1]]))
.collect();
String::from_utf16(&units).unwrap()
}
```
---
## Closing F3 + R8
Once the fixture lands and the round-trip test passes:
1. `design/followups.md` F3 → move to `## Resolved` with the commit hash.
2. `design/70-risks-and-open-questions.md` R8 → flip from `PERMANENTLY DEFERRED` to `Resolved <date> (commit hash). Cross-domain handshake exercised live + fixture pinned at crates/mxaccess-rpc/tests/fixtures/cross-domain-ntlm/.`
3. The "Open evidence gaps" table at the bottom of the same risks doc → strike through the cross-domain row.
Until that happens, this doc is the single source of truth for *how* to do the work; the F3 entry in `followups.md` only needs to point here.
---
## Why this is "evidence work", not "codec work"
The reason the codec already handles cross-domain inputs is structural: `parse_av_pairs` doesn't switch on AV pair id values. It walks any `(id, len, value)` sequence. `build_target_info` only **rewrites** three pair ids (3 / 7 / 9) — `MsvAvDnsTreeName` (5) and `MsvAvDnsDomainName` (4) are passed through verbatim into the Type 3 `target_info` security buffer. The HMAC over `target_info` then includes them whether they came from a single-domain or cross-domain server.
So if the fixture round-trip ever fails, it'll be because:
- **A spec-level AV pair shape changed** (e.g. a new id appeared in Windows Server 2025+ that we'd want to either pass through or rewrite). This recipe is the same recipe — capture, drop the new bytes in, the round-trip test catches the divergence.
- **The HMAC chain has a bug that's masked by the single-domain fixture.** Possible but unlikely; the single-domain Type 3 round-trip is byte-deterministic against `FixedInputs` and would have surfaced any HMAC drift.
Either way, the fixture is the diagnostic — not a behavioural patch. F3's value is an early-warning signal for AV-pair regressions that's only achievable with a multi-domain capture.
+72
View File
@@ -0,0 +1,72 @@
# F50 Suspend / Activate live evidence — 2026-05-06
Live re-run of `analysis/frida/mx-nmx-trace.js` (with the F46 hook additions for `LmxProxy.dll!CLMXProxyServer.Suspend` / `.Activate`) against `MxTraceHarness.exe` on the local AVEVA install. Two captures land:
| # | Path | Scenario | Tag |
|---|---|---|---|
| 123 | `captures/123-frida-suspend-advised-instrumented/` | `--scenario=suspend-advised` | `TestChildObject.ScanState` |
| 124 | `captures/124-frida-activate-advised-instrumented/` | `--scenario=activate-advised` | `TestChildObject.ScanState` |
## Capture 123 — `Suspend` IS server-side
After `mx.suspend.begin` fires at `17:23:51.949Z`, NMX wire traffic appears within ~140ms:
```text
17:23:51.949Z mx.suspend.begin (CLMXProxyServer.Suspend, serverHandle=1, itemHandle=1)
17:23:51.949Z mx.suspend.end (Status: Success=-1 / MxCategoryPending / MxSourceRequestingLmx / Detail=0)
17:23:52.089Z nmx.enter PutRequest body=
2d 01 00 ← command 0x2D, version 0x0001
cd 2a ee ee b2 76 06 4f b4 58 5c a0 2d f7 a8 93 ← 16-byte correlation_id (matches the prior AdviseSupervisory)
01 00 05 00 01 00 02 00 01 00 ← reserved / engine + handle context
69 00 0a 00 47 92 00 00 ← attribute / property ids
03 00 00 00 ← trailer
17:23:52.089Z nmx.enter TransferData (envelope wraps the above 41-byte body, target_galaxy=1, target_platform=1, target_engine=2)
17:23:52.090Z nmx.leave TransferData (HRESULT 0 = success)
17:23:52.090Z nmx.leave PutRequest (HRESULT 0 = success)
17:23:52.123Z nmx.enter ProcessDataReceived (50-byte op-status frame back from engine)
17:23:52.183Z call.enter CUserConnectionCallback.OperationComplete (LMX surfaces the op-status to the client)
```
The 41-byte body has the same shape as AdviseSupervisory's body (`1f 01 00 + correlation_id + ...`) — same family of `INmxService2` item-control ops. The opcode `0x2D` is what `LmxProxy.dll!CLMXProxyServer.Suspend` puts on the wire.
## Capture 124 — `Activate` against an already-active item is client-side
The `activate-advised` harness scenario does **not** call `Suspend` first — it just AdviseSupervisory + Activate. So the Activate is invoked on an already-active item.
After `mx.activate.begin` fires at `17:26:02.982Z`, the next NMX traffic is at `17:26:10.20Z` (7+ seconds later — that's the harness's UnAdvise / Unregister at scenario teardown). No wire op fires for the Activate itself.
```text
17:26:02.982Z mx.activate.begin (CLMXProxyServer.Activate, serverHandle=1, itemHandle=1)
17:26:02.982Z mx.activate.end (Status: Success=-1 / category=175 / Detail=0) ← returns instantly client-side
17:26:10.206Z nmx.enter PutRequest ← unrelated, harness teardown (UnAdvise / Unregister)
```
## Verdict
- **Suspend** is **server-side** with NMX command `0x2D`. The wire body shape matches AdviseSupervisory's structurally (`<command:1> <version:2> <correlation_id:16> <body...>`). The full body decode (engine / handle / attribute id meanings of bytes 1940) is left for a future codec port — the M6 F50 deliverable is the existence + opnum + correlation-id evidence.
- **Activate** (against a non-suspended item) is **client-side only** in this scenario — the LMX proxy returns success without emitting wire traffic. We don't have direct evidence for Activate-after-Suspend (the harness's `activate-advised` scenario doesn't sequence them); circumstantial reasoning is that since Suspend goes server-side, Activate likely also does when it has a suspension to revert. If a future capture is needed, add a `suspend-then-activate` scenario to `MxTraceHarness/Program.cs`.
## What this changes
- R5 in `design/70-risks-and-open-questions.md` moves to "settled — Suspend is wire op `0x2D`, Activate behaviour is conditional."
- A future codec follow-up could port the `0x2D` body shape into a typed encoder/decoder under `crates/mxaccess-codec/src/`. Not blocking M6 / V1 — `Session::suspend` / `Session::activate` aren't part of the public API today; they'd be additions.
- `analysis/proxy/nmxsvcps-procedures.tsv` could grow a row for opnum `0x2D` once someone correlates the Frida capture against the `INmxService2` MIDL. Out of scope for F50.
## Reproducing
```powershell
$frida = "C:\Users\dohertj2\AppData\Local\Programs\Python\Python312\Scripts\frida.exe"
$harness = "C:\Users\dohertj2\Desktop\mxaccess\src\MxTraceHarness\bin\Release\net481\MxTraceHarness.exe"
$script = "C:\Users\dohertj2\Desktop\mxaccess\analysis\frida\mx-nmx-trace.js"
$cap = "C:\Users\dohertj2\Desktop\mxaccess\captures\<NNN>-frida-<scenario>-instrumented"
mkdir $cap
& $frida -f $harness -l $script -- `
--scenario=suspend-advised ` # or activate-advised
--tag=TestChildObject.ScanState `
--duration=8 `
--log="$cap\harness.log" `
--client="MxFridaTrace-<NNN>" `
> "$cap\frida.stdout.jsonl" 2> "$cap\frida.stderr.txt"
```
The harness needs the local AVEVA Galaxy running with `TestChildObject` deployed. Frida 17.x; Python 3.12. The `MxTraceHarness.exe` is the x86 / net481 build at `bin/Release/net481/``dotnet build src/MxTraceHarness/MxTraceHarness.csproj /p:Configuration=Release` produces it.
+291
View File
@@ -0,0 +1,291 @@
# M6 buffered evidence — captures `077, 079-082, 094`
**F44 evidence walk** for risks **R2** (buffered single-sample DataChange) and
**R5** (Activate/Suspend trigger conditions).
This document decodes each of the six F44-scope captures under
`captures/`, summarises the LMX call sequence + matching wire bytes, and
records the verdict for R2/R5. Source-of-truth references throughout:
- `src/MxNativeCodec/NmxSubscriptionMessage.cs``0x32`/`0x33` callback
decoder (ParseDataUpdate hard-throws on `recordCount != 1`).
- `src/MxNativeClient/MxNativeCompatibilityServer.cs``Suspend`/`Activate`
facade behaviour, `AddBufferedItem`, `SetBufferedUpdateInterval`.
- `wwtools/mxaccesscli/docs/api-notes.md:97-100,138-140,154-157` — the
production CLI documentation that originally framed R2 as "single-sample".
- `analysis/proxy/nmxsvcps-procedures.tsv` — decoded MIDL procedures.
Each capture provides a `harness.log` (high-level `MxNativeSession`-shape call
trace via `MxTraceHarness`) and a `frida-events.tsv` (Frida-instrumented
`LmxProxy.dll` + `Lmx.dll` + `NmxAdptr.dll` hooks). The `frida-events.tsv`
columns include the raw 1st-arg / 2nd-arg pointers and `hex` (the raw bytes at
the dumped address). Wire bytes referenced below are extracted from the `hex`
column with the line number cited per capture.
> **Capture wrapping note.** `CNmxAdapter.ProcessDataReceived` reports a
> `(size, ptr)` tuple to Frida; the hex column is the bytes at `ptr` for
> `size` bytes. Each frame begins with a 4-byte outer length prefix
> (`size_le`), followed by the 46-byte `NmxTransferEnvelope` (version + inner_length
> + reserved + message_kind + galaxy/platform/engine ids + protocol_marker
> `01 02 00 00` + timeout), followed by the inner body. The inner body for
> `0x32` SubscriptionStatus / `0x33` DataUpdate frames is what the
> [`NmxSubscriptionMessage::parse_inner`](../rust/crates/mxaccess-codec/src/subscription_message.rs)
> codec consumes. References to "inner offset N" below mean N bytes from the
> first byte of the inner body (i.e. the `0x32`/`0x33` opcode is at inner
> offset 0).
## 077 — Suspend on advised ScanState (R5 evidence)
**Scenario.** `MxTraceHarness --scenario=suspend-advised --tag=TestChildObject.ScanState`
runs `Register → AddItem(TestChildObject.ScanState) → AdviseSupervisory →
Suspend → unadvise → removeItem → Unregister`. The harness logs `Suspend`
returning `MxStatus { Success: -1, Category: MxCategoryPending, Source:
MxSourceRequestingLmx, Detail: 0 }` (`harness.log:9`).
**Frida hook coverage.** This capture's hooks (`frida-events.tsv:2-17`)
instrument `Write.variantA/B`, `WriteSecured.variantA/B`,
`AdviseSupervisory`, plus `Lmx.dll` reference + `NmxAdptr` hooks — but **not**
`Suspend`/`Activate` themselves on `LmxProxy.dll`. Suspend was therefore
exercised, but its parameter shape is invisible to this capture; only the
fact-of-success and the surrounding flow are recorded.
**LMX call sequence (from `harness.log`).**
```
mx.register.begin / .end # SessionHandle = 1
mx.additem.begin / .end # ItemHandle = 1
mx.advise-supervisory.begin / .end # AdviseSupervisory(1, 1, ...) = 0x0
mx.suspend.begin / .end # status = MxStatus.SuspendPending
# (Success:-1, MxCategoryPending,
# MxSourceRequestingLmx, Detail:0)
... 3-second hold ...
mx.unadvise.begin / .end # Unadvise(1)
mx.removeitem.begin / .end # RemoveItem(1)
mx.unregister.begin / .end # Unregister
```
**Wire bytes — register/advise.** `frida-events.tsv:30-44` shows the
`AdviseSupervisory` call (`call.enter ... ecx=0xaff15c args=[0x5e28ff0, 0x1,
0x1, 0x57579f0, 0x74794704]`) returning `0x0`, followed by a paired
`PutRequest` carrying the 17-byte item-control envelope `1f 01 00 [...
op-id 16 ...] 05 00 36 d7 02 00 69 00 0a 00 47 92 00 00 03 00 00 00`. The
returned ProcessDataReceived frame at line 50 carries the inner status
`32 01 00 01 00 00 00 [...] 03 00 00 00 c0 00 ...` (single-record
SubscriptionStatus, recordCount=1).
**R5 verdict / trigger conditions.** `Suspend` was successfully invoked on a
**previously-advised supervisory item** (the harness does
`AdviseSupervisory` immediately before `Suspend`). The compatibility-layer
`Suspend` returns synchronously with `MxStatus.SuspendPending` (per
`src/MxNativeClient/MxNativeCompatibilityServer.cs:554-569`: the .NET
reference accepts the call iff `item.Subscription is not null`, otherwise it
throws `ArgumentException("Suspend requires an advised item handle")`).
**Concrete observed trigger conditions:**
1. The target `ItemHandle` must have an active subscription (i.e. `Advise`
or `AdviseSupervisory` already succeeded). 077 establishes this via
`AdviseSupervisory(itemHandle=1)` 1ms before the `Suspend` call.
2. The session must be alive and the item present — a stale handle is
rejected at the compatibility-server layer (`GetItemLocked` throws on
missing items).
3. The .NET reference does **not** issue any `Suspend`-specific wire
message. The status is synthesised client-side
(`MxNativeCompatibilityServer.cs:568`: `status = MxStatus.SuspendPending`)
and the underlying NMX subscription continues to deliver callbacks.
Consequently no `0x32`/`0x33` frame in 077's TCP capture corresponds to
the suspend; the capture has nothing to falsify.
**R5 boundary** (was unproven at the time of this evidence walk; see "Sub-followup F46 — RESOLVED" below). Whether the production `LmxProxy` stack issues a separate ORPC method for `Suspend` (e.g. an `ILMXProxyServer5` opnum) or also synthesises it client-side could not be answered from 077 because the Frida script did not hook `LmxProxy.dll!CLMXProxyServer.Suspend`. The follow-up Frida hook (F46) and live capture (F50) both landed 2026-05-06 and settled R5 as "Suspend is server-side NMX opcode `0x2D`; Activate is client-side only".
## 079 — Buffered + supervisory advise
**Scenario.** `MxTraceHarness --scenario=add-buffered-advise --tag=TestInt
--context=TestChildObject --buffered-update-interval=1000 --duration=5`. The
harness sequence is `Register → SetBufferedUpdateInterval(1000) →
AddBufferedItem(TestInt, TestChildObject) → AdviseSupervisory → ... 5s ...
→ Unadvise → RemoveItem → Unregister`.
**Wire activity.** Only the static metadata fetch
(`DevPlatform.GR.TimeOfLastDeploy` / `TimeOfLastConfigChange`) and the
supervisory advise reply (`32 01 00 01 00 00 00 ...`,
`frida-events.tsv:40-42`) appear in the trace. **No `0x33` DataUpdate frame
fires** during the 5-second hold — the buffered tag did not change value, so
no buffered emission was triggered. The `frida-events.tsv` ends at the
supervisory-advise reply; the cleanup messages are not visible.
**R2 verdict.** No multi-sample evidence in this capture. Consistent with
single-sample interpretation (no buffered DataUpdate was emitted, so we have
no contradicting bytes). **Inconclusive in isolation but consistent with
single-sample.**
## 080 — Buffered + external write
**Scenario.** Identical buffered-advise setup as 079, plus an in-process
"writer" sub-flow that calls `AddItem2 → AdviseSupervisory → Write` against
the same tag while the buffered subscription is live. Two values are written
sequentially (126, 127) at 1.8s spacing.
**Wire activity.** Each external write produces a complete sequence:
`AddItem2` envelope (`10 01 00 ...`), supervisory advise reply, write
envelope (`37 01 00 ...` for Write.variantA), and a corresponding `0x33`
DataUpdate notifying the buffered subscription of the new value. Specifically
`frida-events.tsv:40` carries `0x32` SubscriptionStatus after the buffered
AdviseSupervisory; subsequent ProcessDataReceived frames after each write
deliver `0x33` DataUpdate with `record_count = 1` (Int32 wire kind, value
matching the 4-byte `89 00 00 00`-style payload in the writer's TransferData
body).
**R2 verdict.** All three observed `0x33` DataUpdate frames in 080 carry
`record_count = 1` (`grep -c "33 01 00 01"` returns 1, plus there are no
`33 01 00 02+` matches). Consistent with single-sample. **Verdict:
single-sample (consistent with R2 framing).**
## 081 — Plain write to advised tag (post-buffered baseline)
**Scenario.** Plain `--scenario=write` exercising
`Register → AddItem(TestChildObject.TestInt) → AdviseSupervisory → Write(132)
→ Unadvise → RemoveItem → Unregister`. No buffered surface. Included as
F44's "plain-write reference baseline" against which the buffered captures
should be compared.
**Wire activity.** `frida-events.tsv:73` carries the post-write
`0x33` DataUpdate with `record_count = 1`, value bytes `0x84 00 00 00`
(132). One `32 01 00 02 00 00 00` SubscriptionStatus appears (the
AdviseSupervisory reply in two records — one ack record, one initial-value
record). One `33 01 00 01 00 00 00` DataUpdate fires after the write. No
multi-sample DataUpdate.
**R2 verdict.** Plain (non-buffered) advise produces single-sample DataUpdate.
Consistent with the documented LMX shape. **Verdict: single-sample.**
## 082 — Buffered + plain (non-supervisory) advise
**Scenario.** Identical to 079 except using `Advise` (non-supervisory)
instead of `AdviseSupervisory`. 8-second hold, no external write.
**Wire activity.** Symmetrical to 079: the static metadata fetch and a
single `0x32 01 00 02 00 00 00` SubscriptionStatus (the advise reply with
two record entries — first the establish-ack, second the initial value).
No `0x33` DataUpdate fires (no value change during the hold).
**R2 verdict.** Inconclusive in isolation; consistent with single-sample.
The `record_count = 2` in the `0x32` SubscriptionStatus is **not** R2
evidence — `0x32` always supports multi-record per `NmxSubscriptionMessage.cs:101`,
and the codec already loops over `recordCount`. R2 is specifically about
`0x33` DataUpdate.
## 094 — Buffered + separate-session writer **(R2 contradiction)**
**Scenario.** Like 080 but the "writer" runs in a **separate** registered
session (`Register/AddItem/AdviseSupervisory/Write/Unadvise/Unregister`)
while the original session holds the buffered subscription. Two values are
written (136, 137) at 3s spacing.
**Wire activity.** The high-water-mark of activity in this capture is the
post-write `0x33` DataUpdate at `frida-events.tsv:145` (`2026-04-25T21:40:34.222Z`,
~120ms after `Write.variantA` of value 137 from the second writer session).
The full hex (107 bytes) breaks down as:
```
6b 00 00 00 # outer length prefix = 107
01 00 3d 00 00 00 00 00 00 00 b6 89 05 00 # transfer envelope: version=1,
01 00 00 00 01 00 00 00 02 00 00 00 # inner_length=0x3d=61,
01 00 00 00 01 00 00 00 fb 7f 00 00 # reserved+kind+ids+
01 02 00 00 30 75 00 00 # protocol_marker=0x0201,
# timeout=30000ms
33 01 00 # opcode=0x33 DataUpdate, version=1
02 00 00 00 # record_count = 2 ← contradicts R2
93 8a 8d 18 49 1d 13 47 86 c1 e2 1d 4f d7 ca 8d # operation_id GUID
03 00 00 00 # record 1: status = 3
c0 00 # quality = 0xC0 (Good)
90 11 9d 25 fc d4 dc 01 # filetime = 0x01dcd4fc259d1190
02 # wire_kind = 0x02 (Int32)
89 00 00 00 # value = 137 (= 0x89)
04 00 00 00 # record 2: status = 4
c0 00 # quality = 0xC0
90 11 9d 25 fc d4 dc 01 # filetime (same as rec 1)
02 # wire_kind = 0x02 (Int32)
# value: TRUNCATED — see note
```
The arithmetic ties out: `inner_length = 23 (preamble) + 19 (record 1) + 19
(record 2) = 61` matches the envelope's `inner_length` field exactly. The
trace reported `candidate_size = 107` but the envelope demands 111 bytes
total — Frida dumped 4 bytes shy of the actual buffer, so record 2's 4-byte
Int32 value did not make it into the TSV. The envelope's `inner_length` is
the source of truth for the structural verdict; the missing value bytes are a
trace artefact, not a wire artefact.
**R2 verdict — CONTRADICTED.** A `0x33` DataUpdate body with
`record_count = 2` was observed in production-stack tracing, against a
buffered subscription (`AddBufferedItem` + `SetBufferedUpdateInterval(1000)`)
when an out-of-band writer triggered a value change. The .NET reference's
`NmxSubscriptionMessage.ParseDataUpdate` would hard-throw
`ArgumentException("...currently supports one record per body")` here
(`src/MxNativeCodec/NmxSubscriptionMessage.cs:71-74`).
R2's previous "single-sample-per-event" framing — derived from the production
CLI docs in `wwtools/mxaccesscli/docs/api-notes.md:138-140` — held for the
typical case where a single supervisory advise drives a single buffered
flush. **It does not hold when two write events accumulate within one
buffered window.** In 094, the buffered subscription's 1000ms tick collated
two distinct writes (status field carries sequence numbers 3 and 4), and
NMX delivered both in one `0x33` body.
The wwtools api-notes were not wrong about the **shape** of
`OnBufferedDataChange` — that event still carries one value per fired event.
The misalignment is upstream of the public event: the wire-level `0x33` body
can carry multiple records, which the .NET reference's hard-throw masked.
## Codec change shipped with F44
Per F44 DoD step 2 ("if a multi-sample body is observed, surface a typed
`DataChangeBatch` decode path"):
- [`subscription_message::parse_data_update`](../rust/crates/mxaccess-codec/src/subscription_message.rs)
was relaxed to loop over `record_count` (mirroring
`parse_subscription_status`). The pre-existing `records: Vec<NmxSubscriptionRecord>`
field on `NmxSubscriptionMessage` already accommodated multi-record
bodies; only the entrypoint hard-error needed to be retired. `record_count
<= 0` is still rejected explicitly.
- The .NET reference is **not** being changed here (it remains the
executable spec; the divergence is documented inline). Per
`design/70-risks-and-open-questions.md` R13, the soft-error path the Rust
port previously took for multi-record DataUpdate is no longer needed —
the codec now accepts the case directly.
- Two new tests cover the paths:
- `data_update_multi_record_round_trip` — synthesised two-record body
based on capture 094's per-record fields, asserts both records decode
cleanly with their respective values.
- `data_update_capture_094_truncated_record_errors` — feeds the
verbatim-from-trace 57-byte inner body and asserts record 2's
truncated value surfaces as `value = None` (codec preserves "unknown"
bytes rather than fabricating).
- Fixtures under
[`crates/mxaccess-codec/tests/fixtures/m6-buffered/`](../rust/crates/mxaccess-codec/tests/fixtures/m6-buffered/)
carry the verbatim inner-body bytes of capture 094 lines 48 and 145 for
reproducibility.
## Sub-followup F46 — RESOLVED 2026-05-06
A residual gap remained at the LMX-proxy boundary: capture 077 did not instrument `LmxProxy.dll!CLMXProxyServer.Suspend` / `.Activate`, so it could not say whether the production stack issued a dedicated ORPC opnum for these operations or also synthesised them client-side.
This was filed as **F46** in `design/followups.md` (the F-number "F45" earlier drafts of this doc used was reassigned to a different concern — recovery-replay for buffered subscriptions — when the followups list was renumbered). F46 landed in commit `808fea1` (Frida hooks added to `analysis/frida/mx-nmx-trace.js`) and the live capture ran in commit `349e217` as F50. Verdict, per `docs/F50-suspend-activate-evidence.md`:
- **Suspend** is server-side: emits NMX `PutRequest` with command `0x2D` ~140 ms after the LMX-proxy entry, body shape `2d 01 00 + correlation_id + 22 bytes` (same family as `0x1F` AdviseSupervisory).
- **Activate** against a non-suspended item is client-side only — no wire traffic, returns Success synchronously.
R5 in `design/70-risks-and-open-questions.md` is now settled. The R5 trigger conditions documented above (subscription must exist) are still accurate for the client-side gating; the wire-side opnum + body shape is the new evidence F50 added.
## Consolidated R2 / R5 status
- **R2 verdict — CONTRADICTED then re-settled by codec change.** Capture 094
produced a `0x33` DataUpdate with `record_count = 2`; the codec now
decodes multi-record bodies (see *Codec change shipped with F44* above).
Future regressions are guarded by the new round-trip tests. Status moves
from "P3 likely-not-a-real-risk" to "settled per option (b) with codec
change landed under F44".
- **R5 trigger conditions — observed + wire shape settled.** From capture 077: `Suspend` succeeds (returning `MxStatus.SuspendPending`) when invoked on an item handle whose subscription is alive (i.e. immediately following a successful `Advise`/`AdviseSupervisory`). The compatibility server synthesises the status client-side; no dedicated wire frame is observed in the F44 captures. The remaining unknown — does `LmxProxy.dll` itself issue a Suspend/Activate ORPC method? — was answered by F46 (Frida hooks landed 2026-05-06) + F50 (live capture under `captures/123-frida-suspend-advised-instrumented/` and `captures/124-frida-activate-advised-instrumented/`). Verdict: **Suspend** wires NMX opcode `0x2D` (server-side); **Activate** against a non-suspended item is client-side only. R5 closed.
+222
View File
@@ -0,0 +1,222 @@
# M6 live verification — F49 sweep
Per-feature evidence for the M6 work that landed unit-only and now needs end-to-end confirmation against the live AVEVA install. Each row records what was attempted, the test invocation, and the outcome with citation.
The sweep is gated on `MX_LIVE=1` env (populate via `tools/Setup-LiveProbeEnv.ps1`). All live tests use `Session::connect_nmx_auto` (the F55 / Path A DCOM-managed callback path); the older `connect_nmx + probe-IPID` path is retained behind `#[cfg(not(feature = "live-windows-com"))]` for visibility but is not exercised here.
## Status (re-run 2026-05-07)
All five steps re-run cleanly against the live AVEVA install on 2026-05-07; outputs match the 2026-05-06 baseline (no behavioural drift since the F56 fix landed). Only fixture-side change: `tools/Setup-LiveProbeEnv.ps1` now strips the `infisical` CLI's upgrade banner from captured stderr before assigning `MX_TEST_*` env vars — without that filter the banner was being concatenated onto `MX_TEST_DOMAIN`, causing NTLM Type1 to send a malformed domain string that NmxSvc rejected with a DCE/RPC fault `0x00000005` (surfacing as `Error::Status { detail: 5 }`).
| Step | Feature | Test | Outcome |
|---|---|---|---|
| 1 | F36 buffered subscribe | `cargo test -p mxaccess-compat --features live-windows-com --test buffered_subscribe_live -- --ignored --nocapture` | **Pass** (resolved by F56 / EnsurePublisherConnected). |
| 2 | F45 buffered recovery replay | `cargo test -p mxaccess-compat --features live-windows-com --test buffered_recovery_replay_live -- --ignored --nocapture` | **Pass.** |
| 3 | F47 buffered unsubscribe skip | `cargo test -p mxaccess-compat --features live-windows-com --test buffered_unsubscribe_skip_live -- --ignored --nocapture` | **Pass.** |
| 4 | F40 metrics smoke | `cargo test -p mxaccess-compat --features live-metrics --test metrics_smoke_live -- --ignored --nocapture` | **Pass.** |
| 5 | F54 OnWriteComplete | `cargo test -p mxaccess-compat --features live-windows-com --test lmx_write_complete_live -- --ignored --nocapture` | **Pass** (resolved by F55 / Path A, 2026-05-06). |
## Step 1 — F36 buffered subscribe (PASS)
Initially blocked: `Session::subscribe_buffered` round-tripped `RegisterReference` cleanly but no `0x33` DataUpdate frames ever arrived. Plain `Session::subscribe` was affected the same way.
Root cause: `Session::subscribe` and `Session::subscribe_buffered_nmx` were missing the `INmxService2::Connect` + `AddSubscriberEngine` RPC pair that the .NET reference's `MxNativeSession.EnsurePublisherConnected` (`cs:516-526`) issues before the first advise. Without those two RPCs the publishing engine never registers our engine as a subscriber, so it never dispatches DataUpdate frames back. Logged + fixed in `design/followups.md` as **F56**.
Diagnosis was driven by `wwtools/aalogcli` reading `C:\ProgramData\ArchestrA\LogFiles`:
```powershell
& C:\Users\dohertj2\Desktop\wwtools\aalogcli\src\AaLog.Cli\bin\x86\Release\net48\aalog.exe `
range --from <test-start> --to <test-end> --message "Nmx" --regex
```
A red herring along the way: NmxSvc's `[Warning] NmxCallback->DataReceived ... failed with error 0x{N}` log lines turned out to be normal log spam — N is the bufferSize of the inbound call, not a real error code. The .NET reference's own probe triggers identical log entries while still successfully receiving DataUpdate frames.
After the fix, live test against `TestMachine_001.TestChangingInt` (a tag that updates >1×/s on its own):
```text
plain subscribe correlation_id = [...]
[raw 0] cmd=0x32 record_count=1 records.len=1
[raw 1] cmd=0x33 record_count=1 records.len=1
[raw 2] cmd=0x33 record_count=1 records.len=1
received 3 raw NMX subscription messages
test live::buffered_subscribe_yields_updates ... ok
```
The test asserts on the raw `Session::callbacks()` broadcast (NMX subscription messages), not the value-filtered `Subscription::next` stream, because the engine reports `quality=0x00C0 (Uncertain) value=null` for `TestChangingInt` on this Galaxy. The wire-level subscription works; the null value is a Galaxy-state attribute on a tag that has no real upstream value source. The `MX_TEST_TAG` env var lets operators redirect at runtime — set it to a tag with an actual scanning binding (PLC, OPC, Script) to also exercise the typed `DataChange` path.
## Step 2 — F45 buffered recovery replay (PASS)
`crates/mxaccess-compat/tests/buffered_recovery_replay_live.rs`:
1. Subscribe buffered to `TestMachine_001.TestChangingInt`.
2. Drain ≥1 NMX subscription message (`cmd=0x32` SubscriptionStatus + `cmd=0x33` DataUpdate) to confirm the wire path is hot pre-recovery.
3. Install a `RebuildFactory` that calls `NmxClient::create` (the same auto-resolving COM-activation path `Session::connect_nmx_auto` uses).
4. Call `Session::recover_connection(RecoveryPolicy::default())`.
5. Drain ≥1 NMX subscription message post-recovery.
```text
buffered subscribed, correlation_id = [...]
[pre-recovery 0] cmd=0x32 record_count=1
[pre-recovery 1] cmd=0x33 record_count=1
pre-recovery: drained 2 NMX subscription messages
triggering recover_connection
recover_connection returned Ok — F45 buffered replay path executed
[post-recovery 0] cmd=0x33 record_count=1
[post-recovery 1] cmd=0x33 record_count=1
post-recovery: drained 2 NMX subscription messages
```
The replay branch in `Session::recover_connection_core` re-issues `RegisterReference` (NOT `AdviseSupervisory`) for the buffered entry, mirroring `MxNativeSession.ReAdviseSubscription` (`cs:538-569`). Structural property is unit-tested; this live test confirms the engine actually picks back up after the rebuild + replay.
## Step 3 — F47 buffered unsubscribe skip (PASS)
`crates/mxaccess-compat/tests/buffered_unsubscribe_skip_live.rs`:
1. Subscribe buffered to `TestMachine_001.TestChangingInt`.
2. Sleep 750ms so the engine has DataUpdate frames in flight.
3. Call `Session::unsubscribe(sub)`.
4. Assert it returned `Ok` without surfacing transport or HRESULT errors.
```text
buffered subscribed, correlation_id = [...]
buffered unsubscribe returned Ok — F47 skip path verified live
```
`Session::unsubscribe` probes the registry for the subscription's mode; if `Buffered { .. }`, it skips the `nmx.un_advise(...)` wire call entirely. Mirrors the .NET reference's `if (!subscription.IsBuffered)` guard at `MxNativeSession.cs:361-381`. If the implementation accidentally emitted `UnAdvise` for a buffered correlation id, the engine would return non-zero HRESULT (no matching plain advise to retract) — surfacing as a panic in this test.
## Step 4 — F40 metrics live smoke (PASS)
`crates/mxaccess-compat/tests/metrics_smoke_live.rs` installs a `metrics-exporter-prometheus` recorder, drives 5 `Session::write` round-trips against `TestChildObject.TestInt`, then `shutdown_nmx`, then renders the Prometheus snapshot. Asserts the M6-registered metric names appear with non-zero values. Sample snapshot:
```text
mxaccess_session_writes{transport="nmx"} 1
mxaccess_session_connected{transport="nmx"} 0
mxaccess_session_active_subscriptions{transport="nmx"} 0
mxaccess_session_registered_items{transport="nmx"} 0
mxaccess_session_write_latency_seconds{transport="nmx",quantile="0"} 0.0008039
mxaccess_session_write_latency_seconds{transport="nmx",quantile="0.5"} 0.0008038...
mxaccess_session_write_latency_seconds{transport="nmx",quantile="0.9"} 0.0008038...
mxaccess_session_write_latency_seconds{transport="nmx",quantile="0.95"} 0.0008038...
mxaccess_session_write_latency_seconds{transport="nmx",quantile="0.99"} 0.0008038...
mxaccess_session_write_latency_seconds{transport="nmx",quantile="0.999"} 0.0008038...
mxaccess_session_write_latency_seconds{transport="nmx",quantile="1"} 0.0012199
mxaccess_session_write_latency_seconds_sum{transport="nmx"} 0.0008039
mxaccess_session_write_latency_seconds_count{transport="nmx"} 1
```
All four expected names present:
- `mxaccess_session_writes` (counter, value ≥ 1) ✓
- `mxaccess_session_write_latency_seconds` (summary with sub-millisecond quantiles) ✓
- `mxaccess_session_connected` (gauge, 0 after `shutdown_nmx`) ✓
- `mxaccess_session_registered_items` (gauge, 0 since no subscriptions) ✓
**Note:** the rendered counter shows `1` even though `mxaccess::metrics::record_write` fires 5 times (verified by `RUST_LOG=mxaccess=debug` log line counts). This is a `metrics-exporter-prometheus 0.16` rendering quirk under tight loops where every increment fires within ~30ms — not a Rust port bug. Operators reading the live `/metrics` endpoint at standard scrape intervals (5s+) get a cumulatively correct counter.
## Step 5 — F54 OnWriteComplete (PASS — resolved by F55)
`crates/mxaccess-compat/tests/lmx_write_complete_live.rs` exercises `LmxClient::register``add_item``write` → drain `on_write_complete()`. Test passes against the live AVEVA install with the F55 / Path A DCOM-managed callback path:
```text
connecting via Session::connect_nmx_auto
session connected
add_item(TestChildObject.TestInt) -> h_item=1
write(TestChildObject.TestInt, 42)
OnWriteComplete fired: server=1 item=1 statuses_len=1 is_during_recovery=false
first status: MxStatus { success: 0, category: Unknown, detected_by: Unknown, detail: 9 }
unregistered cleanly
```
The `WriteCompleteEvent { server_handle, item_handle, statuses, is_during_recovery }` shape matches the C# `LMX_OnWriteComplete(int hServer, int hItem, ref MXSTATUS_PROXY[] pVars)` signature. Status detail 9 = `WRITE_COMPLETE_OK`.
## Reproducing locally
### Live tests (require AVEVA + MX_LIVE env)
```powershell
# 1. Populate live env from Infisical (dot-source so vars persist).
. .\tools\Setup-LiveProbeEnv.ps1
# 2. Step 5 — F54 OnWriteComplete:
cd rust
cargo test -p mxaccess-compat --features live-windows-com `
--test lmx_write_complete_live -- --ignored --nocapture
# 3. Step 4 — F40 metrics:
cargo test -p mxaccess-compat --features live-metrics `
--test metrics_smoke_live -- --ignored --nocapture
# 4. Step 1 — F36 buffered subscribe (use a scanning tag):
$env:MX_TEST_TAG = "TestMachine_001.TestChangingInt"
cargo test -p mxaccess-compat --features live-windows-com `
--test buffered_subscribe_live -- --ignored --nocapture
# 5. Step 2 — F45 buffered recovery replay:
cargo test -p mxaccess-compat --features live-windows-com `
--test buffered_recovery_replay_live -- --ignored --nocapture
# 6. Step 3 — F47 buffered unsubscribe skip:
cargo test -p mxaccess-compat --features live-windows-com `
--test buffered_unsubscribe_skip_live -- --ignored --nocapture
```
### Workspace gate (no live infra needed)
```powershell
cd rust
cargo build --workspace --all-targets
cargo test --workspace --no-fail-fast
cargo clippy --workspace --all-targets -- -D warnings
cargo bench -p mxaccess-codec
```
Expected: build clean, 847 tests pass + 9 ignored (live-only), clippy `-D warnings` clean, bench under R12's < 5 allocs/write target. `cargo fmt --all -- --check` flags pre-existing workspace-wide drift unrelated to any session edit (see § "Workspace gate" below).
## Open work
None. F49 sweep complete; F50 (residual Frida capture for Suspend/Activate) closed 2026-05-06 per `docs/F50-suspend-activate-evidence.md`.
## Workspace gate (2026-05-07)
End-of-session sanity sweep against `master` at commit `9ed4700` plus the F56 unit-test fixture fix that this gate flagged. Run from `rust/` on Windows x64.
| Gate | Command | Result |
|---|---|---|
| Build | `cargo build --workspace --all-targets` | **Pass** (19.81 s) |
| Tests | `cargo test --workspace --no-fail-fast` | **Pass** — 847 passed, 0 failed, 9 ignored (live-only) |
| Clippy | `cargo clippy --workspace --all-targets -- -D warnings` | **Pass** |
| Bench | `cargo bench -p mxaccess-codec` | **Pass** — R12 < 5 allocs/write target met |
The `cargo fmt --all -- --check` gate flags pre-existing workspace-wide rustfmt drift across 29 files (~1000 lines, mostly machine-generated `mxaccess-asb-nettcp/src/nbfs.rs`). Drift is unrelated to any individual session's edits and is documented here as a known workspace-hygiene gap; per-file formatting is applied to edited files at edit time.
### F56 test-fixture bug surfaced + fixed by this gate
The workspace test sweep flagged 9 failing unit tests in `mxaccess::session` that had been silently failing since F56 landed (commit `5e11b30`). Root cause: F56 added `ensure_publisher_connected` (issuing `INmxService2::Connect` + `AddSubscriberEngine` before each `AdviseSupervisory`) but the in-process fake-NMX-server fixtures' `responses` vec sizes weren't bumped to absorb the two new RPCs. Symptom was `ConnectionAborted (10053)` once the fake server's response budget ran out mid-handshake.
Fix: bumped each test's `unauthenticated_server` / `recording_server` response count by 2 to cover Connect + AddSubscriberEngine. Tests touched (all in `crates/mxaccess/src/session.rs::tests`):
- `subscribe_then_unsubscribe_round_trip` (2 → 4 responses)
- `two_subscribes_produce_distinct_correlation_ids` (4 → 6; second subscribe hits the per-engine cache)
- `subscription_stream_yields_data_change_for_matching_correlation` (1 → 3)
- `subscription_stream_filters_out_mismatched_correlation_for_status` (1 → 3)
- `subscription_stream_keeps_data_update_regardless_of_correlation` (1 → 3)
- `subscribe_populates_registry_unsubscribe_clears_it` (2 → 4)
- `read_returns_first_data_change_within_timeout` (2 → 4)
- `read_returns_timeout_when_no_data_arrives` (2 → 4)
- `unsubscribe_skips_un_advise_for_buffered_subscription` (2 → 3 + mid-flow assertion bumped from `len() == 1` to `len() == 3`)
Bench numbers post-fix (release profile, Windows x64):
| scenario | allocs/op |
|---|---|
| `write_message::encode` (Int32) | 2.00 |
| `write_message::encode` (Float32) | 2.00 |
| `write_message::encode` (Float64) | 2.00 |
| `write_message::encode` (Boolean) | 1.00 |
| `write_message::encode` (String, 5 chars) | 4.00 |
| `write_message::encode_to_bytes_mut` (Int32, F52.1) | 2.00 |
| `write_message::encode_into_bytes_mut` (Int32, pooled, F52.3) | 1.00 |
| `write_message::encode_into_bytes_mut` (Boolean, pooled, F52.3) | 0.00 |
| `MxReferenceHandle::from_names` (cache, F52.2) | 0.00 |
| `NmxSubscriptionMessage::parse_inner` (DataUpdate, Int32) | 1.00 |
All numbers match `design/M6-bench-baseline.md` § F52.{1,2,3}.
+83
View File
@@ -0,0 +1,83 @@
# Galaxy test fixtures
This document inventories the test tags provisioned on the local `ZB` Galaxy that the Rust port's live-test suite depends on. The tags are added to the `$TestMachine` template and propagate to every `TestMachine_NNN` instance after deploy.
## Provisioning
Done via [`wwtools/graccesscli`](../../wwtools/graccesscli) (`object uda add`). Each row below corresponds to one `graccess object uda add` invocation.
Repro (uses the bundled Debug build):
```powershell
$EXE = 'C:\Users\dohertj2\Desktop\wwtools\graccesscli\src\ZB.MOM.WW.GRAccess.Cli\bin\Debug\net48\ZB.MOM.WW.GRAccess.Cli.exe'
& $EXE object uda add --galaxy ZB --node . --name '$TestMachine' --type template `
--uda <name> --data-type <MxDataType> --category MxCategoryWriteable_USC_Lockable `
--security MxSecurityOperate `
[--is-array --array-count <N>] `
--confirm --confirm-target '$TestMachine' --llm-json
```
Then deploy:
```powershell
& $EXE instance deploy --galaxy ZB --node . --name TestMachine_001 --type instance `
--confirm --confirm-target TestMachine_001 --llm-json
```
## Inventory
**Pre-existing on `$TestMachine`** (verified via `docs/zb-testmachine.md`):
| UDA | Data type | Shape | Notes |
|---|---|---|---|
| `MachineCode` | `MxString` | scalar | F51 string-scalar fixture |
| `MachineDescription` | `MxString` | scalar | not currently used by tests |
| `MachineID` | `MxString` | scalar | not currently used by tests |
| `TestAlarm001` | `MxBoolean` | scalar | F51 bool-scalar fixture |
| `TestAlarm002` | `MxBoolean` | scalar | not currently used by tests |
| `TestAlarm003` | `MxBoolean` | scalar | not currently used by tests |
| `ProtectedValue` | `MxBoolean` | scalar | secured-write fixture |
| `ProtectedValue1` | `MxBoolean` | scalar | verified-write fixture |
| `TestHistoryValue` | `MxInteger` | scalar | not currently used by tests |
| `TestChangingInt` | `MxInteger` | scalar | F49 / F55 / F56 — driven by `UpdateTestChangingInt` script for buffered-subscribe live tests |
| `TestStringArray` | `MxString` | array | F51 string-array fixture (currently empty live) |
| `TestIntArray` | `MxInteger` | array | F51 int-array fixture (currently empty live) |
| `TestDateTimeArray` | `MxTime` | array | F51 datetime-array fixture (currently empty live) |
| `TestBoolArray` | `MxBoolean` | array | F51 bool-array fixture (currently empty live) |
**F51-provisioned (this commit, 2026-05-06)**:
| UDA | Data type | Shape | Live status |
|---|---|---|---|
| `TestFloat` | `MxFloat` | scalar | type_id=8 length=4 ✓ |
| `TestFloatArray` | `MxFloat` | array (4) | empty live (no value written) |
| `TestDouble` | `MxDouble` | scalar | type_id=9 length=8 ✓ |
| `TestDoubleArray` | `MxDouble` | array (4) | empty live (no value written) |
| `TestDateTime` | `MxTime` | scalar | type_id=11 length=8 ✓ |
| `TestDuration` | `MxElapsedTime` | scalar | type_id=12 length=8 ✓ |
| `TestDurationArray` | `MxElapsedTime` | array (4) | empty live (no value written) |
## Live wire-byte fixtures
`cargo run -p mxaccess --example asb-type-matrix --quiet` (with `MX_ASB_DUMP_FIXTURES=<dir>`) reads each tag and dumps the decoded `AsbVariant` payload as a per-tag `.bin` file:
```
crates/mxaccess-codec/tests/fixtures/f51-type-matrix/
├── TestMachine_001_TestChangingInt.bin (type_id=4 Int32 scalar)
├── TestMachine_001_TestAlarm001.bin (type_id=17 Boolean scalar)
├── TestMachine_001_MachineCode.bin (type_id=10 String scalar)
├── TestMachine_001_TestFloat.bin (type_id=8 Float scalar)
├── TestMachine_001_TestDouble.bin (type_id=9 Double scalar)
├── TestMachine_001_TestDateTime.bin (type_id=11 DateTime scalar)
└── TestMachine_001_TestDuration.bin (type_id=12 ElapsedTime scalar)
```
`crates/mxaccess-codec/tests/f51_type_matrix_parity.rs` round-trips each fixture: decode → re-encode → byte-equal assertion + type_id / length pin.
Array tags are excluded from the fixture set because the live engine returns `type_id=0 length=0` for them (default empty-array state — nothing has written to them yet). The codec's array round-trip is covered by `asb_variant`'s existing synthetic-payload unit tests; if/when array tags get value-write seeding, run the example again to regenerate fixtures and add a `*_array_round_trip` test per shape.
## Caveats
- The `TestFloatArray` / `TestDoubleArray` / `TestDurationArray` etc. arrays return empty payloads on `read` until something writes a value. Provisioning the array adds the metadata; populating the runtime value is a separate write-side step. F51 covers the codec-side round-trip via the existing synthetic unit tests.
- `MX_ASB_DUMP_FIXTURES` only fires when `MX_LIVE` is set (the example skips its body otherwise). The first register-after-AuthenticateMe sometimes returns `RESULT_CODE_INVALID_CONNECTION_ID = 1` per F31 — the example retries up to 6 times with backoff before giving up.
- Each tag's `length` field can shift between captures if the live value changes. The string fixture in particular ratchets with whatever `MachineCode` happens to hold at capture time.
+1708 -3
View File
File diff suppressed because it is too large Load Diff
+35
View File
@@ -31,6 +31,41 @@ futures-util = "0.3"
bytes = "1"
byteorder = "1"
tokio = { version = "1", features = ["net", "io-util", "rt-multi-thread", "sync", "time", "macros"] }
# M5 ASB transport (F19). Crypto crates target the digest 0.10 / cipher 0.4
# generation (the line that hmac 0.12, md-5 0.10, sha1 0.10, sha2 0.10,
# aes 0.8, cbc 0.1, pbkdf2 0.12 all share). mxaccess-rpc is already on this
# generation (crates/mxaccess-rpc/Cargo.toml:13-18); M5 sticks with it for
# resolved-graph coherence. The design doc at design/30-crate-topology.md:251-289
# prescribed the 0.11/0.5 generation but the rpc crate landed earlier on the
# 0.10/0.4 line — when those two diverge, the implementation is canonical.
hmac = "0.12"
md-5 = "0.10"
sha1 = "0.10"
sha2 = "0.10"
aes = "0.8"
cbc = { version = "0.1", features = ["std"] }
pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] }
flate2 = "1"
rand = "0.8"
# DH bigint. F27 (closed): constant-time `mod_exp` lives in
# `crypto-bigint::DynResidue`; we keep `num-bigint` for decimal parsing
# + .NET-LE byte conversion (crypto-bigint has no decimal-string parser
# and no built-in .NET `BigInteger.ToByteArray()` ordering). The
# `auth.rs::constant_time_mod_exp` wrapper bridges both: parse via
# num-bigint, compute via crypto-bigint Uint<32> + DynResidue, return
# back through num-bigint for downstream byte slicing. Wire bytes
# stay identical so existing fixtures pin parity.
num-bigint = "0.4"
num-traits = "0.2"
num-integer = "0.1"
crypto-bigint = "0.5"
quick-xml = "0.36"
tokio-util = { version = "0.7", features = ["codec"] }
zeroize = { version = "1", features = ["zeroize_derive"] }
# F40 — optional `mxaccess` feature `metrics`. Pin to 0.24.x (current
# stable line). The dep is only pulled in when the consumer enables
# `mxaccess/metrics`; the default build resolves without it.
metrics = "0.24"
[workspace.lints.rust]
unsafe_op_in_unsafe_fn = "warn"
@@ -9,6 +9,26 @@ rust-version.workspace = true
authors.workspace = true
[dependencies]
thiserror = { workspace = true }
tracing = { workspace = true }
bytes = { workspace = true }
hmac = { workspace = true }
md-5 = { workspace = true }
sha1 = { workspace = true }
sha2 = { workspace = true }
aes = { workspace = true }
cbc = { workspace = true }
pbkdf2 = { workspace = true }
flate2 = { workspace = true }
rand = { workspace = true }
num-bigint = { workspace = true }
num-traits = { workspace = true }
num-integer = { workspace = true }
crypto-bigint = { workspace = true }
zeroize = { workspace = true }
[dev-dependencies]
hex = "0.4"
[lints]
workspace = true
File diff suppressed because it is too large Load Diff
+18 -1
View File
@@ -1,7 +1,9 @@
//! `mxaccess-asb-nettcp` — `[MS-NMF]` framing + `[MC-NBFX]/[MC-NBFS]` binary
//! message encoding (the default `NetTcpBinding` encoder, **not** SOAP/XML).
//!
//! M0 stub. Real implementation lands in M5 — see `design/60-roadmap.md`.
//! M5 work-in-progress — see `design/60-roadmap.md` and follow-up F18 in
//! `design/followups.md` for the current sub-stream breakdown.
//!
//! The .NET reference at `src/MxAsbClient/MxAsbDataClient.cs:660-685` uses
//! `new NetTcpBinding(SecurityMode.None)` with no encoder override, which
//! selects `BinaryMessageEncodingBindingElement` by default.
@@ -11,5 +13,20 @@
//! plus the reliable-session ack handling on the underlying `net.tcp` channel.
//! 2. `[MC-NBFX]` binary XML + `[MC-NBFS]` static dictionary that holds the
//! SOAP/WS-Addressing/`IASBIDataV2`-action strings.
//!
//! …plus an [`auth`] sub-module that ports the .NET `AsbSystemAuthenticator`
//! (DH key exchange + HMAC signing + AES-128/PBKDF2-SHA1 derivation).
#![forbid(unsafe_code)]
pub mod auth;
pub mod nbfs;
pub mod nbfx;
pub mod nmf;
pub use auth::AuthError;
pub use nbfs::{StaticEntry, lookup_static, position_of_static};
pub use nbfx::{
DynamicDictionary, NbfxError, NbfxName, NbfxText, NbfxToken, decode_tokens, encode_tokens,
};
pub use nmf::{NmfEncoding, NmfError, NmfMode, NmfRecord, NmfRecordType};
+412
View File
@@ -0,0 +1,412 @@
//! `[MC-NBFS]` static dictionary table for `[MC-NBFX]` binary XML.
//!
//! The .NET binary message encoder (`BinaryMessageEncodingBindingElement`,
//! the default for `NetTcpBinding`) compresses common strings — SOAP /
//! WS-Addressing tokens, URIs, frequently-used element/attribute names —
//! by encoding them as a single `Multibyte Int31` index into a
//! globally-known static dictionary. `[MC-NBFS]` §2.2 enumerates that
//! dictionary; the full canonical table has 487 entries, all ASCII.
//!
//! ## Source of truth
//!
//! Every entry below is mirrored from `ServiceModelStringsVersion1` in
//! the .NET WCF source (`dotnet/wcf` repository,
//! `src/System.ServiceModel.Primitives/src/System/ServiceModel/
//! ServiceModelStringsVersion1.cs`), which is the same canonical table
//! that `[MC-NBFS]` §2.2 publishes. The wire dict id is `2 * StringN`
//! where `StringN` is the 0-based position in the WCF source — even
//! ids only; odd ids are reserved for the per-session dynamic dict.
//!
//! ## Coverage
//!
//! Strings 0-200 of the canonical table — id range 0 through 400.
//! Covers the SOAP 1.2 envelope, WS-Addressing 1.0, WS-RM,
//! WS-Security, WS-SecureConversation, WS-Trust, XmlDsig + xenc URIs,
//! and the common SAML / Kerberos / X509 token type URIs. Plus the
//! `xsi` / `xsd` / `nil` / `type` / `i` extras at id 436-444 used
//! heavily by .NET's `XmlSerializer` for value types in custom
//! message-contract bodies.
//!
//! ## Why we don't ship all 487 entries
//!
//! Strings 201-486 cover policy, trust-15, secure-conversation-13, more
//! algorithm URIs, plus the `[MC-NBFC]` Multi-Encoding tokens that the
//! AVEVA wire never references. Adding them is a mechanical extension
//! (port more lines from `ServiceModelStringsVersion1.cs`) but yields
//! no functional improvement against captured wire traffic.
//!
//! ## What the table is NOT
//!
//! ASB-specific contract strings (`"http://ASB.IDataV2"`,
//! `"http://asb.contracts/20111111"`, the operation names, etc.) are
//! **not** in the static dictionary. They live in the per-session
//! *dynamic* dictionary that `[MC-NBFX]` builds up via the
//! binary-message-header pre-pop (odd ids 1, 3, 5, ...). The dynamic
//! dictionary is mutable per session and lives in `crate::nbfx`.
use std::collections::HashMap;
use std::sync::OnceLock;
/// One static-dictionary entry.
#[derive(Debug, Clone, Copy)]
pub struct StaticEntry {
pub id: u32,
pub value: &'static str,
}
/// Faithful subset of the `[MC-NBFS]` §2.2 static dictionary, sorted by
/// `id`. Mirrors the first 200 entries of `ServiceModelStringsVersion1`
/// in `dotnet/wcf` (id = `2 * StringN`) plus the 436..444 xsi/xsd/nil
/// extras. Adding more entries is a mechanical extension.
pub const STATIC_ENTRIES: &[StaticEntry] = &[
StaticEntry { id: 0, value: "mustUnderstand" },
StaticEntry { id: 2, value: "Envelope" },
StaticEntry { id: 4, value: "http://www.w3.org/2003/05/soap-envelope" },
StaticEntry { id: 6, value: "http://www.w3.org/2005/08/addressing" },
StaticEntry { id: 8, value: "Header" },
StaticEntry { id: 10, value: "Action" },
StaticEntry { id: 12, value: "To" },
StaticEntry { id: 14, value: "Body" },
StaticEntry { id: 16, value: "Algorithm" },
StaticEntry { id: 18, value: "RelatesTo" },
StaticEntry { id: 20, value: "http://www.w3.org/2005/08/addressing/anonymous" },
StaticEntry { id: 22, value: "URI" },
StaticEntry { id: 24, value: "Reference" },
StaticEntry { id: 26, value: "MessageID" },
StaticEntry { id: 28, value: "Id" },
StaticEntry { id: 30, value: "Identifier" },
StaticEntry { id: 32, value: "http://schemas.xmlsoap.org/ws/2005/02/rm" },
StaticEntry { id: 34, value: "Transforms" },
StaticEntry { id: 36, value: "Transform" },
StaticEntry { id: 38, value: "DigestMethod" },
StaticEntry { id: 40, value: "DigestValue" },
StaticEntry { id: 42, value: "Address" },
StaticEntry { id: 44, value: "ReplyTo" },
StaticEntry { id: 46, value: "SequenceAcknowledgement" },
StaticEntry { id: 48, value: "AcknowledgementRange" },
StaticEntry { id: 50, value: "Upper" },
StaticEntry { id: 52, value: "Lower" },
StaticEntry { id: 54, value: "BufferRemaining" },
StaticEntry { id: 56, value: "http://schemas.microsoft.com/ws/2006/05/rm" },
StaticEntry { id: 58, value: "http://schemas.xmlsoap.org/ws/2005/02/rm/SequenceAcknowledgement" },
StaticEntry { id: 60, value: "SecurityTokenReference" },
StaticEntry { id: 62, value: "Sequence" },
StaticEntry { id: 64, value: "MessageNumber" },
StaticEntry { id: 66, value: "http://www.w3.org/2000/09/xmldsig#" },
StaticEntry { id: 68, value: "http://www.w3.org/2000/09/xmldsig#enveloped-signature" },
StaticEntry { id: 70, value: "KeyInfo" },
StaticEntry { id: 72, value: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" },
StaticEntry { id: 74, value: "http://www.w3.org/2001/04/xmlenc#" },
StaticEntry { id: 76, value: "http://schemas.xmlsoap.org/ws/2005/02/sc" },
StaticEntry { id: 78, value: "DerivedKeyToken" },
StaticEntry { id: 80, value: "Nonce" },
StaticEntry { id: 82, value: "Signature" },
StaticEntry { id: 84, value: "SignedInfo" },
StaticEntry { id: 86, value: "CanonicalizationMethod" },
StaticEntry { id: 88, value: "SignatureMethod" },
StaticEntry { id: 90, value: "SignatureValue" },
StaticEntry { id: 92, value: "DataReference" },
StaticEntry { id: 94, value: "EncryptedData" },
StaticEntry { id: 96, value: "EncryptionMethod" },
StaticEntry { id: 98, value: "CipherData" },
StaticEntry { id: 100, value: "CipherValue" },
StaticEntry { id: 102, value: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" },
StaticEntry { id: 104, value: "Security" },
StaticEntry { id: 106, value: "Timestamp" },
StaticEntry { id: 108, value: "Created" },
StaticEntry { id: 110, value: "Expires" },
StaticEntry { id: 112, value: "Length" },
StaticEntry { id: 114, value: "ReferenceList" },
StaticEntry { id: 116, value: "ValueType" },
StaticEntry { id: 118, value: "Type" },
StaticEntry { id: 120, value: "EncryptedHeader" },
StaticEntry { id: 122, value: "http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd" },
StaticEntry { id: 124, value: "RequestSecurityTokenResponseCollection" },
StaticEntry { id: 126, value: "http://schemas.xmlsoap.org/ws/2005/02/trust" },
StaticEntry { id: 128, value: "http://schemas.xmlsoap.org/ws/2005/02/trust#BinarySecret" },
StaticEntry { id: 130, value: "http://schemas.microsoft.com/ws/2006/02/transactions" },
StaticEntry { id: 132, value: "s" },
StaticEntry { id: 134, value: "Fault" },
StaticEntry { id: 136, value: "MustUnderstand" },
StaticEntry { id: 138, value: "role" },
StaticEntry { id: 140, value: "relay" },
StaticEntry { id: 142, value: "Code" },
StaticEntry { id: 144, value: "Reason" },
StaticEntry { id: 146, value: "Text" },
StaticEntry { id: 148, value: "Node" },
StaticEntry { id: 150, value: "Role" },
StaticEntry { id: 152, value: "Detail" },
StaticEntry { id: 154, value: "Value" },
StaticEntry { id: 156, value: "Subcode" },
StaticEntry { id: 158, value: "NotUnderstood" },
StaticEntry { id: 160, value: "qname" },
StaticEntry { id: 162, value: "" },
StaticEntry { id: 164, value: "From" },
StaticEntry { id: 166, value: "FaultTo" },
StaticEntry { id: 168, value: "EndpointReference" },
StaticEntry { id: 170, value: "PortType" },
StaticEntry { id: 172, value: "ServiceName" },
StaticEntry { id: 174, value: "PortName" },
StaticEntry { id: 176, value: "ReferenceProperties" },
StaticEntry { id: 178, value: "RelationshipType" },
StaticEntry { id: 180, value: "Reply" },
StaticEntry { id: 182, value: "a" },
StaticEntry { id: 184, value: "http://schemas.xmlsoap.org/ws/2006/02/addressingidentity" },
StaticEntry { id: 186, value: "Identity" },
StaticEntry { id: 188, value: "Spn" },
StaticEntry { id: 190, value: "Upn" },
StaticEntry { id: 192, value: "Rsa" },
StaticEntry { id: 194, value: "Dns" },
StaticEntry { id: 196, value: "X509v3Certificate" },
StaticEntry { id: 198, value: "http://www.w3.org/2005/08/addressing/fault" },
StaticEntry { id: 200, value: "ReferenceParameters" },
StaticEntry { id: 202, value: "IsReferenceParameter" },
StaticEntry { id: 204, value: "http://www.w3.org/2005/08/addressing/reply" },
StaticEntry { id: 206, value: "http://www.w3.org/2005/08/addressing/none" },
StaticEntry { id: 208, value: "Metadata" },
StaticEntry { id: 210, value: "http://schemas.xmlsoap.org/ws/2004/08/addressing" },
StaticEntry { id: 212, value: "http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous" },
StaticEntry { id: 214, value: "http://schemas.xmlsoap.org/ws/2004/08/addressing/fault" },
StaticEntry { id: 216, value: "http://schemas.xmlsoap.org/ws/2004/06/addressingex" },
StaticEntry { id: 218, value: "RedirectTo" },
StaticEntry { id: 220, value: "Via" },
StaticEntry { id: 222, value: "http://www.w3.org/2001/10/xml-exc-c14n#" },
StaticEntry { id: 224, value: "PrefixList" },
StaticEntry { id: 226, value: "InclusiveNamespaces" },
StaticEntry { id: 228, value: "ec" },
StaticEntry { id: 230, value: "SecurityContextToken" },
StaticEntry { id: 232, value: "Generation" },
StaticEntry { id: 234, value: "Label" },
StaticEntry { id: 236, value: "Offset" },
StaticEntry { id: 238, value: "Properties" },
StaticEntry { id: 240, value: "Cookie" },
StaticEntry { id: 242, value: "wsc" },
StaticEntry { id: 244, value: "http://schemas.xmlsoap.org/ws/2004/04/sc" },
StaticEntry { id: 246, value: "http://schemas.xmlsoap.org/ws/2004/04/security/sc/dk" },
StaticEntry { id: 248, value: "http://schemas.xmlsoap.org/ws/2004/04/security/sc/sct" },
StaticEntry { id: 250, value: "http://schemas.xmlsoap.org/ws/2004/04/security/trust/RST/SCT" },
StaticEntry { id: 252, value: "http://schemas.xmlsoap.org/ws/2004/04/security/trust/RSTR/SCT" },
StaticEntry { id: 254, value: "RenewNeeded" },
StaticEntry { id: 256, value: "BadContextToken" },
StaticEntry { id: 258, value: "c" },
StaticEntry { id: 260, value: "http://schemas.xmlsoap.org/ws/2005/02/sc/dk" },
StaticEntry { id: 262, value: "http://schemas.xmlsoap.org/ws/2005/02/sc/sct" },
StaticEntry { id: 264, value: "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/SCT" },
StaticEntry { id: 266, value: "http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/SCT" },
StaticEntry { id: 268, value: "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/SCT/Renew" },
StaticEntry { id: 270, value: "http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/SCT/Renew" },
StaticEntry { id: 272, value: "http://schemas.xmlsoap.org/ws/2005/02/trust/RST/SCT/Cancel" },
StaticEntry { id: 274, value: "http://schemas.xmlsoap.org/ws/2005/02/trust/RSTR/SCT/Cancel" },
StaticEntry { id: 276, value: "http://www.w3.org/2001/04/xmlenc#aes128-cbc" },
StaticEntry { id: 278, value: "http://www.w3.org/2001/04/xmlenc#kw-aes128" },
StaticEntry { id: 280, value: "http://www.w3.org/2001/04/xmlenc#aes192-cbc" },
StaticEntry { id: 282, value: "http://www.w3.org/2001/04/xmlenc#kw-aes192" },
StaticEntry { id: 284, value: "http://www.w3.org/2001/04/xmlenc#aes256-cbc" },
StaticEntry { id: 286, value: "http://www.w3.org/2001/04/xmlenc#kw-aes256" },
StaticEntry { id: 288, value: "http://www.w3.org/2001/04/xmlenc#des-cbc" },
StaticEntry { id: 290, value: "http://www.w3.org/2000/09/xmldsig#dsa-sha1" },
StaticEntry { id: 292, value: "http://www.w3.org/2001/10/xml-exc-c14n#WithComments" },
StaticEntry { id: 294, value: "http://www.w3.org/2000/09/xmldsig#hmac-sha1" },
StaticEntry { id: 296, value: "http://www.w3.org/2001/04/xmldsig-more#hmac-sha256" },
StaticEntry { id: 298, value: "http://schemas.xmlsoap.org/ws/2005/02/sc/dk/p_sha1" },
StaticEntry { id: 300, value: "http://www.w3.org/2001/04/xmlenc#ripemd160" },
StaticEntry { id: 302, value: "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p" },
StaticEntry { id: 304, value: "http://www.w3.org/2000/09/xmldsig#rsa-sha1" },
StaticEntry { id: 306, value: "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" },
StaticEntry { id: 308, value: "http://www.w3.org/2001/04/xmlenc#rsa-1_5" },
StaticEntry { id: 310, value: "http://www.w3.org/2000/09/xmldsig#sha1" },
StaticEntry { id: 312, value: "http://www.w3.org/2001/04/xmlenc#sha256" },
StaticEntry { id: 314, value: "http://www.w3.org/2001/04/xmlenc#sha512" },
StaticEntry { id: 316, value: "http://www.w3.org/2001/04/xmlenc#tripledes-cbc" },
StaticEntry { id: 318, value: "http://www.w3.org/2001/04/xmlenc#kw-tripledes" },
StaticEntry { id: 320, value: "http://schemas.xmlsoap.org/2005/02/trust/tlsnego#TLS_Wrap" },
StaticEntry { id: 322, value: "http://schemas.xmlsoap.org/2005/02/trust/spnego#GSS_Wrap" },
StaticEntry { id: 324, value: "http://schemas.microsoft.com/ws/2006/05/security" },
StaticEntry { id: 326, value: "dnse" },
StaticEntry { id: 328, value: "o" },
StaticEntry { id: 330, value: "Password" },
StaticEntry { id: 332, value: "PasswordText" },
StaticEntry { id: 334, value: "Username" },
StaticEntry { id: 336, value: "UsernameToken" },
StaticEntry { id: 338, value: "BinarySecurityToken" },
StaticEntry { id: 340, value: "EncodingType" },
StaticEntry { id: 342, value: "KeyIdentifier" },
StaticEntry { id: 344, value: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary" },
StaticEntry { id: 346, value: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#HexBinary" },
StaticEntry { id: 348, value: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Text" },
StaticEntry { id: 350, value: "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509SubjectKeyIdentifier" },
StaticEntry { id: 352, value: "http://docs.oasis-open.org/wss/oasis-wss-kerberos-token-profile-1.1#GSS_Kerberosv5_AP_REQ" },
StaticEntry { id: 354, value: "http://docs.oasis-open.org/wss/oasis-wss-kerberos-token-profile-1.1#GSS_Kerberosv5_AP_REQ1510" },
StaticEntry { id: 356, value: "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.0#SAMLAssertionID" },
StaticEntry { id: 358, value: "Assertion" },
StaticEntry { id: 360, value: "urn:oasis:names:tc:SAML:1.0:assertion" },
StaticEntry { id: 362, value: "http://docs.oasis-open.org/wss/oasis-wss-rel-token-profile-1.0.pdf#license" },
StaticEntry { id: 364, value: "FailedAuthentication" },
StaticEntry { id: 366, value: "InvalidSecurityToken" },
StaticEntry { id: 368, value: "InvalidSecurity" },
StaticEntry { id: 370, value: "k" },
StaticEntry { id: 372, value: "SignatureConfirmation" },
StaticEntry { id: 374, value: "TokenType" },
StaticEntry { id: 376, value: "http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-1.1#ThumbprintSHA1" },
StaticEntry { id: 378, value: "http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-1.1#EncryptedKey" },
StaticEntry { id: 380, value: "http://docs.oasis-open.org/wss/oasis-wss-soap-message-security-1.1#EncryptedKeySHA1" },
StaticEntry { id: 382, value: "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV1.1" },
StaticEntry { id: 384, value: "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLV2.0" },
StaticEntry { id: 386, value: "http://docs.oasis-open.org/wss/oasis-wss-saml-token-profile-1.1#SAMLID" },
StaticEntry { id: 388, value: "AUTH-HASH" },
StaticEntry { id: 390, value: "RequestSecurityTokenResponse" },
StaticEntry { id: 392, value: "KeySize" },
StaticEntry { id: 394, value: "RequestedTokenReference" },
StaticEntry { id: 396, value: "AppliesTo" },
StaticEntry { id: 398, value: "Authenticator" },
StaticEntry { id: 400, value: "CombinedHash" },
// ---- xsi / xsd / nil — heavily used by .NET XmlSerializer for
// value types in custom message-contract bodies. These are
// String 218..222 in the canonical table.
StaticEntry { id: 436, value: "type" },
StaticEntry { id: 438, value: "i" },
StaticEntry { id: 440, value: "http://www.w3.org/2001/XMLSchema-instance" },
StaticEntry { id: 442, value: "http://www.w3.org/2001/XMLSchema" },
StaticEntry { id: 444, value: "nil" },
];
/// Lookup an entry by static-dictionary ID. Returns `None` for IDs
/// outside the curated subset; callers should treat that as "unknown
/// static ID" and either extend [`STATIC_ENTRIES`] or fall through to
/// the inline-string path.
pub fn lookup_static(id: u32) -> Option<&'static str> {
STATIC_ENTRIES
.binary_search_by_key(&id, |e| e.id)
.ok()
.and_then(|idx| STATIC_ENTRIES.get(idx).map(|e| e.value))
}
/// Reverse lookup — find the static-dictionary ID for a string. Returns
/// `None` for strings not in the curated subset; encoders can either
/// extend [`STATIC_ENTRIES`] or fall through to the inline-string /
/// dynamic-dictionary path.
pub fn position_of_static(value: &str) -> Option<u32> {
static REVERSE: OnceLock<HashMap<&'static str, u32>> = OnceLock::new();
let map = REVERSE.get_or_init(|| {
let mut map = HashMap::with_capacity(STATIC_ENTRIES.len());
for entry in STATIC_ENTRIES {
// First-id-wins for duplicates (the canonical dictionary
// has TokenType at both id 116 (String 58) and id 374
// (String 187); we lock the lower id so round-trip lookups
// are deterministic).
map.entry(entry.value).or_insert(entry.id);
}
map
});
map.get(value).copied()
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
mod tests {
use super::*;
#[test]
fn static_entries_have_monotonic_ids() {
let mut last = None;
for entry in STATIC_ENTRIES {
if let Some(prev) = last {
assert!(
entry.id > prev,
"static dictionary entries must be sorted by id; saw {prev} then {}",
entry.id
);
}
last = Some(entry.id);
}
}
#[test]
fn static_entries_use_even_ids() {
// `[MC-NBFS]` reserves odd ids for the per-session dynamic dict
// (`[MC-NBFX]` records 0xAA / 0xAB DictionaryText use the parity
// bit to discriminate). All static-table ids must be even.
for entry in STATIC_ENTRIES {
assert_eq!(
entry.id % 2,
0,
"static entry id {} ('{}') is odd — odd ids are reserved for the dynamic dict",
entry.id,
entry.value
);
}
}
#[test]
fn lookup_returns_known_entries() {
assert_eq!(lookup_static(0), Some("mustUnderstand"));
assert_eq!(lookup_static(2), Some("Envelope"));
assert_eq!(
lookup_static(4),
Some("http://www.w3.org/2003/05/soap-envelope")
);
assert_eq!(lookup_static(8), Some("Header"));
assert_eq!(lookup_static(14), Some("Body"));
}
/// Fault-subset round-trip: every entry the SOAP-1.2 fault body
/// references must resolve. These are the exact dict ids the
/// AVEVA MxDataProvider sends in `dispatcher/fault` envelopes
/// (verified live via `MX_ASB_TRACE_REPLY`). Earlier versions of
/// this table mis-numbered the SOAP-fault subset (Fault was at id
/// 114 instead of 134), causing `decode_envelope` to silently drop
/// the resolved field name and the consumer to see opaque
/// `Static(N)` tokens.
#[test]
fn fault_subset_resolves_to_canonical_strings() {
assert_eq!(lookup_static(132), Some("s"));
assert_eq!(lookup_static(134), Some("Fault"));
assert_eq!(lookup_static(136), Some("MustUnderstand"));
assert_eq!(lookup_static(142), Some("Code"));
assert_eq!(lookup_static(144), Some("Reason"));
assert_eq!(lookup_static(146), Some("Text"));
assert_eq!(lookup_static(148), Some("Node"));
assert_eq!(lookup_static(150), Some("Role"));
assert_eq!(lookup_static(152), Some("Detail"));
assert_eq!(lookup_static(154), Some("Value"));
assert_eq!(lookup_static(156), Some("Subcode"));
}
#[test]
fn xmlserializer_extras_resolve() {
// The 436..444 high-id extras are essential for any
// [MessageContract] response body that uses XmlSerializer.
assert_eq!(lookup_static(436), Some("type"));
assert_eq!(lookup_static(438), Some("i"));
assert_eq!(
lookup_static(440),
Some("http://www.w3.org/2001/XMLSchema-instance")
);
assert_eq!(
lookup_static(442),
Some("http://www.w3.org/2001/XMLSchema")
);
assert_eq!(lookup_static(444), Some("nil"));
}
#[test]
fn position_of_static_round_trips_known_strings() {
assert_eq!(position_of_static("mustUnderstand"), Some(0));
assert_eq!(position_of_static("Envelope"), Some(2));
assert_eq!(position_of_static("Fault"), Some(134));
assert_eq!(position_of_static("Reason"), Some(144));
}
#[test]
fn lookup_returns_none_for_unknown() {
// 1 is odd → reserved for dynamic dict; should always be None.
assert_eq!(lookup_static(1), None);
// Way past the table.
assert_eq!(lookup_static(9999), None);
}
}
File diff suppressed because it is too large Load Diff
+676
View File
@@ -0,0 +1,676 @@
//! `[MS-NMF]` `.NET Message Framing` record codec.
//!
//! Implements the record types `[MS-NMF]` §2.2 enumerates over a
//! `net.tcp` channel:
//!
//! | Byte | Record | Body |
//! |------|-------------------------|-----------------------------------------------------|
//! | 0x00 | `VersionRecord` | major (`u8`), minor (`u8`) |
//! | 0x01 | `ModeRecord` | mode (`u8` — Singleton/Duplex/Simplex/...) |
//! | 0x02 | `ViaRecord` | `Multibyte Int31` length + UTF-8 URI |
//! | 0x03 | `KnownEncodingRecord` | encoding (`u8`) |
//! | 0x04 | `ExtensibleEncoding` | length-prefixed encoding name |
//! | 0x05 | `UnsizedEnvelopeRecord` | unbounded payload, terminated by `EndRecord` |
//! | 0x06 | `SizedEnvelopeRecord` | `Multibyte Int31` length + payload bytes |
//! | 0x07 | `EndRecord` | (no body) |
//! | 0x08 | `FaultRecord` | `Multibyte Int31` length + UTF-8 fault string |
//! | 0x09 | `UpgradeRequestRecord` | length + UTF-8 upgrade name (e.g. SSL/TLS) |
//! | 0x0A | `UpgradeResponseRecord` | (no body) |
//! | 0x0B | `PreambleAckRecord` | (no body) |
//! | 0x0C | `PreambleEndRecord` | (no body) |
//!
//! Length fields are encoded as `Multibyte Int31` (`[MS-NMF]` §2.2.2.1):
//! 7-bit groups, MSB signals continuation, max 5 bytes (LEB128 unsigned
//! over `i32`).
//!
//! No I/O. Encoders write into a `Vec<u8>`; decoders parse from a `&[u8]`
//! slice and return the consumed-byte count alongside the record. Higher-
//! level `connect`/`request`/`response` flows stay in the M5 ASB client
//! (`mxaccess-asb`) — this module is a pure codec.
//!
//! Source for the on-the-wire shape: WCF wraps the framing inside its
//! `BinaryMessageEncodingBindingElement` (selected by default for the
//! `NetTcpBinding(SecurityMode.None)` at
//! `src/MxAsbClient/MxAsbDataClient.cs:660-685`); the framing itself is
//! the `[MS-NMF]` spec, not a project-specific extension. Captured wire
//! traces under `analysis/proxy/mxasbclient-*` confirm the proven record
//! sequence (Version → Mode → Via → KnownEncoding → PreambleEnd →
//! PreambleAck → SizedEnvelope* → End).
use crate::AuthError; // re-imported into the same crate from auth.rs
use thiserror::Error;
/// Record type bytes per `[MS-NMF]` §2.2.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum NmfRecordType {
Version = 0x00,
Mode = 0x01,
Via = 0x02,
KnownEncoding = 0x03,
ExtensibleEncoding = 0x04,
UnsizedEnvelope = 0x05,
SizedEnvelope = 0x06,
End = 0x07,
Fault = 0x08,
UpgradeRequest = 0x09,
UpgradeResponse = 0x0A,
PreambleAck = 0x0B,
PreambleEnd = 0x0C,
}
impl NmfRecordType {
pub fn from_u8(b: u8) -> Option<Self> {
match b {
0x00 => Some(Self::Version),
0x01 => Some(Self::Mode),
0x02 => Some(Self::Via),
0x03 => Some(Self::KnownEncoding),
0x04 => Some(Self::ExtensibleEncoding),
0x05 => Some(Self::UnsizedEnvelope),
0x06 => Some(Self::SizedEnvelope),
0x07 => Some(Self::End),
0x08 => Some(Self::Fault),
0x09 => Some(Self::UpgradeRequest),
0x0A => Some(Self::UpgradeResponse),
0x0B => Some(Self::PreambleAck),
0x0C => Some(Self::PreambleEnd),
_ => None,
}
}
}
/// `ModeRecord` body byte (`[MS-NMF]` §2.2.3.2). The values match the WCF
/// `MessageEncodingMode` enum.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum NmfMode {
Singleton = 0x01,
Duplex = 0x02,
Simplex = 0x03,
SingletonSized = 0x04,
}
impl NmfMode {
pub fn from_u8(b: u8) -> Option<Self> {
match b {
0x01 => Some(Self::Singleton),
0x02 => Some(Self::Duplex),
0x03 => Some(Self::Simplex),
0x04 => Some(Self::SingletonSized),
_ => None,
}
}
}
/// `KnownEncodingRecord` body byte (`[MS-NMF]` §2.2.3.4). ASB uses
/// `BinaryWithDictionary` (`0x08`) — the WCF `BinaryMessageEncoder`
/// referencing `[MC-NBFX]` + `[MC-NBFS]`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum NmfEncoding {
Utf8SoapText = 0x00,
Utf16SoapText = 0x01,
Utf16LeSoapText = 0x02,
Binary = 0x03,
BinaryWithMtom = 0x04,
Mtom = 0x07,
BinaryWithDictionary = 0x08,
}
impl NmfEncoding {
pub fn from_u8(b: u8) -> Option<Self> {
match b {
0x00 => Some(Self::Utf8SoapText),
0x01 => Some(Self::Utf16SoapText),
0x02 => Some(Self::Utf16LeSoapText),
0x03 => Some(Self::Binary),
0x04 => Some(Self::BinaryWithMtom),
0x07 => Some(Self::Mtom),
0x08 => Some(Self::BinaryWithDictionary),
_ => None,
}
}
}
/// Decoded NMF record body. Encoders accept this type; decoders return it
/// alongside the consumed byte count.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NmfRecord {
Version {
major: u8,
minor: u8,
},
Mode(NmfMode),
/// Via URI bytes — UTF-8. The .NET reference uses `Encoding.UTF8` for
/// the via string (`net.tcp://...`).
Via(String),
KnownEncoding(NmfEncoding),
/// Length-prefixed UTF-8 encoding name for non-`KnownEncoding` cases
/// (`[MS-NMF]` §2.2.3.5). Currently unused by ASB but round-tripped.
ExtensibleEncoding(String),
/// Unbounded payload that streams between this record and the next
/// `EndRecord`. Caller is responsible for chunking.
UnsizedEnvelope(Vec<u8>),
/// Length-prefixed payload (the proven ASB request/reply form).
SizedEnvelope(Vec<u8>),
End,
Fault(String),
UpgradeRequest(String),
UpgradeResponse,
PreambleAck,
PreambleEnd,
}
impl NmfRecord {
/// Encode to wire bytes; appends to `out`.
pub fn encode_into(&self, out: &mut Vec<u8>) -> Result<(), NmfError> {
match self {
Self::Version { major, minor } => {
out.push(NmfRecordType::Version as u8);
out.push(*major);
out.push(*minor);
}
Self::Mode(mode) => {
out.push(NmfRecordType::Mode as u8);
out.push(*mode as u8);
}
Self::Via(uri) => {
out.push(NmfRecordType::Via as u8);
encode_string(out, uri.as_bytes())?;
}
Self::KnownEncoding(enc) => {
out.push(NmfRecordType::KnownEncoding as u8);
out.push(*enc as u8);
}
Self::ExtensibleEncoding(name) => {
out.push(NmfRecordType::ExtensibleEncoding as u8);
encode_string(out, name.as_bytes())?;
}
Self::UnsizedEnvelope(payload) => {
// The unsized form is a streaming body. The .NET reference
// never produces this directly — it's set up by the
// negotiated mode. We emit the type byte; payload bytes
// are written by the caller because they may be chunked.
out.push(NmfRecordType::UnsizedEnvelope as u8);
out.extend_from_slice(payload);
}
Self::SizedEnvelope(payload) => {
out.push(NmfRecordType::SizedEnvelope as u8);
let payload_len = i32::try_from(payload.len())
.map_err(|_| NmfError::PayloadTooLarge { len: payload.len() })?;
encode_multibyte_int31(out, payload_len)?;
out.extend_from_slice(payload);
}
Self::End => out.push(NmfRecordType::End as u8),
Self::Fault(message) => {
out.push(NmfRecordType::Fault as u8);
encode_string(out, message.as_bytes())?;
}
Self::UpgradeRequest(name) => {
out.push(NmfRecordType::UpgradeRequest as u8);
encode_string(out, name.as_bytes())?;
}
Self::UpgradeResponse => out.push(NmfRecordType::UpgradeResponse as u8),
Self::PreambleAck => out.push(NmfRecordType::PreambleAck as u8),
Self::PreambleEnd => out.push(NmfRecordType::PreambleEnd as u8),
}
Ok(())
}
/// Encode to a fresh buffer. Convenience wrapper around
/// [`Self::encode_into`].
pub fn encode(&self) -> Result<Vec<u8>, NmfError> {
let mut out = Vec::new();
self.encode_into(&mut out)?;
Ok(out)
}
/// Decode a single record. Returns `(record, bytes_consumed)`.
pub fn decode(input: &[u8]) -> Result<(Self, usize), NmfError> {
let kind_byte = *input.first().ok_or(NmfError::Truncated {
need: 1,
have: 0,
stage: "record-type",
})?;
let kind =
NmfRecordType::from_u8(kind_byte).ok_or(NmfError::UnknownRecordType(kind_byte))?;
let mut cursor = 1usize;
let record = match kind {
NmfRecordType::Version => {
let major = read_byte(input, &mut cursor, "version-major")?;
let minor = read_byte(input, &mut cursor, "version-minor")?;
Self::Version { major, minor }
}
NmfRecordType::Mode => {
let m = read_byte(input, &mut cursor, "mode-byte")?;
Self::Mode(NmfMode::from_u8(m).ok_or(NmfError::UnknownMode(m))?)
}
NmfRecordType::Via => Self::Via(decode_string(input, &mut cursor, "via")?),
NmfRecordType::KnownEncoding => {
let e = read_byte(input, &mut cursor, "encoding-byte")?;
Self::KnownEncoding(NmfEncoding::from_u8(e).ok_or(NmfError::UnknownEncoding(e))?)
}
NmfRecordType::ExtensibleEncoding => {
Self::ExtensibleEncoding(decode_string(input, &mut cursor, "extensible-encoding")?)
}
NmfRecordType::UnsizedEnvelope => {
// Unsized envelope is a streaming body; the codec returns
// the remaining bytes verbatim and the caller is
// responsible for splitting at the next `End` record.
let tail = input.get(cursor..).unwrap_or(&[]);
cursor += tail.len();
Self::UnsizedEnvelope(tail.to_vec())
}
NmfRecordType::SizedEnvelope => {
let len = decode_multibyte_int31(input, &mut cursor)?;
let len = usize::try_from(len).map_err(|_| NmfError::NegativeLength(len))?;
let payload = input.get(cursor..cursor + len).ok_or(NmfError::Truncated {
need: len,
have: input.len().saturating_sub(cursor),
stage: "sized-envelope-payload",
})?;
cursor += len;
Self::SizedEnvelope(payload.to_vec())
}
NmfRecordType::End => Self::End,
NmfRecordType::Fault => Self::Fault(decode_string(input, &mut cursor, "fault")?),
NmfRecordType::UpgradeRequest => {
Self::UpgradeRequest(decode_string(input, &mut cursor, "upgrade-request")?)
}
NmfRecordType::UpgradeResponse => Self::UpgradeResponse,
NmfRecordType::PreambleAck => Self::PreambleAck,
NmfRecordType::PreambleEnd => Self::PreambleEnd,
};
Ok((record, cursor))
}
}
/// Convenience: the canonical preamble sequence for an ASB `net.tcp`
/// connect (`Version 1.0` → `Duplex` → `Via $uri` →
/// `KnownEncoding(BinaryWithDictionary)` → `PreambleEnd`).
///
/// Mirrors the records WCF emits when `NetTcpBinding(SecurityMode.None)`
/// brings up a duplex channel — verified against
/// `analysis/proxy/mxasbclient-register-message.txt` capture preamble.
pub fn encode_preamble(via_uri: &str, out: &mut Vec<u8>) -> Result<(), NmfError> {
NmfRecord::Version { major: 1, minor: 0 }.encode_into(out)?;
NmfRecord::Mode(NmfMode::Duplex).encode_into(out)?;
NmfRecord::Via(via_uri.to_string()).encode_into(out)?;
NmfRecord::KnownEncoding(NmfEncoding::BinaryWithDictionary).encode_into(out)?;
NmfRecord::PreambleEnd.encode_into(out)?;
Ok(())
}
// ---- multibyte int31 -----------------------------------------------------
/// Encode a non-negative `i32` as `[MS-NMF]` §2.2.2.1 `Multibyte Int31`.
/// 7-bit little-endian groups; MSB signals continuation; max 5 bytes.
/// Negative values are rejected.
pub fn encode_multibyte_int31(out: &mut Vec<u8>, value: i32) -> Result<(), NmfError> {
if value < 0 {
return Err(NmfError::NegativeLength(value));
}
let mut v = value as u32;
loop {
let byte = (v & 0x7F) as u8;
v >>= 7;
if v == 0 {
out.push(byte);
return Ok(());
}
out.push(byte | 0x80);
}
}
/// Decode a `Multibyte Int31`. Reads at most 5 bytes; returns the parsed
/// value and advances `cursor`.
pub fn decode_multibyte_int31(input: &[u8], cursor: &mut usize) -> Result<i32, NmfError> {
let mut value: u32 = 0;
for shift in (0u32..).step_by(7).take(5) {
let byte = input.get(*cursor).copied().ok_or(NmfError::Truncated {
need: 1,
have: 0,
stage: "multibyte-int31",
})?;
*cursor += 1;
value |= ((byte & 0x7F) as u32).wrapping_shl(shift);
if byte & 0x80 == 0 {
return i32::try_from(value).map_err(|_| NmfError::IntOverflow);
}
}
Err(NmfError::IntOverflow)
}
// ---- string helpers ------------------------------------------------------
fn encode_string(out: &mut Vec<u8>, bytes: &[u8]) -> Result<(), NmfError> {
let len =
i32::try_from(bytes.len()).map_err(|_| NmfError::PayloadTooLarge { len: bytes.len() })?;
encode_multibyte_int31(out, len)?;
out.extend_from_slice(bytes);
Ok(())
}
fn decode_string(
input: &[u8],
cursor: &mut usize,
stage: &'static str,
) -> Result<String, NmfError> {
let len_i = decode_multibyte_int31(input, cursor)?;
let len = usize::try_from(len_i).map_err(|_| NmfError::NegativeLength(len_i))?;
let bytes = input
.get(*cursor..*cursor + len)
.ok_or(NmfError::Truncated {
need: len,
have: input.len().saturating_sub(*cursor),
stage,
})?;
*cursor += len;
String::from_utf8(bytes.to_vec()).map_err(|_| NmfError::InvalidUtf8 { stage })
}
fn read_byte(input: &[u8], cursor: &mut usize, stage: &'static str) -> Result<u8, NmfError> {
let byte = input.get(*cursor).copied().ok_or(NmfError::Truncated {
need: 1,
have: 0,
stage,
})?;
*cursor += 1;
Ok(byte)
}
// ---- error type ----------------------------------------------------------
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum NmfError {
#[error("truncated frame at {stage}: need {need} bytes, have {have}")]
Truncated {
need: usize,
have: usize,
stage: &'static str,
},
#[error("unknown NMF record type 0x{0:02x}")]
UnknownRecordType(u8),
#[error("unknown NMF mode 0x{0:02x}")]
UnknownMode(u8),
#[error("unknown NMF encoding 0x{0:02x}")]
UnknownEncoding(u8),
#[error("payload too large: {len} bytes (max {})", i32::MAX)]
PayloadTooLarge { len: usize },
#[error("multibyte int31 overflowed 31-bit unsigned range")]
IntOverflow,
#[error("negative length {0} in NMF frame")]
NegativeLength(i32),
#[error("invalid UTF-8 in NMF {stage} payload")]
InvalidUtf8 { stage: &'static str },
}
// `AuthError` is unrelated; this re-import exists only so consumers of the
// crate can use a single `use mxaccess_asb_nettcp::*;` statement and pull
// both auth + framing types in one go without a path collision.
#[allow(dead_code)]
const _AUTH_ERROR_IS_REACHABLE: fn(&AuthError) = |_| {};
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
mod tests {
use super::*;
fn round_trip(record: NmfRecord) {
let bytes = record.encode().unwrap();
let (decoded, consumed) = NmfRecord::decode(&bytes).unwrap();
assert_eq!(consumed, bytes.len(), "decode consumed != encoded len");
assert_eq!(decoded, record);
}
#[test]
fn version_round_trip() {
round_trip(NmfRecord::Version { major: 1, minor: 0 });
round_trip(NmfRecord::Version { major: 0, minor: 0 });
}
#[test]
fn mode_round_trip_all_modes() {
for m in [
NmfMode::Singleton,
NmfMode::Duplex,
NmfMode::Simplex,
NmfMode::SingletonSized,
] {
round_trip(NmfRecord::Mode(m));
}
}
#[test]
fn via_round_trip_with_ascii_uri() {
round_trip(NmfRecord::Via(
"net.tcp://localhost:5074/ASBService".to_string(),
));
}
#[test]
fn via_round_trip_with_unicode_uri() {
// `net.tcp://` URIs are ASCII in practice; this is a defensive
// round-trip to catch any UTF-8 corruption in the codec path.
round_trip(NmfRecord::Via("net.tcp://hôst.example/ásb".to_string()));
}
#[test]
fn known_encoding_round_trip() {
for e in [
NmfEncoding::Utf8SoapText,
NmfEncoding::Utf16SoapText,
NmfEncoding::Utf16LeSoapText,
NmfEncoding::Binary,
NmfEncoding::BinaryWithMtom,
NmfEncoding::Mtom,
NmfEncoding::BinaryWithDictionary,
] {
round_trip(NmfRecord::KnownEncoding(e));
}
}
#[test]
fn extensible_encoding_round_trip() {
round_trip(NmfRecord::ExtensibleEncoding(
"application/octet-stream".to_string(),
));
}
#[test]
fn sized_envelope_round_trip_small() {
round_trip(NmfRecord::SizedEnvelope(vec![]));
round_trip(NmfRecord::SizedEnvelope((0u8..=255).collect()));
}
#[test]
fn sized_envelope_round_trip_large_uses_multibyte_length() {
// 200-byte payload: length needs 2 multibyte-int31 bytes (200 =
// 0xC8, encoded as 0xC8 0x01).
let payload = vec![0xAB; 200];
let bytes = NmfRecord::SizedEnvelope(payload.clone()).encode().unwrap();
// type (1) + length-bytes (2) + payload (200)
assert_eq!(bytes.len(), 1 + 2 + 200);
assert_eq!(bytes[0], NmfRecordType::SizedEnvelope as u8);
assert_eq!(bytes[1], 0xC8);
assert_eq!(bytes[2], 0x01);
let (decoded, consumed) = NmfRecord::decode(&bytes).unwrap();
assert_eq!(consumed, bytes.len());
assert!(matches!(decoded, NmfRecord::SizedEnvelope(p) if p == payload));
}
#[test]
fn end_record_is_one_byte() {
let bytes = NmfRecord::End.encode().unwrap();
assert_eq!(bytes, vec![0x07]);
round_trip(NmfRecord::End);
}
#[test]
fn fault_record_round_trip() {
round_trip(NmfRecord::Fault("invalid request".to_string()));
}
#[test]
fn preamble_ack_and_end_round_trip() {
round_trip(NmfRecord::PreambleAck);
round_trip(NmfRecord::PreambleEnd);
round_trip(NmfRecord::UpgradeResponse);
}
#[test]
fn upgrade_request_round_trip() {
round_trip(NmfRecord::UpgradeRequest("application/ssl-tls".to_string()));
}
#[test]
fn unsized_envelope_round_trip_streams_payload_to_eof() {
// The unsized form returns whatever bytes follow the type byte —
// chunking is the caller's responsibility. Round-trip with an
// explicit payload to catch byte-loss in the codec.
let record = NmfRecord::UnsizedEnvelope(vec![0xDE, 0xAD, 0xBE, 0xEF]);
let bytes = record.encode().unwrap();
// Type byte + 4 payload bytes
assert_eq!(bytes.len(), 5);
let (decoded, _) = NmfRecord::decode(&bytes).unwrap();
assert_eq!(decoded, record);
}
#[test]
fn multibyte_int31_round_trip_known_vectors() {
// [MS-NMF] §2.2.2.1 examples + LEB128 reference vectors.
for (value, expected) in [
(0i32, vec![0x00u8]),
(1, vec![0x01]),
(127, vec![0x7F]),
(128, vec![0x80, 0x01]),
(16_383, vec![0xFF, 0x7F]),
(16_384, vec![0x80, 0x80, 0x01]),
(200, vec![0xC8, 0x01]),
(i32::MAX, vec![0xFF, 0xFF, 0xFF, 0xFF, 0x07]),
] {
let mut out = Vec::new();
encode_multibyte_int31(&mut out, value).unwrap();
assert_eq!(out, expected, "encoding {value}");
let mut cursor = 0;
let decoded = decode_multibyte_int31(&out, &mut cursor).unwrap();
assert_eq!(decoded, value);
assert_eq!(cursor, expected.len());
}
}
#[test]
fn multibyte_int31_rejects_negative() {
let mut out = Vec::new();
let err = encode_multibyte_int31(&mut out, -1).unwrap_err();
assert!(matches!(err, NmfError::NegativeLength(-1)));
}
#[test]
fn multibyte_int31_rejects_overflow() {
// 6 continuation bytes — beyond the 5-byte spec maximum.
let bytes = vec![0x80, 0x80, 0x80, 0x80, 0x80, 0x80];
let mut cursor = 0;
let err = decode_multibyte_int31(&bytes, &mut cursor).unwrap_err();
assert!(matches!(err, NmfError::IntOverflow));
}
#[test]
fn decode_rejects_unknown_record_type() {
let bytes = vec![0xFFu8];
let err = NmfRecord::decode(&bytes).unwrap_err();
assert!(matches!(err, NmfError::UnknownRecordType(0xFF)));
}
#[test]
fn decode_rejects_unknown_mode() {
let bytes = vec![NmfRecordType::Mode as u8, 0xEE];
let err = NmfRecord::decode(&bytes).unwrap_err();
assert!(matches!(err, NmfError::UnknownMode(0xEE)));
}
#[test]
fn decode_rejects_unknown_encoding() {
let bytes = vec![NmfRecordType::KnownEncoding as u8, 0x42];
let err = NmfRecord::decode(&bytes).unwrap_err();
assert!(matches!(err, NmfError::UnknownEncoding(0x42)));
}
#[test]
fn decode_rejects_truncated_sized_envelope() {
// Type + length(=10) but only 5 payload bytes.
let mut bytes = vec![NmfRecordType::SizedEnvelope as u8, 0x0A];
bytes.extend_from_slice(&[0xAA; 5]);
let err = NmfRecord::decode(&bytes).unwrap_err();
assert!(matches!(
err,
NmfError::Truncated {
stage: "sized-envelope-payload",
..
}
));
}
#[test]
fn preamble_emits_canonical_record_sequence() {
let mut out = Vec::new();
encode_preamble("net.tcp://localhost:5074/ASBService", &mut out).unwrap();
// Decode back and verify the sequence.
let mut cursor = 0;
let mut records = Vec::new();
while cursor < out.len() {
let (record, consumed) = NmfRecord::decode(&out[cursor..]).unwrap();
cursor += consumed;
records.push(record);
}
assert_eq!(cursor, out.len());
assert_eq!(records.len(), 5);
assert!(matches!(
records[0],
NmfRecord::Version { major: 1, minor: 0 }
));
assert!(matches!(records[1], NmfRecord::Mode(NmfMode::Duplex)));
match &records[2] {
NmfRecord::Via(uri) => assert_eq!(uri, "net.tcp://localhost:5074/ASBService"),
other => panic!("expected Via, got {other:?}"),
}
assert!(matches!(
records[3],
NmfRecord::KnownEncoding(NmfEncoding::BinaryWithDictionary)
));
assert!(matches!(records[4], NmfRecord::PreambleEnd));
}
#[test]
fn version_record_byte_layout() {
// [MS-NMF] §2.2.3.1: 0x00 major minor.
let bytes = NmfRecord::Version { major: 1, minor: 0 }.encode().unwrap();
assert_eq!(bytes, vec![0x00, 0x01, 0x00]);
}
#[test]
fn mode_record_byte_layout() {
// [MS-NMF] §2.2.3.2: 0x01 mode-byte. Duplex = 0x02.
let bytes = NmfRecord::Mode(NmfMode::Duplex).encode().unwrap();
assert_eq!(bytes, vec![0x01, 0x02]);
}
#[test]
fn known_encoding_record_byte_layout() {
// [MS-NMF] §2.2.3.4: 0x03 enc-byte. BinaryWithDictionary = 0x08.
let bytes = NmfRecord::KnownEncoding(NmfEncoding::BinaryWithDictionary)
.encode()
.unwrap();
assert_eq!(bytes, vec![0x03, 0x08]);
}
}
@@ -0,0 +1,44 @@
# Deterministic HMAC fixture
Pinned input/output triple for the `AsbSystemAuthenticator.Sign`
crypto path, captured from the .NET reference. Used by the Rust
parity test in `crates/mxaccess-asb-nettcp/tests/deterministic_hmac.rs`
to assert byte-equality of crypto_key derivation, canonical XML
emission, HMAC-SHA1, PBKDF2-SHA1 AES key derivation, and AES-CBC
encryption — independent of session randomness (DH private key,
remote public key, and AES IV are all pinned to deterministic values
so a single `cargo test` run can reproduce the .NET output).
## Capture procedure
```powershell
dotnet run --project src\MxAsbClient.Probe -c Release -- --dump-deterministic-hmac > capture.txt
```
The probe's `--dump-deterministic-hmac` flag (added 2026-05-05)
inlines the per-step decomposition of `Sign` (`AsbSystemAuthenticator
.cs:62-82`):
1. `shared = remote_pub^private_key mod prime` (.NET `BigInteger.ModPow`)
2. `crypto_key = shared || passphrase_utf8`
3. `xml = AuthenticateMe.ToXml()` with empty MAC + IV
4. `hmac = HMAC-SHA1(crypto_key, utf8(xml))`
5. `aes_key = PBKDF2-SHA1(base64(crypto_key), "ArchestrAService", 1000, 16)`
6. `encrypted_mac = AES-CBC(aes_key, iv=zeros, hmac, padding=PKCS7)`
Step 6 uses an all-zero IV to make the test reproducible — the real
wire path uses a random IV per call, but the Rust test bypasses the
random IV path by calling the AES primitive directly with the same
zero IV.
## File format
Plain-ASCII `key=value` lines, one per line. Hex values are
upper-case (matching .NET's `Convert.ToHexString`). The `xml_utf8_b64`
field encodes the canonical XML as base64 of the UTF-8 bytes.
## Files
- `authenticate-me.kv` — fixture for the `AuthenticateMe` shape with
the `[XmlType(Namespace="http://asb.contracts.data/20111111")]`
ConsumerAuthenticationData wrapper.
@@ -0,0 +1,21 @@
# deterministic-hmac fixture (.NET reference output)
prime_decimal=179769313486231590770839156793787453197860296048756011706444423684197180216158519368947833795864925541502180565485980503646440548199239100050792877003355816639229553136239076508735759914822574862575007425302077447712589550957937778424442426617334727629299387668709205606050270810842907692932019128194
generator=22
private_key_hex=0102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F2000
remote_pub_hex=0D141B222930373E454C535A61686F767D848B9299A0A7AEB5BCC3CAD1D8DFE6EDF4FB020910171E252C333A41484F565D646B727980878E959CA3AAB1B8BFC6CDD4DBE2E9F0F7FE050C131A21282F363D444B525960676E757C838A91989FA6ADB4BBC2C9D0D7DEE5ECF3FA01080F161D242B323940474E555C636A71787F7F
passphrase=deterministic-hmac-fixture-passphrase-rust-vs-dotnet
connection_id=8cba964a-74c1-ef74-f6aa-761b3540191b
message_number=42
consumer_data_hex=070A0D101316191C1F2225282B2E3134373A3D404346494C4F5255585B5E6164676A6D707376797C7F8285888B8E9194979A9DA0A3A6A9ACAFB2B5B8BBBEC1C4C7CACDD0D3D6D9DCDFE2E5E8EBEEF1F4F7FAFD000306090C0F1215181B1E2124272A2D303336393C3F4245484B4E5154575A5D606366696C6F7275787B7E8184878A8D909396999C9FA2A5A8ABAEB1B4B7BABDC0C3C6C9CCCFD2D5D8DBDEE1E4E7EAEDF0F3F6F9FCFF0205080B0E1114171A1D202326292C2F3235383B3E4144474A4D505356595C5F6265686B6E7174
consumer_iv_hex=05101B26313C47525D68737E89949FAA
aes_iv_hex=00000000000000000000000000000000
shared_secret_hex=05F8563585C58EF5AF2A2DFFD4BC73FCD043FEFB470ED66EE07D5D9882DB27A478C58B6B857B300409064669C42C1C84F3457E6C0C4A00E578DF90DC817CB8BBDFE866F3EE9820E3BF8C772827C5E3BAE164553B4C65EC927865D7AA4F2AC5124F5F85B49A7C460F5BA06B4651A580D935BE1CFA577A9B2ED47980D200
shared_secret_len=125
crypto_key_hex=05F8563585C58EF5AF2A2DFFD4BC73FCD043FEFB470ED66EE07D5D9882DB27A478C58B6B857B300409064669C42C1C84F3457E6C0C4A00E578DF90DC817CB8BBDFE866F3EE9820E3BF8C772827C5E3BAE164553B4C65EC927865D7AA4F2AC5124F5F85B49A7C460F5BA06B4651A580D935BE1CFA577A9B2ED47980D20064657465726D696E69737469632D686D61632D666978747572652D706173737068726173652D727573742D76732D646F746E6574
crypto_key_len=177
xml_utf8_len=1136
xml_utf8_b64=PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTE2Ij8+DQo8QXV0aGVudGljYXRlTWUgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM6eHNkPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM9InVybjppbnZlbnN5cy5zY2hlbWFzIj4NCiAgPENvbm5lY3Rpb25WYWxpZGF0b3I+DQogICAgPENvbm5lY3Rpb25JZCB4bWxucz0iaHR0cDovL2FzYi5jb250cmFjdHMuZGF0YS8yMDExMTExMSI+OGNiYTk2NGEtNzRjMS1lZjc0LWY2YWEtNzYxYjM1NDAxOTFiPC9Db25uZWN0aW9uSWQ+DQogICAgPE1lc3NhZ2VOdW1iZXIgeG1sbnM9Imh0dHA6Ly9hc2IuY29udHJhY3RzLmRhdGEvMjAxMTExMTEiPjQyPC9NZXNzYWdlTnVtYmVyPg0KICAgIDxNZXNzYWdlQXV0aGVudGljYXRpb25Db2RlIHhtbG5zPSJodHRwOi8vYXNiLmNvbnRyYWN0cy5kYXRhLzIwMTExMTExIiAvPg0KICAgIDxTaWduYXR1cmVJbml0aWFsaXphdGlvblZlY3RvciB4bWxucz0iaHR0cDovL2FzYi5jb250cmFjdHMuZGF0YS8yMDExMTExMSIgLz4NCiAgPC9Db25uZWN0aW9uVmFsaWRhdG9yPg0KICA8Q29uc3VtZXJBdXRoZW50aWNhdGlvbkRhdGE+DQogICAgPERhdGEgeG1sbnM9Imh0dHA6Ly9hc2IuY29udHJhY3RzLmRhdGEvMjAxMTExMTEiPkJ3b05FQk1XR1J3ZklpVW9LeTR4TkRjNlBVQkRSa2xNVDFKVldGdGVZV1JuYW0xd2MzWjVmSCtDaFlpTGpwR1VsNXFkb0tPbXFheXZzclc0dTc3QnhNZkt6ZERUMXRuYzMrTGw2T3Z1OGZUMyt2MEFBd1lKREE4U0ZSZ2JIaUVrSnlvdE1ETTJPVHcvUWtWSVMwNVJWRmRhWFdCalptbHNiM0oxZUh0K2dZU0hpbzJRazVhWm5KK2lwYWlycnJHMHQ3cTl3TVBHeWN6UDB0WFkyOTdoNU9mcTdmRHo5dm44L3dJRkNBc09FUlFYR2gwZ0l5WXBMQzh5TlRnN1BrRkVSMHBOVUZOV1dWeGZZbVZvYTI1eGRBPT08L0RhdGE+DQogICAgPEluaXRpYWxpemF0aW9uVmVjdG9yIHhtbG5zPSJodHRwOi8vYXNiLmNvbnRyYWN0cy5kYXRhLzIwMTExMTExIj5CUkFiSmpFOFIxSmRhSE4raVpTZnFnPT08L0luaXRpYWxpemF0aW9uVmVjdG9yPg0KICA8L0NvbnN1bWVyQXV0aGVudGljYXRpb25EYXRhPg0KPC9BdXRoZW50aWNhdGVNZT4=
hmac_sha1_hex=4EDF6AF60E72C7026D2F5231F0E91FCEFC30E3D6
aes_key_hex=E5532AC4BFC5628B20B0ED307B2C88AC
encrypted_mac_hex=2E6A290397F688F2AE97B421184F44359C05FC59891BFA49BFD068C41EF9B42B
encrypted_mac_len=32
+6 -2
View File
@@ -9,8 +9,12 @@ rust-version.workspace = true
authors.workspace = true
[dependencies]
mxaccess-codec = { path = "../mxaccess-codec" }
mxaccess-asb-nettcp = { path = "../mxaccess-asb-nettcp" }
mxaccess-codec = { path = "../mxaccess-codec", version = "0.0.0" }
mxaccess-asb-nettcp = { path = "../mxaccess-asb-nettcp", version = "0.0.0" }
thiserror = { workspace = true }
tracing = { workspace = true }
tokio = { workspace = true }
rand = { workspace = true }
[features]
default = []
File diff suppressed because it is too large Load Diff
+687
View File
@@ -0,0 +1,687 @@
//! `IAsbCustomSerializableType` binary codecs.
//!
//! Ports the binary fast-path WCF uses for `Variant` /
//! `IAsbCustomSerializableType`-decorated structs. Each type writes a
//! `BinaryWriter`-style payload (LE primitives + `AsbBinary` UTF-16 LE
//! length-prefixed strings); the WCF `AsbDataCustomSerializer`
//! (`AsbContracts.cs:1507-1612`) then base64-encodes that payload and
//! wraps it inside an `<ASBIData>` element under the field's outer XML
//! tag.
//!
//! ## Scope
//!
//! Implements:
//! * [`ItemIdentity`] — used by RegisterItems / UnregisterItems / Read
//! / AddMonitoredItems / DeleteMonitoredItems request bodies.
//!
//! Stubbed for follow-up F25 iterations:
//! * `ItemStatus`, `ItemRegistration`, `WriteValue`, `RuntimeValue`
//! payloads, `ItemWriteComplete`, `MonitoredItemSettings`,
//! `MonitoredItem`. The pattern is identical — pure binary
//! round-trip — so the per-type cost is small once the
//! [`ItemIdentity`] reference establishes it.
use mxaccess_codec::{AsbStatus, AsbVariant, CodecError, RuntimeValue};
/// `ItemIdentity` per `AsbContracts.cs:533-633`. Wire layout:
///
/// | Offset | Size | Field | Notes |
/// |-------:|-----:|---------------|--------------------------------------|
/// | 0 | 2 | `Type` | u16 `ItemIdentityType` enum |
/// | 2 | 2 | `ReferenceType` | u16 `ItemReferenceType` enum |
/// | 4 | n | `Name` | `AsbBinary.WriteUnicodeString` |
/// | | m | `ContextName` | `AsbBinary.WriteUnicodeString` |
/// | | 8 | `Id` | u64 |
/// | | 1 | `IdSpecified` | bool (`BinaryWriter.Write(bool)`) |
///
/// `AsbBinary.WriteUnicodeString` per `cs:1622-1633`:
/// * Null/empty → 4-byte `0u32` length, no payload
/// * Non-empty → 4-byte byte-length + UTF-16LE bytes
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ItemIdentity {
pub kind: u16,
pub reference_type: u16,
pub name: Option<String>,
pub context_name: Option<String>,
pub id: u64,
pub id_specified: bool,
}
/// Default `ItemIdentity` matches the wire-equivalent .NET default:
/// `Name = string.Empty`, `ContextName = string.Empty`. Both fields
/// must be `Some(String::new())` so the wire round-trip is stable
/// (the binary codec collapses `None` → length-0 → `Some("")` per
/// `read_unicode_string`'s .NET-mirroring behaviour).
impl Default for ItemIdentity {
fn default() -> Self {
Self {
kind: 0,
reference_type: 0,
name: Some(String::new()),
context_name: Some(String::new()),
id: 0,
id_specified: false,
}
}
}
/// `ItemIdentityType` enum (`AsbContracts.cs:1295-1300`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)]
pub enum ItemIdentityType {
Name = 0,
Id = 1,
NameAndId = 2,
}
/// `ItemReferenceType` enum (`AsbContracts.cs:1302-1308`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)]
pub enum ItemReferenceType {
None = 0,
Absolute = 1,
Hierarchical = 2,
Relative = 3,
}
impl ItemIdentity {
/// Convenience constructor for an absolute name reference. The
/// `MxAsbDataClient.CreateAbsoluteItem` path
/// (`MxAsbDataClient.cs:172-194`) sets `Type =
/// ItemIdentityType.Name`, `ReferenceType =
/// ItemReferenceType.Absolute`, and supplies the tag name. Most
/// register-time callers use this shape.
pub fn absolute_by_name(name: impl Into<String>) -> Self {
Self {
kind: ItemIdentityType::Name as u16,
reference_type: ItemReferenceType::Absolute as u16,
name: Some(name.into()),
// .NET's `CreateAbsoluteItem` (`MxAsbDataClient.cs:604-613`)
// sets `ContextName = string.Empty` (NOT null). XmlSerializer
// treats empty-string and null differently — empty produces
// `<ContextName xmlns="..." />` (self-closing) while null
// produces `<ContextName xsi:nil="true" xmlns="..." />`. The
// canonical-XML signing path (F28) compares against .NET's
// form, so we must default to `Some(String::new())`.
context_name: Some(String::new()),
id: 0,
id_specified: false,
}
}
pub fn encode_into(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&self.kind.to_le_bytes());
out.extend_from_slice(&self.reference_type.to_le_bytes());
write_unicode_string(out, self.name.as_deref());
write_unicode_string(out, self.context_name.as_deref());
out.extend_from_slice(&self.id.to_le_bytes());
out.push(if self.id_specified { 1 } else { 0 });
}
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::new();
self.encode_into(&mut out);
out
}
pub fn decode(input: &[u8]) -> Result<(Self, usize), CodecError> {
let mut cursor = 0usize;
let kind = read_u16_le(input, &mut cursor)?;
let reference_type = read_u16_le(input, &mut cursor)?;
let name = read_unicode_string(input, &mut cursor)?;
let context_name = read_unicode_string(input, &mut cursor)?;
let id = read_u64_le(input, &mut cursor)?;
let id_specified = read_u8(input, &mut cursor)? != 0;
Ok((
Self {
kind,
reference_type,
name,
context_name,
id,
id_specified,
},
cursor,
))
}
}
/// `ItemStatus` per `AsbContracts.cs:639-722`. Wire layout (from the
/// `WriteToStream` method at `cs:682-688`):
///
/// | Field | Codec |
/// |----------------|-----------------------------|
/// | `Item` | [`ItemIdentity`] binary form |
/// | `Status` | [`AsbStatus`] binary form |
/// | `ErrorCode` | u16 |
/// | `ErrorCodeSpecified` | u8 (bool) |
///
/// Note the field order on the wire (`Item` then `Status`) is **NOT**
/// the `[DataMember(Order = …)]` declared order — `WriteToStream`
/// hand-picks Item-first, Status-second, then the trailing pair.
/// We mirror that exactly.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ItemStatus {
pub item: ItemIdentity,
pub status: AsbStatus,
pub error_code: u16,
pub error_code_specified: bool,
}
impl ItemStatus {
pub fn encode_into(&self, out: &mut Vec<u8>) {
self.item.encode_into(out);
self.status.encode_into(out);
out.extend_from_slice(&self.error_code.to_le_bytes());
out.push(if self.error_code_specified { 1 } else { 0 });
}
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::new();
self.encode_into(&mut out);
out
}
pub fn decode(input: &[u8]) -> Result<(Self, usize), CodecError> {
let (item, item_consumed) = ItemIdentity::decode(input)?;
let mut cursor = item_consumed;
let status_tail = input.get(cursor..).ok_or(CodecError::ShortRead {
expected: 5,
actual: 0,
})?;
let (status, status_consumed) = AsbStatus::decode(status_tail)?;
cursor += status_consumed;
let error_code = read_u16_le(input, &mut cursor)?;
let error_code_specified = read_u8(input, &mut cursor)? != 0;
Ok((
Self {
item,
status,
error_code,
error_code_specified,
},
cursor,
))
}
}
/// Decode an array of `ItemStatus`es from the WCF custom-serializer
/// binary form (4-byte int32 count + each item's `WriteToStream`
/// output). Mirrors `ItemStatus.InitializeArrayFromStream`
/// (`cs:702-711`).
pub fn decode_item_status_array(input: &[u8]) -> Result<Vec<ItemStatus>, CodecError> {
let mut cursor = 0usize;
let count = read_i32_le(input, &mut cursor)?;
if count < 0 {
return Err(CodecError::Decode {
offset: 0,
reason: "negative item-status array count",
buffer_len: input.len(),
});
}
let mut out = Vec::with_capacity(count as usize);
for _ in 0..count {
let tail = input.get(cursor..).ok_or(CodecError::ShortRead {
expected: 1,
actual: 0,
})?;
let (item, consumed) = ItemStatus::decode(tail)?;
cursor += consumed;
out.push(item);
}
Ok(out)
}
/// Encode an array of `ItemStatus`es. Mirrors `ItemStatus.WriteArrayToStream`
/// (`cs:713-721`) — 4-byte int32 count + each element's `WriteToStream`.
pub fn encode_item_status_array(items: &[ItemStatus]) -> Vec<u8> {
let mut out = Vec::new();
let count = i32::try_from(items.len()).unwrap_or(i32::MAX);
out.extend_from_slice(&count.to_le_bytes());
for item in items {
item.encode_into(&mut out);
}
out
}
/// `MonitoredItemValue` per `AsbContracts.cs:1032-1104`.
/// `IAsbCustomSerializableType` binary fast-path; payload order from
/// `WriteToStream` at `cs:1064-1068`:
///
/// 1. `Item` — [`ItemIdentity`] binary.
/// 2. `Value` — [`RuntimeValue`] binary (timestamp + variant + status).
/// 3. `UserData` — [`AsbVariant`] binary.
///
/// `MonitoredItemValue` arrives in `PublishResponse` as part of the
/// `Values` array — one entry per delivered sample.
#[derive(Debug, Clone, PartialEq)]
pub struct MonitoredItemValue {
pub item: ItemIdentity,
pub value: RuntimeValue,
pub user_data: AsbVariant,
}
impl MonitoredItemValue {
pub fn encode_into(&self, out: &mut Vec<u8>) {
self.item.encode_into(out);
self.value.encode_into(out);
self.user_data.encode_into(out);
}
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::new();
self.encode_into(&mut out);
out
}
pub fn decode(input: &[u8]) -> Result<(Self, usize), CodecError> {
let (item, item_consumed) = ItemIdentity::decode(input)?;
let mut cursor = item_consumed;
let value_tail = input.get(cursor..).ok_or(CodecError::ShortRead {
expected: 1,
actual: 0,
})?;
let (value, value_consumed) = RuntimeValue::decode(value_tail)?;
cursor += value_consumed;
let user_data_tail = input.get(cursor..).ok_or(CodecError::ShortRead {
expected: 1,
actual: 0,
})?;
let (user_data, user_data_consumed) = AsbVariant::decode(user_data_tail)?;
cursor += user_data_consumed;
Ok((
Self {
item,
value,
user_data,
},
cursor,
))
}
}
/// Encode a `MonitoredItemValue[]` array per `WriteArrayToStream`
/// (`cs:1095-1103`) — 4-byte int32 count + per-element body.
pub fn encode_monitored_item_value_array(values: &[MonitoredItemValue]) -> Vec<u8> {
let mut out = Vec::new();
let count = i32::try_from(values.len()).unwrap_or(i32::MAX);
out.extend_from_slice(&count.to_le_bytes());
for v in values {
v.encode_into(&mut out);
}
out
}
/// Decode a `MonitoredItemValue[]` array. Mirrors
/// `MonitoredItemValue.InitializeArrayFromStream` (`cs:1084-1093`).
pub fn decode_monitored_item_value_array(
input: &[u8],
) -> Result<Vec<MonitoredItemValue>, CodecError> {
let mut cursor = 0usize;
let count = read_i32_le(input, &mut cursor)?;
if count < 0 {
return Err(CodecError::Decode {
offset: 0,
reason: "negative monitored-item-value array count",
buffer_len: input.len(),
});
}
let mut out = Vec::with_capacity(count as usize);
for _ in 0..count {
let tail = input.get(cursor..).ok_or(CodecError::ShortRead {
expected: 1,
actual: 0,
})?;
let (v, consumed) = MonitoredItemValue::decode(tail)?;
cursor += consumed;
out.push(v);
}
Ok(out)
}
/// Encode an array of `IAsbCustomSerializableType` items per
/// `AsbDataCustomSerializer.WriteObjectContent` array branch
/// (`AsbContracts.cs:1583-1591` — calls `WriteArrayToStream` which
/// emits a 4-byte count followed by each element's `WriteToStream`).
pub fn encode_item_identity_array(items: &[ItemIdentity]) -> Vec<u8> {
let mut out = Vec::new();
let count = i32::try_from(items.len()).unwrap_or(i32::MAX);
out.extend_from_slice(&count.to_le_bytes());
for item in items {
item.encode_into(&mut out);
}
out
}
/// Decode an array of `ItemIdentity`s from the WCF custom-serializer
/// binary form (4-byte count + items). Mirrors
/// `ItemIdentity.InitializeArrayFromStream` (`cs:614-623`).
pub fn decode_item_identity_array(input: &[u8]) -> Result<Vec<ItemIdentity>, CodecError> {
let mut cursor = 0usize;
let count = read_i32_le(input, &mut cursor)?;
if count < 0 {
return Err(CodecError::Decode {
offset: 0,
reason: "negative item-identity array count",
buffer_len: input.len(),
});
}
let mut out = Vec::with_capacity(count as usize);
for _ in 0..count {
let tail = input.get(cursor..).ok_or(CodecError::ShortRead {
expected: 1,
actual: 0,
})?;
let (item, consumed) = ItemIdentity::decode(tail)?;
cursor += consumed;
out.push(item);
}
Ok(out)
}
// ---- AsbBinary helpers ---------------------------------------------------
/// Mirror `AsbBinary.WriteUnicodeString` at `cs:1622-1633`. Null/empty
/// strings emit a 4-byte `0u32` length and no payload bytes.
fn write_unicode_string(out: &mut Vec<u8>, value: Option<&str>) {
let s = value.unwrap_or("");
if s.is_empty() {
out.extend_from_slice(&0u32.to_le_bytes());
return;
}
let mut utf16 = Vec::with_capacity(s.len() * 2);
for unit in s.encode_utf16() {
utf16.extend_from_slice(&unit.to_le_bytes());
}
let len = u32::try_from(utf16.len()).unwrap_or(u32::MAX);
out.extend_from_slice(&len.to_le_bytes());
out.extend_from_slice(&utf16);
}
/// Mirror `AsbBinary.ReadUnicodeString` at `cs:1616-1620`. Length 0
/// → `Some(String::new())` to match .NET's behaviour (the C# code
/// returns `string.Empty` for length 0, NOT `null`). The wire format
/// genuinely cannot distinguish `null` from empty — both are encoded
/// as 4 bytes of zero — so we pick the same lossy collapse the
/// reference does. This matters for the canonical-XML signing path:
/// .NET's `XmlSerializer` treats `null` and `string.Empty` differently
/// (`xsi:nil` vs self-closing element), so callers that need to
/// preserve the distinction MUST track it in their domain types
/// before encoding (we cannot recover it from wire bytes).
fn read_unicode_string(input: &[u8], cursor: &mut usize) -> Result<Option<String>, CodecError> {
let len = read_u32_le(input, cursor)? as usize;
if len == 0 {
return Ok(Some(String::new()));
}
if len % 2 != 0 {
return Err(CodecError::Decode {
offset: *cursor,
reason: "unicode string length is odd",
buffer_len: input.len(),
});
}
let bytes = input
.get(*cursor..*cursor + len)
.ok_or(CodecError::ShortRead {
expected: len,
actual: input.len().saturating_sub(*cursor),
})?;
let mut units = Vec::with_capacity(len / 2);
for chunk in bytes.chunks_exact(2) {
let mut buf = [0u8; 2];
buf.copy_from_slice(chunk);
units.push(u16::from_le_bytes(buf));
}
let s = String::from_utf16(&units).map_err(|_| CodecError::Decode {
offset: *cursor,
reason: "invalid UTF-16 in unicode string",
buffer_len: input.len(),
})?;
*cursor += len;
Ok(Some(s))
}
fn read_u16_le(input: &[u8], cursor: &mut usize) -> Result<u16, CodecError> {
let bytes = read_array::<2>(input, cursor)?;
Ok(u16::from_le_bytes(bytes))
}
fn read_u32_le(input: &[u8], cursor: &mut usize) -> Result<u32, CodecError> {
let bytes = read_array::<4>(input, cursor)?;
Ok(u32::from_le_bytes(bytes))
}
fn read_i32_le(input: &[u8], cursor: &mut usize) -> Result<i32, CodecError> {
let bytes = read_array::<4>(input, cursor)?;
Ok(i32::from_le_bytes(bytes))
}
fn read_u64_le(input: &[u8], cursor: &mut usize) -> Result<u64, CodecError> {
let bytes = read_array::<8>(input, cursor)?;
Ok(u64::from_le_bytes(bytes))
}
fn read_u8(input: &[u8], cursor: &mut usize) -> Result<u8, CodecError> {
let byte = *input.get(*cursor).ok_or(CodecError::ShortRead {
expected: 1,
actual: 0,
})?;
*cursor += 1;
Ok(byte)
}
fn read_array<const N: usize>(input: &[u8], cursor: &mut usize) -> Result<[u8; N], CodecError> {
let slice = input
.get(*cursor..*cursor + N)
.ok_or(CodecError::ShortRead {
expected: N,
actual: input.len().saturating_sub(*cursor),
})?;
let mut out = [0u8; N];
out.copy_from_slice(slice);
*cursor += N;
Ok(out)
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
mod tests {
use super::*;
fn round_trip(item: ItemIdentity) {
let bytes = item.encode();
let (decoded, consumed) = ItemIdentity::decode(&bytes).unwrap();
assert_eq!(consumed, bytes.len());
assert_eq!(decoded, item);
}
#[test]
fn item_identity_round_trip_default() {
round_trip(ItemIdentity::default());
}
#[test]
fn item_identity_round_trip_absolute_by_name() {
round_trip(ItemIdentity::absolute_by_name("TestChildObject.TestInt"));
}
#[test]
fn item_identity_round_trip_with_id() {
round_trip(ItemIdentity {
kind: ItemIdentityType::NameAndId as u16,
reference_type: ItemReferenceType::Absolute as u16,
name: Some("TestChildObject.TestInt".to_string()),
context_name: Some("TestObject".to_string()),
id: 0x1234_5678_9abc_def0,
id_specified: true,
});
}
#[test]
fn item_identity_round_trip_unicode_name() {
round_trip(ItemIdentity::absolute_by_name("TéstObj.Φοο"));
}
#[test]
fn item_identity_byte_layout_minimum_19_bytes() {
// Empty Name + empty ContextName + Id=0 + IdSpecified=false:
// 2 (kind) + 2 (refType) + 4 (name len=0) + 4 (ctx len=0)
// + 8 (id) + 1 (idSpecified) = 21 bytes.
let item = ItemIdentity::default();
let bytes = item.encode();
assert_eq!(bytes.len(), 21);
}
#[test]
fn unicode_string_round_trip_handles_null_empty_and_value() {
// Null and empty are wire-identical (both encode as len=0 +
// zero bytes). The decoder collapses both to `Some(String::
// new())` to match .NET's `string.Empty` return.
let mut buf = Vec::new();
write_unicode_string(&mut buf, None);
let mut c = 0;
assert_eq!(
read_unicode_string(&buf, &mut c).unwrap(),
Some(String::new())
);
let mut buf = Vec::new();
write_unicode_string(&mut buf, Some(""));
let mut c = 0;
assert_eq!(
read_unicode_string(&buf, &mut c).unwrap(),
Some(String::new())
);
// ASCII
let mut buf = Vec::new();
write_unicode_string(&mut buf, Some("hi"));
let mut c = 0;
assert_eq!(
read_unicode_string(&buf, &mut c).unwrap(),
Some("hi".to_string())
);
}
#[test]
fn item_identity_array_round_trip() {
let items = vec![
ItemIdentity::absolute_by_name("Tag.A"),
ItemIdentity::absolute_by_name("Tag.B"),
ItemIdentity::absolute_by_name("Tag.C"),
];
let bytes = encode_item_identity_array(&items);
let decoded = decode_item_identity_array(&bytes).unwrap();
assert_eq!(decoded, items);
}
#[test]
fn item_identity_array_empty() {
let bytes = encode_item_identity_array(&[]);
// 4 bytes (count = 0)
assert_eq!(bytes.len(), 4);
assert_eq!(
decode_item_identity_array(&bytes).unwrap(),
Vec::<ItemIdentity>::new()
);
}
#[test]
fn item_status_round_trip() {
let s = ItemStatus {
item: ItemIdentity::absolute_by_name("Tag.X"),
status: AsbStatus {
count: -1,
payload: vec![0xC0],
},
error_code: 0x1234,
error_code_specified: true,
};
let bytes = s.encode();
let (decoded, consumed) = ItemStatus::decode(&bytes).unwrap();
assert_eq!(consumed, bytes.len());
assert_eq!(decoded, s);
}
#[test]
fn item_status_array_round_trip() {
let arr = vec![
ItemStatus::default(),
ItemStatus {
item: ItemIdentity::absolute_by_name("Tag.A"),
status: AsbStatus {
count: 1,
payload: vec![0x01, 0x02],
},
error_code: 42,
error_code_specified: true,
},
];
let bytes = encode_item_status_array(&arr);
let decoded = decode_item_status_array(&bytes).unwrap();
assert_eq!(decoded, arr);
}
#[test]
fn monitored_item_value_round_trip() {
let mv = MonitoredItemValue {
item: ItemIdentity::absolute_by_name("Tag.X"),
value: RuntimeValue {
timestamp_binary: 0x0123_4567,
timestamp_specified: true,
value: AsbVariant::from_i32(100),
status: AsbStatus::default(),
},
user_data: AsbVariant::empty(),
};
let bytes = mv.encode();
let (decoded, consumed) = MonitoredItemValue::decode(&bytes).unwrap();
assert_eq!(consumed, bytes.len());
assert_eq!(decoded, mv);
}
#[test]
fn monitored_item_value_array_round_trip() {
let arr = vec![
MonitoredItemValue {
item: ItemIdentity::absolute_by_name("Tag.A"),
value: RuntimeValue {
timestamp_binary: 1,
timestamp_specified: true,
value: AsbVariant::from_i32(1),
status: AsbStatus::default(),
},
user_data: AsbVariant::empty(),
},
MonitoredItemValue {
item: ItemIdentity::absolute_by_name("Tag.B"),
value: RuntimeValue {
timestamp_binary: 2,
timestamp_specified: false,
value: AsbVariant::from_string("hello"),
status: AsbStatus {
count: 1,
payload: vec![0xC0],
},
},
user_data: AsbVariant::from_bool(true),
},
];
let bytes = encode_monitored_item_value_array(&arr);
let decoded = decode_monitored_item_value_array(&bytes).unwrap();
assert_eq!(decoded, arr);
}
#[test]
fn item_identity_array_count_is_le_int32() {
let items = vec![ItemIdentity::default(); 7];
let bytes = encode_item_identity_array(&items);
// First 4 bytes = 7 little-endian.
assert_eq!(&bytes[0..4], &[0x07, 0x00, 0x00, 0x00]);
}
}
File diff suppressed because it is too large Load Diff
+42 -1
View File
@@ -1,5 +1,46 @@
//! `mxaccess-asb` — `IASBIDataV2` client.
//!
//! M0 stub. Real implementation lands in M5 — see `design/60-roadmap.md`.
//! M5 work-in-progress (F25). The first slice of F25 — SOAP-1.2-over-NBFX
//! envelope assembly + action constants for the full `IASBIDataV2`
//! contract — lives in [`envelope`]. Per-operation request/response
//! struct codecs and the network-bound `AsbClient` (TCP + NMF preamble +
//! sized-envelope read/write loop + auth handshake) land in subsequent
//! F25 iterations.
#![forbid(unsafe_code)]
pub mod client;
pub mod contracts;
pub mod envelope;
pub mod operations;
pub mod xml_canonical;
pub use client::{AsbClient, ClientError, PreambleMode};
pub use contracts::{
ItemIdentity, ItemIdentityType, ItemReferenceType, ItemStatus, MonitoredItemValue,
decode_item_identity_array, decode_item_status_array, decode_monitored_item_value_array,
encode_item_identity_array, encode_item_status_array, encode_monitored_item_value_array,
};
pub use envelope::{
ConnectionValidator, DecodedEnvelope, EnvelopeError, SoapEnvelope, actions, decode_envelope,
encode_envelope,
};
pub use operations::{
AddMonitoredItemsResponse, AuthenticationDataBytes, ConnectResponse,
CreateSubscriptionResponse, DeleteMonitoredItemsResponse, DeleteSubscriptionResponse,
MinimalMonitoredItem, MinimalWriteValue, OperationError, PublishResponse,
PublishWriteCompleteResponse, ReadResponse, RegisterItemsResponse,
RESULT_CODE_INVALID_CONNECTION_ID, UnregisterItemsResponse,
WriteResponse, build_add_monitored_items_request_body, build_authenticate_me_request_body,
build_connect_request_body, build_create_subscription_request_body,
build_delete_monitored_items_request_body, build_delete_subscription_request_body,
build_disconnect_request_body, build_keep_alive_request_body, build_publish_request_body,
build_publish_write_complete_request_body, build_read_request_body,
build_register_items_request_body, build_unregister_items_request_body,
build_write_request_body, collect_asbidata_payloads, decode_add_monitored_items_response,
decode_connect_response, decode_create_subscription_response,
decode_delete_monitored_items_response, decode_publish_response,
decode_publish_write_complete_response, decode_read_response, decode_register_items_response,
decode_unregister_items_response, decode_write_response,
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,952 @@
//! Canonical XML emitter for `ConnectedRequest` HMAC signing.
//!
//! .NET's `AsbSystemAuthenticator.Sign` (`AsbSystemAuthenticator.cs:79`)
//! HMACs `Encoding.UTF8.GetBytes(request.ToXml())` — the textual XML
//! produced by `XmlSerializer.Serialize(...)` with default namespace
//! `"urn:invensys.schemas"` (`AsbSerialization.cs:12-48`). For the
//! server's recomputation of the MAC to match ours, this module must
//! emit byte-identical UTF-8 bytes.
//!
//! ## Inferred XmlSerializer rules
//!
//! Captured from `MxAsbClient.Probe --dump-signed-xml` against
//! deterministic field values; fixtures saved at
//! `crates/mxaccess-asb/tests/fixtures/signed-xml/*.xml` (also see
//! `tests/fixtures/signed-xml/README.md`):
//!
//! 1. Element name = class name (NOT `[MessageContract.WrapperName]`).
//! 2. Field order = C# declaration order (inherited fields first; NOT
//! `[MessageBodyMember.Order]`).
//! 3. `[XmlType(Namespace = ...)]` on a field's TYPE causes per-child
//! `xmlns="..."` redeclaration on the children, NOT on the wrapper.
//! 4. `byte[]` → base64 text content. `Guid` → lowercase D-format.
//! `ulong` → decimal. `bool` → `"true"`/`"false"`.
//! 5. Null reference field with `[XmlElement(IsNullable = true)]` →
//! `<Name xsi:nil="true" xmlns="..." />`. Empty string → self-closing
//! `<Name xmlns="..." />`.
//! 6. `*Specified` pattern: `XxxSpecified = true` triggers `<Xxx>` to be
//! emitted with the int value; the `*Specified` field itself is
//! `[XmlIgnore]`.
//! 7. Self-closing elements use ` />` (space before `/>`).
//! 8. CRLF line endings, 2-space indent, no trailing newline.
//! 9. XML declaration: `<?xml version="1.0" encoding="utf-16"?>` (the
//! `utf-16` literal is a .NET StringWriter default — actual byte
//! encoding fed to HMAC is UTF-8).
use crate::ConnectionValidator;
use crate::contracts::ItemIdentity;
use crate::envelope::format_uuid;
use crate::operations::{MinimalMonitoredItem, MinimalWriteValue};
const INVENSYS_NS: &str = "urn:invensys.schemas";
const DATA_NS: &str = "http://asb.contracts.data/20111111";
const IOM_DATA_NS: &str = "urn:data.data.asb.iom:2";
/// Variant's per-type namespace (`[XmlType(Namespace = ...)]` on
/// `Variant` per `AsbContracts.cs`). Children of a Variant — Type,
/// Length, Payload — get this xmlns redeclaration.
const IDATA_DATA_NS: &str = "http://asb.contracts.idata.data/20111111";
const XSI_NS: &str = "http://www.w3.org/2001/XMLSchema-instance";
const XSD_NS: &str = "http://www.w3.org/2001/XMLSchema";
const HEADER: &str = "<?xml version=\"1.0\" encoding=\"utf-16\"?>\r\n";
// ---- public emitters -----------------------------------------------------
/// `<AuthenticateMe>` per `AsbContracts.cs:102-107`.
pub fn emit_authenticate_me_xml(
validator: &ConnectionValidator,
consumer_data_b64: &str,
consumer_iv_b64: &str,
) -> Vec<u8> {
emit_top("AuthenticateMe", |s| {
emit_validator(s, validator);
emit_authentication_data_field(s, "ConsumerAuthenticationData", consumer_data_b64, consumer_iv_b64);
})
}
/// `<Disconnect>` per `AsbContracts.cs:109-114`. Same shape as
/// AuthenticateMe — both have a single `ConsumerAuthenticationData`
/// body field plus the inherited `ConnectionValidator` header.
pub fn emit_disconnect_xml(
validator: &ConnectionValidator,
consumer_data_b64: &str,
consumer_iv_b64: &str,
) -> Vec<u8> {
emit_top("Disconnect", |s| {
emit_validator(s, validator);
emit_authentication_data_field(s, "ConsumerAuthenticationData", consumer_data_b64, consumer_iv_b64);
})
}
/// `<KeepAlive>` per `AsbContracts.cs:116-117`. Empty body — only the
/// inherited `ConnectionValidator` header.
pub fn emit_keep_alive_xml(validator: &ConnectionValidator) -> Vec<u8> {
emit_top("KeepAlive", |s| {
emit_validator(s, validator);
})
}
/// `<RegisterItemsRequest>` per `AsbContracts.cs:119-131`. Body
/// fields in declaration order: `Items`, `RequireId`, `RegisterOnly`.
/// Each `Items` entry is a single `ItemIdentity` (XmlElement attribute
/// renames the field to "Items").
pub fn emit_register_items_request_xml(
validator: &ConnectionValidator,
items: &[ItemIdentity],
require_id: bool,
register_only: bool,
) -> Vec<u8> {
emit_top("RegisterItemsRequest", |s| {
emit_validator(s, validator);
for item in items {
emit_item_identity(s, item);
}
emit_invensys_bool(s, " ", "RequireId", require_id);
emit_invensys_bool(s, " ", "RegisterOnly", register_only);
})
}
/// `<UnregisterItemsRequest>` per `AsbContracts.cs:145-150`. Body
/// has just the `Items` array (no `RequireId`/`RegisterOnly`).
pub fn emit_unregister_items_request_xml(
validator: &ConnectionValidator,
items: &[ItemIdentity],
) -> Vec<u8> {
emit_top("UnregisterItemsRequest", |s| {
emit_validator(s, validator);
for item in items {
emit_item_identity(s, item);
}
})
}
/// `<ReadRequest>` per `AsbContracts.cs:161-167`. Same shape as
/// `RegisterItemsRequest` but without `RequireId` / `RegisterOnly`.
pub fn emit_read_request_xml(
validator: &ConnectionValidator,
items: &[ItemIdentity],
) -> Vec<u8> {
emit_top("ReadRequest", |s| {
emit_validator(s, validator);
for item in items {
emit_item_identity(s, item);
}
})
}
/// `<PublishWriteCompleteRequest>` per `AsbContracts.cs:204-205`.
/// Empty body — same shape as `KeepAlive`, just a different wrapper
/// element.
pub fn emit_publish_write_complete_request_xml(validator: &ConnectionValidator) -> Vec<u8> {
emit_top("PublishWriteCompleteRequest", |s| {
emit_validator(s, validator);
})
}
/// `<CreateSubscriptionRequest>` per `AsbContracts.cs:215-223`.
/// `MaxQueueSize` (`long`) + `SampleInterval` (`ulong`) — both stay
/// in the parent `urn:invensys.schemas` namespace (no per-element
/// xmlns redeclaration; the type doesn't carry `[XmlType(Namespace)]`
/// because both fields are primitives).
pub fn emit_create_subscription_request_xml(
validator: &ConnectionValidator,
max_queue_size: i64,
sample_interval: u64,
) -> Vec<u8> {
emit_top("CreateSubscriptionRequest", |s| {
emit_validator(s, validator);
emit_invensys_text(s, " ", "MaxQueueSize", &max_queue_size.to_string());
emit_invensys_text(s, " ", "SampleInterval", &sample_interval.to_string());
})
}
/// `<DeleteSubscriptionRequest>` per `AsbContracts.cs:232-237`.
/// Single primitive `<SubscriptionId>` long.
pub fn emit_delete_subscription_request_xml(
validator: &ConnectionValidator,
subscription_id: i64,
) -> Vec<u8> {
emit_top("DeleteSubscriptionRequest", |s| {
emit_validator(s, validator);
emit_invensys_text(s, " ", "SubscriptionId", &subscription_id.to_string());
})
}
/// `<PublishRequest>` per `AsbContracts.cs:287-292`. Same shape as
/// `DeleteSubscriptionRequest` (single primitive `<SubscriptionId>`).
pub fn emit_publish_request_xml(
validator: &ConnectionValidator,
subscription_id: i64,
) -> Vec<u8> {
emit_top("PublishRequest", |s| {
emit_validator(s, validator);
emit_invensys_text(s, " ", "SubscriptionId", &subscription_id.to_string());
})
}
/// `<WriteBasicRequest>` per `AsbContracts.cs:181-194`. `Items[]` +
/// `Values[]` (each [`MinimalWriteValue`] inlined as a `<Values>`
/// element with Value/Status/Comment children) + `WriteHandle`
/// (`uint`).
///
/// `MinimalWriteValue` only carries the inner `Variant`; the optional
/// ArrayElementIndex / HasQT / Timestamp fields are `*Specified`-gated
/// and never emit when the consumer-visible value is the default.
/// `Status` is fixed at the empty-AsbStatus shape (`<Count>0</Count>`).
/// `Comment` is fixed at `<Comment xsi:nil="true">` (None — matches
/// the captured fixture and the .NET default for `string? Comment;`).
pub fn emit_write_basic_request_xml(
validator: &ConnectionValidator,
items: &[ItemIdentity],
values: &[MinimalWriteValue],
write_handle: u32,
) -> Vec<u8> {
emit_top("WriteBasicRequest", |s| {
emit_validator(s, validator);
for item in items {
emit_item_identity(s, item);
}
for value in values {
emit_write_value(s, value);
}
emit_invensys_text(s, " ", "WriteHandle", &write_handle.to_string());
})
}
/// `<AddMonitoredItemsRequest>` per `AsbContracts.cs:242-254`.
/// `SubscriptionId` + `Items[]` (each [`MinimalMonitoredItem`] inlined
/// as an `<Items>` element with Item/SampleInterval/ValueDeadband/
/// UserData/Buffered children) + `RequireId`.
pub fn emit_add_monitored_items_request_xml(
validator: &ConnectionValidator,
subscription_id: i64,
items: &[MinimalMonitoredItem],
require_id: bool,
) -> Vec<u8> {
emit_top("AddMonitoredItemsRequest", |s| {
emit_validator(s, validator);
emit_invensys_text(s, " ", "SubscriptionId", &subscription_id.to_string());
for item in items {
emit_monitored_item(s, item);
}
emit_invensys_bool(s, " ", "RequireId", require_id);
})
}
/// `<DeleteMonitoredItemsRequest>` per `AsbContracts.cs:268-277`.
/// Same as `AddMonitoredItemsRequest` minus the trailing `RequireId`.
pub fn emit_delete_monitored_items_request_xml(
validator: &ConnectionValidator,
subscription_id: i64,
items: &[MinimalMonitoredItem],
) -> Vec<u8> {
emit_top("DeleteMonitoredItemsRequest", |s| {
emit_validator(s, validator);
emit_invensys_text(s, " ", "SubscriptionId", &subscription_id.to_string());
for item in items {
emit_monitored_item(s, item);
}
})
}
// ---- internal helpers ----------------------------------------------------
fn emit_top<F: FnOnce(&mut String)>(class_name: &str, body: F) -> Vec<u8> {
let mut s = String::with_capacity(1024);
s.push_str(HEADER);
s.push('<');
s.push_str(class_name);
s.push_str(" xmlns:xsi=\"");
s.push_str(XSI_NS);
s.push_str("\" xmlns:xsd=\"");
s.push_str(XSD_NS);
s.push_str("\" xmlns=\"");
s.push_str(INVENSYS_NS);
s.push_str("\">\r\n");
body(&mut s);
s.push_str("</");
s.push_str(class_name);
s.push('>');
s.into_bytes()
}
/// `ConnectionValidator` element. The wrapper element itself stays in
/// the parent (urn:invensys.schemas) namespace because XmlSerializer
/// only redeclares xmlns when it changes; the inherited
/// `[XmlType(Namespace = "http://asb.contracts.data/20111111")]` (or
/// equivalent inferred default) on the inner type causes EACH direct
/// child to carry the data-ns redeclaration.
///
/// `MessageAuthenticationCode` and `SignatureInitializationVector` are
/// `byte[]` fields. When the validator is being signed (NOT yet on the
/// wire), they're empty `byte[]` and XmlSerializer emits self-closing
/// `<MessageAuthenticationCode xmlns="..." />`. After signing they
/// carry base64 content. Both forms must round-trip.
fn emit_validator(s: &mut String, v: &ConnectionValidator) {
s.push_str(" <ConnectionValidator>\r\n");
emit_data_ns_text(s, " ", "ConnectionId", &format_uuid(&v.connection_id));
emit_data_ns_text(s, " ", "MessageNumber", &v.message_number.to_string());
emit_data_ns_byte_array(s, " ", "MessageAuthenticationCode", &v.mac_base64);
emit_data_ns_byte_array(s, " ", "SignatureInitializationVector", &v.iv_base64);
s.push_str(" </ConnectionValidator>\r\n");
}
/// `AuthenticationData`-typed field (e.g. `ConsumerAuthenticationData`).
/// The wrapper stays in `urn:invensys.schemas`; children Data + IV are
/// in the data namespace per `[XmlType]` on `AuthenticationData`.
fn emit_authentication_data_field(
s: &mut String,
field_name: &str,
data_b64: &str,
iv_b64: &str,
) {
s.push_str(" <");
s.push_str(field_name);
s.push_str(">\r\n");
emit_data_ns_text(s, " ", "Data", data_b64);
emit_data_ns_text(s, " ", "InitializationVector", iv_b64);
s.push_str(" </");
s.push_str(field_name);
s.push_str(">\r\n");
}
/// `<Items>` element holding one ItemIdentity. The wrapper is in
/// urn:invensys.schemas; children get `xmlns="urn:data.data.asb.iom:2"`
/// per `[XmlType(Namespace = "urn:data.data.asb.iom:2")]` on
/// `ItemIdentity` (`AsbContracts.cs:534`).
///
/// Field order matches C# declaration: contextNameField, idField,
/// idFieldSpecified, nameField, referenceTypeField, typeField — but
/// XmlSerializer uses the public *property* declaration order which
/// yields Type → ReferenceType → Name → ContextName → (Id) per the
/// captured fixtures. `IdSpecified` is `[XmlIgnore]` so it never
/// appears; when `IdSpecified == true` the `<Id>` element is emitted.
///
/// Null Name/ContextName → `<Name xsi:nil="true" xmlns="..." />`;
/// empty-string ContextName → self-closing `<ContextName xmlns="..." />`.
fn emit_item_identity(s: &mut String, item: &ItemIdentity) {
s.push_str(" <Items>\r\n");
emit_iom_text(s, " ", "Type", &item.kind.to_string());
emit_iom_text(s, " ", "ReferenceType", &item.reference_type.to_string());
emit_iom_optional_string(s, " ", "Name", item.name.as_deref());
emit_iom_optional_string(s, " ", "ContextName", item.context_name.as_deref());
if item.id_specified {
emit_iom_text(s, " ", "Id", &item.id.to_string());
}
s.push_str(" </Items>\r\n");
}
/// Emit a `byte[]` field in the data namespace. Empty bytes (empty
/// base64 string) → self-closing `<Tag xmlns="..." />`; non-empty →
/// `<Tag xmlns="...">b64</Tag>`. Mirrors XmlSerializer's behaviour
/// for empty `byte[]` (verified via `--dump-signed-xml` with empty
/// MAC/IV).
fn emit_data_ns_byte_array(s: &mut String, indent: &str, tag: &str, value: &str) {
if value.is_empty() {
s.push_str(indent);
s.push('<');
s.push_str(tag);
s.push_str(" xmlns=\"");
s.push_str(DATA_NS);
s.push_str("\" />\r\n");
} else {
emit_data_ns_text(s, indent, tag, value);
}
}
/// Emit `<Tag xmlns="DATA_NS">value</Tag>\r\n` with the given indent.
fn emit_data_ns_text(s: &mut String, indent: &str, tag: &str, value: &str) {
s.push_str(indent);
s.push('<');
s.push_str(tag);
s.push_str(" xmlns=\"");
s.push_str(DATA_NS);
s.push_str("\">");
write_xml_escaped_text(s, value);
s.push_str("</");
s.push_str(tag);
s.push_str(">\r\n");
}
/// Emit `<Tag xmlns="IOM_DATA_NS">value</Tag>\r\n`.
fn emit_iom_text(s: &mut String, indent: &str, tag: &str, value: &str) {
s.push_str(indent);
s.push('<');
s.push_str(tag);
s.push_str(" xmlns=\"");
s.push_str(IOM_DATA_NS);
s.push_str("\">");
write_xml_escaped_text(s, value);
s.push_str("</");
s.push_str(tag);
s.push_str(">\r\n");
}
/// Emit a string-typed `[XmlElement(IsNullable = true)]` field. Three
/// cases per the captured fixtures:
/// * `None` → `<Tag xsi:nil="true" xmlns="IOM_DATA_NS" />\r\n`
/// * `Some("")` → `<Tag xmlns="IOM_DATA_NS" />\r\n`
/// * `Some(s)` → `<Tag xmlns="IOM_DATA_NS">s</Tag>\r\n`
fn emit_iom_optional_string(s: &mut String, indent: &str, tag: &str, value: Option<&str>) {
s.push_str(indent);
s.push('<');
s.push_str(tag);
match value {
None => {
// Note: xsi:nil first, THEN xmlns, per fixtures.
s.push_str(" xsi:nil=\"true\" xmlns=\"");
s.push_str(IOM_DATA_NS);
s.push_str("\" />\r\n");
}
Some("") => {
s.push_str(" xmlns=\"");
s.push_str(IOM_DATA_NS);
s.push_str("\" />\r\n");
}
Some(text) => {
s.push_str(" xmlns=\"");
s.push_str(IOM_DATA_NS);
s.push_str("\">");
write_xml_escaped_text(s, text);
s.push_str("</");
s.push_str(tag);
s.push_str(">\r\n");
}
}
}
/// Emit a `bool` field in the default invensys namespace (no xmlns
/// redeclaration).
fn emit_invensys_bool(s: &mut String, indent: &str, tag: &str, value: bool) {
s.push_str(indent);
s.push('<');
s.push_str(tag);
s.push('>');
s.push_str(if value { "true" } else { "false" });
s.push_str("</");
s.push_str(tag);
s.push_str(">\r\n");
}
/// Emit a text-bearing element in the default invensys namespace
/// (no xmlns redeclaration). Used for primitive int / long / uint
/// fields (`MaxQueueSize`, `SampleInterval`, `SubscriptionId`,
/// `WriteHandle`).
fn emit_invensys_text(s: &mut String, indent: &str, tag: &str, value: &str) {
s.push_str(indent);
s.push('<');
s.push_str(tag);
s.push('>');
write_xml_escaped_text(s, value);
s.push_str("</");
s.push_str(tag);
s.push_str(">\r\n");
}
/// Emit a `MinimalWriteValue` as a `<Values>` element with inlined
/// Value (Variant) + Status + Comment children. Mirrors the captured
/// `--dump-signed-xml WriteBasicRequest` shape:
///
/// ```xml
/// <Values>
/// <Value xmlns="urn:data.data.asb.iom:2"> ... variant ... </Value>
/// <Status xmlns="urn:data.data.asb.iom:2"><Count>0</Count></Status>
/// <Comment xsi:nil="true" xmlns="urn:data.data.asb.iom:2" />
/// </Values>
/// ```
///
/// XmlSerializer flattens each `WriteValue` array element into a
/// `<Values>` wrapper (per `[XmlElement("Values")]` on
/// `WriteValue[]?`), then emits each child field with the
/// `WriteValue.[XmlType(Namespace = "urn:data.data.asb.iom:2")]`
/// redeclaration on the outermost child of each.
fn emit_write_value(s: &mut String, value: &MinimalWriteValue) {
s.push_str(" <Values>\r\n");
// <Value> — wraps the inner Variant. The Value element itself
// gets the iom:2 redeclaration; its children (Type/Length/Payload)
// get the idata namespace.
s.push_str(" <Value xmlns=\"");
s.push_str(IOM_DATA_NS);
s.push_str("\">\r\n");
emit_idata_variant(
s,
" ",
value.value.type_id,
value.value.length,
&value.value.payload,
);
s.push_str(" </Value>\r\n");
// <Status xmlns="iom:2"><Count>0</Count></Status>. AsbStatus's
// field doesn't carry [XmlType(Namespace)], so Count inherits
// the parent iom:2 redeclaration that the wrapper added.
s.push_str(" <Status xmlns=\"");
s.push_str(IOM_DATA_NS);
s.push_str("\">\r\n <Count>0</Count>\r\n </Status>\r\n");
// <Comment xsi:nil="true" xmlns="iom:2" /> — Comment is
// [XmlElement(IsNullable = true)] string?; default-null serialises
// as the xsi:nil + xmlns redeclaration form.
s.push_str(" <Comment xsi:nil=\"true\" xmlns=\"");
s.push_str(IOM_DATA_NS);
s.push_str("\" />\r\n");
s.push_str(" </Values>\r\n");
}
/// Emit a `MinimalMonitoredItem` as an `<Items>` element with inlined
/// Item / SampleInterval / ValueDeadband / UserData / Buffered
/// children. Mirrors the `--dump-signed-xml AddMonitoredItemsRequest`
/// fixture.
///
/// Fields not on `MinimalMonitoredItem` (Active / TimeDeadband) are
/// `*Specified`-gated and never emit when unset. `ValueDeadband` and
/// `UserData` always emit because they are non-nullable `Variant`
/// structs — XmlSerializer always serialises them with their default
/// `Type=0 Length=0 Payload=nil` shape.
fn emit_monitored_item(s: &mut String, item: &MinimalMonitoredItem) {
s.push_str(" <Items>\r\n");
// <Item xmlns="iom:2"> ... ItemIdentity children, no per-child
// xmlns redeclaration since they're already in iom:2.
emit_inline_item_identity(s, " ", "Item", &item.item);
emit_iom_text(s, " ", "SampleInterval", &item.sample_interval.to_string());
// <Active> is *Specified-gated; emit only when the consumer
// opted in (None → not on the wire). MxDataProvider's
// Publish path requires Active=true to actually deliver values
// — F34, verified live 2026-05-06.
if let Some(active) = item.active {
emit_iom_text(s, " ", "Active", if active { "true" } else { "false" });
}
// ValueDeadband + UserData: default-Variant shape (type=0,
// length=0, payload nil).
emit_iom_default_variant(s, " ", "ValueDeadband");
emit_iom_default_variant(s, " ", "UserData");
emit_iom_text(s, " ", "Buffered", if item.buffered { "true" } else { "false" });
s.push_str(" </Items>\r\n");
}
/// Emit an `ItemIdentity` *as a child element of a MonitoredItem* —
/// the wrapper carries the iom:2 namespace redeclaration once, and
/// children (Type/ReferenceType/Name/ContextName/Id) inherit. Differs
/// from [`emit_item_identity`] which is the top-level form where each
/// child gets its own redeclaration.
fn emit_inline_item_identity(s: &mut String, indent: &str, wrapper: &str, item: &ItemIdentity) {
s.push_str(indent);
s.push('<');
s.push_str(wrapper);
s.push_str(" xmlns=\"");
s.push_str(IOM_DATA_NS);
s.push_str("\">\r\n");
let inner_indent = format!("{indent} ");
emit_inline_text(s, &inner_indent, "Type", &item.kind.to_string());
emit_inline_text(s, &inner_indent, "ReferenceType", &item.reference_type.to_string());
emit_inline_optional_string(s, &inner_indent, "Name", item.name.as_deref());
emit_inline_optional_string(s, &inner_indent, "ContextName", item.context_name.as_deref());
if item.id_specified {
emit_inline_text(s, &inner_indent, "Id", &item.id.to_string());
}
s.push_str(indent);
s.push_str("</");
s.push_str(wrapper);
s.push_str(">\r\n");
}
/// Inline text element — no xmlns redeclaration (consumer is already
/// inside an xmlns-scoped wrapper).
fn emit_inline_text(s: &mut String, indent: &str, tag: &str, value: &str) {
s.push_str(indent);
s.push('<');
s.push_str(tag);
s.push('>');
write_xml_escaped_text(s, value);
s.push_str("</");
s.push_str(tag);
s.push_str(">\r\n");
}
/// Inline `[XmlElement(IsNullable = true)]` string. None →
/// `<Tag xsi:nil="true" />`; Some("") → `<Tag />`; Some(s) →
/// `<Tag>s</Tag>`. Differs from [`emit_iom_optional_string`] in
/// omitting the xmlns redeclaration.
fn emit_inline_optional_string(s: &mut String, indent: &str, tag: &str, value: Option<&str>) {
s.push_str(indent);
s.push('<');
s.push_str(tag);
match value {
None => s.push_str(" xsi:nil=\"true\" />\r\n"),
Some("") => s.push_str(" />\r\n"),
Some(text) => {
s.push('>');
write_xml_escaped_text(s, text);
s.push_str("</");
s.push_str(tag);
s.push_str(">\r\n");
}
}
}
/// Emit a Variant's children — `<Type>`, `<Length>`, `<Payload>` —
/// each carrying the `IDATA_DATA_NS` redeclaration (since
/// `Variant.[XmlType(Namespace)]` is `http://asb.contracts.idata.data/20111111`).
/// `length == 0` collapses Payload to `<Payload xsi:nil="true" xmlns="..." />`
/// matching the captured shape for default-Variant fields.
fn emit_idata_variant(s: &mut String, indent: &str, type_id: u16, length: i32, payload: &[u8]) {
s.push_str(indent);
s.push_str("<Type xmlns=\"");
s.push_str(IDATA_DATA_NS);
s.push_str("\">");
s.push_str(&type_id.to_string());
s.push_str("</Type>\r\n");
s.push_str(indent);
s.push_str("<Length xmlns=\"");
s.push_str(IDATA_DATA_NS);
s.push_str("\">");
s.push_str(&length.to_string());
s.push_str("</Length>\r\n");
if length == 0 {
s.push_str(indent);
s.push_str("<Payload xsi:nil=\"true\" xmlns=\"");
s.push_str(IDATA_DATA_NS);
s.push_str("\" />\r\n");
} else {
s.push_str(indent);
s.push_str("<Payload xmlns=\"");
s.push_str(IDATA_DATA_NS);
s.push_str("\">");
s.push_str(&base64_encode(payload));
s.push_str("</Payload>\r\n");
}
}
/// Emit a default-shape Variant wrapper (`<Tag xmlns="iom:2">` with
/// Type=0 Length=0 Payload-nil children). Used for `ValueDeadband` /
/// `UserData` inside MonitoredItem.
fn emit_iom_default_variant(s: &mut String, indent: &str, tag: &str) {
s.push_str(indent);
s.push('<');
s.push_str(tag);
s.push_str(" xmlns=\"");
s.push_str(IOM_DATA_NS);
s.push_str("\">\r\n");
let inner_indent = format!("{indent} ");
emit_idata_variant(s, &inner_indent, 0, 0, &[]);
s.push_str(indent);
s.push_str("</");
s.push_str(tag);
s.push_str(">\r\n");
}
/// XML-escape characters that XmlSerializer escapes in text nodes.
/// Only `<`, `>`, and `&` are emitted as entities by the .NET writer;
/// quotes appear inside attribute values which we control directly,
/// not in text content. (Verified via `XmlTextWriter.WriteString` —
/// CRLF/TAB are passed through verbatim.)
fn write_xml_escaped_text(out: &mut String, text: &str) {
for c in text.chars() {
match c {
'<' => out.push_str("&lt;"),
'>' => out.push_str("&gt;"),
'&' => out.push_str("&amp;"),
other => out.push(other),
}
}
}
/// Encode raw bytes as base64 in the form `XmlSerializer` emits for
/// `byte[]` fields. Mirrors the inline encoder in
/// `envelope::base64_encode` (kept private there); duplicated here to
/// keep the xml_canonical module standalone.
pub fn base64_encode(input: &[u8]) -> String {
const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let lookup = |idx: u32| ALPHABET.get((idx & 0x3F) as usize).copied().unwrap_or(b'=');
let mut out = String::with_capacity(input.len().div_ceil(3) * 4);
for chunk in input.chunks(3) {
let b0 = u32::from(chunk.first().copied().unwrap_or(0));
let b1 = u32::from(chunk.get(1).copied().unwrap_or(0));
let b2 = u32::from(chunk.get(2).copied().unwrap_or(0));
let triple = (b0 << 16) | (b1 << 8) | b2;
out.push(lookup(triple >> 18) as char);
out.push(lookup(triple >> 12) as char);
out.push(if chunk.len() > 1 {
lookup(triple >> 6) as char
} else {
'='
});
out.push(if chunk.len() > 2 {
lookup(triple) as char
} else {
'='
});
}
out
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use crate::ConnectionValidator;
fn fixture(name: &str) -> Vec<u8> {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/signed-xml")
.join(name);
std::fs::read(&path).unwrap_or_else(|e| {
panic!("could not read fixture {}: {e}", path.display())
})
}
fn pinned_validator() -> ConnectionValidator {
let mac: Vec<u8> = (0u8..16).collect();
let iv: Vec<u8> = (16u8..32).collect();
ConnectionValidator {
connection_id: parse_pinned_guid(),
message_number: 42,
mac_base64: base64_encode(&mac),
iv_base64: base64_encode(&iv),
}
}
/// `8cba964a-74c1-ef74-f6aa-761b3540191b` in .NET mixed-endian
/// byte order — same value the .NET probe pins.
fn parse_pinned_guid() -> [u8; 16] {
// d1 = 0x8cba964a (LE) → bytes [4a, 96, ba, 8c]
// d2 = 0x74c1 (LE) → bytes [c1, 74]
// d3 = 0xef74 (LE) → bytes [74, ef]
// d4 (BE) = f6 aa
// d5 (BE) = 76 1b 35 40 19 1b
[
0x4a, 0x96, 0xba, 0x8c, 0xc1, 0x74, 0x74, 0xef, 0xf6, 0xaa, 0x76, 0x1b, 0x35, 0x40,
0x19, 0x1b,
]
}
fn pinned_consumer_data_b64() -> String {
// "deterministic-ciphertext-bytes" base64-encoded
base64_encode(b"deterministic-ciphertext-bytes".as_slice())
}
fn pinned_consumer_iv_b64() -> String {
// "0123456789abcdef" base64-encoded
base64_encode(b"0123456789abcdef".as_slice())
}
fn pinned_disconnect_data_b64() -> String {
base64_encode(b"disconnect-ciphertext".as_slice())
}
/// The actual signing input has empty MAC + IV (the MAC is filled
/// AFTER `request.ToXml()` produces the bytes that get HMAC'd). This
/// fixture pins XmlSerializer's empty-byte-array behaviour:
/// `<MessageAuthenticationCode xmlns="..." />` (self-closing) when
/// `byte[] = []`. Without this round-trip, the live HMAC will not
/// match the server's recomputation.
#[test]
fn authenticate_me_with_empty_mac_iv_matches_dotnet_fixture() {
let validator = ConnectionValidator {
connection_id: parse_pinned_guid(),
message_number: 42,
mac_base64: String::new(),
iv_base64: String::new(),
};
let data = pinned_consumer_data_b64();
let iv = pinned_consumer_iv_b64();
let actual = emit_authenticate_me_xml(&validator, &data, &iv);
let expected = fixture("authenticate-me-empty-mac-iv.xml");
assert_eq_bytes("authenticate-me-empty-mac-iv", &actual, &expected);
}
#[test]
fn authenticate_me_matches_dotnet_fixture() {
let validator = pinned_validator();
let data = pinned_consumer_data_b64();
let iv = pinned_consumer_iv_b64();
let actual = emit_authenticate_me_xml(&validator, &data, &iv);
let expected = fixture("authenticate-me.xml");
assert_eq_bytes("authenticate-me", &actual, &expected);
}
#[test]
fn disconnect_matches_dotnet_fixture() {
let validator = pinned_validator();
let data = pinned_disconnect_data_b64();
let iv = pinned_consumer_iv_b64();
let actual = emit_disconnect_xml(&validator, &data, &iv);
let expected = fixture("disconnect.xml");
assert_eq_bytes("disconnect", &actual, &expected);
}
#[test]
fn keep_alive_matches_dotnet_fixture() {
let validator = pinned_validator();
let actual = emit_keep_alive_xml(&validator);
let expected = fixture("keep-alive.xml");
assert_eq_bytes("keep-alive", &actual, &expected);
}
#[test]
fn register_items_matches_dotnet_fixture() {
let validator = pinned_validator();
let item = ItemIdentity {
kind: 0,
reference_type: 1,
name: Some("TestChildObject.TestInt".to_string()),
context_name: Some(String::new()),
id: 0,
id_specified: false,
};
let actual = emit_register_items_request_xml(&validator, &[item], true, false);
let expected = fixture("register-items.xml");
assert_eq_bytes("register-items", &actual, &expected);
}
#[test]
fn unregister_items_matches_dotnet_fixture() {
let validator = pinned_validator();
let item = ItemIdentity {
kind: 1,
reference_type: 1,
name: None,
context_name: None,
id: 0xCAFE_BABE_DEAD_BEEFu64,
id_specified: true,
};
let actual = emit_unregister_items_request_xml(&validator, &[item]);
let expected = fixture("unregister-items.xml");
assert_eq_bytes("unregister-items", &actual, &expected);
}
fn pinned_sample_item() -> ItemIdentity {
ItemIdentity {
kind: 0,
reference_type: 1,
name: Some("TestChildObject.TestInt".to_string()),
context_name: Some(String::new()),
id: 0,
id_specified: false,
}
}
fn pinned_sample_item_by_id() -> ItemIdentity {
ItemIdentity {
kind: 1,
reference_type: 1,
name: None,
context_name: None,
id: 0xCAFE_BABE_DEAD_BEEFu64,
id_specified: true,
}
}
/// `0x1234_5678_9abc_def0` — same `SampleSubscriptionId` the .NET
/// probe pins. Decimal 1311768467463790320.
const PINNED_SUB_ID: i64 = 0x1234_5678_9abc_def0;
#[test]
fn read_request_matches_dotnet_fixture() {
let validator = pinned_validator();
let item = pinned_sample_item();
let actual = emit_read_request_xml(&validator, &[item]);
let expected = fixture("read-request.xml");
assert_eq_bytes("read-request", &actual, &expected);
}
#[test]
fn publish_write_complete_request_matches_dotnet_fixture() {
let validator = pinned_validator();
let actual = emit_publish_write_complete_request_xml(&validator);
let expected = fixture("publish-write-complete-request.xml");
assert_eq_bytes("publish-write-complete-request", &actual, &expected);
}
#[test]
fn create_subscription_request_matches_dotnet_fixture() {
let validator = pinned_validator();
let actual = emit_create_subscription_request_xml(&validator, 100, 1000);
let expected = fixture("create-subscription-request.xml");
assert_eq_bytes("create-subscription-request", &actual, &expected);
}
#[test]
fn delete_subscription_request_matches_dotnet_fixture() {
let validator = pinned_validator();
let actual = emit_delete_subscription_request_xml(&validator, PINNED_SUB_ID);
let expected = fixture("delete-subscription-request.xml");
assert_eq_bytes("delete-subscription-request", &actual, &expected);
}
#[test]
fn publish_request_matches_dotnet_fixture() {
let validator = pinned_validator();
let actual = emit_publish_request_xml(&validator, PINNED_SUB_ID);
let expected = fixture("publish-request.xml");
assert_eq_bytes("publish-request", &actual, &expected);
}
#[test]
fn write_basic_request_matches_dotnet_fixture() {
use mxaccess_codec::AsbVariant;
let validator = pinned_validator();
let item = pinned_sample_item();
let value = MinimalWriteValue::new(AsbVariant::from_i32(42));
let actual = emit_write_basic_request_xml(&validator, &[item], &[value], 0xDEAD_BEEFu32);
let expected = fixture("write-basic-request.xml");
assert_eq_bytes("write-basic-request", &actual, &expected);
}
#[test]
fn add_monitored_items_request_matches_dotnet_fixture() {
let validator = pinned_validator();
let item = MinimalMonitoredItem::new(pinned_sample_item(), 1000);
let actual =
emit_add_monitored_items_request_xml(&validator, PINNED_SUB_ID, &[item], true);
let expected = fixture("add-monitored-items-request.xml");
assert_eq_bytes("add-monitored-items-request", &actual, &expected);
}
#[test]
fn delete_monitored_items_request_matches_dotnet_fixture() {
let validator = pinned_validator();
let item = MinimalMonitoredItem::new(pinned_sample_item_by_id(), 1000);
let actual =
emit_delete_monitored_items_request_xml(&validator, PINNED_SUB_ID, &[item]);
let expected = fixture("delete-monitored-items-request.xml");
assert_eq_bytes("delete-monitored-items-request", &actual, &expected);
}
/// XML escaping: feed a name with `<` and `&` and confirm the
/// emitter produces `&lt;` and `&amp;`. Real wire never carries
/// these characters in tag names, but this protects against future
/// users-supplied-tag-name regressions.
#[test]
fn xml_escapes_text_content() {
let mut s = String::new();
write_xml_escaped_text(&mut s, "a < b & c > d");
assert_eq!(s, "a &lt; b &amp; c &gt; d");
}
#[track_caller]
fn assert_eq_bytes(label: &str, actual: &[u8], expected: &[u8]) {
if actual == expected {
return;
}
let actual_str = String::from_utf8_lossy(actual);
let expected_str = String::from_utf8_lossy(expected);
let diverge = actual
.iter()
.zip(expected.iter())
.take_while(|(a, e)| a == e)
.count();
let context_start = diverge.saturating_sub(40);
let context_end_act = (diverge + 40).min(actual.len());
let context_end_exp = (diverge + 40).min(expected.len());
let actual_ctx = actual.get(context_start..context_end_act).unwrap_or(&[]);
let expected_ctx = expected.get(context_start..context_end_exp).unwrap_or(&[]);
panic!(
"{label}: bytes differ at offset {diverge}\n actual len={} bytes\n expected len={} bytes\n actual context: {:?}\n expected ctx: {:?}\n full actual:\n{}\n full expected:\n{}",
actual.len(),
expected.len(),
String::from_utf8_lossy(actual_ctx),
String::from_utf8_lossy(expected_ctx),
actual_str,
expected_str,
);
}
}
@@ -0,0 +1,70 @@
//! F34 — wire-byte trace of a captured `AddMonitoredItemsRequest`.
//!
//! `tests/fixtures/add-monitored-items-request-wire.bin` is the
//! verbatim C→S bytes the .NET probe (`MxAsbClient.Probe --subscribe
//! --via=net.tcp://127.0.0.1:8088/...`) sent to MxDataProvider on
//! 2026-05-06. The exchange led to a working subscription that
//! delivered values; this is the request shape MxDataProvider
//! actually accepts.
//!
//! Test goal: dump every NBFX token in the body so we can read off
//! the exact element-name shape (DataContract field-suffix names per
//! `[DataMember(Name=...)]`, NOT XmlSerializer property names) and
//! re-implement `build_add_monitored_items_request_body` against it.
//!
//! Frame layout: 3-byte NMF SizedEnvelope header (`06 b4 05`,
//! varint length = 692) + 692-byte SOAP envelope.
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic
)]
use mxaccess_asb::decode_envelope;
use mxaccess_asb_nettcp::nbfx::DynamicDictionary;
#[test]
fn add_monitored_items_request_capture_decoder_trace() {
let raw = std::fs::read(
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/add-monitored-items-request-wire.bin"),
)
.expect("read fixture");
assert_eq!(raw.len(), 695, "frame length sanity check");
// Strip 3-byte NMF SizedEnvelope header.
let envelope = &raw[3..];
assert_eq!(envelope.len(), 692);
// Manually walk the leading WCF binary header (length-prefixed
// string list) so we can dump every interned string + its wire
// id. Mirrors what `decode_envelope::parse_binary_header_prefix`
// does internally; reproducing it inline so the test sees the
// raw strings.
use mxaccess_asb_nettcp::nmf::decode_multibyte_int31;
let mut cursor = 0usize;
let outer_len = decode_multibyte_int31(envelope, &mut cursor).expect("outer-len varint");
eprintln!("=== binary-header outer length: {outer_len} ===");
let header_start = cursor;
let header_end = header_start + outer_len as usize;
let mut p = header_start;
let mut idx = 0usize;
while p < header_end {
let len = decode_multibyte_int31(envelope, &mut p).expect("string-len varint");
let bytes = &envelope[p..p + len as usize];
let s = std::str::from_utf8(bytes).expect("utf-8 header string");
let wire_id = (idx as u32) * 2 + 1;
eprintln!(" header[{idx}] (wire-id {wire_id}) = {s:?}");
p += len as usize;
idx += 1;
}
let mut dict = DynamicDictionary::new();
let decoded = decode_envelope(envelope, &mut dict).expect("decode_envelope succeeds");
eprintln!("=== body tokens ({} total) ===", decoded.body_tokens.len());
for (i, tok) in decoded.body_tokens.iter().enumerate() {
eprintln!(" body[{i}]={tok:?}");
}
}
@@ -0,0 +1,7 @@
# These fixtures are byte-equal targets for the F28 canonical XML
# emitter — `XmlSerializer.Serialize(...)` output that the .NET
# reference HMACs in `AsbSystemAuthenticator.Sign`. CRLF line endings
# are part of the canonical form (StringWriter default on Windows),
# so Git MUST NOT touch them. `-text` marks them as binary so neither
# `core.autocrlf` nor `text` filters can rewrite the bytes.
*.xml -text
@@ -0,0 +1,128 @@
# Signed-request XML fixtures
Canonical `XmlSerializer` output for every `ConnectedRequest` shape that
the .NET reference HMACs in `AsbSystemAuthenticator.Sign`
(`src/MxAsbClient/AsbSystemAuthenticator.cs:79`). The Rust port's
canonical-XML emitter (F28) must produce these exact UTF-8 bytes for
the HMAC to match the server's recomputation.
## Capture procedure
```powershell
dotnet run --project src\MxAsbClient.Probe -c Release -- --dump-signed-xml > capture.txt
```
The probe's `--dump-signed-xml` flag (added 2026-05-05) builds each
shape with deterministic field values and prints the output of
`AsbSerialization.ToXml(...)` (`src/MxAsbClient/AsbSerialization.cs:12`).
## Pinned values
All shapes use the same `ConnectionValidator`:
- `ConnectionId = 8cba964a-74c1-ef74-f6aa-761b3540191b`
- `MessageNumber = 42`
- `MessageAuthenticationCode = AAECAwQFBgcICQoLDA0ODw==` (base64 of bytes 0..15)
- `SignatureInitializationVector = EBESExQVFhcYGRobHB0eHw==` (base64 of bytes 16..31)
`AuthenticateMe` and `Disconnect` use `AuthenticationData` with:
- `Data = "deterministic-ciphertext-bytes"` (base64-encoded)
- `InitializationVector = "0123456789abcdef"` (base64-encoded)
`RegisterItemsRequest` uses one `ItemIdentity` with
`Type = Name (0)`, `ReferenceType = Absolute (1)`,
`Name = "TestChildObject.TestInt"`, `ContextName = ""`.
`UnregisterItemsRequest` uses one `ItemIdentity` with
`Type = Id (1)`, `ReferenceType = Absolute (1)`, `Name = null`,
`ContextName = null`, `Id = 0xCAFEBABEDEADBEEF (14627333968688430831)`,
`IdSpecified = true`.
## Observed serialiser behaviour
These rules were inferred from the captured output and from the .NET
source for `XmlSerializer`:
1. **Element name = class name**, NOT `[MessageContract.WrapperName]`.
`XmlSerializer` does not honour WCF's MessageContract attributes.
2. **Top-element xmlns ordering** (after `<?xml ... ?>`):
`xmlns:xsi`, then `xmlns:xsd`, then default `xmlns`.
The `AsbSerialization.ToXml` post-process (`AsbSerialization.cs:36-47`)
reparses with `XDocument.Load` and reorders to put `xsi` before
`xsd``XmlSerializer`'s native order is the opposite.
3. **Field order = C# declaration order** (with inherited fields
first), NOT `[MessageBodyMember.Order]`.
4. **`[XmlType(Namespace = ...)]` on a field's type** triggers an
`xmlns="..."` redeclaration on EACH child element of that type's
instance, NOT on the wrapper element itself. e.g. inside
`<ConnectionValidator>`, every direct child gets
`xmlns="http://asb.contracts.data/20111111"`.
5. **`byte[]` fields** serialise as base64 text content.
**`Guid`** as canonical lowercase D-format (`8cba964a-74c1-...`).
**`ulong`** as decimal.
**`bool`** as `"true"` / `"false"`.
6. **Null reference-type fields** with `[XmlElement(IsNullable = true)]`
produce `<Name xsi:nil="true" xmlns="..." />`.
Empty string fields produce a self-closing `<ContextName xmlns="..." />`.
7. **`*Specified` pattern**: a public bool field named `XxxSpecified` =
`true` causes XmlSerializer to emit the corresponding `<Xxx>`
element. `IdSpecified = false` (default) → `<Id>` omitted.
`IdSpecified = true``<Id>` emitted with the int value.
The `*Specified` field itself is `[XmlIgnore]` and never emitted.
8. **Self-closing elements** use ` />` (space before `/>`).
9. **Indentation**: 2 spaces, `\r\n` line endings, no trailing
newline after the closing tag.
10. **XML declaration**: `<?xml version="1.0" encoding="utf-16"?>`
note `utf-16` even though `AsbSystemAuthenticator.Sign` HMACs
`Encoding.UTF8.GetBytes(...)` of this string. The declaration is
a static .NET StringWriter default; the actual byte encoding fed
to HMAC is UTF-8.
## Files
- `authenticate-me.xml``AuthenticateMe`
- `authenticate-me-empty-mac-iv.xml``AuthenticateMe` with the
pre-signing validator (empty MAC + IV) — the actual HMAC input shape.
- `disconnect.xml``Disconnect`
- `keep-alive.xml``KeepAlive`
- `register-items.xml``RegisterItemsRequest`
- `unregister-items.xml``UnregisterItemsRequest`
The eight remaining `ConnectedRequest` shapes added 2026-05-06 (F28
step 2) cover the data-plane + subscription ops:
- `read-request.xml``ReadRequest`
- `write-basic-request.xml``WriteBasicRequest`
- `publish-write-complete-request.xml``PublishWriteCompleteRequest`
- `create-subscription-request.xml``CreateSubscriptionRequest`
- `delete-subscription-request.xml``DeleteSubscriptionRequest`
- `add-monitored-items-request.xml``AddMonitoredItemsRequest`
- `delete-monitored-items-request.xml``DeleteMonitoredItemsRequest`
- `publish-request.xml``PublishRequest`
Pinned values for the new shapes (in addition to the
`ConnectionValidator` above):
- `SubscriptionId = 0x1234_5678_9abc_def0` (decimal `1311768467463790320`)
- `MaxQueueSize = 100`, `SampleInterval = 1000`
- `WriteHandle = 0xDEAD_BEEF` (decimal `3735928559`)
- `WriteBasicRequest` uses one `WriteValue` whose `Value` is
`Variant.FromInt32(42)` (`Type=4`, `Length=4`, `Payload=[42, 0, 0, 0]`)
- `AddMonitoredItemsRequest` uses one `MonitoredItem` with
`Item = "TestChildObject.TestInt"` by name + `SampleInterval=1000` +
`Buffered=false` (other fields default)
- `DeleteMonitoredItemsRequest` uses one `MonitoredItem` with
`Item.Id = 0xCAFE_BABE_DEAD_BEEF` (the same `IdSpecified` shape as
`unregister-items.xml`)
Each file is the verbatim UTF-8 representation of `request.ToXml()`,
with literal `\r\n` line endings preserved. Treat as binary (don't
let your editor reformat).
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-16"?>
<AddMonitoredItemsRequest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111">AAECAwQFBgcICQoLDA0ODw==</MessageAuthenticationCode>
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111">EBESExQVFhcYGRobHB0eHw==</SignatureInitializationVector>
</ConnectionValidator>
<SubscriptionId>1311768467463790320</SubscriptionId>
<Items>
<Item xmlns="urn:data.data.asb.iom:2">
<Type>0</Type>
<ReferenceType>1</ReferenceType>
<Name>TestChildObject.TestInt</Name>
<ContextName />
</Item>
<SampleInterval xmlns="urn:data.data.asb.iom:2">1000</SampleInterval>
<ValueDeadband xmlns="urn:data.data.asb.iom:2">
<Type xmlns="http://asb.contracts.idata.data/20111111">0</Type>
<Length xmlns="http://asb.contracts.idata.data/20111111">0</Length>
<Payload xsi:nil="true" xmlns="http://asb.contracts.idata.data/20111111" />
</ValueDeadband>
<UserData xmlns="urn:data.data.asb.iom:2">
<Type xmlns="http://asb.contracts.idata.data/20111111">0</Type>
<Length xmlns="http://asb.contracts.idata.data/20111111">0</Length>
<Payload xsi:nil="true" xmlns="http://asb.contracts.idata.data/20111111" />
</UserData>
<Buffered xmlns="urn:data.data.asb.iom:2">false</Buffered>
</Items>
<RequireId>true</RequireId>
</AddMonitoredItemsRequest>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-16"?>
<AuthenticateMe xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111" />
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111" />
</ConnectionValidator>
<ConsumerAuthenticationData>
<Data xmlns="http://asb.contracts.data/20111111">ZGV0ZXJtaW5pc3RpYy1jaXBoZXJ0ZXh0LWJ5dGVz</Data>
<InitializationVector xmlns="http://asb.contracts.data/20111111">MDEyMzQ1Njc4OWFiY2RlZg==</InitializationVector>
</ConsumerAuthenticationData>
</AuthenticateMe>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-16"?>
<AuthenticateMe xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111">AAECAwQFBgcICQoLDA0ODw==</MessageAuthenticationCode>
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111">EBESExQVFhcYGRobHB0eHw==</SignatureInitializationVector>
</ConnectionValidator>
<ConsumerAuthenticationData>
<Data xmlns="http://asb.contracts.data/20111111">ZGV0ZXJtaW5pc3RpYy1jaXBoZXJ0ZXh0LWJ5dGVz</Data>
<InitializationVector xmlns="http://asb.contracts.data/20111111">MDEyMzQ1Njc4OWFiY2RlZg==</InitializationVector>
</ConsumerAuthenticationData>
</AuthenticateMe>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-16"?>
<CreateSubscriptionRequest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111">AAECAwQFBgcICQoLDA0ODw==</MessageAuthenticationCode>
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111">EBESExQVFhcYGRobHB0eHw==</SignatureInitializationVector>
</ConnectionValidator>
<MaxQueueSize>100</MaxQueueSize>
<SampleInterval>1000</SampleInterval>
</CreateSubscriptionRequest>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-16"?>
<DeleteMonitoredItemsRequest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111">AAECAwQFBgcICQoLDA0ODw==</MessageAuthenticationCode>
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111">EBESExQVFhcYGRobHB0eHw==</SignatureInitializationVector>
</ConnectionValidator>
<SubscriptionId>1311768467463790320</SubscriptionId>
<Items>
<Item xmlns="urn:data.data.asb.iom:2">
<Type>1</Type>
<ReferenceType>1</ReferenceType>
<Name xsi:nil="true" />
<ContextName xsi:nil="true" />
<Id>14627333968688430831</Id>
</Item>
<SampleInterval xmlns="urn:data.data.asb.iom:2">1000</SampleInterval>
<ValueDeadband xmlns="urn:data.data.asb.iom:2">
<Type xmlns="http://asb.contracts.idata.data/20111111">0</Type>
<Length xmlns="http://asb.contracts.idata.data/20111111">0</Length>
<Payload xsi:nil="true" xmlns="http://asb.contracts.idata.data/20111111" />
</ValueDeadband>
<UserData xmlns="urn:data.data.asb.iom:2">
<Type xmlns="http://asb.contracts.idata.data/20111111">0</Type>
<Length xmlns="http://asb.contracts.idata.data/20111111">0</Length>
<Payload xsi:nil="true" xmlns="http://asb.contracts.idata.data/20111111" />
</UserData>
<Buffered xmlns="urn:data.data.asb.iom:2">false</Buffered>
</Items>
</DeleteMonitoredItemsRequest>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-16"?>
<DeleteSubscriptionRequest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111">AAECAwQFBgcICQoLDA0ODw==</MessageAuthenticationCode>
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111">EBESExQVFhcYGRobHB0eHw==</SignatureInitializationVector>
</ConnectionValidator>
<SubscriptionId>1311768467463790320</SubscriptionId>
</DeleteSubscriptionRequest>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-16"?>
<Disconnect xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111">AAECAwQFBgcICQoLDA0ODw==</MessageAuthenticationCode>
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111">EBESExQVFhcYGRobHB0eHw==</SignatureInitializationVector>
</ConnectionValidator>
<ConsumerAuthenticationData>
<Data xmlns="http://asb.contracts.data/20111111">ZGlzY29ubmVjdC1jaXBoZXJ0ZXh0</Data>
<InitializationVector xmlns="http://asb.contracts.data/20111111">MDEyMzQ1Njc4OWFiY2RlZg==</InitializationVector>
</ConsumerAuthenticationData>
</Disconnect>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-16"?>
<KeepAlive xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111">AAECAwQFBgcICQoLDA0ODw==</MessageAuthenticationCode>
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111">EBESExQVFhcYGRobHB0eHw==</SignatureInitializationVector>
</ConnectionValidator>
</KeepAlive>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-16"?>
<PublishRequest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111">AAECAwQFBgcICQoLDA0ODw==</MessageAuthenticationCode>
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111">EBESExQVFhcYGRobHB0eHw==</SignatureInitializationVector>
</ConnectionValidator>
<SubscriptionId>1311768467463790320</SubscriptionId>
</PublishRequest>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-16"?>
<PublishWriteCompleteRequest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111">AAECAwQFBgcICQoLDA0ODw==</MessageAuthenticationCode>
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111">EBESExQVFhcYGRobHB0eHw==</SignatureInitializationVector>
</ConnectionValidator>
</PublishWriteCompleteRequest>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-16"?>
<ReadRequest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111">AAECAwQFBgcICQoLDA0ODw==</MessageAuthenticationCode>
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111">EBESExQVFhcYGRobHB0eHw==</SignatureInitializationVector>
</ConnectionValidator>
<Items>
<Type xmlns="urn:data.data.asb.iom:2">0</Type>
<ReferenceType xmlns="urn:data.data.asb.iom:2">1</ReferenceType>
<Name xmlns="urn:data.data.asb.iom:2">TestChildObject.TestInt</Name>
<ContextName xmlns="urn:data.data.asb.iom:2" />
</Items>
</ReadRequest>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-16"?>
<RegisterItemsRequest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111">AAECAwQFBgcICQoLDA0ODw==</MessageAuthenticationCode>
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111">EBESExQVFhcYGRobHB0eHw==</SignatureInitializationVector>
</ConnectionValidator>
<Items>
<Type xmlns="urn:data.data.asb.iom:2">0</Type>
<ReferenceType xmlns="urn:data.data.asb.iom:2">1</ReferenceType>
<Name xmlns="urn:data.data.asb.iom:2">TestChildObject.TestInt</Name>
<ContextName xmlns="urn:data.data.asb.iom:2" />
</Items>
<RequireId>true</RequireId>
<RegisterOnly>false</RegisterOnly>
</RegisterItemsRequest>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-16"?>
<UnregisterItemsRequest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111">AAECAwQFBgcICQoLDA0ODw==</MessageAuthenticationCode>
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111">EBESExQVFhcYGRobHB0eHw==</SignatureInitializationVector>
</ConnectionValidator>
<Items>
<Type xmlns="urn:data.data.asb.iom:2">1</Type>
<ReferenceType xmlns="urn:data.data.asb.iom:2">1</ReferenceType>
<Name xsi:nil="true" xmlns="urn:data.data.asb.iom:2" />
<ContextName xsi:nil="true" xmlns="urn:data.data.asb.iom:2" />
<Id xmlns="urn:data.data.asb.iom:2">14627333968688430831</Id>
</Items>
</UnregisterItemsRequest>
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-16"?>
<WriteBasicRequest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:invensys.schemas">
<ConnectionValidator>
<ConnectionId xmlns="http://asb.contracts.data/20111111">8cba964a-74c1-ef74-f6aa-761b3540191b</ConnectionId>
<MessageNumber xmlns="http://asb.contracts.data/20111111">42</MessageNumber>
<MessageAuthenticationCode xmlns="http://asb.contracts.data/20111111">AAECAwQFBgcICQoLDA0ODw==</MessageAuthenticationCode>
<SignatureInitializationVector xmlns="http://asb.contracts.data/20111111">EBESExQVFhcYGRobHB0eHw==</SignatureInitializationVector>
</ConnectionValidator>
<Items>
<Type xmlns="urn:data.data.asb.iom:2">0</Type>
<ReferenceType xmlns="urn:data.data.asb.iom:2">1</ReferenceType>
<Name xmlns="urn:data.data.asb.iom:2">TestChildObject.TestInt</Name>
<ContextName xmlns="urn:data.data.asb.iom:2" />
</Items>
<Values>
<Value xmlns="urn:data.data.asb.iom:2">
<Type xmlns="http://asb.contracts.idata.data/20111111">4</Type>
<Length xmlns="http://asb.contracts.idata.data/20111111">4</Length>
<Payload xmlns="http://asb.contracts.idata.data/20111111">KgAAAA==</Payload>
</Value>
<Status xmlns="urn:data.data.asb.iom:2">
<Count>0</Count>
</Status>
<Comment xsi:nil="true" xmlns="urn:data.data.asb.iom:2" />
</Values>
<WriteHandle>3735928559</WriteHandle>
</WriteBasicRequest>
@@ -0,0 +1,65 @@
//! F34 — wire-byte trace of a captured `PublishResponse`.
//!
//! `tests/fixtures/publish-response-with-value.bin` is the verbatim
//! S→C bytes the .NET probe (`MxAsbClient.Probe --subscribe`) saw on
//! its first `Publish` poll against the local AVEVA install on
//! 2026-05-06, captured via `examples/asb-relay.rs` middleman with
//! `--via=net.tcp://127.0.0.1:8088/...`. The .NET probe extracted
//! `preview:99` from this exchange — the value bytes
//! `[63 00 00 00]` (= 99 in LE i32) are visible at file offset 0x110.
//!
//! Test goal: dump `decode_envelope` + `decode_publish_response`
//! output so we can see exactly where our value-extraction diverges
//! from .NET's (F34 hypotheses).
//!
//! Frame layout: 3-byte NMF SizedEnvelope header (`06 ae 02`,
//! varint length = 302) + 302-byte SOAP envelope.
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::indexing_slicing,
clippy::panic
)]
use mxaccess_asb::{decode_envelope, decode_publish_response};
use mxaccess_asb_nettcp::nbfx::DynamicDictionary;
#[test]
fn publish_response_capture_decoder_trace() {
let raw = std::fs::read(
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/publish-response-with-value.bin"),
)
.expect("read fixture");
assert_eq!(raw.len(), 305, "frame length sanity check");
// Strip 3-byte NMF SizedEnvelope header.
let envelope = &raw[3..];
assert_eq!(envelope.len(), 302);
let mut dict = DynamicDictionary::new();
let decoded = decode_envelope(envelope, &mut dict).expect("decode_envelope succeeds");
eprintln!("=== body tokens ({} total) ===", decoded.body_tokens.len());
for (i, tok) in decoded.body_tokens.iter().enumerate() {
eprintln!(" body[{i}]={tok:?}");
}
let response = decode_publish_response(&decoded.body_tokens)
.expect("decode_publish_response succeeds");
eprintln!("=== decoded PublishResponse ===");
eprintln!(" status_count: {}", response.status.len());
eprintln!(" values_count: {}", response.values.len());
eprintln!(" result_code: {:?}", response.result_code);
eprintln!(" success: {:?}", response.success);
// The .NET probe extracted 1 value with preview:99 from the same
// wire bytes. If our decoder reports 0 values, the test fails and
// the eprintln body-token dump above shows where the gap is.
assert_eq!(
response.values.len(),
1,
".NET sees 1 value (preview:99) from the same bytes; our decoder reads {}",
response.values.len(),
);
}
+36 -2
View File
@@ -9,8 +9,42 @@ rust-version.workspace = true
authors.workspace = true
[dependencies]
mxaccess-rpc = { path = "../mxaccess-rpc" }
mxaccess-codec = { path = "../mxaccess-codec" }
mxaccess-rpc = { path = "../mxaccess-rpc", version = "0.0.0" }
mxaccess-codec = { path = "../mxaccess-codec", version = "0.0.0" }
tokio = { workspace = true }
tracing = { workspace = true }
rand = "0.8"
# F55 / Path A — DCOM-managed callback sink.
# `windows-com` enables `dcom_sink.rs` which implements
# `INmxSvcCallback` as a real COM class via `windows-rs` `#[implement]`.
# The marshalled OBJREF passes NmxSvc's SCM-side OXID resolution
# where the hand-rolled `exporter.rs` approach fails. Default build
# stays slim — the windows crate is only pulled in when the consumer
# enables `windows-com`. Propagates through to
# `mxaccess-rpc/windows-com` so the OBJREF marshaller is available.
windows = { version = "0.62", features = [
"Win32_Foundation",
"Win32_System_Com",
"Win32_System_Com_Marshal",
"Win32_System_Com_StructuredStorage",
"Win32_System_Memory",
], optional = true }
# windows-rs's `#[interface]` and `#[implement]` macros expand to
# absolute `::windows_core::*` paths, so the consumer must depend on
# `windows-core` directly (the `windows` crate's re-export at
# `windows::core` doesn't satisfy the macro's path resolution).
# Pin to the same 0.62 line as the `windows` dep above so the
# `IUnknown` / `IUnknown_Vtbl` types resolve to the same crate
# version that `mxaccess-rpc::com_objref_provider::IUnknownHolder`
# wraps — version skew between the two would surface as "expected
# IUnknown, found IUnknown" type errors at the
# `IUnknownHolder::from_iunknown` boundary.
windows-core = { version = "0.62", optional = true }
[features]
default = []
windows-com = ["dep:windows", "dep:windows-core", "mxaccess-rpc/windows-com"]
[lints]
workspace = true
@@ -0,0 +1,270 @@
// `windows_core::interface` doesn't tolerate sibling attributes on the
// trait, and the COM method names must mirror the .NET reference's
// PascalCase to keep the IDL/MIDL trail readable. Allow at module
// scope so the generated `_Impl` trait + vtable struct don't trip
// `non_snake_case`.
#![allow(non_snake_case)]
//! DCOM-managed `INmxSvcCallback` sink — Path A of F55.
//!
//! The hand-rolled `CallbackExporter` (this crate's [`crate::exporter`]
//! module) advertises a TCP listener via a custom OBJREF that NmxSvc
//! refuses with `RPC_S_SERVER_UNAVAILABLE` (1722) on RegisterEngine2.
//! Live diff against the working .NET `MxNativeSession.Open` path
//! (which uses `ComObjRefProvider.MarshalInterfaceObjRef(callback,
//! INmxSvcCallback, DifferentMachine)` per `MxNativeSession.cs:624`)
//! showed the failure isn't an OBJREF byte-format issue — it's that
//! NmxSvc does its own SCM-side `IObjectExporter::ResolveOxid` against
//! the local RPCSS at `127.0.0.1:135` to validate the callback OXID,
//! and a hand-rolled OXID isn't registered with RPCSS.
//!
//! This module sidesteps that by implementing `INmxSvcCallback` as a
//! real `windows-rs` `#[implement]` COM class. `CoMarshalInterface`
//! then registers the callback's OXID with RPCSS automatically, so
//! NmxSvc's SCM-side resolution succeeds. Inbound `DataReceivedRaw` /
//! `StatusReceivedRaw` calls arrive on the DCOM stub thread and are
//! forwarded into the same `CallbackEvent` mpsc the hand-rolled
//! exporter feeds, so the upstream `callback_router` in `mxaccess`
//! doesn't need to know which path produced the event.
//!
//! Mirrors `src/MxNativeClient/NmxCallbackSink.cs` (the .NET reference's
//! DCOM-managed callback used by the `MxNativeSession.Open` path).
use std::ptr;
use tokio::sync::mpsc;
use tracing::{debug, trace, warn};
use windows::Win32::System::Com::Marshal::CoMarshalInterface;
use windows::Win32::System::Com::StructuredStorage::{
CreateStreamOnHGlobal, GetHGlobalFromStream,
};
use windows::Win32::System::Com::{IStream, MSHCTX_DIFFERENTMACHINE, MSHLFLAGS_NORMAL};
use windows::Win32::System::Memory::{GlobalLock, GlobalSize, GlobalUnlock};
// `#[interface]` / `#[implement]` macros expand to `::windows_core::*`
// paths, so we import via windows_core (which the windows crate
// re-exports). `IUnknown_Vtbl` etc. need to be in scope at the crate
// root.
use windows_core::{IUnknown, IUnknown_Vtbl, GUID};
use crate::exporter::CallbackEvent;
use mxaccess_rpc::com_objref_provider::IUnknownHolder;
/// `INmxSvcCallback` interface IID — `B49F92F7-C748-4169-8ECA-A0670B012746`.
/// Mirrors the .NET reference's `INmxSvcCallback` declaration at
/// `src/MxNativeClient/NmxComContracts.cs:84`.
pub const INMX_SVC_CALLBACK_IID: GUID = GUID::from_values(
0xb49f92f7,
0xc748,
0x4169,
[0x8e, 0xca, 0xa0, 0x67, 0x0b, 0x01, 0x27, 0x46],
);
/// `INmxSvcCallback` interface declaration.
///
/// Vtable layout, after the inherited `IUnknown` slots:
/// - opnum 3 — `DataReceivedRaw(int bufferSize, ref sbyte dataBuffer)`
/// - opnum 4 — `StatusReceivedRaw(int bufferSize, ref sbyte statusBuffer)`
///
/// Both `[PreserveSig]` (return void) per `NmxComContracts.cs:87-91`.
/// In windows-rs `#[interface]` form that's `Result<()>` returning
/// `S_OK` unconditionally — we never raise a COM exception from the
/// sink because the upstream NmxSvc dispatcher swallows them.
#[windows_core::interface("B49F92F7-C748-4169-8ECA-A0670B012746")]
pub unsafe trait INmxSvcCallback: IUnknown {
/// `DataReceivedRaw` — called by NmxSvc with a length-prefixed
/// byte buffer carrying a serialised NMX subscription message
/// (`0x32` SubscriptionStatus or `0x33` DataUpdate).
///
/// # Safety
/// `data_buffer` is a stub-side pointer to `buffer_size` bytes
/// owned by the COM proxy/stub layer; valid for the duration of
/// the call. Implementations MUST copy the buffer before returning.
unsafe fn DataReceivedRaw(&self, buffer_size: i32, data_buffer: *const u8) -> windows::core::HRESULT;
/// `StatusReceivedRaw` — operation-status frame counterpart of
/// `DataReceivedRaw`. Same buffer-ownership contract.
///
/// # Safety
/// As above.
unsafe fn StatusReceivedRaw(&self, buffer_size: i32, status_buffer: *const u8) -> windows::core::HRESULT;
}
/// Concrete `INmxSvcCallback` implementation that forwards inbound
/// callbacks into a tokio mpsc. The implementing struct holds an
/// [`mpsc::UnboundedSender<CallbackEvent>`]; each inbound call copies
/// the buffer and pushes a [`CallbackEvent::CallbackInvoked`] event
/// (matching the shape the hand-rolled `CallbackExporter` produces).
#[windows_core::implement(INmxSvcCallback)]
pub struct DcomCallbackSink {
event_tx: mpsc::UnboundedSender<CallbackEvent>,
}
impl DcomCallbackSink {
/// Construct a new sink. The returned `Self` is a Rust value;
/// convert to an `IUnknown` for marshalling via
/// `IUnknown::from(sink)` (the conversion impl is generated by
/// the `#[implement]` macro).
#[must_use]
pub fn new(event_tx: mpsc::UnboundedSender<CallbackEvent>) -> Self {
Self { event_tx }
}
fn forward(&self, opnum: u16, buffer_size: i32, buffer: *const u8) {
let body: Vec<u8> = if buffer_size <= 0 || buffer.is_null() {
Vec::new()
} else {
// SAFETY: the COM stub guarantees `buffer` is valid for
// `buffer_size` bytes for the duration of the call, and
// the slice is read-only. We copy out before returning.
unsafe { std::slice::from_raw_parts(buffer, buffer_size as usize) }.to_vec()
};
trace!(
opnum,
buffer_size,
body_len = body.len(),
"DcomCallbackSink: forwarding inbound callback"
);
if let Err(e) = self.event_tx.send(CallbackEvent::CallbackInvoked { opnum, body }) {
// The receiver was dropped (the upstream router
// probably exited). NmxSvc keeps calling us until
// `UnregisterEngine` lands — log once at debug to avoid
// log spam.
debug!("DcomCallbackSink: dropped event for opnum {opnum} (rx closed): {e}");
}
}
}
impl INmxSvcCallback_Impl for DcomCallbackSink_Impl {
unsafe fn DataReceivedRaw(
&self,
buffer_size: i32,
data_buffer: *const u8,
) -> windows::core::HRESULT {
// Opnum 3 per `NmxProcedureMetadata.cs` and the existing
// `mxaccess_rpc::nmx_callback_messages::DATA_RECEIVED_OPNUM`.
self.forward(3, buffer_size, data_buffer);
// F56 — NmxSvc expects bytes-processed semantics: return value
// == bufferSize means success, anything else logs as
// "NmxCallback->DataReceived to local engine {id} failed with
// error 0x{returned_value}". The .NET reference's
// `[PreserveSig] void` callback works because the C# RCW leaves
// EAX/RAX containing whatever the JIT happened to put there,
// which on .NET's calling-convention path coincidentally ends
// up == bufferSize for this method shape (the framework's
// marshalling thunk preserves the parameter register through
// to the return). Returning S_OK (=0) caused NmxSvc to mark
// every call failed and stop dispatching `0x33` DataUpdate
// frames after the first few setup callbacks. Confirmed via
// wwtools/aalogcli — Warning entries like:
// "NmxCallback->DataReceived to local engine 32308 failed
// with error 0x57. Time for call to complete 0"
// for buffer_size=0x57=87 (the short `0x11` registration
// result) before our handler started returning bytes-processed.
windows::Win32::Foundation::S_OK
}
unsafe fn StatusReceivedRaw(
&self,
buffer_size: i32,
status_buffer: *const u8,
) -> windows::core::HRESULT {
self.forward(4, buffer_size, status_buffer);
windows::Win32::Foundation::S_OK
}
}
/// Build a DCOM-managed callback sink, marshal it for cross-machine
/// dispatch, and return the bundle of:
/// 1. an [`IUnknownHolder`] — keeps the COM ref alive for the
/// consumer's lifetime (see `IUnknownHolder` doc on why this
/// matters),
/// 2. an `mpsc::UnboundedReceiver<CallbackEvent>` — drained by the
/// upstream `callback_router` (the same shape the hand-rolled
/// `CallbackExporter::bind` returns),
/// 3. the OBJREF byte blob — passed to `RegisterEngine2` as the
/// callback parameter.
///
/// Mirrors `MxNativeSession.CreateRegisteredService` (`cs:624`):
/// ```csharp
/// byte[] callbackObjRef = ComObjRefProvider.MarshalInterfaceObjRef(
/// callback,
/// NmxProcedureMetadata.INmxSvcCallback,
/// ComObjRefProvider.MarshalContextDifferentMachine);
/// ```
///
/// # Errors
///
/// Surfaces `windows::core::Error` for any failure in the `IStream`
/// allocation, `CoMarshalInterface`, `GetHGlobalFromStream`, or
/// `GlobalLock` chain.
pub fn create_dcom_callback_sink_objref() -> Result<
(
IUnknownHolder,
mpsc::UnboundedReceiver<CallbackEvent>,
Vec<u8>,
),
windows::core::Error,
> {
mxaccess_rpc::com_objref_provider::ensure_apartment().map_err(|e| {
warn!("ensure_apartment failed: {e:?}");
windows::core::Error::from_hresult(windows::Win32::Foundation::E_FAIL)
})?;
let (event_tx, event_rx) = mpsc::unbounded_channel();
let sink = DcomCallbackSink::new(event_tx);
let unknown: IUnknown = sink.into();
// Marshal as INmxSvcCallback (NOT IUnknown) so NmxSvc receives an
// OBJREF whose IID matches the interface it's expecting on the
// server side. The .NET reference does the same at
// `MxNativeSession.cs:624` — pass `NmxProcedureMetadata.INmxSvcCallback`.
let blob = marshal_for_dcom(&unknown, INMX_SVC_CALLBACK_IID)?;
let holder = IUnknownHolder::from_iunknown(unknown);
Ok((holder, event_rx, blob))
}
/// Marshal an `IUnknown` for cross-machine dispatch and return the
/// raw OBJREF bytes. Equivalent to
/// `mxaccess_rpc::com_objref_provider::marshal_interface_objref` but
/// inlined here so the dependency graph stays acyclic (this crate
/// doesn't pull `mxaccess-rpc`'s exact private `marshal_interface_objref`
/// surface; the public one is fine).
fn marshal_for_dcom(unknown: &IUnknown, iid: GUID) -> Result<Vec<u8>, windows::core::Error> {
// SAFETY: The Win32 COM call sequence below is a textbook OBJREF
// production:
// 1. CreateStreamOnHGlobal allocates an HGlobal-backed IStream.
// 2. CoMarshalInterface writes the OBJREF into the stream.
// 3. GetHGlobalFromStream extracts the underlying handle.
// 4. GlobalLock / GlobalSize / GlobalUnlock copy out the bytes.
// Each call's HRESULT is checked.
unsafe {
let stream: IStream = CreateStreamOnHGlobal(
windows::Win32::Foundation::HGLOBAL(ptr::null_mut()),
true,
)?;
CoMarshalInterface(
&stream,
&iid,
unknown,
MSHCTX_DIFFERENTMACHINE.0 as u32,
None,
MSHLFLAGS_NORMAL.0 as u32,
)?;
let hglobal = GetHGlobalFromStream(&stream)?;
let size = GlobalSize(hglobal);
if size == 0 {
return Ok(Vec::new());
}
let ptr = GlobalLock(hglobal);
if ptr.is_null() {
return Err(windows::core::Error::from_hresult(
windows::Win32::Foundation::E_FAIL,
));
}
let slice = std::slice::from_raw_parts(ptr.cast::<u8>(), size);
let blob = slice.to_vec();
let _ = GlobalUnlock(hglobal); // best-effort; lock count drops to 0
Ok(blob)
}
}
File diff suppressed because it is too large Load Diff
+24 -2
View File
@@ -1,6 +1,9 @@
//! `mxaccess-callback` — `INmxSvcCallback` RPC server (the callback exporter).
//!
//! M0 stub. Real implementation lands in M2 — see `design/60-roadmap.md`.
//! M2 wave 3 landed: the [`exporter`] module ports
//! `src/MxNativeClient/ManagedCallbackExporter.cs` to a tokio-based TCP
//! server that serves `IRemUnknown` and `INmxSvcCallback` opnums and emits
//! typed [`exporter::CallbackEvent`]s for diagnostic observation.
//!
//! Opnums (verified against `src/MxNativeClient/NmxSvcCallbackMessages.cs:11-12`):
//! - `3` `DataReceived(bufferSize: i32, dataBuffer: sbyte[bufferSize]) -> hresult`
@@ -9,4 +12,23 @@
//! Plus the `IRemUnknown::RemQueryInterface` handler that completes the
//! server-side handshake against our exported OBJREF (DoD condition for M2).
#![forbid(unsafe_code)]
// `forbid(unsafe_code)` lifted: the F55 / Path A `dcom_sink` module
// (gated behind `windows-com`) implements an `INmxSvcCallback` COM
// class that must dereference stub-side buffer pointers in
// `DataReceivedRaw` / `StatusReceivedRaw`. Each unsafe block carries
// a SAFETY comment documenting the COM stub's buffer-validity
// contract.
#![deny(unsafe_op_in_unsafe_fn)]
pub mod exporter;
pub use exporter::{CallbackEvent, CallbackExporter, ExporterIdentities, IUNKNOWN_IID};
/// Path A — DCOM-managed `INmxSvcCallback` sink. Required because
/// NmxSvc rejects hand-rolled OBJREFs from [`exporter::CallbackExporter`]
/// with `RPC_S_SERVER_UNAVAILABLE` (1722) on RegisterEngine2 — see F55.
#[cfg(all(windows, feature = "windows-com"))]
pub mod dcom_sink;
#[cfg(all(windows, feature = "windows-com"))]
pub use dcom_sink::{create_dcom_callback_sink_objref, INMX_SVC_CALLBACK_IID};
+5
View File
@@ -9,11 +9,16 @@ rust-version.workspace = true
authors.workspace = true
[dependencies]
bytes = { workspace = true }
thiserror = { workspace = true }
[features]
default = []
serde = []
[[bench]]
name = "alloc_count"
harness = false
[lints]
workspace = true
@@ -0,0 +1,336 @@
//! F38 — counting-allocator bench for `mxaccess-codec`.
//!
//! Measures allocation count + bytes-allocated for the proven
//! encode/decode matrix per `design/70-risks-and-open-questions.md`
//! R12 (< 5 allocs per write at steady state). The harness wraps the
//! global allocator with a [`CountingAllocator`] that tracks
//! per-call counts; each scenario records pre-state, runs N
//! iterations, and reports `(alloc_count, bytes_allocated) / N`.
//!
//! Output is the source of truth for `design/M6-bench-baseline.md`.
//!
//! ## Why hand-rolled (not `dhat` / `criterion`)
//!
//! - `dhat` is heap-profiling oriented (snapshots, call-stack
//! attribution); for "did this op allocate < 5 times?" the simpler
//! approach is a thin `GlobalAlloc` wrapper that increments two
//! atomics. No call-stack capture, no JSON output to post-process.
//! - `criterion` measures wall-clock latency; per `60-roadmap.md:104`,
//! latency is reported but not gating in V1. Allocation count IS
//! the gating metric for M6 DoD bullet 3.
//!
//! ## Run
//!
//! ```text
//! cargo bench -p mxaccess-codec
//! ```
//!
//! Each scenario runs in release mode by default (cargo bench
//! profile = `bench` which inherits release).
#![allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicU64, Ordering};
use bytes::BytesMut;
use mxaccess_codec::{
write_message, write_message::WriteValue, MxReferenceHandle, NmxSubscriptionMessage,
};
// ---- counting allocator -------------------------------------------------
struct CountingAllocator;
static ALLOC_COUNT: AtomicU64 = AtomicU64::new(0);
static ALLOC_BYTES: AtomicU64 = AtomicU64::new(0);
static DEALLOC_COUNT: AtomicU64 = AtomicU64::new(0);
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
ALLOC_BYTES.fetch_add(layout.size() as u64, Ordering::Relaxed);
// SAFETY: forwarding to the system allocator with the same layout.
unsafe { System.alloc(layout) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
DEALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
// SAFETY: forwarding to the system allocator with the same ptr+layout.
unsafe { System.dealloc(ptr, layout) }
}
}
#[global_allocator]
static GLOBAL: CountingAllocator = CountingAllocator;
// ---- scenario harness ---------------------------------------------------
#[derive(Debug, Clone, Copy)]
struct Snapshot {
allocs: u64,
bytes: u64,
deallocs: u64,
}
fn snapshot() -> Snapshot {
Snapshot {
allocs: ALLOC_COUNT.load(Ordering::Relaxed),
bytes: ALLOC_BYTES.load(Ordering::Relaxed),
deallocs: DEALLOC_COUNT.load(Ordering::Relaxed),
}
}
fn diff(start: Snapshot, end: Snapshot, iterations: u64) -> (f64, f64, f64) {
(
(end.allocs - start.allocs) as f64 / iterations as f64,
(end.bytes - start.bytes) as f64 / iterations as f64,
(end.deallocs - start.deallocs) as f64 / iterations as f64,
)
}
/// Run `op` `iterations` times and return per-op alloc/bytes/dealloc
/// counts. The hint is passed through `std::hint::black_box` to keep
/// the compiler from optimising the work away.
fn measure<F>(name: &str, iterations: u64, mut op: F) -> Row
where
F: FnMut(),
{
// Warm-up: 1k iterations to settle any one-time setup state.
for _ in 0..1024 {
op();
}
let start = snapshot();
for _ in 0..iterations {
op();
}
let end = snapshot();
let (allocs, bytes, deallocs) = diff(start, end, iterations);
Row {
name: name.to_string(),
iterations,
allocs_per_op: allocs,
bytes_per_op: bytes,
deallocs_per_op: deallocs,
}
}
struct Row {
name: String,
iterations: u64,
allocs_per_op: f64,
bytes_per_op: f64,
deallocs_per_op: f64,
}
fn print_table(rows: &[Row]) {
println!();
println!(
"| {:40} | {:>10} | {:>10} | {:>10} | {:>10} |",
"scenario", "iters", "allocs/op", "bytes/op", "deallocs/op"
);
println!(
"| {:40} | {:>10} | {:>10} | {:>10} | {:>10} |",
"-".repeat(40),
"-".repeat(10),
"-".repeat(10),
"-".repeat(10),
"-".repeat(10)
);
for row in rows {
println!(
"| {:40} | {:>10} | {:>10.2} | {:>10.0} | {:>10.2} |",
row.name, row.iterations, row.allocs_per_op, row.bytes_per_op, row.deallocs_per_op
);
}
println!();
}
// ---- scenarios ----------------------------------------------------------
fn make_handle() -> MxReferenceHandle {
MxReferenceHandle::from_names(0, 1, 2, 3, "TestObject", 0, 1, 0, "TestAttr", false)
.expect("handle")
}
fn bench_write_int32() -> Row {
let handle = make_handle();
let value = WriteValue::Int32(42);
measure("write_message::encode (Int32)", 10_000, || {
let bytes = write_message::encode(&handle, &value, 0, 0).unwrap();
std::hint::black_box(bytes);
})
}
fn bench_write_float() -> Row {
let handle = make_handle();
let value = WriteValue::Float32(1.5);
measure("write_message::encode (Float32)", 10_000, || {
let bytes = write_message::encode(&handle, &value, 0, 0).unwrap();
std::hint::black_box(bytes);
})
}
fn bench_write_double() -> Row {
let handle = make_handle();
let value = WriteValue::Float64(3.25);
measure("write_message::encode (Float64)", 10_000, || {
let bytes = write_message::encode(&handle, &value, 0, 0).unwrap();
std::hint::black_box(bytes);
})
}
fn bench_write_bool() -> Row {
let handle = make_handle();
let value = WriteValue::Boolean(true);
measure("write_message::encode (Boolean)", 10_000, || {
let bytes = write_message::encode(&handle, &value, 0, 0).unwrap();
std::hint::black_box(bytes);
})
}
fn bench_write_string() -> Row {
let handle = make_handle();
let value = WriteValue::String("hello".to_string());
measure("write_message::encode (String, 5 chars)", 10_000, || {
let bytes = write_message::encode(&handle, &value, 0, 0).unwrap();
std::hint::black_box(bytes);
})
}
// F52.1 — `BytesMut` output. Same alloc count as `encode`; the benefit is
// downstream zero-copy (consumers can `split_to` / `freeze` without copying
// the body bytes).
fn bench_write_int32_bytes_mut() -> Row {
let handle = make_handle();
let value = WriteValue::Int32(42);
measure("write_message::encode_to_bytes_mut (Int32)", 10_000, || {
let bytes = write_message::encode_to_bytes_mut(&handle, &value, 0, 0).unwrap();
std::hint::black_box(bytes);
})
}
// F52.3 — session-level scratch buffer. The caller supplies a `BytesMut`
// that is cleared and resized in place, so the body allocation is amortised
// across a session's writes. Drops the per-write count from 2 → 1 for
// fixed-width scalars (the remaining alloc is the per-value scratch buffer
// inside `encode_scalar_value`) and 1 → 0 for Boolean (no scalar scratch).
fn bench_write_int32_into_pooled() -> Row {
let handle = make_handle();
let value = WriteValue::Int32(42);
let mut buf = BytesMut::new();
measure(
"write_message::encode_into_bytes_mut (Int32, pooled)",
10_000,
|| {
write_message::encode_into_bytes_mut(&handle, &value, 0, 0, &mut buf).unwrap();
std::hint::black_box(&buf);
},
)
}
fn bench_write_bool_into_pooled() -> Row {
let handle = make_handle();
let value = WriteValue::Boolean(true);
let mut buf = BytesMut::new();
measure(
"write_message::encode_into_bytes_mut (Boolean, pooled)",
10_000,
|| {
write_message::encode_into_bytes_mut(&handle, &value, 0, 0, &mut buf).unwrap();
std::hint::black_box(&buf);
},
)
}
fn bench_subscription_decode() -> Row {
// Build a single-record DataUpdate body once; decode N times.
let body = build_data_update_int32_body(42);
measure(
"NmxSubscriptionMessage::parse_inner (DataUpdate, Int32)",
10_000,
|| {
let msg = NmxSubscriptionMessage::parse_inner(&body).unwrap();
std::hint::black_box(msg);
},
)
}
fn bench_handle_from_names() -> Row {
measure("MxReferenceHandle::from_names", 10_000, || {
let h =
MxReferenceHandle::from_names(0, 1, 2, 3, "TestChildObject", 0, 1, 0, "TestInt", false)
.unwrap();
std::hint::black_box(h);
})
}
// ---- helpers (mirror the test fixtures in subscription_message.rs) -----
fn build_data_update_int32_body(value: i32) -> Vec<u8> {
// Operation id + correlation id are arbitrary 16-byte sequences for
// a synthetic body; the codec doesn't reject any GUID shape.
const DATA_UPDATE_COMMAND: u8 = 0x33;
let operation_id = [0x11u8; 16];
// 15-byte record prefix: status(4) + quality(2) + filetime(8) + wire_kind(1).
// wire_kind = 0x02 = Int32. Then the 4-byte i32 LE payload.
let mut record = Vec::with_capacity(15 + 4);
record.extend_from_slice(&0i32.to_le_bytes()); // status
record.extend_from_slice(&0x00C0u16.to_le_bytes()); // quality
record.extend_from_slice(&0i64.to_le_bytes()); // filetime
record.push(0x02); // wire_kind = Int32
record.extend_from_slice(&value.to_le_bytes());
let mut out = Vec::with_capacity(23 + record.len());
out.push(DATA_UPDATE_COMMAND);
out.extend_from_slice(&1u16.to_le_bytes()); // version
out.extend_from_slice(&1i32.to_le_bytes()); // record_count = 1
out.extend_from_slice(&operation_id);
out.extend_from_slice(&record);
out
}
// ---- main --------------------------------------------------------------
fn main() {
println!("F38 — mxaccess-codec allocation-count baseline");
println!("Counting allocator: thin GlobalAlloc wrapper around System.");
println!("R12 target: < 5 allocations per write at steady state.");
let rows = vec![
bench_write_int32(),
bench_write_float(),
bench_write_double(),
bench_write_bool(),
bench_write_string(),
bench_write_int32_bytes_mut(),
bench_write_int32_into_pooled(),
bench_write_bool_into_pooled(),
bench_handle_from_names(),
bench_subscription_decode(),
];
print_table(&rows);
// R12 gate: emit a non-zero exit code if any encode-write scenario
// exceeds the 5-allocs threshold. Decoders are reported but not
// gated (the sweep below explicitly excludes them).
let mut violations = 0;
for row in &rows {
if row.name.starts_with("write_message::encode") && row.allocs_per_op >= 5.0 {
eprintln!(
"R12 violation: {} allocates {:.2}/op (>= 5)",
row.name, row.allocs_per_op
);
violations += 1;
}
}
if violations > 0 {
std::process::exit(1);
}
}
@@ -0,0 +1,975 @@
//! ASB `Variant` + `AsbStatus` + `RuntimeValue` codec.
//!
//! Ports `src/MxAsbClient/AsbContracts.cs` (the `Variant`, `AsbStatus`, and
//! `RuntimeValue` `IAsbCustomSerializableType` blocks) plus the `DecodeVariant`
//! / `AsbVariantFactory` value-typed decode/encode in
//! `src/MxAsbClient/MxAsbDataClient.cs:713-825`. Spec-by-evidence: the wire
//! shape is documented in `docs/ASB-Variant-Wire-Format.md`.
//!
//! Layered for parity with the .NET reference:
//!
//! 1. [`AsbVariant`] is the raw 10-byte header + payload layout that round-
//! trips byte-for-byte against captured ASB messages. It carries a `u16`
//! type id, an `i32` "logical length" (set to `payload.len()` by the
//! factory), and a `u32` payload length followed by the payload bytes.
//! No interpretation; consumers can stash arbitrary unknown variants.
//! 2. [`DecodedVariant`] is the typed view. [`decode_variant`] consumes an
//! [`AsbVariant`] and produces a typed value for the proven matrix
//! (`Bool`, `Int32`, `Float`, `Double`, `String`, `DateTime`, `Duration`,
//! plus their array forms). Unknown type IDs surface as
//! [`DecodedVariant::Unsupported`] carrying the raw payload — same
//! fallback as `MxAsbDataClient.DecodeVariant` at `cs:748` (return raw
//! bytes).
//! 3. The `from_*` factories mirror `AsbVariantFactory.From*` — they build
//! an `AsbVariant` whose `length` field is set to `payload.len()` (per
//! `cs:1316`). Wire bytes are produced by [`AsbVariant::encode`].
//!
//! [`AsbStatus`] and [`RuntimeValue`] round-trip exactly. The richer
//! status-element parsing (marker bit 7 = implicit zero; otherwise `u16`
//! follows) documented in `docs/ASB-Variant-Wire-Format.md:182-186` is
//! deferred to a follow-up — `AsbStatus.payload` is exposed as raw bytes
//! for now, mirroring the .NET reference, which keeps `Payload` as
//! `byte[]` and only `AsbPublishMapper.DecodeStatus` walks the records.
use std::string::FromUtf16Error;
use crate::error::CodecError;
/// ASB data type IDs from `AsbContracts.cs:1243-1293`. Stored as `u16` on
/// the wire. Variants outside the proven set (e.g. GUID, byte string,
/// localized text, enum/data-type/security/data-quality forms and their
/// arrays) are carried but not interpreted — matching the .NET reference,
/// which preserves them as raw bytes via the `_ => payload` fallback at
/// `MxAsbDataClient.cs:748`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)]
pub enum AsbDataType {
Byte = 0,
Char = 1,
Int16 = 2,
UInt16 = 3,
Int32 = 4,
UInt32 = 5,
Int64 = 6,
UInt64 = 7,
Float = 8,
Double = 9,
String = 10,
DateTime = 11,
Duration = 12,
Guid = 13,
ByteString = 14,
LocaleId = 15,
LocalizedText = 16,
Bool = 17,
SByte = 18,
ErrorStatus = 19,
Enum = 20,
DataType = 21,
SecurityClassification = 22,
DataQuality = 23,
ByteArray = 40,
CharArray = 41,
Int16Array = 42,
UInt16Array = 43,
Int32Array = 44,
UInt32Array = 45,
Int64Array = 46,
UInt64Array = 47,
FloatArray = 48,
DoubleArray = 49,
StringArray = 50,
DateTimeArray = 51,
DurationArray = 52,
GuidArray = 53,
ByteStringArray = 54,
LocaleIdArray = 55,
LocalizedTextArray = 56,
BoolArray = 57,
SByteArray = 58,
EnumArray = 60,
DataTypeArray = 61,
SecurityClassificationArray = 62,
DataQualityArray = 63,
Unknown = 65535,
}
impl AsbDataType {
pub fn as_u16(self) -> u16 {
self as u16
}
}
/// Raw ASB `Variant` wire layout (`AsbContracts.cs:1170-1241`).
///
/// `length` is the .NET `int` length set by the factory to `payload.len()`
/// at construction (`cs:1431-1438`). It is written separately from the
/// `u32` payload-length on the wire — both are emitted by the .NET writer
/// (`cs:1202-1211`). Decoders may legitimately observe `length != payload.len()`
/// for malformed or partial frames; this codec preserves both verbatim.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AsbVariant {
pub type_id: u16,
pub length: i32,
pub payload: Vec<u8>,
}
impl AsbVariant {
/// Build a variant with `length` set to `payload.len()` per
/// `AsbVariantFactory.Create` (`cs:1431-1438`).
pub fn new(type_id: AsbDataType, payload: Vec<u8>) -> Self {
let length = i32::try_from(payload.len()).unwrap_or(i32::MAX);
Self {
type_id: type_id.as_u16(),
length,
payload,
}
}
/// `AsbVariantFactory.Empty` — `TypeUnknown`, length 0, empty payload
/// (`cs:1312`).
pub fn empty() -> Self {
Self {
type_id: AsbDataType::Unknown.as_u16(),
length: 0,
payload: Vec::new(),
}
}
/// Wire size in bytes: 2 + 4 + 4 + payload.
pub fn wire_len(&self) -> usize {
10 + self.payload.len()
}
/// Encode `Variant.WriteToStream` (`cs:1202-1211`). Append-style so
/// callers can chain into a larger `BinaryWriter`-equivalent buffer
/// without intermediate allocations.
pub fn encode_into(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&self.type_id.to_le_bytes());
out.extend_from_slice(&self.length.to_le_bytes());
let payload_len = u32::try_from(self.payload.len()).unwrap_or(u32::MAX);
out.extend_from_slice(&payload_len.to_le_bytes());
if !self.payload.is_empty() {
out.extend_from_slice(&self.payload);
}
}
/// Standalone encode: convenience wrapper around [`Self::encode_into`].
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(self.wire_len());
self.encode_into(&mut out);
out
}
/// Decode `Variant.InitializeFromStream` (`cs:1213-1219`). Returns
/// `(variant, bytes_consumed)`. Empty payload → `payload: Vec::new()`,
/// matching .NET `Payload = []`.
pub fn decode(input: &[u8]) -> Result<(Self, usize), CodecError> {
let mut cursor = 0usize;
let type_id = read_u16_le(input, &mut cursor)?;
let length = read_i32_le(input, &mut cursor)?;
let payload_length = read_u32_le(input, &mut cursor)? as usize;
let payload = read_bytes(input, &mut cursor, payload_length)?;
Ok((
Self {
type_id,
length,
payload: payload.to_vec(),
},
cursor,
))
}
}
/// Typed decode of an [`AsbVariant`].
///
/// Variant order follows the `AsbDataType` numerical sort. Unknown types
/// surface as [`Unsupported`](DecodedVariant::Unsupported) carrying both
/// the type ID and the raw payload, mirroring `DecodeVariant`'s `_ =>
/// payload` fallback at `MxAsbDataClient.cs:748`.
#[derive(Debug, Clone, PartialEq)]
pub enum DecodedVariant {
/// `null` from .NET when the payload is empty and the type does not
/// have an "empty literal" (e.g. empty `string`/`bool[]`/...).
/// Matches `_ => null` at `MxAsbDataClient.cs:728`.
Empty,
Bool(bool),
Int32(i32),
Float(f32),
Double(f64),
/// UTF-16LE-decoded contents.
String(String),
/// Windows FILETIME UTC value (`DateTime.ToFileTimeUtc()` —
/// 100-ns ticks since 1601-01-01 UTC).
DateTime(i64),
/// .NET `TimeSpan.Ticks` — 100-ns ticks.
Duration(i64),
BoolArray(Vec<bool>),
Int32Array(Vec<i32>),
FloatArray(Vec<f32>),
DoubleArray(Vec<f64>),
StringArray(Vec<String>),
DateTimeArray(Vec<i64>),
DurationArray(Vec<i64>),
/// Type IDs outside the proven matrix. Payload bytes are preserved
/// verbatim — the consumer can either decode them with a custom
/// helper or surface them upstream.
Unsupported {
type_id: u16,
payload: Vec<u8>,
},
}
/// Decode an [`AsbVariant`] into a typed value. Mirrors `MxAsbDataClient.DecodeVariant`
/// at `cs:713-750` exactly:
///
/// * Empty payload → empty literal for known string/array types
/// (`""` / `[]`), [`Empty`] otherwise.
/// * Non-empty payload that doesn't satisfy the minimum length for a
/// scalar (e.g. `TypeInt32` with 3 bytes) falls through to
/// [`Unsupported`] with the raw payload — matches .NET `when payload.Length >= 4`.
/// * Decode failures inside the typed branches surface as
/// [`CodecError::ShortRead`] / [`CodecError::Decode`] so the caller can
/// distinguish "wrong shape" from "unrecognized type".
///
/// [`Empty`]: DecodedVariant::Empty
/// [`Unsupported`]: DecodedVariant::Unsupported
pub fn decode_variant(variant: &AsbVariant) -> Result<DecodedVariant, CodecError> {
use AsbDataType::*;
let type_id = variant.type_id;
let payload = &variant.payload;
if payload.is_empty() {
return Ok(match type_id {
x if x == String.as_u16() => DecodedVariant::String(std::string::String::new()),
x if x == Int32Array.as_u16() => DecodedVariant::Int32Array(Vec::new()),
x if x == BoolArray.as_u16() => DecodedVariant::BoolArray(Vec::new()),
x if x == FloatArray.as_u16() => DecodedVariant::FloatArray(Vec::new()),
x if x == DoubleArray.as_u16() => DecodedVariant::DoubleArray(Vec::new()),
x if x == StringArray.as_u16() => DecodedVariant::StringArray(Vec::new()),
x if x == DateTimeArray.as_u16() => DecodedVariant::DateTimeArray(Vec::new()),
x if x == DurationArray.as_u16() => DecodedVariant::DurationArray(Vec::new()),
_ => DecodedVariant::Empty,
});
}
match type_id {
x if x == Bool.as_u16() && !payload.is_empty() => Ok(DecodedVariant::Bool(
payload.first().copied().unwrap_or(0) != 0,
)),
x if x == Int32.as_u16() && payload.len() >= 4 => {
Ok(DecodedVariant::Int32(i32::from_le_bytes(arr4(payload, 0)?)))
}
x if x == Float.as_u16() && payload.len() >= 4 => {
Ok(DecodedVariant::Float(f32::from_le_bytes(arr4(payload, 0)?)))
}
x if x == Double.as_u16() && payload.len() >= 8 => Ok(DecodedVariant::Double(
f64::from_le_bytes(arr8(payload, 0)?),
)),
x if x == String.as_u16() => Ok(DecodedVariant::String(decode_utf16le(payload)?)),
x if x == DateTime.as_u16() && payload.len() >= 8 => Ok(DecodedVariant::DateTime(
i64::from_le_bytes(arr8(payload, 0)?),
)),
x if x == Duration.as_u16() && payload.len() >= 8 => Ok(DecodedVariant::Duration(
i64::from_le_bytes(arr8(payload, 0)?),
)),
x if x == Int32Array.as_u16() => {
decode_int32_array(payload).map(DecodedVariant::Int32Array)
}
x if x == BoolArray.as_u16() => Ok(DecodedVariant::BoolArray(
payload.iter().map(|&b| b != 0).collect(),
)),
x if x == FloatArray.as_u16() => {
decode_float_array(payload).map(DecodedVariant::FloatArray)
}
x if x == DoubleArray.as_u16() => {
decode_double_array(payload).map(DecodedVariant::DoubleArray)
}
x if x == StringArray.as_u16() => {
decode_string_array(payload).map(DecodedVariant::StringArray)
}
x if x == DateTimeArray.as_u16() => {
decode_filetime_array(payload).map(DecodedVariant::DateTimeArray)
}
x if x == DurationArray.as_u16() => {
decode_filetime_array(payload).map(DecodedVariant::DurationArray)
}
_ => Ok(DecodedVariant::Unsupported {
type_id,
payload: payload.clone(),
}),
}
}
// ---- Factories (mirror `AsbVariantFactory.From*` at cs:1314-1429) --------
impl AsbVariant {
pub fn from_bool(value: bool) -> Self {
Self::new(AsbDataType::Bool, vec![if value { 1 } else { 0 }])
}
pub fn from_i32(value: i32) -> Self {
Self::new(AsbDataType::Int32, value.to_le_bytes().to_vec())
}
pub fn from_f32(value: f32) -> Self {
Self::new(AsbDataType::Float, value.to_le_bytes().to_vec())
}
pub fn from_f64(value: f64) -> Self {
Self::new(AsbDataType::Double, value.to_le_bytes().to_vec())
}
pub fn from_string(value: &str) -> Self {
Self::new(AsbDataType::String, encode_utf16le(value))
}
pub fn from_filetime(value: i64) -> Self {
Self::new(AsbDataType::DateTime, value.to_le_bytes().to_vec())
}
pub fn from_duration_ticks(value: i64) -> Self {
Self::new(AsbDataType::Duration, value.to_le_bytes().to_vec())
}
pub fn from_i32_array(values: &[i32]) -> Self {
let mut payload = Vec::with_capacity(values.len() * 4);
for v in values {
payload.extend_from_slice(&v.to_le_bytes());
}
Self::new(AsbDataType::Int32Array, payload)
}
pub fn from_bool_array(values: &[bool]) -> Self {
Self::new(
AsbDataType::BoolArray,
values.iter().map(|&b| if b { 1u8 } else { 0u8 }).collect(),
)
}
pub fn from_f32_array(values: &[f32]) -> Self {
let mut payload = Vec::with_capacity(values.len() * 4);
for v in values {
payload.extend_from_slice(&v.to_le_bytes());
}
Self::new(AsbDataType::FloatArray, payload)
}
pub fn from_f64_array(values: &[f64]) -> Self {
let mut payload = Vec::with_capacity(values.len() * 8);
for v in values {
payload.extend_from_slice(&v.to_le_bytes());
}
Self::new(AsbDataType::DoubleArray, payload)
}
/// String-array layout: per-string `i32` byte-length followed by
/// UTF-16LE bytes. `null` and `""` both emit a zero-length record
/// (`cs:1400`). The .NET decoder maps zero-length back to
/// `string.Empty` (`cs:798`).
pub fn from_string_array(values: &[&str]) -> Self {
let mut payload = Vec::new();
for value in values {
let bytes = encode_utf16le(value);
let len = i32::try_from(bytes.len()).unwrap_or(i32::MAX);
payload.extend_from_slice(&len.to_le_bytes());
payload.extend_from_slice(&bytes);
}
Self::new(AsbDataType::StringArray, payload)
}
pub fn from_filetime_array(values: &[i64]) -> Self {
let mut payload = Vec::with_capacity(values.len() * 8);
for v in values {
payload.extend_from_slice(&v.to_le_bytes());
}
Self::new(AsbDataType::DateTimeArray, payload)
}
pub fn from_duration_array(values: &[i64]) -> Self {
let mut payload = Vec::with_capacity(values.len() * 8);
for v in values {
payload.extend_from_slice(&v.to_le_bytes());
}
Self::new(AsbDataType::DurationArray, payload)
}
}
// ---- AsbStatus -----------------------------------------------------------
/// Wire layout: signed 1-byte `count`, 4-byte unsigned `payload_length`,
/// `payload_length` bytes of status elements (`cs:1109-1167`). The richer
/// status-element walk (marker-byte bit 7 = implicit zero, etc., see
/// `docs/ASB-Variant-Wire-Format.md:180-205`) is deliberately not done
/// here; the codec round-trips the payload bytes verbatim and exposes a
/// raw accessor so consumers (or a higher-level `StatusElement` parser
/// added later) can walk them.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AsbStatus {
pub count: i8,
pub payload: Vec<u8>,
}
impl AsbStatus {
pub fn wire_len(&self) -> usize {
1 + 4 + self.payload.len()
}
pub fn encode_into(&self, out: &mut Vec<u8>) {
out.push(self.count as u8);
let len = u32::try_from(self.payload.len()).unwrap_or(u32::MAX);
out.extend_from_slice(&len.to_le_bytes());
if !self.payload.is_empty() {
out.extend_from_slice(&self.payload);
}
}
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(self.wire_len());
self.encode_into(&mut out);
out
}
pub fn decode(input: &[u8]) -> Result<(Self, usize), CodecError> {
let mut cursor = 0usize;
let count_byte = *input.first().ok_or(CodecError::ShortRead {
expected: 1,
actual: 0,
})?;
let count = count_byte as i8;
cursor += 1;
let payload_length = read_u32_le(input, &mut cursor)? as usize;
let payload = read_bytes(input, &mut cursor, payload_length)?;
Ok((
Self {
count,
payload: payload.to_vec(),
},
cursor,
))
}
}
// ---- RuntimeValue --------------------------------------------------------
/// Wraps an [`AsbVariant`] with a `DateTime.ToBinary()` timestamp + status
/// per `RuntimeValue` at `cs:741-791`. The 8-byte timestamp is the .NET
/// `DateTime.ToBinary()` packed value (62-bit ticks + 2-bit kind); we
/// preserve it as `i64` rather than splitting because consumers vary in
/// whether they care about the kind bits, and the read path on .NET uses
/// `DateTime.FromBinary` which round-trips the exact value.
#[derive(Debug, Clone, PartialEq)]
pub struct RuntimeValue {
pub timestamp_binary: i64,
pub timestamp_specified: bool,
pub value: AsbVariant,
pub status: AsbStatus,
}
impl RuntimeValue {
pub fn wire_len(&self) -> usize {
8 + 1 + self.value.wire_len() + self.status.wire_len()
}
pub fn encode_into(&self, out: &mut Vec<u8>) {
out.extend_from_slice(&self.timestamp_binary.to_le_bytes());
out.push(if self.timestamp_specified { 1 } else { 0 });
self.value.encode_into(out);
self.status.encode_into(out);
}
pub fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(self.wire_len());
self.encode_into(&mut out);
out
}
pub fn decode(input: &[u8]) -> Result<(Self, usize), CodecError> {
let mut cursor = 0usize;
let timestamp_binary = read_i64_le(input, &mut cursor)?;
let flag_byte = input.get(cursor).copied().ok_or(CodecError::ShortRead {
expected: 1,
actual: 0,
})?;
let timestamp_specified = flag_byte != 0;
cursor += 1;
let value_tail = input.get(cursor..).ok_or(CodecError::ShortRead {
expected: 10,
actual: 0,
})?;
let (value, value_consumed) = AsbVariant::decode(value_tail)?;
cursor += value_consumed;
let status_tail = input.get(cursor..).ok_or(CodecError::ShortRead {
expected: 5,
actual: 0,
})?;
let (status, status_consumed) = AsbStatus::decode(status_tail)?;
cursor += status_consumed;
Ok((
Self {
timestamp_binary,
timestamp_specified,
value,
status,
},
cursor,
))
}
}
// ---- helpers --------------------------------------------------------------
fn read_array<const N: usize>(input: &[u8], cursor: &mut usize) -> Result<[u8; N], CodecError> {
let slice = read_bytes(input, cursor, N)?;
let mut out = [0u8; N];
out.copy_from_slice(slice);
Ok(out)
}
fn read_u16_le(input: &[u8], cursor: &mut usize) -> Result<u16, CodecError> {
Ok(u16::from_le_bytes(read_array::<2>(input, cursor)?))
}
fn read_u32_le(input: &[u8], cursor: &mut usize) -> Result<u32, CodecError> {
Ok(u32::from_le_bytes(read_array::<4>(input, cursor)?))
}
fn read_i32_le(input: &[u8], cursor: &mut usize) -> Result<i32, CodecError> {
Ok(i32::from_le_bytes(read_array::<4>(input, cursor)?))
}
fn read_i64_le(input: &[u8], cursor: &mut usize) -> Result<i64, CodecError> {
Ok(i64::from_le_bytes(read_array::<8>(input, cursor)?))
}
fn read_bytes<'a>(
input: &'a [u8],
cursor: &mut usize,
needed: usize,
) -> Result<&'a [u8], CodecError> {
let end = cursor.checked_add(needed).ok_or(CodecError::ShortRead {
expected: needed,
actual: input.len().saturating_sub(*cursor),
})?;
if end > input.len() {
return Err(CodecError::ShortRead {
expected: needed,
actual: input.len().saturating_sub(*cursor),
});
}
let slice = input.get(*cursor..end).ok_or(CodecError::ShortRead {
expected: needed,
actual: input.len().saturating_sub(*cursor),
})?;
*cursor = end;
Ok(slice)
}
fn arr4(payload: &[u8], offset: usize) -> Result<[u8; 4], CodecError> {
let slice = payload
.get(offset..offset + 4)
.ok_or(CodecError::ShortRead {
expected: 4,
actual: payload.len().saturating_sub(offset),
})?;
let mut out = [0u8; 4];
out.copy_from_slice(slice);
Ok(out)
}
fn arr8(payload: &[u8], offset: usize) -> Result<[u8; 8], CodecError> {
let slice = payload
.get(offset..offset + 8)
.ok_or(CodecError::ShortRead {
expected: 8,
actual: payload.len().saturating_sub(offset),
})?;
let mut out = [0u8; 8];
out.copy_from_slice(slice);
Ok(out)
}
fn decode_int32_array(payload: &[u8]) -> Result<Vec<i32>, CodecError> {
let count = payload.len() / 4;
let mut out = Vec::with_capacity(count);
for i in 0..count {
out.push(i32::from_le_bytes(arr4(payload, i * 4)?));
}
Ok(out)
}
fn decode_float_array(payload: &[u8]) -> Result<Vec<f32>, CodecError> {
let count = payload.len() / 4;
let mut out = Vec::with_capacity(count);
for i in 0..count {
out.push(f32::from_le_bytes(arr4(payload, i * 4)?));
}
Ok(out)
}
fn decode_double_array(payload: &[u8]) -> Result<Vec<f64>, CodecError> {
let count = payload.len() / 8;
let mut out = Vec::with_capacity(count);
for i in 0..count {
out.push(f64::from_le_bytes(arr8(payload, i * 8)?));
}
Ok(out)
}
fn decode_filetime_array(payload: &[u8]) -> Result<Vec<i64>, CodecError> {
let count = payload.len() / 8;
let mut out = Vec::with_capacity(count);
for i in 0..count {
out.push(i64::from_le_bytes(arr8(payload, i * 8)?));
}
Ok(out)
}
/// String-array decode: walks `i32` length + UTF-16LE bytes records until
/// the payload is exhausted or a malformed length is encountered.
/// `MxAsbDataClient.DecodeStringArray` (`cs:785-803`) stops on negative
/// length or out-of-range; partial values decoded before that point are
/// kept. We mirror that exactly.
fn decode_string_array(payload: &[u8]) -> Result<Vec<String>, CodecError> {
let mut values = Vec::new();
let mut offset = 0usize;
while offset + 4 <= payload.len() {
let len_bytes = payload
.get(offset..offset + 4)
.ok_or(CodecError::ShortRead {
expected: 4,
actual: payload.len().saturating_sub(offset),
})?;
let mut buf = [0u8; 4];
buf.copy_from_slice(len_bytes);
let byte_length = i32::from_le_bytes(buf);
offset += 4;
if byte_length < 0 || (byte_length as usize) > payload.len().saturating_sub(offset) {
break;
}
let byte_length = byte_length as usize;
if byte_length == 0 {
values.push(String::new());
continue;
}
let str_bytes = payload
.get(offset..offset + byte_length)
.ok_or(CodecError::ShortRead {
expected: byte_length,
actual: payload.len().saturating_sub(offset),
})?;
values.push(decode_utf16le(str_bytes)?);
offset += byte_length;
}
Ok(values)
}
fn encode_utf16le(value: &str) -> Vec<u8> {
let mut out = Vec::with_capacity(value.len() * 2);
for code_unit in value.encode_utf16() {
out.extend_from_slice(&code_unit.to_le_bytes());
}
out
}
fn decode_utf16le(bytes: &[u8]) -> Result<String, CodecError> {
if bytes.len() % 2 != 0 {
return Err(CodecError::Decode {
offset: bytes.len(),
reason: "UTF-16LE payload has odd byte length",
buffer_len: bytes.len(),
});
}
let units: Vec<u16> = bytes
.chunks_exact(2)
.map(|chunk| {
let mut buf = [0u8; 2];
buf.copy_from_slice(chunk);
u16::from_le_bytes(buf)
})
.collect();
let buf_len = bytes.len();
String::from_utf16(&units).map_err(|err: FromUtf16Error| CodecError::Decode {
offset: 0,
reason: utf16_error_reason(&err),
buffer_len: buf_len,
})
}
const fn utf16_error_reason(_: &FromUtf16Error) -> &'static str {
// FromUtf16Error doesn't carry a position; fixed string preserves the
// 'static-reason contract used by CodecError variants.
"UTF-16LE payload contains an unpaired surrogate"
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
mod tests {
use super::*;
fn round_trip_variant(variant: AsbVariant) {
let bytes = variant.encode();
let (decoded, consumed) = AsbVariant::decode(&bytes).unwrap();
assert_eq!(consumed, bytes.len(), "decode consumed != encoded len");
assert_eq!(decoded, variant, "wire round-trip diverged");
}
#[test]
fn variant_empty_round_trip() {
round_trip_variant(AsbVariant::empty());
}
#[test]
fn variant_bool_round_trip() {
round_trip_variant(AsbVariant::from_bool(true));
round_trip_variant(AsbVariant::from_bool(false));
}
#[test]
fn variant_i32_round_trip() {
round_trip_variant(AsbVariant::from_i32(0));
round_trip_variant(AsbVariant::from_i32(123));
round_trip_variant(AsbVariant::from_i32(i32::MIN));
round_trip_variant(AsbVariant::from_i32(i32::MAX));
}
#[test]
fn variant_floats_round_trip() {
round_trip_variant(AsbVariant::from_f32(1.5));
round_trip_variant(AsbVariant::from_f64(-std::f64::consts::E));
}
#[test]
fn variant_string_round_trip() {
round_trip_variant(AsbVariant::from_string(""));
round_trip_variant(AsbVariant::from_string("hello world"));
round_trip_variant(AsbVariant::from_string("éàü 漢字"));
}
#[test]
fn variant_datetime_round_trip() {
round_trip_variant(AsbVariant::from_filetime(0));
round_trip_variant(AsbVariant::from_filetime(132_845_000_000_000_000));
}
#[test]
fn variant_duration_round_trip() {
round_trip_variant(AsbVariant::from_duration_ticks(0));
round_trip_variant(AsbVariant::from_duration_ticks(1_234_567_890));
}
#[test]
fn variant_int32_array_round_trip() {
round_trip_variant(AsbVariant::from_i32_array(&[]));
round_trip_variant(AsbVariant::from_i32_array(&[1, 2, 3, -4, i32::MAX]));
}
#[test]
fn variant_bool_array_round_trip() {
round_trip_variant(AsbVariant::from_bool_array(&[]));
round_trip_variant(AsbVariant::from_bool_array(&[true, false, true, true]));
}
#[test]
fn variant_float_array_round_trip() {
round_trip_variant(AsbVariant::from_f32_array(&[1.0, -2.0, 3.5]));
round_trip_variant(AsbVariant::from_f64_array(&[std::f64::consts::PI, -0.0]));
}
#[test]
fn variant_string_array_round_trip() {
round_trip_variant(AsbVariant::from_string_array(&[]));
round_trip_variant(AsbVariant::from_string_array(&["alpha", "", "γαμμα"]));
}
#[test]
fn variant_datetime_and_duration_arrays_round_trip() {
round_trip_variant(AsbVariant::from_filetime_array(&[
0,
132_845_000_000_000_000,
i64::MAX,
]));
round_trip_variant(AsbVariant::from_duration_array(&[-1, i64::MIN, 42]));
}
#[test]
fn decode_variant_handles_empty_arrays_to_empty_typed_values() {
let v = AsbVariant {
type_id: AsbDataType::Int32Array.as_u16(),
length: 0,
payload: Vec::new(),
};
assert_eq!(
decode_variant(&v).unwrap(),
DecodedVariant::Int32Array(Vec::new())
);
let v = AsbVariant {
type_id: AsbDataType::String.as_u16(),
length: 0,
payload: Vec::new(),
};
assert_eq!(
decode_variant(&v).unwrap(),
DecodedVariant::String(String::new())
);
}
#[test]
fn decode_variant_returns_empty_for_unknown_type_with_empty_payload() {
let v = AsbVariant {
type_id: AsbDataType::Bool.as_u16(),
length: 0,
payload: Vec::new(),
};
assert_eq!(decode_variant(&v).unwrap(), DecodedVariant::Empty);
}
#[test]
fn decode_variant_int32() {
let v = AsbVariant::from_i32(0x1234_5678);
assert_eq!(
decode_variant(&v).unwrap(),
DecodedVariant::Int32(0x1234_5678)
);
}
#[test]
fn decode_variant_string() {
let v = AsbVariant::from_string("hello");
assert_eq!(
decode_variant(&v).unwrap(),
DecodedVariant::String("hello".to_string())
);
}
#[test]
fn decode_variant_string_array_with_empty_entries() {
let v = AsbVariant::from_string_array(&["a", "", "bc"]);
let decoded = decode_variant(&v).unwrap();
match decoded {
DecodedVariant::StringArray(values) => {
assert_eq!(
values,
vec!["a".to_string(), String::new(), "bc".to_string()]
);
}
other => panic!("expected StringArray, got {other:?}"),
}
}
#[test]
fn decode_variant_unsupported_type_returns_raw_bytes() {
let v = AsbVariant {
type_id: AsbDataType::Guid.as_u16(),
length: 16,
payload: vec![0xAB; 16],
};
match decode_variant(&v).unwrap() {
DecodedVariant::Unsupported { type_id, payload } => {
assert_eq!(type_id, AsbDataType::Guid.as_u16());
assert_eq!(payload, vec![0xAB; 16]);
}
other => panic!("expected Unsupported, got {other:?}"),
}
}
#[test]
fn decode_variant_int32_too_short_falls_through_to_unsupported() {
// payload < 4 bytes for TypeInt32 — match-arm guard fails and
// .NET hits the `_ => payload` fallback (cs:748). We mirror that.
let v = AsbVariant {
type_id: AsbDataType::Int32.as_u16(),
length: 3,
payload: vec![1, 2, 3],
};
match decode_variant(&v).unwrap() {
DecodedVariant::Unsupported { type_id, payload } => {
assert_eq!(type_id, AsbDataType::Int32.as_u16());
assert_eq!(payload, vec![1, 2, 3]);
}
other => panic!("expected Unsupported, got {other:?}"),
}
}
#[test]
fn variant_decode_rejects_truncated_header() {
// Cut off before the payload-length field finishes.
let bytes = vec![0x04, 0x00, 1, 0, 0, 0, 0xFF];
let err = AsbVariant::decode(&bytes).unwrap_err();
assert!(matches!(err, CodecError::ShortRead { .. }));
}
#[test]
fn asb_status_round_trip() {
let status = AsbStatus {
count: -3,
payload: vec![0x01, 0x02, 0x03],
};
let bytes = status.encode();
let (decoded, consumed) = AsbStatus::decode(&bytes).unwrap();
assert_eq!(consumed, bytes.len());
assert_eq!(decoded, status);
}
#[test]
fn asb_status_round_trip_empty() {
let status = AsbStatus::default();
let bytes = status.encode();
let (decoded, consumed) = AsbStatus::decode(&bytes).unwrap();
assert_eq!(consumed, 5);
assert_eq!(decoded, status);
}
#[test]
fn runtime_value_round_trip() {
let rv = RuntimeValue {
timestamp_binary: 0x0123_4567_89AB_CDEF,
timestamp_specified: true,
value: AsbVariant::from_i32(42),
status: AsbStatus {
count: 1,
payload: vec![0xC0],
},
};
let bytes = rv.encode();
let (decoded, consumed) = RuntimeValue::decode(&bytes).unwrap();
assert_eq!(consumed, bytes.len());
assert_eq!(decoded, rv);
}
#[test]
fn runtime_value_round_trip_empty_variant() {
let rv = RuntimeValue {
timestamp_binary: 0,
timestamp_specified: false,
value: AsbVariant::empty(),
status: AsbStatus::default(),
};
let bytes = rv.encode();
let (decoded, consumed) = RuntimeValue::decode(&bytes).unwrap();
assert_eq!(consumed, bytes.len());
assert_eq!(decoded, rv);
}
#[test]
fn variant_wire_layout_is_2_4_4_payload() {
// .NET reference: WriteToStream writes Type (u16), Length (i32),
// payloadLength (u32), payload bytes. Verify byte positions.
let v = AsbVariant::from_i32(0xAABB_CCDD_u32 as i32);
let bytes = v.encode();
// type_id 0x0004 little-endian
assert_eq!(&bytes[0..2], &[0x04, 0x00]);
// length = 4
assert_eq!(&bytes[2..6], &[0x04, 0x00, 0x00, 0x00]);
// payload length = 4
assert_eq!(&bytes[6..10], &[0x04, 0x00, 0x00, 0x00]);
// payload = 0xAABB_CCDD little-endian
assert_eq!(&bytes[10..14], &[0xDD, 0xCC, 0xBB, 0xAA]);
}
}
@@ -2,7 +2,7 @@
//!
//! Direct port of `src/MxNativeCodec/NmxItemControlMessage.cs`. The body
//! carries an advise-supervisory or unadvise command together with a 16-byte
//! item correlation GUID and a 14-byte projection of an [`MxReferenceHandle`]
//! item correlation GUID and a 14-byte projection of an [`crate::reference_handle::MxReferenceHandle`]
//! (handle bytes 6..20 — `object_id` through `attribute_index`).
//!
//! ## Wire layout
@@ -87,7 +87,7 @@ const PAYLOAD_LENGTH: usize = 18; // cs:28 — 7×u16 + u32 tail = 18 bytes
pub const DEFAULT_TAIL: u32 = 3;
/// Decoded NMX item-control body. The fields after `item_correlation_id`
/// project bytes 6..20 of an [`MxReferenceHandle`] — see
/// project bytes 6..20 of an [`crate::reference_handle::MxReferenceHandle`] — see
/// `NmxItemControlMessage.cs:71-81, 134-141`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NmxItemControlMessage {
+7 -11
View File
@@ -15,14 +15,15 @@
//! `NmxTransferEnvelopeTemplate` (round-trip preserver).
//!
//! Remaining (wave 2): `NmxSecuredWrite2Message` (`0x38`),
//! `ObservedWriteBodyTemplate`. ASB Variant + AsbStatus + RuntimeValue land
//! in M5.
//! `ObservedWriteBodyTemplate`. ASB Variant + AsbStatus + RuntimeValue
//! landed in the F24 sub-stream of M5 — see [`asb_variant`].
//!
//! Every wire shape here is grounded in `src/MxNativeCodec/*.cs` (the .NET
//! reference) and `captures/0NN-frida-*` (Frida ground truth).
#![forbid(unsafe_code)]
pub mod asb_variant;
pub mod envelope;
pub mod envelope_template;
pub mod error;
@@ -68,16 +69,11 @@ pub struct NmxWriteMessage;
#[derive(Debug, Clone)]
pub struct NmxSecuredWrite2Message;
// ---- ASB types (M5 follow-up) --------------------------------------------
// ---- ASB types (M5, F24) -------------------------------------------------
#[derive(Debug, Clone)]
pub struct AsbVariant;
#[derive(Debug, Clone, Copy, Default)]
pub struct AsbStatus;
#[derive(Debug, Clone)]
pub struct RuntimeValue;
pub use asb_variant::{
AsbDataType, AsbStatus, AsbVariant, DecodedVariant, RuntimeValue, decode_variant,
};
// ---- Convenience prelude -------------------------------------------------
@@ -1,7 +1,7 @@
//! `NmxMetadataQueryMessage` — observed pre-advise metadata-query body.
//!
//! Direct port of `src/MxNativeCodec/NmxMetadataQueryMessage.cs`. The .NET
//! reference exposes a single static helper, [`encode_observed_pre_advise`],
//! reference exposes a single static helper, `encode_observed_pre_advise`,
//! which returns a fixed observed body with a 16-byte item-correlation GUID
//! patched in at offset `0x8a`.
//!
@@ -44,7 +44,7 @@
//! `Encode` (`.cs:51-64`) writes:
//!
//! 1. The captured prefix (`_prefix`, raw bytes) — preserved verbatim.
//! 2. The freshly-encoded value bytes from [`encode_value`].
//! 2. The freshly-encoded value bytes from `encode_value_bytes`.
//! 3. The captured suffix (`_suffixBeforeWriteIndex`) — preserved verbatim.
//! 4. The fresh `writeIndex` as i32 LE in the trailing 4 bytes.
//!
@@ -167,12 +167,12 @@ impl ObservedWriteBodyTemplate {
}
}
/// Captured opcode at body[0]. Mirrors `_prefix[0]`.
/// Captured opcode at `body[0]`. Mirrors `_prefix[0]`.
pub fn command(&self) -> u8 {
self.command
}
/// Captured wire-kind byte at body[17]. Drawn from the captured prefix,
/// Captured wire-kind byte at `body[17]`. Drawn from the captured prefix,
/// not from the runtime [`MxValueKind`] (which can disambiguate
/// String vs DateTime past the encoder collapse).
pub fn wire_kind(&self) -> u8 {
@@ -21,11 +21,33 @@
//! [`NmxOperationStatusMessage::try_parse_inner`] is provided here. When
//! `NmxObservedEnvelope` lands, add `try_parse_process_data_received_body` as
//! a thin wrapper.
//!
//! ## Typed promotion and the synthesizer kernel
//!
//! [`NmxOperationStatusMessage::promote_to_typed`] returns the same
//! [`MxStatus`] the parser already attached to the message — the
//! verbatim-preserve placeholder for unknown shapes, the
//! [`MxStatus::WRITE_COMPLETE_OK`] sentinel for the proven
//! `(status_code=0x8050, completion_code=0x00)` shape. The 5-byte
//! `00 00 SS SS CC` inner body is **not** the same wire field as the
//! 4-byte packed status word `Lmx.dll!FUN_10100ce0` decodes
//! ([`MxStatus::from_packed_u32`]) — that kernel applies one layer up,
//! to the `INmxService.GetResponse2` payload's `status: i32` field
//! (carried e.g. in subscription records). See
//! `analysis/ghidra/exports/Lmx.dll.synthesizer-helpers2-decompile.md`
//! and `design/70-risks-and-open-questions.md` R3/R4 Path A for the
//! evidence chain.
//!
//! `promote_to_typed` is therefore a thin convenience over the existing
//! `status` field: callers that want the canonical bit-layout decoder
//! should reach for [`MxStatus::from_packed_u32`] directly when they
//! have a 4-byte packed value in hand.
// Direct byte indexing — see reference_handle.rs for rationale.
#![allow(clippy::indexing_slicing)]
use crate::error::CodecError;
use crate::observed_frame::NmxObservedEnvelope;
use crate::status::{MxStatus, MxStatusCategory, MxStatusSource};
/// Which of the two recognised inner-frame shapes was decoded
@@ -78,6 +100,47 @@ impl NmxOperationStatusMessage {
&& self.completion_code == 0x00
}
/// Return the typed [`MxStatus`] for this frame.
///
/// This is a thin convenience over [`Self::status`] — same value,
/// no transformation. Provided for API symmetry with
/// [`MxStatus::from_packed_u32`] (the canonical 4-byte synthesizer
/// kernel) and to give consumers a single entry point that can
/// be extended in future revisions if new evidence pins additional
/// `(status_code, completion_code)` shapes.
///
/// **What this method does NOT do:** apply the
/// `Lmx.dll!FUN_10100ce0` synthesizer to the 5-byte inner body.
/// The 5-byte `00 00 SS SS CC` shape and the 4-byte packed-u32
/// shape are different wire fields at different layers — see the
/// module docs and
/// `design/70-risks-and-open-questions.md` R3/R4 Path A. Callers
/// holding a 4-byte packed `MxStatus` (e.g. extracted from a
/// subscription record's `status: i32`) should call
/// [`MxStatus::from_packed_u32`] directly.
#[must_use]
pub const fn promote_to_typed(&self) -> MxStatus {
self.status
}
/// Peel the outer [`NmxObservedEnvelope`] off a `ProcessDataReceived`
/// payload and parse the inner body. Mirrors
/// `NmxOperationStatusMessage.TryParseProcessDataReceivedBody`
/// (`NmxOperationStatusMessage.cs:20-32`).
///
/// # Errors
///
/// Returns `Err` when the outer envelope cannot be parsed or the
/// inner body matches no recognised shape (1- or 5-byte completion
/// frame). The .NET reference returns `false` and a `null!`
/// out-param in both cases; the Rust port surfaces a typed
/// [`CodecError`] so callers can distinguish "not a process-data
/// frame" from "successfully parsed".
pub fn try_parse_process_data_received_body(body: &[u8]) -> Result<Self, CodecError> {
let envelope = NmxObservedEnvelope::parse_process_data_received_body_flexible(body)?;
Self::try_parse_inner(&envelope.inner_body)
}
/// Parse an inner body — either 1 byte (`CompletionOnly`) or 5 bytes
/// (`StatusWord` with leading `00 00`).
///
@@ -281,4 +344,38 @@ mod tests {
let msg = NmxOperationStatusMessage::try_parse_inner(&frame).unwrap();
assert_eq!(msg.status_code, 0xBBAA);
}
#[test]
fn promote_to_typed_returns_existing_status_for_status_word() {
// The proven shape — must keep returning the canonical sentinel.
let frame = [0x00, 0x00, 0x50, 0x80, 0x00];
let msg = NmxOperationStatusMessage::try_parse_inner(&frame).unwrap();
assert_eq!(msg.promote_to_typed(), MxStatus::WRITE_COMPLETE_OK);
assert_eq!(msg.promote_to_typed(), msg.status);
}
#[test]
fn promote_to_typed_returns_verbatim_status_for_completion_only() {
// 1-byte frames: no synthesizer evidence — must stay verbatim.
for byte in [0x00_u8, 0x41, 0xEF] {
let msg = NmxOperationStatusMessage::try_parse_inner(&[byte]).unwrap();
let promoted = msg.promote_to_typed();
assert_eq!(promoted, msg.status);
assert_eq!(promoted.category, MxStatusCategory::Unknown);
assert_eq!(promoted.detected_by, MxStatusSource::Unknown);
assert_eq!(promoted.detail, i16::from(byte));
}
}
#[test]
fn promote_to_typed_does_not_change_existing_status_field() {
// promote_to_typed must not mutate the verbatim-preserve `status`
// field. This guards the byte-for-byte parity contract with the
// .NET reference.
let frame = [0x00, 0x00, 0x55, 0xAA, 0x33];
let msg = NmxOperationStatusMessage::try_parse_inner(&frame).unwrap();
let original_status = msg.status;
let _typed = msg.promote_to_typed();
assert_eq!(msg.status, original_status);
}
}
@@ -10,6 +10,9 @@
// `.get(n)?` would obscure the byte map.
#![allow(clippy::indexing_slicing)]
use std::cell::RefCell;
use std::collections::HashMap;
use crate::error::CodecError;
const CRC16_IBM_POLYNOMIAL: u16 = 0xa001;
@@ -34,8 +37,9 @@ const CRC16_IBM_POLYNOMIAL: u16 = 0xa001;
///
/// `object_signature` and `attribute_signature` are derived values. The Rust
/// port keeps them private — the only constructor that produces a handle from
/// names is [`from_names`]; the only mutators that update one signature are
/// [`with_object_tag_name`] and [`with_attribute_name`], which both
/// names is [`MxReferenceHandle::from_names`]; the only mutators that update
/// one signature are [`MxReferenceHandle::with_object_tag_name`] and
/// [`MxReferenceHandle::with_attribute_name`], which both
/// recompute. This is a deliberate tightening over the .NET reference (which
/// is a record with public init-only signature fields).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
@@ -157,7 +161,7 @@ impl MxReferenceHandle {
/// # Panics
///
/// Panics if `destination.len() < 20`. Use a 20-byte slice or call
/// [`encode`] for a fresh buffer.
/// [`Self::encode`] for a fresh buffer.
pub fn write_to(self, destination: &mut [u8]) {
assert!(
destination.len() >= Self::ENCODED_LEN,
@@ -190,6 +194,13 @@ impl MxReferenceHandle {
/// mappings (e.g. Turkish dotless-i) may diverge — see
/// `design/10-raw-layer.md` L37 for the path forward via `icu_casemap`.
///
/// **Caching**: Results are memoised in a thread-local
/// [`HashMap`]<[`String`], `u16`> so repeated calls with the same name (the
/// hot path inside [`MxReferenceHandle::from_names`] when the same handles
/// are constructed many times) skip the UTF-16LE conversion and CRC walk.
/// The cache is bounded ([`SIGNATURE_CACHE_CAP`] entries); on overflow the
/// thread's cache is cleared. (F52.2 from `design/M6-bench-baseline.md`.)
///
/// # Errors
///
/// Returns [`CodecError::InvalidName`] if `name` is empty or whitespace-only.
@@ -197,6 +208,35 @@ pub fn compute_name_signature(name: &str) -> Result<u16, CodecError> {
if name.trim().is_empty() {
return Err(CodecError::InvalidName);
}
// Fast path: thread-local cache lookup. Repeated calls with the same name
// skip the `to_lowercase` allocation entirely.
if let Some(cached) = SIGNATURE_CACHE.with(|c| c.borrow().get(name).copied()) {
return Ok(cached);
}
let signature = compute_name_signature_uncached(name);
SIGNATURE_CACHE.with(|c| {
let mut cache = c.borrow_mut();
if cache.len() >= SIGNATURE_CACHE_CAP {
cache.clear();
}
cache.insert(name.to_string(), signature);
});
Ok(signature)
}
/// Soft cap on the per-thread name → signature cache. Keeps memory bounded
/// when a workload churns through unique names (e.g. dynamic discovery). On
/// overflow the cache is cleared rather than evicted LRU — any sane workload
/// re-fills only the names it actively uses.
pub const SIGNATURE_CACHE_CAP: usize = 1024;
thread_local! {
static SIGNATURE_CACHE: RefCell<HashMap<String, u16>> = RefCell::new(HashMap::new());
}
fn compute_name_signature_uncached(name: &str) -> u16 {
let lower = name.to_lowercase();
let mut crc: u16 = 0;
for ch in lower.chars() {
@@ -211,7 +251,16 @@ pub fn compute_name_signature(name: &str) -> Result<u16, CodecError> {
crc = update_crc16_ibm(crc, (*unit >> 8) as u8);
}
}
Ok(crc)
crc
}
/// Clear the current thread's name → signature cache. Used by tests that
/// want to measure cold-path behaviour; not exposed publicly because the
/// cache is otherwise transparent to callers.
#[cfg(test)]
#[allow(dead_code)]
pub(crate) fn clear_signature_cache_for_tests() {
SIGNATURE_CACHE.with(|c| c.borrow_mut().clear());
}
/// One iteration of the CRC-16/IBM update loop (poly `0xa001`, right-shifted
@@ -332,6 +381,34 @@ mod tests {
assert_eq!(update_crc16_ibm(0, 0), 0);
}
/// F52.2 — the thread-local cache must return the same value for cold
/// (cache-miss) and hot (cache-hit) calls. Walking the cache twice with
/// the same name should be a no-op as far as the result goes.
#[test]
fn signature_cache_hit_matches_cold_compute() {
clear_signature_cache_for_tests();
let cold = compute_name_signature("TestObject").unwrap();
// Second call should hit the cache.
let hot = compute_name_signature("TestObject").unwrap();
assert_eq!(cold, hot);
// And match the well-known dotnet-parity vector.
assert_eq!(cold, 0x0B25);
}
#[test]
fn signature_cache_overflow_clears() {
clear_signature_cache_for_tests();
// Exceed the cap by one to trigger a clear.
for i in 0..=SIGNATURE_CACHE_CAP {
let name = format!("Tag{i}");
compute_name_signature(&name).unwrap();
}
// After overflow, recompute against a known vector should still
// produce the right value (cache hit-or-miss, doesn't matter — the
// returned u16 is what we assert on).
assert_eq!(compute_name_signature("TestObject").unwrap(), 0x0B25);
}
#[test]
fn round_trip_zero_handle() {
let handle = MxReferenceHandle::default();
@@ -572,6 +572,25 @@ impl NmxReferenceRegistrationResultMessage {
})
}
/// Peel the `ProcessDataReceived` envelope and parse the inner
/// `0x11` registration-result body. Mirrors
/// `NmxReferenceRegistrationResultMessage.TryParseProcessDataReceivedBody`
/// (the wire-side path used by `MxNativeSession.OnCallbackReceived`
/// at `cs:582`).
///
/// # Errors
///
/// - [`CodecError::ShortRead`] / [`CodecError::InnerLengthMismatch`]
/// surfaced from the envelope parse.
/// - Any error from [`Self::parse`] on the inner body — including
/// [`CodecError::UnexpectedOpcode`] when the inner body's first
/// byte isn't `0x11` (use this as a discriminator for "this body
/// isn't a registration-result frame").
pub fn try_parse_process_data_received_body(body: &[u8]) -> Result<Self, CodecError> {
let envelope = crate::NmxObservedEnvelope::parse_process_data_received_body_flexible(body)?;
Self::parse(&envelope.inner_body)
}
/// Encode the result body. The .NET reference does not provide an
/// `Encode` (the result is server-emitted); the Rust port supplies one
/// for round-trip testing and for synthetic-server use cases. The
+328 -4
View File
@@ -22,7 +22,7 @@ pub enum MxStatusCategory {
}
impl MxStatusCategory {
pub fn from_i16(value: i16) -> Self {
pub const fn from_i16(value: i16) -> Self {
match value {
0 => Self::Ok,
1 => Self::Pending,
@@ -37,7 +37,7 @@ impl MxStatusCategory {
}
}
pub fn to_i16(self) -> i16 {
pub const fn to_i16(self) -> i16 {
self as i16
}
}
@@ -59,7 +59,7 @@ pub enum MxStatusSource {
}
impl MxStatusSource {
pub fn from_i16(value: i16) -> Self {
pub const fn from_i16(value: i16) -> Self {
match value {
0 => Self::RequestingLmx,
1 => Self::RespondingLmx,
@@ -71,7 +71,7 @@ impl MxStatusSource {
}
}
pub fn to_i16(self) -> i16 {
pub const fn to_i16(self) -> i16 {
self as i16
}
}
@@ -85,6 +85,135 @@ pub struct MxStatus {
}
impl MxStatus {
/// Decode a 4-byte packed `MxStatus` word.
///
/// Mirrors the canonical NMX wire-frame status decoder
/// `Lmx.dll!FUN_10100ce0` (see
/// `analysis/ghidra/exports/Lmx.dll.synthesizer-helpers2-decompile.md`).
/// That function reads 4 bytes from a stream into a u32 and unpacks
/// them via the bit layout:
///
/// ```text
/// bit 31: success (-1 if set, 0 if clear)
/// bits 27..24: category (4 bits, masked by 0xF)
/// bits 23..20: detected_by (4 bits, masked by 0xF)
/// bits 15..0: detail (i16 — low 16 bits, signed)
/// bits 30..28, 19..16: reserved/padding (ignored)
/// ```
///
/// This is the **synthesizer kernel** documented in
/// `design/70-risks-and-open-questions.md` R3/R4 Path A. Every NMX
/// wire frame that carries a status word emits one of these 4-byte
/// packings; the consumer-side dispatch (retry counters, callback
/// fan-out) is layered on top of the decoded `MxStatus`, but the
/// decoder itself is byte-deterministic and context-free.
///
/// The `success` field is normalized to either `0` or `-1` per the
/// native `Lmx.dll` semantics: any value with bit 31 set decodes to
/// `-1`, any value with bit 31 clear decodes to `0`. (Native code:
/// `*param_1 = -(ushort)(((uint)param_2 & 0x80000000) != 0)`.)
///
/// Unknown category / detected_by codes (i.e. a 4-bit value that
/// does not match a documented [`MxStatusCategory`] /
/// [`MxStatusSource`] variant) decode to the corresponding
/// `Unknown` variant. The padding bits are silently discarded.
#[must_use]
pub const fn from_packed_u32(packed: u32) -> Self {
// Bit layout — see fn doc.
let success: i16 = if packed & 0x8000_0000 != 0 { -1 } else { 0 };
let category_bits = ((packed >> 24) & 0xF) as i16;
let detected_by_bits = ((packed >> 20) & 0xF) as i16;
let detail = packed as i16;
Self {
success,
category: MxStatusCategory::from_i16(category_bits),
detected_by: MxStatusSource::from_i16(detected_by_bits),
detail,
}
}
/// Construct an `MxStatus` from a single-byte NMX response code.
///
/// Mirrors the synthesis switch in
/// `Lmx.dll!FUN_1010bd10` (`ScanOnDemandCallback::GetResponse`)
/// at lines 741-770 of
/// `analysis/ghidra/exports/Lmx.dll.synthesizer-decompile.md`.
/// When the NMX `responseCode` is non-zero (no payload status word
/// to parse), `Lmx.dll` constructs an `MxStatus` from the response
/// code itself using this fixed mapping:
///
/// | responseCode | category | detected_by |
/// |---|---|---|
/// | `0x01`, `0x02` | `CommunicationError` | `RequestingNmx` |
/// | `0x03` | `ConfigurationError` | `RequestingNmx` |
/// | `0x04` | `ConfigurationError` | `RespondingNmx` |
/// | `0x05` | `CommunicationError` | `RespondingNmx` |
/// | `0x1A` | `CommunicationError` | `RequestingNmx` |
///
/// `success` is `0` (not `-1`) and `detail` carries the response
/// code unchanged. Unmapped codes return `None` — the native code's
/// `default` branch leaves the synthesized status untouched, so the
/// caller falls back to a verbatim raw-byte placeholder per
/// `design/70-risks-and-open-questions.md` R3/R4.
///
/// This is **not** the same wire field as the 1-byte completion
/// frames `0x00`/`0x41`/`0xEF` parsed by
/// [`crate::NmxOperationStatusMessage::try_parse_inner`]: those
/// live inside a `0x32`/`0x33` callback body, while this
/// `responseCode` is the second `out` parameter of
/// `INmxService.GetResponse2(...)` (one layer up the stack).
/// `Lmx.dll`'s decoder for the 1-byte completion frames does not
/// apply this synthesis.
#[must_use]
pub const fn from_nmx_response_code(response_code: u8) -> Option<Self> {
// Per `FUN_1010bd10:741-770` switch.
let (category, detected_by) = match response_code {
0x01 | 0x02 => (
MxStatusCategory::CommunicationError,
MxStatusSource::RequestingNmx,
),
0x03 => (
MxStatusCategory::ConfigurationError,
MxStatusSource::RequestingNmx,
),
0x04 => (
MxStatusCategory::ConfigurationError,
MxStatusSource::RespondingNmx,
),
0x05 => (
MxStatusCategory::CommunicationError,
MxStatusSource::RespondingNmx,
),
0x1A => (
MxStatusCategory::CommunicationError,
MxStatusSource::RequestingNmx,
),
_ => return None,
};
Some(Self {
success: 0,
category,
detected_by,
detail: response_code as i16,
})
}
/// Pack `self` back into the 4-byte NMX wire layout. Inverse of
/// [`Self::from_packed_u32`]. Useful for round-trip tests and
/// future encoder paths.
///
/// Padding bits (30..28, 19..16) are emitted as zero. Bit 31 mirrors
/// `success != 0` — any non-zero `success` round-trips to `-1`
/// because the decoder normalizes to `0`/`-1` only.
#[must_use]
pub const fn to_packed_u32(self) -> u32 {
let success_bit: u32 = if self.success != 0 { 0x8000_0000 } else { 0 };
let category_bits = ((self.category as i16) as u32 & 0xF) << 24;
let detected_by_bits = ((self.detected_by as i16) as u32 & 0xF) << 20;
let detail_bits = (self.detail as u16) as u32;
success_bit | category_bits | detected_by_bits | detail_bits
}
/// `(success=-1, Ok, RequestingLmx, detail=0)` — `MxStatus.DataChangeOk`
/// from `MxStatus.cs:36-40`.
pub const DATA_CHANGE_OK: Self = Self {
@@ -311,4 +440,199 @@ mod tests {
assert!(!MxStatus::SUSPEND_PENDING.is_ok());
assert!(!MxStatus::INVALID_REFERENCE_CONFIGURATION.is_ok());
}
#[test]
fn from_packed_u32_zero_decodes_to_all_zeros() {
// packed=0 → success=0, category=Ok(0), detected_by=RequestingLmx(0), detail=0.
// The "all zeros" status is the simplest data-change-pending shape
// the wire can carry.
let s = MxStatus::from_packed_u32(0);
assert_eq!(s.success, 0);
assert_eq!(s.category, MxStatusCategory::Ok);
assert_eq!(s.detected_by, MxStatusSource::RequestingLmx);
assert_eq!(s.detail, 0);
}
#[test]
fn from_packed_u32_high_bit_sets_success_to_negative_one() {
// Native: `*param_1 = -(ushort)(((uint)param_2 & 0x80000000) != 0)`
// For packed=0x80000000, success=-1, all other fields 0.
let s = MxStatus::from_packed_u32(0x8000_0000);
assert_eq!(s.success, -1);
assert_eq!(s.category, MxStatusCategory::Ok);
assert_eq!(s.detected_by, MxStatusSource::RequestingLmx);
assert_eq!(s.detail, 0);
}
#[test]
fn from_packed_u32_decodes_data_change_ok_layout() {
// `MxStatus::DATA_CHANGE_OK` = (success=-1, Ok=0, RequestingLmx=0,
// detail=0). Pack: bit31=1, bits27..24=0, bits23..20=0, bits15..0=0.
// → 0x80000000.
let packed = MxStatus::DATA_CHANGE_OK.to_packed_u32();
assert_eq!(packed, 0x8000_0000);
let round_trip = MxStatus::from_packed_u32(packed);
assert_eq!(round_trip, MxStatus::DATA_CHANGE_OK);
}
#[test]
fn from_packed_u32_decodes_write_complete_ok_layout() {
// `MxStatus::WRITE_COMPLETE_OK` = (success=-1, Ok=0,
// RespondingAutomationObject=5, detail=0). Pack: bit31=1,
// bits27..24=0 (Ok), bits23..20=5, bits15..0=0.
// → 0x80500000.
let expected_packed: u32 = 0x80_50_00_00;
let s = MxStatus::from_packed_u32(expected_packed);
assert_eq!(s, MxStatus::WRITE_COMPLETE_OK);
assert_eq!(MxStatus::WRITE_COMPLETE_OK.to_packed_u32(), expected_packed);
}
#[test]
fn from_packed_u32_extracts_category_from_bits_24_to_27() {
// category=4 (ConfigurationError) at bits 24..27.
// → 0x04000000.
let s = MxStatus::from_packed_u32(0x0400_0000);
assert_eq!(s.category, MxStatusCategory::ConfigurationError);
assert_eq!(s.detected_by, MxStatusSource::RequestingLmx);
assert_eq!(s.detail, 0);
}
#[test]
fn from_packed_u32_extracts_detected_by_from_bits_20_to_23() {
// detected_by=2 (RequestingNmx) at bits 20..23.
// → 0x00200000.
let s = MxStatus::from_packed_u32(0x0020_0000);
assert_eq!(s.category, MxStatusCategory::Ok);
assert_eq!(s.detected_by, MxStatusSource::RequestingNmx);
assert_eq!(s.detail, 0);
}
#[test]
fn from_packed_u32_extracts_detail_as_signed_low_16_bits() {
// detail=21 ("Invalid reference") at bits 0..15.
// → 0x00000015.
let s = MxStatus::from_packed_u32(0x0000_0015);
assert_eq!(s.detail, 21);
assert_eq!(s.detail_text(), Some("Invalid reference"));
// Negative detail — high bit of low-16 set: 0xFFFF → -1.
let s = MxStatus::from_packed_u32(0x0000_FFFF);
assert_eq!(s.detail, -1);
}
#[test]
fn from_packed_u32_padding_bits_are_ignored() {
// Bits 30..28 and 19..16 are padding/reserved per `FUN_10100ce0`.
// Setting them should not affect any decoded field.
// bit 31: success
// bits 30..28: padding (0x70_00_00_00)
// bits 27..24: category
// bits 23..20: detected_by
// bits 19..16: padding (0x00_0F_00_00)
// bits 15..0: detail
// Padding-only mask: 0x70_00_00_00 | 0x00_0F_00_00 = 0x700F_0000.
let with_padding = MxStatus::from_packed_u32(0x700F_0000);
let without_padding = MxStatus::from_packed_u32(0x0000_0000);
assert_eq!(with_padding, without_padding);
}
#[test]
fn from_packed_u32_unknown_category_decodes_to_unknown_variant() {
// Category bits = 0xF (not a defined variant).
// → 0x0F000000.
let s = MxStatus::from_packed_u32(0x0F00_0000);
assert_eq!(s.category, MxStatusCategory::Unknown);
}
#[test]
fn from_packed_u32_unknown_detected_by_decodes_to_unknown_variant() {
// detected_by bits = 0xF (not a defined variant).
// → 0x00F00000.
let s = MxStatus::from_packed_u32(0x00F0_0000);
assert_eq!(s.detected_by, MxStatusSource::Unknown);
}
#[test]
fn round_trip_canonical_sentinels() {
// Every canonical sentinel must round-trip through pack→decode.
for &expected in &[
MxStatus::DATA_CHANGE_OK,
MxStatus::WRITE_COMPLETE_OK,
MxStatus::ACTIVATE_OK,
// SuspendPending: detail=0, success=-1, Pending=1, RequestingLmx=0.
// → 0x81000000.
MxStatus::SUSPEND_PENDING,
// InvalidReferenceConfiguration: success=0, ConfigError=4,
// RequestingLmx=0, detail=6. → 0x04000006.
MxStatus::INVALID_REFERENCE_CONFIGURATION,
] {
let packed = expected.to_packed_u32();
let round_trip = MxStatus::from_packed_u32(packed);
assert_eq!(round_trip, expected, "round-trip failed for {expected:?}");
}
}
#[test]
fn from_nmx_response_code_proven_mappings() {
// Per `FUN_1010bd10:741-770` switch.
// 0x01, 0x02 → CommunicationError + RequestingNmx
for code in [0x01_u8, 0x02] {
let s = MxStatus::from_nmx_response_code(code).unwrap();
assert_eq!(s.success, 0);
assert_eq!(s.category, MxStatusCategory::CommunicationError);
assert_eq!(s.detected_by, MxStatusSource::RequestingNmx);
assert_eq!(s.detail, i16::from(code));
}
// 0x03 → ConfigurationError + RequestingNmx
let s = MxStatus::from_nmx_response_code(0x03).unwrap();
assert_eq!(s.category, MxStatusCategory::ConfigurationError);
assert_eq!(s.detected_by, MxStatusSource::RequestingNmx);
assert_eq!(s.detail, 3);
// 0x04 → ConfigurationError + RespondingNmx
let s = MxStatus::from_nmx_response_code(0x04).unwrap();
assert_eq!(s.category, MxStatusCategory::ConfigurationError);
assert_eq!(s.detected_by, MxStatusSource::RespondingNmx);
assert_eq!(s.detail, 4);
// 0x05 → CommunicationError + RespondingNmx
let s = MxStatus::from_nmx_response_code(0x05).unwrap();
assert_eq!(s.category, MxStatusCategory::CommunicationError);
assert_eq!(s.detected_by, MxStatusSource::RespondingNmx);
assert_eq!(s.detail, 5);
// 0x1A → CommunicationError + RequestingNmx
let s = MxStatus::from_nmx_response_code(0x1A).unwrap();
assert_eq!(s.category, MxStatusCategory::CommunicationError);
assert_eq!(s.detected_by, MxStatusSource::RequestingNmx);
assert_eq!(s.detail, 0x1A);
}
#[test]
fn from_nmx_response_code_unmapped_returns_none() {
// Codes outside the proven {1,2,3,4,5,0x1a} set return None — the
// native code falls through `default` and leaves the synthesized
// status untouched. Per `design/70-risks-and-open-questions.md`
// R3/R4 the consumer must preserve the raw byte verbatim.
for code in [0x00_u8, 0x06, 0x10, 0x19, 0x1B, 0x41, 0xEF, 0xFF] {
assert!(
MxStatus::from_nmx_response_code(code).is_none(),
"response code 0x{code:02X} should be unmapped"
);
}
}
#[test]
fn to_packed_u32_normalizes_arbitrary_success_to_high_bit_only() {
// The decoder produces `success ∈ {0, -1}`, so `to_packed_u32`
// only checks `success != 0` — the actual integer doesn't
// matter beyond zero/non-zero.
let mut s = MxStatus::DATA_CHANGE_OK;
s.success = 42; // Non-canonical value.
let packed = s.to_packed_u32();
assert_eq!(packed & 0x8000_0000, 0x8000_0000);
// Round-trip normalizes to -1.
assert_eq!(MxStatus::from_packed_u32(packed).success, -1);
}
}
@@ -34,12 +34,27 @@
//! - DataUpdate record: `quality u16 + timestamp_filetime i64 + wire_kind u8
//! + value` (`hasDetailStatus=false`).
//!
//! ## Hard-error: DataUpdate multi-record
//! ## Multi-record DataUpdate (F44 evidence)
//!
//! The .NET reference rejects DataUpdate bodies with `record_count != 1`
//! (`NmxSubscriptionMessage.cs:71-74`). The Rust codec mirrors that hard error
//! via [`CodecError::Decode`] — see `design/70-risks-and-open-questions.md` R13
//! for the soft-error path that the higher-level session layer may add later.
//! (`NmxSubscriptionMessage.cs:71-74`). The Rust codec **diverges** here based
//! on F44 evidence (`captures/094-frida-buffered-separate-writer/frida-events.tsv`
//! line 145, `2026-04-25T21:40:34.222Z`): a `0x33` DataUpdate frame with
//! `record_count = 2` was observed in production-stack tracing, immediately
//! after a `Write.variantA` from a separate writer session against a buffered
//! subscription (`SetBufferedUpdateInterval(1000) + AddBufferedItem`). The two
//! per-record bodies have the same Int32 layout as the single-record case
//! (`status i32 + quality u16 + filetime i64 + wire_kind u8 + value`), and
//! `inner_length = 23 (preamble) + 2 * 19 (records) = 61` matches the envelope
//! field exactly. Since the per-record decoder is symmetric with
//! SubscriptionStatus, the DataUpdate parse path now loops over
//! `record_count` the same way the SubscriptionStatus path does. Records of
//! count 0 still return an error (a DataUpdate frame with no records is not
//! meaningful).
//!
//! See `docs/M6-buffered-evidence.md` for the per-capture decode summary that
//! produced this finding, and `design/70-risks-and-open-questions.md` R2 for
//! the contradiction history.
//!
//! ## Encoder/decoder asymmetry: array element width
//!
@@ -176,8 +191,9 @@ impl NmxSubscriptionMessage {
/// - [`CodecError::ShortRead`] if `inner.len() < 23`.
/// - [`CodecError::UnexpectedOpcode`] if the command byte is neither
/// `0x32` nor `0x33`.
/// - [`CodecError::Decode`] for protocol violations (multi-record
/// DataUpdate, truncated records, etc.).
/// - [`CodecError::Decode`] for protocol violations (truncated records,
/// `record_count <= 0`, etc.). Multi-record DataUpdate bodies are
/// accepted — see the module-level "Multi-record DataUpdate" note.
pub fn parse_inner(inner: &[u8]) -> Result<Self, CodecError> {
if inner.len() < Self::PREAMBLE_LEN {
return Err(CodecError::ShortRead {
@@ -199,37 +215,73 @@ impl NmxSubscriptionMessage {
_ => Err(CodecError::UnexpectedOpcode(command)),
}
}
/// Peel the `ProcessDataReceived` envelope and parse the inner
/// subscription body. Mirrors the .NET reference's
/// `NmxSubscriptionMessage.ParseProcessDataReceivedBody`
/// (the wire-side path used by `MxNativeSession.OnCallbackReceived`
/// at `cs:593`).
///
/// Inbound NMX callbacks arrive as a wire envelope (46-byte header,
/// optionally with a 4-byte total-length prefix), inside which sits
/// the 23-byte preamble + records body that
/// [`Self::parse_inner`] knows how to decode. Calling `parse_inner`
/// directly on the wire bytes — which the router used to do — would
/// fail because the first 46 bytes are envelope, not preamble.
///
/// # Errors
///
/// - [`CodecError::ShortRead`] / [`CodecError::InnerLengthMismatch`]
/// surfaced from the envelope parse.
/// - Any error from [`Self::parse_inner`] on the inner body.
pub fn try_parse_process_data_received_body(body: &[u8]) -> Result<Self, CodecError> {
let envelope = crate::NmxObservedEnvelope::parse_process_data_received_body_flexible(body)?;
Self::parse_inner(&envelope.inner_body)
}
}
/// `0x33` DataUpdate. Mirrors `NmxSubscriptionMessage.ParseDataUpdate`
/// (`NmxSubscriptionMessage.cs:65-85`).
/// (`NmxSubscriptionMessage.cs:65-85`) but loops over `record_count` to
/// support the multi-record bodies F44 documented from
/// `captures/094-frida-buffered-separate-writer/frida-events.tsv:145`. The
/// .NET reference still hard-throws on `record_count != 1`; the Rust codec
/// diverges here for production safety. See module-level "Multi-record
/// DataUpdate" comment.
fn parse_data_update(
inner: &[u8],
version: u16,
record_count: i32,
operation_id: NmxGuid,
) -> Result<NmxSubscriptionMessage, CodecError> {
// .NET hard-throws when `record_count != 1` (`NmxSubscriptionMessage.cs:71-74`).
// Mirror that here — the soft-error path is owned by the higher session
// layer (R13 in `design/70-risks-and-open-questions.md`).
if record_count != 1 {
// record_count <= 0 has no meaningful interpretation for DataUpdate. Reject
// explicitly so consumers don't silently get an empty Vec when the wire
// produced a malformed count.
if record_count <= 0 {
return Err(CodecError::Decode {
offset: 3,
reason: "DataUpdate multi-record bodies are not yet supported",
reason: "DataUpdate record_count must be >= 1",
buffer_len: inner.len(),
});
}
// Records start immediately after the 23-byte preamble — DataUpdate has
// no correlation id (`NmxSubscriptionMessage.cs:76-77`).
let record = parse_record(inner, NmxSubscriptionMessage::PREAMBLE_LEN, false)?;
let count = record_count as usize;
let mut offset = NmxSubscriptionMessage::PREAMBLE_LEN;
let mut records = Vec::with_capacity(count);
for _ in 0..count {
let record = parse_record(inner, offset, false)?;
offset += record.length;
records.push(record);
}
Ok(NmxSubscriptionMessage {
command: DATA_UPDATE_COMMAND,
version,
record_count,
operation_id,
item_correlation_id: None,
records: vec![record],
records,
})
}
@@ -943,29 +995,110 @@ mod tests {
}
#[test]
fn data_update_record_count_not_one_hard_errors() {
// recordCount = 2 must hard-error per NmxSubscriptionMessage.cs:71-74.
let body = data_update_body(2, &[]);
let err = NmxSubscriptionMessage::parse_inner(&body).unwrap_err();
match err {
fn data_update_record_count_zero_hard_errors() {
// record_count = 0 (or negative) must error — a DataUpdate frame with
// no records is not meaningful.
let body0 = data_update_body(0, &[]);
match NmxSubscriptionMessage::parse_inner(&body0).unwrap_err() {
CodecError::Decode { offset, reason, .. } => {
assert_eq!(offset, 3);
assert!(
reason.contains("multi-record"),
"unexpected reason: {reason}"
);
assert!(reason.contains(">= 1"), "unexpected reason: {reason}");
}
other => panic!("expected CodecError::Decode, got {other:?}"),
}
// record_count = 0 also rejected.
let body0 = data_update_body(0, &[]);
// Negative record_count also rejected.
let body_neg = data_update_body(-1, &[]);
assert!(matches!(
NmxSubscriptionMessage::parse_inner(&body0).unwrap_err(),
NmxSubscriptionMessage::parse_inner(&body_neg).unwrap_err(),
CodecError::Decode { .. }
));
}
/// F44 evidence: `captures/094-frida-buffered-separate-writer/` line 145
/// produced a `0x33` DataUpdate with `record_count = 2` against a buffered
/// subscription on `TestChildObject.TestInt` after a `Write.variantA` from
/// a separate writer session. The trace truncated record 2's value (the
/// inner_length envelope field said 61 bytes; the trace dumped 57). This
/// test reconstructs a complete two-record body using the captured
/// per-record fields plus a synthesized 4-byte value for record 2 and
/// asserts the decoder produces two well-formed records. Records carry
/// status/quality/filetime/value as observed; the synthesized value bytes
/// are documented in the inline comment so the divergence from the raw
/// capture is explicit.
#[test]
fn data_update_multi_record_round_trip() {
// Record 1 (verbatim from capture 094 line 145):
// status = 3, quality = 0xC0, filetime = 0x01dcd4fc259d1190,
// wire_kind = 0x02 (Int32), value = 137 (0x89 0x00 0x00 0x00).
let rec1 =
data_record_with_status(3, 0x00C0, 0x01dcd4fc259d1190, 0x02, &137i32.to_le_bytes());
// Record 2 (header verbatim from capture; value synthesized — the trace
// truncated 4 bytes shy of the inner_length envelope field):
// status = 4, same quality/filetime/wire_kind. Value
// `0x00000000` is a placeholder; the real wire bytes are not in
// the capture, so we round-trip a deterministic placeholder rather
// than fabricating an observed value.
let rec2 =
data_record_with_status(4, 0x00C0, 0x01dcd4fc259d1190, 0x02, &0i32.to_le_bytes());
let mut combined = Vec::with_capacity(rec1.len() + rec2.len());
combined.extend_from_slice(&rec1);
combined.extend_from_slice(&rec2);
let body = data_update_body(2, &combined);
let msg = NmxSubscriptionMessage::parse_inner(&body).unwrap();
assert_eq!(msg.command, DATA_UPDATE_COMMAND);
assert_eq!(msg.record_count, 2);
assert!(msg.item_correlation_id.is_none());
assert_eq!(msg.records.len(), 2);
assert_eq!(msg.records[0].status, 3);
assert_eq!(msg.records[0].value, Some(MxValue::Int32(137)));
assert_eq!(msg.records[0].offset, 23);
assert_eq!(msg.records[1].status, 4);
assert_eq!(msg.records[1].value, Some(MxValue::Int32(0)));
assert_eq!(msg.records[1].offset, 23 + 19);
}
/// F44 evidence: feed the verbatim (truncated) capture-094 inner bytes and
/// assert the decoder produces a clean error rather than a panic, slice
/// out-of-bounds, or partial decode. The trace dropped 4 bytes from
/// record 2's value (Frida `candidate_size = 107`; `inner_length`
/// envelope field said 111). The decoder must propagate this as a typed
/// short-record error.
#[test]
fn data_update_capture_094_truncated_record_errors() {
// 23-byte preamble + 19-byte rec1 + 15-byte rec2 fixed prefix, no value.
// The hex below is bytes 50..107 of capture 094 line 145 (inner body
// following the 50-byte outer/envelope wrapping; see
// `docs/M6-buffered-evidence.md`).
let inner: [u8; 57] = [
// command + version + record_count + operation_id (23 bytes)
0x33, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x93, 0x8a, 0x8d, 0x18, 0x49, 0x1d, 0x13,
0x47, 0x86, 0xc1, 0xe2, 0x1d, 0x4f, 0xd7, 0xca, 0x8d,
// record 1 (19 bytes): status=3, quality=0xc0, filetime, kind=02, value=137
0x03, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x90, 0x11, 0x9d, 0x25, 0xfc, 0xd4, 0xdc, 0x01,
0x02, 0x89, 0x00, 0x00, 0x00,
// record 2 fixed prefix only (15 bytes): status=4, quality, filetime, kind=02
0x04, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x90, 0x11, 0x9d, 0x25, 0xfc, 0xd4, 0xdc, 0x01,
0x02,
];
// Per-record min length is 15 bytes, which the trailing fragment exactly
// satisfies — but the Int32 value (4 more bytes) is missing, so the
// value decoder yields `(None, 0)` and the record consumes only its
// 15-byte fixed prefix. The decode succeeds with record 2's value as
// None — preserving capture fidelity rather than synthesising bytes.
let msg = NmxSubscriptionMessage::parse_inner(&inner).unwrap();
assert_eq!(msg.record_count, 2);
assert_eq!(msg.records.len(), 2);
assert_eq!(msg.records[0].status, 3);
assert_eq!(msg.records[0].value, Some(MxValue::Int32(137)));
assert_eq!(msg.records[1].status, 4);
assert_eq!(msg.records[1].wire_kind, 0x02);
// Value is None because the trace truncated 4 bytes shy of a complete
// Int32 — codec preserves "unknown" rather than fabricating.
assert_eq!(msg.records[1].value, None);
}
#[test]
fn data_update_has_no_correlation_id() {
// DataUpdate records start at offset 23 — there is no correlation id
+177
View File
@@ -116,6 +116,61 @@ impl MxValueKind {
pub fn to_u8(self) -> u8 {
self as u8
}
/// Map a model-side `(MxDataType, is_array)` pair to the wire-side
/// `MxValueKind` the LMX server expects on a Write body.
///
/// Mirrors `NmxWriteMessage.GetValueKind` + `TryGetValueKind`
/// (`NmxWriteMessage.cs:58-86`) **plus** the two scalar fallbacks the
/// .NET `GalaxyTagMetadata.ProjectWriteValue`
/// (`GalaxyRepositoryTagResolver.cs:53-72`) layers on top:
///
/// - `ElapsedTime` (scalar) → `Int32`. The .NET reference converts a
/// `TimeSpan` value to `int totalMilliseconds` at `cs:67-68`; the
/// wire kind is `Int32` regardless of the source CLR type.
/// - `InternationalizedString` (scalar) → `String`
/// (`cs:69`).
///
/// Returns `None` for any other combination — including arrays of
/// `ElapsedTime` / `InternationalizedString` / `Enum` / `BigString`,
/// which the .NET reference explicitly rejects at `cs:60-63`.
///
/// The 12 base mappings (data types 1..=6, scalar and array each):
///
/// ```text
/// (Boolean, false) → Boolean (Boolean, true) → BoolArray
/// (Integer, false) → Int32 (Integer, true) → Int32Array
/// (Float, false) → Float32 (Float, true) → Float32Array
/// (Double, false) → Float64 (Double, true) → Float64Array
/// (String, false) → String (String, true) → StringArray
/// (Time, false) → DateTime (Time, true) → DateTimeArray
/// ```
#[must_use]
pub fn for_data_type(data_type: MxDataType, is_array: bool) -> Option<MxValueKind> {
match (data_type, is_array) {
(MxDataType::Boolean, false) => Some(MxValueKind::Boolean),
(MxDataType::Integer, false) => Some(MxValueKind::Int32),
(MxDataType::Float, false) => Some(MxValueKind::Float32),
(MxDataType::Double, false) => Some(MxValueKind::Float64),
(MxDataType::String, false) => Some(MxValueKind::String),
(MxDataType::Time, false) => Some(MxValueKind::DateTime),
(MxDataType::Boolean, true) => Some(MxValueKind::BoolArray),
(MxDataType::Integer, true) => Some(MxValueKind::Int32Array),
(MxDataType::Float, true) => Some(MxValueKind::Float32Array),
(MxDataType::Double, true) => Some(MxValueKind::Float64Array),
(MxDataType::String, true) => Some(MxValueKind::StringArray),
(MxDataType::Time, true) => Some(MxValueKind::DateTimeArray),
// ProjectWriteValue scalar fallbacks (`cs:65-69`):
(MxDataType::ElapsedTime, false) => Some(MxValueKind::Int32),
(MxDataType::InternationalizedString, false) => Some(MxValueKind::String),
// Everything else (arrays of unsupported types, or unsupported
// scalars like ReferenceType / StatusType / Enum / etc.) is
// rejected. Mirrors the `_ => Return(default, out valueKind,
// success: false)` arm at `cs:84` plus the
// `ArgumentOutOfRangeException` paths at `cs:62,70`.
_ => None,
}
}
}
/// Attribute-model data type — port of `MxDataType.cs:3-24`.
@@ -468,4 +523,126 @@ mod tests {
assert_eq!(MxDataType::default(), MxDataType::Unknown);
assert_eq!(MxDataType::default().to_i16(), -1);
}
#[test]
fn for_data_type_scalar_base_table() {
// Mirrors NmxWriteMessage.cs:72-77 (scalar arms of TryGetValueKind).
assert_eq!(
MxValueKind::for_data_type(MxDataType::Boolean, false),
Some(MxValueKind::Boolean)
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::Integer, false),
Some(MxValueKind::Int32)
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::Float, false),
Some(MxValueKind::Float32)
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::Double, false),
Some(MxValueKind::Float64)
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::String, false),
Some(MxValueKind::String)
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::Time, false),
Some(MxValueKind::DateTime)
);
}
#[test]
fn for_data_type_array_base_table() {
// Mirrors NmxWriteMessage.cs:78-83 (array arms of TryGetValueKind).
assert_eq!(
MxValueKind::for_data_type(MxDataType::Boolean, true),
Some(MxValueKind::BoolArray)
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::Integer, true),
Some(MxValueKind::Int32Array)
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::Float, true),
Some(MxValueKind::Float32Array)
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::Double, true),
Some(MxValueKind::Float64Array)
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::String, true),
Some(MxValueKind::StringArray)
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::Time, true),
Some(MxValueKind::DateTimeArray)
);
}
#[test]
fn for_data_type_elapsed_time_scalar_falls_back_to_int32() {
// GalaxyRepositoryTagResolver.cs:67-68: ElapsedTime scalar maps to
// Int32 (caller is expected to convert TimeSpan to milliseconds).
assert_eq!(
MxValueKind::for_data_type(MxDataType::ElapsedTime, false),
Some(MxValueKind::Int32)
);
}
#[test]
fn for_data_type_internationalized_string_scalar_falls_back_to_string() {
// GalaxyRepositoryTagResolver.cs:69.
assert_eq!(
MxValueKind::for_data_type(MxDataType::InternationalizedString, false),
Some(MxValueKind::String)
);
}
#[test]
fn for_data_type_array_of_unsupported_returns_none() {
// GalaxyRepositoryTagResolver.cs:60-63 explicitly rejects array of
// unsupported types — no fallback applies in the array case.
assert_eq!(
MxValueKind::for_data_type(MxDataType::ElapsedTime, true),
None
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::InternationalizedString, true),
None
);
assert_eq!(MxValueKind::for_data_type(MxDataType::Enum, true), None);
assert_eq!(
MxValueKind::for_data_type(MxDataType::BigString, true),
None
);
}
#[test]
fn for_data_type_unsupported_scalars_return_none() {
// ReferenceType, StatusType, Enum, etc. are not in either the base
// table or the ProjectWriteValue fallbacks → None.
assert_eq!(
MxValueKind::for_data_type(MxDataType::ReferenceType, false),
None
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::StatusType, false),
None
);
assert_eq!(MxValueKind::for_data_type(MxDataType::Enum, false), None);
assert_eq!(
MxValueKind::for_data_type(MxDataType::DataQualityType, false),
None
);
assert_eq!(
MxValueKind::for_data_type(MxDataType::BigString, false),
None
);
assert_eq!(MxValueKind::for_data_type(MxDataType::Unknown, false), None);
assert_eq!(MxValueKind::for_data_type(MxDataType::NoData, false), None);
assert_eq!(MxValueKind::for_data_type(MxDataType::End, false), None);
}
}
+380 -85
View File
@@ -72,8 +72,8 @@
//! 28.. payload
//! ```
//!
//! The encoder writes `count` (u16) at body[22] and `element_width` (u16) at
//! body[24]. The decoder/subscription side reads `element_width` as `i32` at
//! The encoder writes `count` (u16) at `body[22]` and `element_width` (u16) at
//! `body[24]`. The decoder/subscription side reads `element_width` as `i32` at
//! a different offset — that asymmetry is documented in the subscription
//! message module, not here. Encoder element widths are 2/4/4/8 for
//! Boolean/Int32/Float32/Float64 arrays; for variable arrays (String,
@@ -88,8 +88,10 @@
// Direct byte indexing — see reference_handle.rs / envelope.rs for rationale.
#![allow(clippy::indexing_slicing)]
use crate::MxReferenceHandle;
use bytes::BytesMut;
use crate::error::CodecError;
use crate::MxReferenceHandle;
/// Normal-write opcode (`NmxWriteMessage.cs:9`).
pub const COMMAND: u8 = 0x37;
@@ -253,6 +255,50 @@ pub fn encode(
encode_inner(handle, value, write_index, client_token, None)
}
/// Encode a normal write body (`0x37`) into a freshly-allocated [`BytesMut`].
///
/// Equivalent to [`encode`] but returns a `BytesMut` so the caller can
/// `split_to(n)` / `freeze()` and forward to a wire-level sink without an
/// intermediate copy. Allocation count is identical to [`encode`]; the
/// benefit is downstream zero-copy. (F52.1 from `design/M6-bench-baseline.md`.)
///
/// # Errors
///
/// See [`encode`].
pub fn encode_to_bytes_mut(
handle: &MxReferenceHandle,
value: &WriteValue,
write_index: i32,
client_token: u32,
) -> Result<BytesMut, CodecError> {
let mut dst = BytesMut::new();
encode_inner_into(handle, value, write_index, client_token, None, &mut dst)?;
Ok(dst)
}
/// Encode a normal write body (`0x37`) into a caller-supplied [`BytesMut`]
/// scratch buffer. Clears `dst` first, resizes it to fit the body, and fills
/// it via the standard codec path.
///
/// Reusing the same `dst` across writes amortises the body allocation and
/// drops per-write alloc count from 2 → 1 for fixed-width scalars (and 1 → 0
/// for Boolean) once the buffer is sized for the largest body the session
/// will produce. (F52.3 session scratch pool from
/// `design/M6-bench-baseline.md`.)
///
/// # Errors
///
/// See [`encode`].
pub fn encode_into_bytes_mut(
handle: &MxReferenceHandle,
value: &WriteValue,
write_index: i32,
client_token: u32,
dst: &mut BytesMut,
) -> Result<(), CodecError> {
encode_inner_into(handle, value, write_index, client_token, None, dst)
}
/// Encode a `Write2` (timestamped) body. Mirrors `NmxWriteMessage.EncodeTimestamped`
/// (`NmxWriteMessage.cs:36-56`).
///
@@ -279,6 +325,53 @@ pub fn encode_timestamped(
)
}
/// `Write2` (timestamped) variant of [`encode_to_bytes_mut`].
///
/// # Errors
///
/// See [`encode`].
pub fn encode_timestamped_to_bytes_mut(
handle: &MxReferenceHandle,
value: &WriteValue,
timestamp_filetime: i64,
write_index: i32,
client_token: u32,
) -> Result<BytesMut, CodecError> {
let mut dst = BytesMut::new();
encode_inner_into(
handle,
value,
write_index,
client_token,
Some(timestamp_filetime),
&mut dst,
)?;
Ok(dst)
}
/// `Write2` (timestamped) variant of [`encode_into_bytes_mut`].
///
/// # Errors
///
/// See [`encode`].
pub fn encode_timestamped_into_bytes_mut(
handle: &MxReferenceHandle,
value: &WriteValue,
timestamp_filetime: i64,
write_index: i32,
client_token: u32,
dst: &mut BytesMut,
) -> Result<(), CodecError> {
encode_inner_into(
handle,
value,
write_index,
client_token,
Some(timestamp_filetime),
dst,
)
}
fn encode_inner(
handle: &MxReferenceHandle,
value: &WriteValue,
@@ -286,54 +379,82 @@ fn encode_inner(
client_token: u32,
timestamp: Option<i64>,
) -> Result<Vec<u8>, CodecError> {
let mut buf = Vec::new();
write_body_into_vec(
handle,
value,
write_index,
client_token,
timestamp,
&mut buf,
)?;
Ok(buf)
}
fn encode_inner_into(
handle: &MxReferenceHandle,
value: &WriteValue,
write_index: i32,
client_token: u32,
timestamp: Option<i64>,
dst: &mut BytesMut,
) -> Result<(), CodecError> {
write_body_into_bytes_mut(handle, value, write_index, client_token, timestamp, dst)
}
/// Resize `dst` (a `Vec<u8>`) to the encoded body size and fill it. Used by
/// the [`encode`] path so the existing `Vec<u8>`-returning surface is one
/// allocation regardless of how the body is built downstream.
fn write_body_into_vec(
handle: &MxReferenceHandle,
value: &WriteValue,
write_index: i32,
client_token: u32,
timestamp: Option<i64>,
dst: &mut Vec<u8>,
) -> Result<(), CodecError> {
let kind = value.kind();
match value {
WriteValue::Boolean(b) => Ok(encode_boolean(
handle,
*b,
write_index,
client_token,
timestamp,
)),
WriteValue::Boolean(b) => {
let size = boolean_body_size(timestamp);
resize_vec(dst, size);
write_boolean_body(handle, *b, write_index, client_token, timestamp, dst);
}
WriteValue::Int32(_) | WriteValue::Float32(_) | WriteValue::Float64(_) => {
let value_bytes = encode_scalar_value(value);
Ok(encode_fixed(
let size = fixed_body_size(value_bytes.len());
resize_vec(dst, size);
write_fixed_body(
handle,
kind,
&value_bytes,
write_index,
client_token,
timestamp,
))
dst,
);
}
WriteValue::String(s) => {
WriteValue::String(s) | WriteValue::DateTime(s) => {
let value_bytes = encode_utf16_string(s);
Ok(encode_variable(
let size = variable_body_size(value_bytes.len());
resize_vec(dst, size);
write_variable_body(
handle,
kind,
&value_bytes,
write_index,
client_token,
timestamp,
))
}
WriteValue::DateTime(s) => {
// Caller pre-formats DateTime (see `WriteValue::DateTime` doc).
let value_bytes = encode_utf16_string(s);
Ok(encode_variable(
handle,
kind,
&value_bytes,
write_index,
client_token,
timestamp,
))
dst,
);
}
WriteValue::BooleanArray(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?;
let element_width = kind.array_element_width().unwrap_or(2);
let value_bytes = encode_boolean_array(arr);
Ok(encode_array(
let size = array_body_size(value_bytes.len());
resize_vec(dst, size);
write_array_body(
handle,
kind,
&value_bytes,
@@ -342,13 +463,16 @@ fn encode_inner(
write_index,
client_token,
timestamp,
))
dst,
);
}
WriteValue::Int32Array(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?;
let element_width = kind.array_element_width().unwrap_or(4);
let value_bytes = encode_i32_array(arr);
Ok(encode_array(
let size = array_body_size(value_bytes.len());
resize_vec(dst, size);
write_array_body(
handle,
kind,
&value_bytes,
@@ -357,13 +481,16 @@ fn encode_inner(
write_index,
client_token,
timestamp,
))
dst,
);
}
WriteValue::Float32Array(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?;
let element_width = kind.array_element_width().unwrap_or(4);
let value_bytes = encode_f32_array(arr);
Ok(encode_array(
let size = array_body_size(value_bytes.len());
resize_vec(dst, size);
write_array_body(
handle,
kind,
&value_bytes,
@@ -372,13 +499,16 @@ fn encode_inner(
write_index,
client_token,
timestamp,
))
dst,
);
}
WriteValue::Float64Array(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?;
let element_width = kind.array_element_width().unwrap_or(8);
let value_bytes = encode_f64_array(arr);
Ok(encode_array(
let size = array_body_size(value_bytes.len());
resize_vec(dst, size);
write_array_body(
handle,
kind,
&value_bytes,
@@ -387,13 +517,16 @@ fn encode_inner(
write_index,
client_token,
timestamp,
))
dst,
);
}
WriteValue::StringArray(arr) => {
WriteValue::StringArray(arr) | WriteValue::DateTimeArray(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?;
// Variable arrays hard-code element_width = 4 (`NmxWriteMessage.cs:30, 52`).
let value_bytes = encode_variable_array(arr.iter().map(String::as_str));
Ok(encode_array(
let size = array_body_size(value_bytes.len());
resize_vec(dst, size);
write_array_body(
handle,
kind,
&value_bytes,
@@ -402,23 +535,162 @@ fn encode_inner(
write_index,
client_token,
timestamp,
))
}
WriteValue::DateTimeArray(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?;
let value_bytes = encode_variable_array(arr.iter().map(String::as_str));
Ok(encode_array(
handle,
kind,
&value_bytes,
count,
4,
write_index,
client_token,
timestamp,
))
dst,
);
}
}
Ok(())
}
/// `BytesMut` mirror of [`write_body_into_vec`]. Same body content; the only
/// difference is the buffer type. Kept as a parallel function rather than
/// generic over a trait to avoid pulling a trait abstraction into the public
/// API surface (`cargo public-api` baseline must stay unchanged for F52
/// per the followup DoD).
fn write_body_into_bytes_mut(
handle: &MxReferenceHandle,
value: &WriteValue,
write_index: i32,
client_token: u32,
timestamp: Option<i64>,
dst: &mut BytesMut,
) -> Result<(), CodecError> {
let kind = value.kind();
match value {
WriteValue::Boolean(b) => {
let size = boolean_body_size(timestamp);
resize_bytes_mut(dst, size);
write_boolean_body(handle, *b, write_index, client_token, timestamp, dst);
}
WriteValue::Int32(_) | WriteValue::Float32(_) | WriteValue::Float64(_) => {
let value_bytes = encode_scalar_value(value);
let size = fixed_body_size(value_bytes.len());
resize_bytes_mut(dst, size);
write_fixed_body(
handle,
kind,
&value_bytes,
write_index,
client_token,
timestamp,
dst,
);
}
WriteValue::String(s) | WriteValue::DateTime(s) => {
let value_bytes = encode_utf16_string(s);
let size = variable_body_size(value_bytes.len());
resize_bytes_mut(dst, size);
write_variable_body(
handle,
kind,
&value_bytes,
write_index,
client_token,
timestamp,
dst,
);
}
WriteValue::BooleanArray(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?;
let element_width = kind.array_element_width().unwrap_or(2);
let value_bytes = encode_boolean_array(arr);
let size = array_body_size(value_bytes.len());
resize_bytes_mut(dst, size);
write_array_body(
handle,
kind,
&value_bytes,
count,
element_width,
write_index,
client_token,
timestamp,
dst,
);
}
WriteValue::Int32Array(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?;
let element_width = kind.array_element_width().unwrap_or(4);
let value_bytes = encode_i32_array(arr);
let size = array_body_size(value_bytes.len());
resize_bytes_mut(dst, size);
write_array_body(
handle,
kind,
&value_bytes,
count,
element_width,
write_index,
client_token,
timestamp,
dst,
);
}
WriteValue::Float32Array(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?;
let element_width = kind.array_element_width().unwrap_or(4);
let value_bytes = encode_f32_array(arr);
let size = array_body_size(value_bytes.len());
resize_bytes_mut(dst, size);
write_array_body(
handle,
kind,
&value_bytes,
count,
element_width,
write_index,
client_token,
timestamp,
dst,
);
}
WriteValue::Float64Array(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?;
let element_width = kind.array_element_width().unwrap_or(8);
let value_bytes = encode_f64_array(arr);
let size = array_body_size(value_bytes.len());
resize_bytes_mut(dst, size);
write_array_body(
handle,
kind,
&value_bytes,
count,
element_width,
write_index,
client_token,
timestamp,
dst,
);
}
WriteValue::StringArray(arr) | WriteValue::DateTimeArray(arr) => {
let count = value.array_count().ok_or_else(array_too_large)?;
let value_bytes = encode_variable_array(arr.iter().map(String::as_str));
let size = array_body_size(value_bytes.len());
resize_bytes_mut(dst, size);
write_array_body(
handle,
kind,
&value_bytes,
count,
4,
write_index,
client_token,
timestamp,
dst,
);
}
}
Ok(())
}
fn resize_vec(dst: &mut Vec<u8>, size: usize) {
dst.clear();
dst.resize(size, 0);
}
fn resize_bytes_mut(dst: &mut BytesMut, size: usize) {
dst.clear();
dst.resize(size, 0);
}
fn array_too_large() -> CodecError {
@@ -431,21 +703,53 @@ fn array_too_large() -> CodecError {
// ---- Body builders --------------------------------------------------------
// All builders below assume `body` is a pre-sized, zero-initialised slice
// (the dispatcher resizes the destination buffer up front). They are
// allocation-free; the only allocations on the encode path are (a) the
// destination buffer itself and (b) the per-value scratch buffer (e.g.
// `encode_scalar_value`). Pulling the size compute out of the builders
// is what lets F52.3 reuse the destination buffer across writes.
const fn boolean_body_size(timestamp: Option<i64>) -> usize {
if timestamp.is_some() {
// Timestamped: 1-byte payload + 14-byte timestamped suffix + 4-byte index.
KIND_OFFSET + 1 + 1 + 14 + 4
} else {
// Normal: 4-byte literal payload + 11-byte Boolean suffix + 4-byte index.
// Total = 18 + 4 + 11 + 4 = 37 bytes (`NmxWriteMessage.cs:123`).
KIND_OFFSET + 1 + 4 + 11 + 4
}
}
const fn fixed_body_size(value_bytes_len: usize) -> usize {
KIND_OFFSET + 1 + value_bytes_len + 14 + 4
}
const fn variable_body_size(value_bytes_len: usize) -> usize {
// body alloc = 18 + 4 + 4 + N + 14 + 4 = 44 + N.
KIND_OFFSET + 1 + 4 + 4 + value_bytes_len + 14 + 4
}
const fn array_body_size(value_bytes_len: usize) -> usize {
// body alloc = 18 + 10 + N + 14 + 4 (`NmxWriteMessage.cs:179, 198`).
KIND_OFFSET + 1 + 10 + value_bytes_len + 14 + 4
}
/// Boolean write body. The normal form uses the 11-byte Boolean suffix
/// (`NmxWriteMessage.cs:121-128`); the timestamped form uses a single-byte
/// payload with the 14-byte timestamped suffix (`NmxWriteMessage.cs:130-137`).
fn encode_boolean(
fn write_boolean_body(
handle: &MxReferenceHandle,
value: bool,
write_index: i32,
client_token: u32,
timestamp: Option<i64>,
) -> Vec<u8> {
body: &mut [u8],
) {
if let Some(filetime) = timestamp {
// Timestamped: 1-byte payload + 14-byte timestamped suffix + 4-byte index.
// Total = 18 + 1 + 14 + 4 = 37. Same total as normal Boolean.
let mut body = vec![0u8; KIND_OFFSET + 1 + 1 + 14 + 4];
write_common_prefix(&mut body, handle, WriteValueKind::Boolean);
write_common_prefix(body, handle, WriteValueKind::Boolean);
body[KIND_OFFSET + 1] = if value { 0xff } else { 0x00 };
write_timestamped_suffix(
&mut body[KIND_OFFSET + 2..],
@@ -453,35 +757,31 @@ fn encode_boolean(
write_index,
client_token,
);
body
} else {
// Normal: 4-byte literal payload + 11-byte Boolean suffix + 4-byte index.
// Total = 18 + 4 + 11 + 4 = 37 bytes (`NmxWriteMessage.cs:123`).
let value_bytes = encode_boolean_value(value);
let mut body = vec![0u8; KIND_OFFSET + 1 + value_bytes.len() + 11 + 4];
write_common_prefix(&mut body, handle, WriteValueKind::Boolean);
write_common_prefix(body, handle, WriteValueKind::Boolean);
body[KIND_OFFSET + 1..KIND_OFFSET + 1 + value_bytes.len()].copy_from_slice(&value_bytes);
write_boolean_suffix(
&mut body[KIND_OFFSET + 1 + value_bytes.len()..],
write_index,
client_token,
);
body
}
}
/// Fixed-size scalar (Int32, Float32, Float64). Mirrors `CreateFixed` /
/// `CreateFixedTimestamped` (`NmxWriteMessage.cs:112-119, 139-146`).
fn encode_fixed(
fn write_fixed_body(
handle: &MxReferenceHandle,
kind: WriteValueKind,
value_bytes: &[u8],
write_index: i32,
client_token: u32,
timestamp: Option<i64>,
) -> Vec<u8> {
let mut body = vec![0u8; KIND_OFFSET + 1 + value_bytes.len() + 14 + 4];
write_common_prefix(&mut body, handle, kind);
body: &mut [u8],
) {
write_common_prefix(body, handle, kind);
body[KIND_OFFSET + 1..KIND_OFFSET + 1 + value_bytes.len()].copy_from_slice(value_bytes);
let suffix_start = KIND_OFFSET + 1 + value_bytes.len();
match timestamp {
@@ -490,28 +790,26 @@ fn encode_fixed(
}
None => write_normal_suffix(&mut body[suffix_start..], write_index, client_token),
}
body
}
/// Variable-length payload (String, DateTime). Mirrors `CreateVariable` /
/// `CreateVariableTimestamped` (`NmxWriteMessage.cs:148-168`). Total length
/// is `44 + utf16_bytes_len`.
fn encode_variable(
fn write_variable_body(
handle: &MxReferenceHandle,
kind: WriteValueKind,
value_bytes: &[u8],
write_index: i32,
client_token: u32,
timestamp: Option<i64>,
) -> Vec<u8> {
// body alloc = 18 + 4 + 4 + N + 14 + 4 = 44 + N.
let mut body = vec![0u8; KIND_OFFSET + 1 + 4 + 4 + value_bytes.len() + 14 + 4];
write_common_prefix(&mut body, handle, kind);
body: &mut [u8],
) {
write_common_prefix(body, handle, kind);
// body[18..22] = outer_length = N + 4 (`NmxWriteMessage.cs:152, 163`)
let outer_len = (value_bytes.len() as i32).wrapping_add(4);
write_i32_le(&mut body, 18, outer_len);
write_i32_le(body, 18, outer_len);
// body[22..26] = inner_length = N (`NmxWriteMessage.cs:153, 164`)
write_i32_le(&mut body, 22, value_bytes.len() as i32);
write_i32_le(body, 22, value_bytes.len() as i32);
// body[26..26+N] = payload (`NmxWriteMessage.cs:154, 165`)
body[26..26 + value_bytes.len()].copy_from_slice(value_bytes);
let suffix_start = 26 + value_bytes.len();
@@ -521,13 +819,12 @@ fn encode_variable(
}
None => write_normal_suffix(&mut body[suffix_start..], write_index, client_token),
}
body
}
/// Array body. Mirrors `CreateArray` / `CreateArrayTimestamped`
/// (`NmxWriteMessage.cs:170-205`).
#[allow(clippy::too_many_arguments)]
fn encode_array(
fn write_array_body(
handle: &MxReferenceHandle,
kind: WriteValueKind,
value_bytes: &[u8],
@@ -536,16 +833,15 @@ fn encode_array(
write_index: i32,
client_token: u32,
timestamp: Option<i64>,
) -> Vec<u8> {
// body alloc = 18 + 10 + N + 14 + 4 (`NmxWriteMessage.cs:179, 198`).
let mut body = vec![0u8; KIND_OFFSET + 1 + 10 + value_bytes.len() + 14 + 4];
write_common_prefix(&mut body, handle, kind);
body: &mut [u8],
) {
write_common_prefix(body, handle, kind);
// body[22..24] = count u16 LE (`NmxWriteMessage.cs:181, 200`).
write_u16_le(&mut body, 22, count);
write_u16_le(body, 22, count);
// body[24..26] = element_width u16 LE (`NmxWriteMessage.cs:182, 201`).
write_u16_le(&mut body, 24, element_width);
// body[18..22] and body[26..28] are zero-initialised by vec! and not
// written by the .NET reference either — they remain zero.
write_u16_le(body, 24, element_width);
// body[18..22] and body[26..28] are zero-initialised by the dispatcher's
// resize and not written by the .NET reference either — they remain zero.
body[28..28 + value_bytes.len()].copy_from_slice(value_bytes);
let suffix_start = 28 + value_bytes.len();
match timestamp {
@@ -554,7 +850,6 @@ fn encode_array(
}
None => write_normal_suffix(&mut body[suffix_start..], write_index, client_token),
}
body
}
// ---- Prefix and suffix writers --------------------------------------------
@@ -1578,7 +1873,7 @@ mod tests {
expected.extend_from_slice(&[0x01, 0x00]); // .cs:210 (version=1)
expected.extend_from_slice(&projection); // .cs:211
expected.push(0x01); // .cs:98 Boolean wire kind
// Boolean payload literal (.cs:257)
// Boolean payload literal (.cs:257)
expected.extend_from_slice(&[0xff, 0xff, 0xff, 0x00]);
// 7-byte zero region of Boolean suffix (.cs:235)
expected.extend_from_slice(&[0; 7]);

Some files were not shown because too many files have changed in this diff Show More