Compare commits
140 Commits
16f2c148e5
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f07da2e12 | |||
| 047125bc11 | |||
| d668d5b7b1 | |||
| 9ed4700eb4 | |||
| 8b50c0fd43 | |||
| cc99a2d9f0 | |||
| ddebab2c2d | |||
| 73e2bd8771 | |||
| ceeaeefa71 | |||
| a0fa5bedfd | |||
| 4e76b44391 | |||
| c7505f9570 | |||
| 8bd66bbe65 | |||
| 349e217ea3 | |||
| b62ffc8c5d | |||
| e77db4306a | |||
| c606736ec3 | |||
| d149143535 | |||
| 5e11b30507 | |||
| c6332c26a1 | |||
| df3457c54a | |||
| af15fe7587 | |||
| 2fc327a8d5 | |||
| 0a274af76f | |||
| c5d611d6fa | |||
| e5b31fadb1 | |||
| 04c10babfb | |||
| 4ff511bbed | |||
| f98ab9846d | |||
| c73a33edd8 | |||
| 460c61df43 | |||
| 4dfc0cee65 | |||
| 0e93e3a8fa | |||
| 25befcb72e | |||
| 1a1830f3bf | |||
| 9b57cf8f3b | |||
| 2281309a86 | |||
| 808fea18a0 | |||
| c7e71e4424 | |||
| 7b15c853d1 | |||
| f0c9dd2214 | |||
| 9e57bfd451 | |||
| 2120dfa965 | |||
| ad1cf2351c | |||
| d5aa152b1f | |||
| a1c4c6203e | |||
| 71c69b80c6 | |||
| e79e289743 | |||
| 34045c2f6d | |||
| 2546710604 | |||
| bedad57b4e | |||
| b1a5f5ff1e | |||
| 101a8b13f5 | |||
| 6762526f09 | |||
| 1de049e114 | |||
| 161b0cedfa | |||
| 4ed1355761 | |||
| 9496322712 | |||
| d03bd04ef5 | |||
| b66f5bb018 | |||
| fb40e4c20b | |||
| 0771664092 | |||
| 983f02921c | |||
| 34d477819b | |||
| ff4ea4d5a9 | |||
| 904f211aba | |||
| 079896c7bc | |||
| cfeb761092 | |||
| 7a5f251ac7 | |||
| 218f4c4ec8 | |||
| cbc95a4684 | |||
| f2f22dfcd1 | |||
| 8e695b9347 | |||
| daa4ea3f16 | |||
| cf9dbaf568 | |||
| 41f2d4c0f2 | |||
| 9501080170 | |||
| 826f7b3f89 | |||
| 5845b5eb12 | |||
| 4ddb6542e1 | |||
| 9063f10b1b | |||
| eb6c689f09 | |||
| 703c540bdc | |||
| cf97eab396 | |||
| 104efc4e9b | |||
| ce27b63010 | |||
| 42ac10a88f | |||
| fd38189f43 | |||
| f14580e0db | |||
| dbb580b2c8 | |||
| d1e887b91b | |||
| 4c4177050c | |||
| c2222b16b0 | |||
| 2867310817 | |||
| d4ee5f3a18 | |||
| 3b09297b27 | |||
| 4ebfd8e3a3 | |||
| e3baeb8803 | |||
| 9876b4ebb4 | |||
| 0441a2e693 | |||
| b543eb1f84 | |||
| c6570dcd06 | |||
| 14bb5297a8 | |||
| 8a0f92b6bc | |||
| 1b1ee1e0b7 | |||
| 321b7963a4 | |||
| 9b8133f725 | |||
| 1e59249662 | |||
| c4bf0a0a04 | |||
| a2b8989cbf | |||
| 25dbd8d3bd | |||
| 5f985588f7 | |||
| 43c10a15ca | |||
| 9dfd1937c2 | |||
| 7611d9e215 | |||
| ed17c07c10 | |||
| a5d31cc2e1 | |||
| 48d3a9d6da | |||
| af939730b1 | |||
| 33edc91234 | |||
| 4863c6dc1f | |||
| 2dc091d0be | |||
| a31237d1d0 | |||
| 2b849aed7a | |||
| f7139f1118 | |||
| 70feb63ea5 | |||
| bf95995573 | |||
| 12cb10c3a1 | |||
| 5cbc330f82 | |||
| d59ce3571c | |||
| 68aa2e30ab | |||
| baea6eaa41 | |||
| d84b066c62 | |||
| 0c772d273d | |||
| ecfcc3f429 | |||
| 432f1102b7 | |||
| b0954b2672 | |||
| ecbf282f6d | |||
| 30138629d3 | |||
| 95bd218183 |
@@ -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
@@ -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
@@ -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 1–4 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.
|
||||
@@ -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 *)¶m_3) {
|
||||
ATL::CComVariant::InternalCopy((CComVariant *)(this_00 + 0xc),(tagVARIANT *)¶m_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),¶m_1,piVar5);
|
||||
uVar1 = param_1;
|
||||
if ((param_1 == *(uint *)(iVar4 + 0x30)) ||
|
||||
(FUN_1000f5ef((void *)(param_1 + 0x3c),¶m_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),¶m_1,piVar1);
|
||||
piVar2 = param_1;
|
||||
if ((param_1 == *(int **)(iVar5 + 0x30)) ||
|
||||
(FUN_1000f5ef(param_1 + 0xf,¶m_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 {}
|
||||
@@ -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:154–163`: 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:164–174`). 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:164–174`) 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 |
|
||||
|
||||
@@ -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 (~30–60s) 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.
|
||||
@@ -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 1–2 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).
|
||||
@@ -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 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 / 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 1–4 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 1–2 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 F19–F26 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).
|
||||
@@ -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 |
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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>
|
||||
@@ -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
@@ -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)>
|
||||
@@ -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.
|
||||
@@ -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 19–40) 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.
|
||||
@@ -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.
|
||||
@@ -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}.
|
||||
@@ -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.
|
||||
Generated
+1708
-3
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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};
|
||||
|
||||
@@ -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
@@ -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.
|
||||
+21
@@ -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
|
||||
@@ -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
@@ -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
@@ -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("<"),
|
||||
'>' => out.push_str(">"),
|
||||
'&' => out.push_str("&"),
|
||||
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 `<` and `&`. 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 < b & c > 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:?}");
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -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).
|
||||
+31
@@ -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>
|
||||
+13
@@ -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>
|
||||
+11
@@ -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>
|
||||
+31
@@ -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>
|
||||
+10
@@ -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>
|
||||
+9
@@ -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(),
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user