# Crate topology ## Workspace layout ``` rust/ Cargo.toml workspace root Cargo.lock rust-toolchain.toml 1.85, stable (matches workspace.package.rust-version) crates/ mxaccess-codec/ pure protocol codec, no I/O mxaccess-galaxy/ Galaxy SQL resolver (tiberius) mxaccess-rpc/ DCE/RPC + NTLMv2 + OXID + OBJREF mxaccess-callback/ INmxSvcCallback RPC server mxaccess-nmx/ INmxService2 client mxaccess-asb-nettcp/ net.tcp framing: MC-NMF + MC-NBFX/NBFS binary message encoder (NetTcpBinding default — see src/MxAsbClient/MxAsbDataClient.cs:660-685) mxaccess-asb/ IASBIDataV2 client mxaccess/ async session + Transport trait + public API examples/ `cargo run --example` only resolves examples connect-write-read.rs owned by a specific crate, so the public-facing subscribe.rs examples live under the top-level `mxaccess` subscribe-buffered.rs crate and are invoked with `-p mxaccess`. asb-subscribe.rs recovery.rs multi-tag.rs secured-write.rs mxaccess-compat/ LMXProxyServer-shaped facade (optional) tests/ fixtures/ copy of ../captures/0NN-frida-* (junctions are Windows-only and don't survive `git clone` cross-platform; symlinks need Developer Mode on Windows — copy is the portable default) ``` The workspace lives at `c:\Users\dohertj2\Desktop\mxaccess\rust\` (sibling of `src/`) per the `CLAUDE.md` directive. The .NET tooling does not look there; cargo treats it as the workspace root. ## Dependency graph ``` +----------------+ | mxaccess-codec | +----------------+ ^ ^ | | +-----------+ +-----------+ | | +---------------+ +-------------------+ | mxaccess-rpc | | mxaccess-asb-nettcp | +---------------+ +-------------------+ ^ ^ | | +-------+---------+ +-------+--------+ | mxaccess-callback| | mxaccess-asb | +------------------+ +----------------+ ^ ^ | | +-------+----------+ | | mxaccess-nmx | | +------------------+ | ^ +-------------------+ | | | mxaccess-galaxy | | | +-------------------+ | | ^ | +--------------+-------------------+ | +---------------+ | mxaccess | (top-level async API) +---------------+ ^ | +-----------------+ | mxaccess-compat | (LMXProxyServer shape) +-----------------+ ``` No cycles. ASB and NMX paths never depend on each other. The `mxaccess-nmx → mxaccess-galaxy` arrow is feature-gated behind `galaxy-resolver` (default-on); consumers building NMX with a custom `Resolver` can drop it. ## Per-crate detail ### `mxaccess-codec` | | | |---|---| | Role | Pure encoder/decoder for NMX wire types and ASB variant | | Targets | All Rust targets *theoretically* (codec is pure, no platform-bound deps); Linux/macOS support is a **stretch goal**, gated on `MX_LIVE` integration tests against a remote AVEVA install. See `60-roadmap.md` and `70-risks-and-open-questions.md` Q3. | | Public deps | `bytes`, `byteorder`, `uuid`, `widestring`, `thiserror` | | Private deps | `proptest` (dev) | | Optional features | `serde` (derives `Serialize`/`Deserialize` on public types) | | Tests | Round-trip every captured fixture; proptest generators for primitives; cross-implementation parity vs `dotnet run --project src\MxNativeCodec.Tests` | ### `mxaccess-galaxy` | | | |---|---| | Role | Galaxy Repository SQL resolver: tag → metadata, user → identity | | Targets | All Rust targets, but Linux integrated-security is a stretch goal — see auth note below | | Deps | `mxaccess-codec`, `tokio`, `tokio-util`, `futures-util`, `thiserror`, `tracing` (`tiberius` is now an optional dep, see below) | | Optional features | `galaxy-resolver` (default-off; pulls `tiberius` and exposes the SQL-backed resolver. Consumers that only need NMX/ASB with a custom `Resolver` impl can leave this off and avoid pulling TDS, native-tls/rustls, and the `winauth` stack). `auth-windows` (default-on for Windows when `galaxy-resolver` is on; selects `tiberius`'s `winauth` SSPI feature for integrated security against domain-joined SQL Server). On Linux, `auth-windows` does **not** apply: integrated security against an MSSQL Galaxy DB requires `tiberius`'s `integrated-auth-gssapi` feature plus a configured Kerberos KDC and `krb5.conf` on the client. Galaxy databases in practice are domain-joined Windows boxes using NTLM/Kerberos integrated auth, so Linux clients without an MIT/Heimdal stack will fail to authenticate; flagged as a **stretch goal** and tracked in `70-risks-and-open-questions.md`. SQL-login fallback is always available cross-platform. | | Tests | Mock SQL fixtures; live integration test gated on `MX_GALAXY_DB` env var | The .NET reference keeps `GalaxyRepositoryTagResolver.cs` inside the `MxNativeClient` namespace (`src/MxNativeClient/GalaxyRepositoryTagResolver.cs:4`). Splitting it into `mxaccess-galaxy` is a Rust-side improvement, not a porting fault: the resolver's only inputs are SQL connection options, its only output is `MxReferenceHandle` (a `mxaccess-codec` type), and the Rust trait `Resolver` is exposed by `mxaccess-nmx` so consumers can plug in their own implementation. With `galaxy-resolver` feature-gated, `mxaccess-nmx` does not transitively pull `tiberius` for consumers who do not need it. **Resolver input contract — `tag_name`-form only.** The Galaxy DB carries two distinct name fields per object: `tag_name` (the runtime read/write name, e.g. `DelmiaReceiver_001`) and `contained_name` (the hierarchy-browsing path, e.g. `TestMachine_001.DelmiaReceiver`). These are **asymmetric and cannot be used interchangeably** — `wwtools/grdb/README.md` calls this out as a critical distinction. `GalaxyRepositoryTagResolver.ResolveSql` keys on `g.tag_name = @objectTagName`; passing a contained-name will silently miss. The Rust `Resolver` trait takes a `tag_name`-form `&str` (e.g. `"TestObject.TestInt"` resolves the `TestObject` tag plus the `TestInt` attribute on it). If a future consumer needs contained-name → tag-name translation, add it as a separate translator that calls `wwtools/grdb/queries/hierarchy.sql`-style logic; **do not** mix the two paths inside `mxaccess-galaxy`. **Galaxy schema version probe.** R10 in `70-risks-and-open-questions.md` flags older Galaxy schema layouts as untested. `wwtools/grdb/` confirms a `dbo.schema_version` table exists with `version_number` / `version_string` / `cdi_version` columns. The Rust resolver should query this at session construction and fail loud (`ConfigError::Galaxy { reason: format!("schema version {version_string} is outside tested range") }`) if the version is outside the proven set. ### `mxaccess-rpc` | | | |---|---| | Role | DCE/RPC PDU codec + NTLMv2 + OBJREF + OXID resolution + RemQI | | Targets | `x86_64-pc-windows-msvc` (primary), `x86_64-pc-windows-gnu`, `x86_64-unknown-linux-gnu` (NTLM-only paths) | | Deps | `mxaccess-codec`, `bytes`, `byteorder`, `tokio`, `hmac`, `md-5`, `rc4`, `rand`, `uuid`, `thiserror`, `tracing` (all crypto crates pinned to the `digest 0.11`/`cipher 0.5` generation per the workspace dependency table) | | Optional features | `windows-com` (default-on Windows; pulls `windows` for `GUID`/`ObjRef` helpers) | | Tests | Unit tests for NTLM message construction + PDU framing; integration tests against captured ObjectExporter responses | ### `mxaccess-callback` | | | |---|---| | Role | TCP listener + RPC server for `INmxSvcCallback` and `IRemUnknown` | | Deps | `mxaccess-rpc`, `mxaccess-codec`, `tokio`, `futures-util`, `tracing`, `thiserror` | | Tests | Unit test that exercises the dispatch table with synthetic Bind/Request PDUs | ### `mxaccess-nmx` | | | |---|---| | Role | `INmxService2` client + raw NMX session façade. Exposes a `Resolver` trait so consumers can plug in any tag-handle resolver. | | Deps | `mxaccess-codec`, `mxaccess-rpc`, `mxaccess-callback`, `tokio`, `tracing`, `thiserror` | | Optional features | `galaxy-resolver` (default-on; pulls `mxaccess-galaxy` and re-exports its SQL-backed `Resolver` impl. Off → `mxaccess-nmx` builds without `tiberius`/TDS, and the consumer supplies their own `Resolver`.) | | Tests | Round-trip TransferData fixtures; live probe gated on `MX_LIVE` | ### `mxaccess-asb-nettcp` | | | |---|---| | Role | net.tcp framing layer. Implements MC-NMF (.NET Message Framing) + MC-NBFX/NBFS (.NET Binary XML / dictionary string table) — the default binary message encoder for `NetTcpBinding`. Reference WCF construction in `src/MxAsbClient/MxAsbDataClient.cs:660-685` is `new NetTcpBinding(SecurityMode.None)` with no encoder override, which selects `BinaryMessageEncodingBindingElement` by default — i.e. *not* SOAP/XML on the wire. The previous name `mxaccess-asb-soap` was a misnomer. | | Visibility | Workspace-internal crate, published alongside the rest of the workspace (no `publish = false` — Cargo refuses `cargo publish` of `mxaccess-asb` if a path-dep here lacks a published version). | | Deps | `bytes`, `tokio`, `[an MC-NBFX/NBFS impl — TODO: evaluate `wcf-binary` crate or hand-roll a dictionary-table codec]`, `quick-xml` (only for the small ASB control-plane XML payloads such as `request.ToXml()` at `src/MxAsbClient/AsbSystemAuthenticator.cs:79`, *not* for net.tcp framing), `flate2`, `aes`, `hmac`, `md-5`, `sha1` (note crate rename — `sha-1` is deprecated upstream, `sha1` is the maintained successor), `sha2`, `pbkdf2`, `num-bigint`, `rand`, `tracing`. All RustCrypto crates are pinned to the `digest 0.11`/`cipher 0.5` generation; see workspace `[workspace.dependencies]` table. | | Tests | Unit tests for net.tcp/MC-NMF framing + DH handshake against captured payloads from `AsbMessageDumpBehavior` | ### `mxaccess-asb` | | | |---|---| | Role | `IASBIDataV2` client | | Deps | `mxaccess-codec`, `mxaccess-asb-nettcp`, `tokio`, `tracing`, `thiserror` | | Optional features | `dpapi` (default-on for Windows targets) — see `SecretProvider` below | | Tests | Round-trip Variant fixtures; live probe gated on `MX_LIVE` | `mxaccess-asb` always exposes a `SecretProvider` trait (single fallible `async fn fetch(&self) -> Result>, Error>`) that the ASB authenticator calls to obtain the shared secret used for the DH-passphrase derivation (`src/MxAsbClient/AsbSystemAuthenticator.cs:28, 134-142` — the secret is mandatory for the handshake; without it ASB cannot authenticate). The trait is **always present** — not feature-gated — so consumers can plug in any source (env var, file, KeyVault, hardcoded test fixture). The `dpapi` feature provides a default Windows-only implementation that reads the secret via `windows::Win32::Security::Cryptography::CryptUnprotectData`. With `dpapi=off`, the crate still compiles and works; the consumer must provide a `SecretProvider` impl explicitly, otherwise `Session::builder()` fails at construction time with `Error::ConfigurationIncomplete { missing: "secret_provider" }`. ### `mxaccess` | | | |---|---| | Role | Public async API: `Session`, `Subscription`, `Transport` trait | | Deps | `mxaccess-codec`, `tokio`, `tokio-util`, `futures-util`, `tracing`, `thiserror`, `async-trait`, `arc-swap` (for cheap clones of session state) | | Optional features | `nmx` (default-on Windows; pulls `mxaccess-nmx`), `asb` (default-on; pulls `mxaccess-asb`), `metrics` (optional `metrics` instrumentation), `serde` (forwards to codec) | | Tests | Integration tests gated on env vars; in-memory `Transport` for unit tests | ### `mxaccess-compat` | | | |---|---| | Role | `LMXProxyServer`-shaped methods on top of `Session` | | Deps | `mxaccess` | | Tests | Method-equivalence tests against captured `MxNativeCompatibilityServer` outputs | ## Build / test commands To be added to `CLAUDE.md` "Common commands" once `rust/` exists: ```powershell # Workspace-wide cargo build --workspace cargo test --workspace cargo clippy --workspace -- -D warnings cargo fmt --check # Single crate cargo build -p mxaccess-codec cargo test -p mxaccess-codec # Live integration tests (require AVEVA install + Galaxy DB) $env:MX_LIVE = "1" $env:MX_GALAXY_DB = "Server=localhost;Database=Galaxy;Integrated Security=True;TrustServerCertificate=True" $env:MX_NMX_HOST = "localhost" cargo test -p mxaccess --features live -- --ignored # Examples (live under `crates/mxaccess/examples/`; `-p mxaccess` is required because # `cargo run --example` only resolves examples that belong to a specific crate) cargo run -p mxaccess --example connect-write-read cargo run -p mxaccess --example subscribe -- --tag TestChildObject.TestInt cargo run -p mxaccess --example asb-subscribe -- --tag TestChildObject.TestInt ``` ## Toolchain & MSRV - MSRV is **1.85**, set both in `rust-toolchain.toml` and in `[workspace.package].rust-version`. Both must stay in lock-step; CI fails if they drift. 1.85 is the floor required by the pinned RustCrypto generation (`digest 0.11` / `cipher 0.5` family — `aes 0.9`, `hmac 0.13`, `md-5 0.11`, `sha1 0.11`, `pbkdf2 0.13`) and by the latest `uuid 1.x`; lowering it forces an older crypto generation that conflicts on the resolved `digest`/`cipher` traits. - Edition **2024** (stable since Rust 1.85, 2025-02). Since MSRV is already 1.85, edition 2024 is a free upgrade. - `rustfmt` default config + 100-column lines committed. - `clippy` with `-D warnings` in CI. - `clippy::unwrap_used`, `clippy::expect_used` set to deny in `mxaccess`, `mxaccess-codec`, `mxaccess-rpc`, `mxaccess-nmx`, `mxaccess-asb`. Allowed in tests via `#[cfg(test)]` overrides. UTF-16LE name decoding in `mxaccess-codec` (`MxReferenceHandle` parsing) must use a fallible helper that maps `String::from_utf16` errors into a typed codec error rather than `.unwrap()`-ing — there is no panicking decode path on the hot wire-parse surface. ## Feature gates summary | Feature | Default | Crate | Effect | |---|---|---|---| | `nmx` | yes (Windows) | `mxaccess` | Enables `NmxTransport`, pulls `mxaccess-nmx` | | `asb` | yes | `mxaccess` | Enables `AsbTransport`, pulls `mxaccess-asb` | | `metrics` | no | `mxaccess` | Emits `metrics` counters/histograms | | `serde` | no | `mxaccess`, `mxaccess-codec` | Derives `Serialize`/`Deserialize` | | `dpapi` | yes (Windows) | `mxaccess-asb` | Provides the Windows DPAPI default `SecretProvider` impl. Off → consumer must supply their own `SecretProvider` (the trait is always present). | | `galaxy-resolver` | yes | `mxaccess-nmx` | Pulls `mxaccess-galaxy` and exposes the SQL-backed `Resolver`. Off → `mxaccess-nmx` ships without `tiberius`/TDS; consumer supplies a custom `Resolver`. | | `auth-windows` | yes (Windows) | `mxaccess-galaxy` | Integrated security (SSPI/`winauth`) for SQL Server. Windows-only; Linux integrated security is a separate stretch goal that requires `tiberius`'s `integrated-auth-gssapi` feature + a configured Kerberos KDC. SQL-login fallback works cross-platform without this feature. | | `windows-com` | yes (Windows) | `mxaccess-rpc` | Uses `windows` crate for GUID/IID helpers | | `live` (test-only) | no | `mxaccess`, `mxaccess-nmx`, `mxaccess-asb` | Enables tests that hit a live AVEVA install | ## Workspace `Cargo.toml` skeleton ```toml [workspace] resolver = "2" members = [ "crates/mxaccess-codec", "crates/mxaccess-galaxy", "crates/mxaccess-rpc", "crates/mxaccess-callback", "crates/mxaccess-nmx", "crates/mxaccess-asb-nettcp", "crates/mxaccess-asb", "crates/mxaccess", "crates/mxaccess-compat", ] [workspace.package] edition = "2024" license = "MIT" # resolved 2026-05-05; LICENSE at repo root repository = "https://github.com//mxaccess" rust-version = "1.85" [workspace.dependencies] bytes = "1" byteorder = "1" uuid = { version = "1", features = ["v4", "v7"] } widestring = "1" thiserror = "1" tokio = { version = "1", features = ["net", "io-util", "rt-multi-thread", "sync", "time", "macros"] } tokio-util = { version = "0.7", features = ["codec"] } futures-util = "0.3" tracing = "0.1" async-trait = "0.1" # RustCrypto generation: digest 0.11 / cipher 0.5 line. All crates here are pinned to that # generation so the resolved `digest` / `cipher` graph is coherent. Bumping any one of these # to the older 0.10/0.12 line will fail to build — pin the generation, not the individual # versions. This generation requires `rust-version = "1.85"` (set below). hmac = "0.13" md-5 = "0.11" sha1 = "0.11" # crate renamed from `sha-1` (deprecated) to `sha1` upstream sha2 = "0.11" rc4 = "0.2" # latest published; on the cipher 0.5 trait reform. rand = "0.8" quick-xml = "0.36" # ASB control-plane XML payloads only (e.g. `request.ToXml()` at # `src/MxAsbClient/AsbSystemAuthenticator.cs:79`); not used for # net.tcp wire framing. aes = "0.9" flate2 = "1" pbkdf2 = "0.13" num-bigint = "0.4" # NOTE: review.md [MAJOR] flags this as not constant-time. The DH # private exponent is long-lived (`AsbSystemAuthenticator.cs:153-166`), # so a side-channel-leaky `mod_exp` is a security regression vs. an # opportunity. Tracked as an explicit follow-up in `70-risks-and-open-questions.md` # to swap to `crypto-bigint` constant-time `mod_exp` once the wire # round-trips against captured DH handshakes. tiberius = { version = "0.12", features = ["chrono", "tds73"] } # `windows` 0.62 is the current line; 0.58 → 0.62 has breaking renames in # `Win32_System_Rpc` and `Win32_Security_Cryptography` so designing against an older # pin wastes work. windows = { version = "0.62", features = [ "Win32_Foundation", "Win32_System_Com", "Win32_Security_Cryptography", "Win32_System_Rpc", ] } proptest = "1" metrics = "0.23" serde = { version = "1", features = ["derive"] } arc-swap = "1" ``` Pin minor versions in CI; keep workspace-level dependency table consistent across crates. ## License **MIT** (resolved 2026-05-05; see `70-risks-and-open-questions.md` Q2). `LICENSE` file lives at the project root (`c:\Users\dohertj2\Desktop\mxaccess\LICENSE`). All crate `Cargo.toml`s inherit `license = "MIT"` via `workspace.package` so each crate publishes correctly. Workspace deps listed above are MIT/Apache-2.0 compatible; MIT alone satisfies every dep's downstream license obligation. The `windows` crate proxy/stub IDL re-emissions are flagged for legal review only if vendored from Microsoft headers — not applicable to typical `windows-rs`-generated code.