48d3a9d6da19ee8cd2a9f909cbfe6d6000b0c50a
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a31237d1d0 |
[M4] mxaccess: Subscription impls Stream<Item = DataChange> (resolves F15)
F15 step 2/2 lands the per-Subscription routing on top of step 1's broadcast layer. Subscription is now a working data-change stream. Subscription type - Now impls futures_util::Stream<Item = Result<DataChange, Error>> via tokio_stream::wrappers::BroadcastStream + a per-message filter. - No longer Clone (broadcast::Receiver isn't Clone). Consumers that want fanout subscribe twice or share via Arc<Mutex<...>>. - Holds the broadcast::Receiver subscribed BEFORE AdviseSupervisory fires — guarantees no updates between advise and stream-creation are dropped. - pending VecDeque buffers records from the current message so each poll_next yields at most one DataChange (Stream contract). Filter logic (records_to_data_changes, mirrors cs:333-343) - 0x32 SubscriptionStatus: keep when msg.item_correlation_id == subscription.correlation_id; drop otherwise. - 0x33 DataUpdate: keep ALL — codec exposes no per-record correlation field, and the .NET filter only checks item_correlation_id (which 0x33 doesn't carry), so DataUpdates fan out to every active subscription. Matches .NET behavior verbatim. - Records with value: None drop silently (mirrors evt.Record.Value is null filter at cs:337). - BroadcastStream Lagged(n) maps to Error::Configuration with the lag count in the detail string. Helpers - filetime_to_system_time(i64) -> SystemTime: inverse of system_time_to_filetime; saturates at Unix epoch for FILETIMEs before 1970 since SystemTime can't portably represent pre-epoch. - record_to_data_change(record, reference) -> Option<DataChange>: builds DataChange from one record, returns None for unparseable value (the codec couldn't decode the wire kind). - Status currently hardcoded to MxStatus::DATA_CHANGE_OK (mirrors NmxSubscriptionRecord.ToDataChangeStatus at NmxSubscriptionMessage.cs:22-25 which the .NET reference itself stubs to the OK constant). Cargo.toml additions: futures-util (workspace) + tokio-stream (0.1 with sync feature for BroadcastStream). Tests (5 new in mxaccess; total 40) - subscription_stream_yields_data_change_for_matching_correlation: build a 0x32 SubscriptionStatus with one Int32 record and the subscription's correlation id, inject through test_inject_sender, observe the DataChange (reference, value, quality match) on the Stream. - subscription_stream_filters_out_mismatched_correlation_for_status: inject 0x32 with wrong correlation id, assert the stream stays pending (timeout-as-success). - subscription_stream_keeps_data_update_regardless_of_correlation: inject 0x33 DataUpdate with one Int32 record (no correlation field on the message); stream still yields the DataChange. - filetime_to_system_time_round_trip: build a SystemTime with .005s precision, round-trip through both helpers, assert equality. - filetime_to_system_time_pre_unix_epoch_saturates: FILETIME 0 (year 1601) → SystemTime::UNIX_EPOCH (saturating clamp). design/followups.md: F15 moved to Resolved with both step commits referenced. Open list: 9 items (was 10). Test count delta: 511 -> 516 (+5). All four DoD gates green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
12cb10c3a1 |
[M4] mxaccess: Session::connect_nmx + write_value + shutdown (wave 1 main)
First working M4 wave 1 slice. Adds session.rs with the connect /
write / shutdown path on top of NmxClient + Resolver, plus a tokio
test that exercises a full round-trip against a hand-rolled server.
Read, subscribe, recovery, and the long-lived connection task land
in wave 2.
Architecture
- Session holds Arc<SessionInner>; SessionInner wraps NmxClient
behind a tokio::sync::Mutex. All RPC ops serialize on that mutex.
Wave 2 will replace it with an mpsc::channel<Op> + dispatcher task
per design/70-risks-and-open-questions.md R15 (drop-time async
cleanup hazards).
- ensure_connected gate stops post-shutdown ops with
Connection::EngineNotRegistered. Shutdown is idempotent via
AtomicBool::swap.
- Manual Debug impl on SessionInner — neither dyn Resolver nor
NmxClient impl Debug.
Public API
- Session::connect_nmx(addr, options, ntlm, service_ipid, resolver,
recovery): validates the policy, opens NmxClient, runs
RegisterEngine2 (no callback yet — wave 2), optionally configures
heartbeat. Returns Error::Connection on non-zero HRESULT.
- Session::write_value(reference, value: WriteValue): resolves the
tag through the configured Resolver, dispatches NmxClient::write.
- Session::resolve_write_kind / resolve_tag: convenience accessors.
- Session::shutdown_nmx: calls UnregisterEngine, idempotent.
Error mapping
- map_nmx / map_transport / map_resolver bridge the inner crate
errors into the public Error enum. NonZeroHresult → InvalidArgument
with the hex code; transport Fault → Status-shaped error;
ResolverError::NotFound → Galaxy { reason: "tag not found: ..." }.
- All three matchers handle their #[non_exhaustive] sources with a
generic catch-all so future variants don't silently break the map.
Tests (8 new in mxaccess; total mxaccess: 19)
- write_value round-trip via in-memory StaticResolver + hand-rolled
unauthenticated DCE/RPC server.
- write_value propagates resolver not-found → Galaxy error.
- write_value propagates non-zero HRESULT → InvalidArgument.
- shutdown is idempotent (second call is a no-op).
- write after shutdown returns EngineNotRegistered.
- resolve_tag and resolve_write_kind work without RPC.
- envelope-kind constants used by Session match codec exports
(sanity guard against codec rename).
mxaccess-nmx: WriteValue now re-exported at crate root.
mxaccess: deps gained mxaccess-nmx/galaxy/rpc + tokio + tracing,
plus async-trait as a dev-dep for the test resolver impl.
Test count delta: 479 -> 487 (+8). All four DoD gates green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
baea6eaa41 |
[M3] mxaccess-galaxy: GalaxyUserProfile + UserResolver trait + role-blob
Lands the user-resolver half of M3 stream A. Pure-Rust foundation — the tiberius-backed SQL impl is logged as F14 and stays gated behind the existing galaxy-resolver Cargo feature. New - role_blob.rs (~270 LoC, 12 tests including a garbage-between-roles edge case) — port of ParseRoleBlob (cs:87-133). Sliding-window scan over hex-decoded UTF-16LE bytes; rejects non-printable code units; case-insensitive dedup. Pure function, no I/O. - user.rs (~290 LoC, 8 tests including 4 tokio-driven InMemoryUserResolver cases) — GalaxyUserProfile (port of cs:5-11) + from_columns helper bridging into role_blob + UserResolver async trait + UserResolverError with NotFound / Backend variants. - sql.rs additions: USER_SELECT_SQL + USER_BY_GUID_SQL + USER_BY_NAME_SQL constants (port of cs:135-148). Inline concatcp! macro composes the base SELECT with each WHERE clause at compile time without pulling const_format. Cargo.toml: added uuid (Galaxy user_guid is a uniqueidentifier). design/followups.md: added F14 (P2) for the tiberius-backed SQL impl behind the galaxy-resolver feature. Test count delta: 427 -> 446 (+19; mxaccess-galaxy 30 -> 49). All four DoD gates green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d84b066c62 |
[M3] mxaccess-galaxy: GalaxyTagMetadata + parser + Resolver trait + SQL
Lands M3 stream A — the pure-Rust foundation of the Galaxy resolver:
the data type, the tag-reference parser, the async trait, and the
canonical SQL strings. Unblocks F13 (NmxClient::write_* wrappers depend
on GalaxyTagMetadata) without pulling in tiberius yet.
New
- metadata.rs (~195 LoC, 7 tests) — GalaxyTagMetadata record (port of
cs:6-73). Includes is_buffer_property + to_reference_handle(galaxy_id)
bridging into mxaccess-codec::MxReferenceHandle::from_names.
- parser.rs (~330 LoC, 12 tests) — ParsedTagReference parser. Handles
Object.Attribute (1 candidate), Object.Primitive.Attribute (2
candidates: primitive-attr first, dotted-attr second per cs:181-185),
and the case-insensitive .property(buffer) suffix. Pure-Rust, no I/O.
- resolver.rs (~200 LoC, 5 tests including a tokio-driven InMemoryResolver
proving the trait is implementable without SQL) — async Resolver trait
+ ResolverError. Default browse returns Backend("not implemented") so
read-only backends don't need to override it.
- sql.rs (~280 LoC, 5 smoke tests) — RESOLVE_SQL + BROWSE_SQL constants
ported byte-for-byte from cs:208-432. Available publicly so any
backend (the planned tiberius impl, a wwtools/grdb snapshot replay,
etc.) can grab the canonical query.
Cargo.toml: added mxaccess-codec (path), async-trait, thiserror;
tokio added as dev-dependency for the resolver-trait async tests.
Deliberately deferred to a later iteration:
- The tiberius-backed Resolver impl behind the galaxy-resolver feature.
- ToValueKind / TryGetValueKind / ProjectWriteValue helpers on
GalaxyTagMetadata (cs:41-72) — these need a MxDataType -> MxValueKind
lookup that the codec doesn't currently expose; landing them with
F13's write-helper iteration keeps the iteration coherent.
Test count delta: 397 -> 427 (+30). All four DoD gates green.
Open followups touched: F13 prerequisite (GalaxyTagMetadata) now in
place; F13 itself stays open until the write helpers wire it up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0c772d273d |
[M3] mxaccess-nmx: NmxClient — 9 raw INmxService2 opnums (stream B)
Lands M3 stream B raw opnum surface: an async NmxClient over the mxaccess-rpc transport that dispatches all 9 INmxService2 procedures (GetPartnerVersion, RegisterEngine2 + WithoutCallback, UnregisterEngine, Connect, AddSubscriberEngine, RemoveSubscriberEngine, SetHeartbeatSendInterval, TransferData) plus a NonZeroHresult error variant that mirrors ThrowIfFailed (cs:563-574). New - crates/mxaccess-nmx/src/client.rs (~580 LoC, 8 tests including 5 real-socket tokio tests against a hand-rolled DCE/RPC server) — port of the raw opnum surface from ManagedNmxService2Client.cs. - NmxClient::connect builds the NTLM-packet-integrity bind path; for tests, NmxClient::from_bound_transport accepts a transport bound any way the caller likes (the test server doesn't validate signatures). - fresh_orpc_this generates a per-call Cid via rand::random(), mirroring the .NET reference's Guid.NewGuid() at every call site. - NmxClientError::NonZeroHresult unifies the .NET reference's Marshal.ThrowExceptionForHR + InvalidOperationException branches so callers see one typed surface for "transport-OK + LMX rejected". Cargo.toml: added tokio, tracing, thiserror, rand to mxaccess-nmx. Two layers of the .NET reference are deliberately out of scope this iteration; both logged as new followups in design/followups.md: - F12 (P1): the auto-resolving Create() factory, which needs windows-rs COM activation (gated by F6) + ComObjRefProvider port. - F13 (P1): the high-level Write*/Advise*/UnAdvise/RegisterReference helpers, which depend on GalaxyTagMetadata from M3 stream A (the Galaxy SQL resolver crate, not yet started). Test count delta: 389 -> 397 (+8). All four DoD gates green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
432f1102b7 |
[M2/M3] mxaccess-rpc: tokio DCE/RPC TCP transport (DceRpcTcpClient port)
Lands the async DCE/RPC TCP client — the transport that bridges the M2
PDU codec to a real socket. Unblocks M3 stream B (mxaccess-nmx, the
NmxClient) and brings F9 (ResolveOxid wrappers) within reach.
New
- transport.rs (~700 LoC, 10 tests including 2 real-socket tokio tests)
— port of src/MxNativeClient/DceRpcTcpClient.cs.
- DceRpcTcpClient::connect/bind/bind_with_managed_ntlm_packet_integrity/
call/call_bound/call_bound_object — async over tokio::net::TcpStream.
- encode_packet_integrity_request: 4-byte 0xBB pad + 8-byte AuthTrailer
+ 16-byte NtlmClientContext::sign signature, frag_length and
auth_length rewritten in the embedded header per cs:201-250.
- encode_request_bytes: PFC_OBJECT_UUID flag (0x80) and inserted
16-byte object UUID slot per cs:269-278.
- TransportError enum unifies io / codec / NTLM / fault / not-connected
surfaces. Mirrors DceRpcFaultException as the typed Fault variant.
- NTLM_AUTH_CONTEXT_ID = 79232 = 0x13580 (cs:90,133) exposed publicly.
Deliberately skipped: BindWithNtlmConnect / BindWithNtlmPacketIntegrity
(SSPI flavours at cs:55-63,108-149) — those wrap .NET's
System.Net.Security.SspiClientContext, which has no portable analogue.
Managed-NTLM path covers what the production Rust client needs.
mxaccess-rpc/Cargo.toml: added tokio (workspace-pinned).
design/followups.md: F9 downgraded P1 → P2 (transport landed; only the
two pure-codec ResolveOxid wrappers remain).
Test count delta: 354 -> 364 (+10).
Open followups touched: F9 partially advanced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
b0954b2672 |
[M2] mxaccess-callback: tokio TCP exporter (wave 3 main)
Lands the M2 wave 3 main course — the INmxSvcCallback callback exporter.
Pure-tokio TCP server that mirrors src/MxNativeClient/ManagedCallbackExporter.cs
and lets a Rust client receive callbacks from NmxSvc.exe.
New
- exporter.rs (~700 LoC, 10 tests) — port of ManagedCallbackExporter.cs.
CallbackExporter::bind starts a TcpListener + accept loop; per-connection
serve task walks Bind / AlterContext / Request / Auth3 PDUs and dispatches
IRemUnknown (opnums 3/4/5) and INmxSvcCallback (opnums 3/4) requests.
Hand-rolled BindAck encoder mirroring cs:226-254 (single acceptance entry,
NDR20 transfer syntax).
- ExporterIdentities { oxid, oid, callback_ipid, rem_unknown_ipid } — exposes
both `random()` (production) and `fixed()` (tests). Mirrors the .NET
RandomUInt64 + Guid.NewGuid pattern at cs:14-20.
- CallbackEvent enum — typed diagnostic stream replacing .NET's
List<string> log (cs:12,33-42,315-321). Variants: ClientConnected,
AcceptError, Bind, Auth3Ignored, Request, RemQueryInterface,
CallbackInvoked, UnhandledRequest, ClientDisconnected, ProtocolError.
- IUNKNOWN_IID const re-exported alongside the other IIDs.
Tests cover real-socket round-trips: Bind+RemQueryInterface (with IUNKNOWN
returning S_OK), Bind+unknown opnum -> Fault, Bind+DataReceived ->
CallbackInvoked event + 12-byte success response, and graceful shutdown.
Test count delta: 344 -> 354 (+10).
Open followups touched: none new. F2 (verify_signature path) still
gated on a live status-frame fixture under tests/fixtures/m2-status-frame/.
F6 / F9 still need the windows-rs and DceRpcTcpClient ports respectively.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
95bd218183 |
[M2] mxaccess-rpc: NTLMv2 + DCE/RPC PDU + OBJREF parser (wave 1)
Lands M2 wave 1 — three pure-Rust modules under crates/mxaccess-rpc with 60 unit tests. Each is a 1:1 port of one .NET reference file: - ntlm.rs (1137 LoC, 19 tests) — `ManagedNtlmClientContext.cs`. NTLMv2 challenge/response, Type1/Type3 builders, sign() with RC4-sealed checksum and per-call sequence advance. Manual `Debug` impl that hides credentials; not Clone (rc4 0.2 cipher state is non-Clone). Pure-Rust crypto via hmac/md-5/md4/rc4 v0.2/rand v0.8 (rc4 0.2 chosen per design/review.md:78). - pdu.rs (1573 LoC, 33 tests) — `DceRpcPdu.cs` + auth-trailer types from `DceRpcAuthentication.cs`. Bind/AlterContext/Auth3/Request/Response/Fault PDUs, NDR20 transfer syntax, auth_value with 4-byte alignment padding, preserved-byte fields per CLAUDE.md unknown-bytes rule. - objref.rs (~470 LoC, 11 tests including a 366-byte captured OBJREF round-trip) — `ComObjRef.cs`. MEOW signature, OXID/OID/IPID, dual-string array with printable-ASCII escaping and security-binding boundary. ComObjRefProvider.cs deferred (windows-rs Win32 wrapper — see F6). Every wire-byte claim cites src/MxNativeClient/<file>.cs:LINE per CLAUDE.md "no fabricated protocol behaviour" rule. Test count delta: 217 → 277 (+60) Open followups touched: F1–F8 (new — see design/followups.md) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fe2a6db786 |
Initial project state: .NET reference, design, Rust port (M0+M1), evidence
rust / build / test / clippy / fmt (push) Has been cancelled
Layout:
- src/ .NET 10 x64 reference: MxNativeCodec, MxNativeClient,
MxAsbClient, probes, tests, harnesses. Executable spec.
- design/ Architectural plan for the Rust port (M0–M6), error
model, protocol invariants, risks (R1–R16), adversarial
review log (review.md).
- rust/ Rust workspace. M0 skeleton + M1 codec parity.
mxaccess-codec: 215 unit tests + 2 cross-implementation
parity tests (byte-identical against .NET reference).
Other crates are M0 stubs awaiting M2+.
- captures/ Frida + netsh + pcap evidence per CLAUDE.md
("captures are evidence, not throwaway logs").
- analysis/ Decompiled C# (frida/proxy/decompiled-*),
Ghidra exports for native DLLs (`exports/` only —
working state at `projects/` and AVEVA's input
binaries at `input/` are gitignored).
- docs/ Reverse-engineering reference docs.
- tools/ Setup-LiveProbeEnv.ps1 (Infisical credential fetcher),
Compute-Crc.ps1 (.NET parity helper).
- .github/workflows/ Rust CI: fmt + build + test + clippy on Windows.
- LICENSE MIT (Joseph Doherty, 2026).
Verified:
- cargo test --workspace → 217 passed (215 unit + 2 .NET parity), 0 failed
- cargo clippy --workspace -- -D warnings → clean
- cargo fmt --all -- --check → clean
- cargo publish --dry-run -p mxaccess-codec → packages cleanly
Excluded from history (see .gitignore):
- **/bin, **/obj, **/target — build artifacts
- analysis/ghidra/projects/ — Ghidra working state (regenerable)
- analysis/ghidra/input/ — AVEVA proprietary DLLs (vendor IP)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|