Initial project state: .NET reference, design, Rust port (M0+M1), evidence
rust / build / test / clippy / fmt (push) Has been cancelled
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>
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
//! `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`],
|
||||
//! which returns a fixed observed body with a 16-byte item-correlation GUID
|
||||
//! patched in at offset `0x8a`.
|
||||
//!
|
||||
//! The body is a captured constant — both segments of the hex literal in
|
||||
//! `NmxMetadataQueryMessage.cs:10-11` are reproduced byte-for-byte below.
|
||||
//! It encodes two metadata queries against `$DevPlatform.GR.TimeOfLastDeploy`
|
||||
//! and `$DevPlatform.GR.TimeOfLastConfigChange`. The Rust port preserves
|
||||
//! every byte; the only mutation is the GUID at offset `0x8a`.
|
||||
|
||||
// Direct byte indexing — see reference_handle.rs for rationale.
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
/// Offset of the 16-byte item-correlation GUID inside the observed body
|
||||
/// (`NmxMetadataQueryMessage.cs:5`).
|
||||
pub const PRE_ADVISE_CORRELATION_OFFSET: usize = 0x8a;
|
||||
|
||||
/// Length of the first hex segment in bytes — `NmxMetadataQueryMessage.cs:10`.
|
||||
const SEGMENT_1_LEN: usize = 160;
|
||||
|
||||
/// Length of the second hex segment in bytes — `NmxMetadataQueryMessage.cs:11`.
|
||||
const SEGMENT_2_LEN: usize = 154;
|
||||
|
||||
/// Length of the observed body in bytes (160 + 154 = 314).
|
||||
pub const PRE_ADVISE_BODY_LEN: usize = SEGMENT_1_LEN + SEGMENT_2_LEN;
|
||||
|
||||
/// First hex segment from `NmxMetadataQueryMessage.cs:10`. Decoded byte-for-byte
|
||||
/// from `Convert.FromHexString(...)` of the literal in the .NET source.
|
||||
const SEGMENT_1: [u8; SEGMENT_1_LEN] = [
|
||||
0x17, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x65, 0x00, 0x71, 0x00, 0x0a, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x08, 0x6a, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x81, 0x44, 0x00, 0x65,
|
||||
0x00, 0x76, 0x00, 0x50, 0x00, 0x6c, 0x00, 0x61, 0x00, 0x74, 0x00, 0x66, 0x00, 0x6f, 0x00, 0x72,
|
||||
0x00, 0x6d, 0x00, 0x2e, 0x00, 0x47, 0x00, 0x52, 0x00, 0x2e, 0x00, 0x54, 0x00, 0x69, 0x00, 0x6d,
|
||||
0x00, 0x65, 0x00, 0x4f, 0x00, 0x66, 0x00, 0x4c, 0x00, 0x61, 0x00, 0x73, 0x00, 0x74, 0x00, 0x44,
|
||||
0x00, 0x65, 0x00, 0x70, 0x00, 0x6c, 0x00, 0x6f, 0x00, 0x79, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
|
||||
0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x01, 0xd0, 0xfc, 0x40, 0x09, 0x1f, 0x01, 0x00, 0xc0, 0xca, 0x9c, 0xcd, 0x32, 0x65,
|
||||
0xb0, 0x46, 0xa5, 0x85, 0xa5, 0x83, 0xb2, 0xe7, 0x7a, 0x5d, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||
];
|
||||
|
||||
/// Second hex segment from `NmxMetadataQueryMessage.cs:11`. Decoded
|
||||
/// byte-for-byte from `Convert.FromHexString(...)` of the literal in the
|
||||
/// .NET source.
|
||||
const SEGMENT_2: [u8; SEGMENT_2_LEN] = [
|
||||
0x17, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x65, 0x00, 0x71, 0x00, 0x0a, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x08, 0x76, 0x00, 0x00, 0x00, 0x4c, 0x00, 0x00, 0x81, 0x44, 0x00, 0x65,
|
||||
0x00, 0x76, 0x00, 0x50, 0x00, 0x6c, 0x00, 0x61, 0x00, 0x74, 0x00, 0x66, 0x00, 0x6f, 0x00, 0x72,
|
||||
0x00, 0x6d, 0x00, 0x2e, 0x00, 0x47, 0x00, 0x52, 0x00, 0x2e, 0x00, 0x54, 0x00, 0x69, 0x00, 0x6d,
|
||||
0x00, 0x65, 0x00, 0x4f, 0x00, 0x66, 0x00, 0x4c, 0x00, 0x61, 0x00, 0x73, 0x00, 0x74, 0x00, 0x43,
|
||||
0x00, 0x6f, 0x00, 0x6e, 0x00, 0x66, 0x00, 0x69, 0x00, 0x67, 0x00, 0x43, 0x00, 0x68, 0x00, 0x61,
|
||||
0x00, 0x6e, 0x00, 0x67, 0x00, 0x65, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x50,
|
||||
0x03, 0x41, 0x09, 0x20, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00,
|
||||
];
|
||||
|
||||
/// Concatenation of `SEGMENT_1 || SEGMENT_2`. Equivalent to the result of
|
||||
/// `Convert.FromHexString` on the joined hex literal at
|
||||
/// `NmxMetadataQueryMessage.cs:10-11`.
|
||||
const OBSERVED_PRE_ADVISE_BODY: [u8; PRE_ADVISE_BODY_LEN] = {
|
||||
let mut out = [0u8; PRE_ADVISE_BODY_LEN];
|
||||
let mut i = 0;
|
||||
while i < SEGMENT_1_LEN {
|
||||
out[i] = SEGMENT_1[i];
|
||||
i += 1;
|
||||
}
|
||||
let mut j = 0;
|
||||
while j < SEGMENT_2_LEN {
|
||||
out[SEGMENT_1_LEN + j] = SEGMENT_2[j];
|
||||
j += 1;
|
||||
}
|
||||
out
|
||||
};
|
||||
|
||||
/// Stateless helpers around the observed metadata-query body.
|
||||
///
|
||||
/// Mirrors the static class `NmxMetadataQueryMessage`
|
||||
/// (`NmxMetadataQueryMessage.cs:3-15`).
|
||||
pub struct NmxMetadataQueryMessage;
|
||||
|
||||
impl NmxMetadataQueryMessage {
|
||||
/// Encode the observed pre-advise body, patching the supplied 16-byte
|
||||
/// GUID into offset `0x8a` (`NmxMetadataQueryMessage.cs:7-14`).
|
||||
///
|
||||
/// `item_correlation_id` is the raw 16-byte little-endian Guid layout —
|
||||
/// the same byte order .NET's `Guid.TryWriteBytes` emits. Callers
|
||||
/// constructing a Guid from Rust types are responsible for using the
|
||||
/// same wire layout (e.g. `windows::core::GUID::to_u128_le().to_le_bytes()`
|
||||
/// or equivalent).
|
||||
pub fn encode_observed_pre_advise(item_correlation_id: [u8; 16]) -> Vec<u8> {
|
||||
let mut body = OBSERVED_PRE_ADVISE_BODY.to_vec();
|
||||
body[PRE_ADVISE_CORRELATION_OFFSET..PRE_ADVISE_CORRELATION_OFFSET + 16]
|
||||
.copy_from_slice(&item_correlation_id);
|
||||
body
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn body_length_is_314() {
|
||||
// 160 + 154 = 314 bytes — derived from the two hex segments at
|
||||
// `NmxMetadataQueryMessage.cs:10-11`.
|
||||
assert_eq!(SEGMENT_1_LEN, 160);
|
||||
assert_eq!(SEGMENT_2_LEN, 154);
|
||||
assert_eq!(PRE_ADVISE_BODY_LEN, 314);
|
||||
assert_eq!(OBSERVED_PRE_ADVISE_BODY.len(), 314);
|
||||
}
|
||||
|
||||
// Compile-time bounds checks: clippy denies `assert!(<const expr>)` at
|
||||
// runtime, so anchor these as `const _: () = assert!(...)` instead. They
|
||||
// still fail the build if the constants drift — at compile time, before
|
||||
// the test runner even spins up.
|
||||
const _: () = assert!(PRE_ADVISE_CORRELATION_OFFSET + 16 <= PRE_ADVISE_BODY_LEN);
|
||||
const _: () = assert!(PRE_ADVISE_CORRELATION_OFFSET + 16 <= SEGMENT_1_LEN);
|
||||
|
||||
#[test]
|
||||
fn correlation_offset_is_0x8a() {
|
||||
assert_eq!(PRE_ADVISE_CORRELATION_OFFSET, 0x8a);
|
||||
// 0x8a (138) + 16 = 154, which is inside the first 160-byte segment.
|
||||
// Anchor checks are above as `const _: () = assert!(...)`.
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observed_guid_in_template_matches_dotnet_capture() {
|
||||
// The captured GUID at offset 0x8a in the literal body
|
||||
// (`NmxMetadataQueryMessage.cs:10` — after the `0xc0` byte at offset 138).
|
||||
let expected = [
|
||||
0xc0, 0xca, 0x9c, 0xcd, 0x32, 0x65, 0xb0, 0x46, 0xa5, 0x85, 0xa5, 0x83, 0xb2, 0xe7,
|
||||
0x7a, 0x5d,
|
||||
];
|
||||
assert_eq!(
|
||||
&OBSERVED_PRE_ADVISE_BODY[0x8a..0x8a + 16],
|
||||
&expected,
|
||||
"the embedded GUID must match the .NET literal byte-for-byte"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guid_is_patched_at_0x8a() {
|
||||
let guid = [
|
||||
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee,
|
||||
0xff, 0x00,
|
||||
];
|
||||
let body = NmxMetadataQueryMessage::encode_observed_pre_advise(guid);
|
||||
assert_eq!(body.len(), PRE_ADVISE_BODY_LEN);
|
||||
assert_eq!(&body[0x8a..0x8a + 16], &guid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bytes_outside_correlation_window_are_unchanged() {
|
||||
// Encode with an all-zero GUID and an all-0xff GUID, compare every
|
||||
// byte outside the patch window — they must be identical.
|
||||
let body_a = NmxMetadataQueryMessage::encode_observed_pre_advise([0u8; 16]);
|
||||
let body_b = NmxMetadataQueryMessage::encode_observed_pre_advise([0xffu8; 16]);
|
||||
for i in 0..PRE_ADVISE_BODY_LEN {
|
||||
if (PRE_ADVISE_CORRELATION_OFFSET..PRE_ADVISE_CORRELATION_OFFSET + 16).contains(&i) {
|
||||
continue;
|
||||
}
|
||||
assert_eq!(body_a[i], body_b[i], "byte {i} should be unchanged");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encoded_body_matches_observed_template_at_known_offsets() {
|
||||
// Spot-check anchor bytes from the .NET hex string. Offsets 0..10
|
||||
// are the `17 01 00 01 01 00 01 00 00 00` header
|
||||
// (`NmxMetadataQueryMessage.cs:10`); offset 160 starts the second
|
||||
// segment with the same 10-byte preamble (`NmxMetadataQueryMessage.cs:11`).
|
||||
let body = NmxMetadataQueryMessage::encode_observed_pre_advise([0u8; 16]);
|
||||
let preamble = [0x17, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00];
|
||||
assert_eq!(&body[0..10], &preamble);
|
||||
assert_eq!(&body[SEGMENT_1_LEN..SEGMENT_1_LEN + 10], &preamble);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fresh_call_does_not_mutate_template() {
|
||||
// Each call must return an independent buffer — patching the result
|
||||
// of one call must not affect a subsequent call.
|
||||
let mut a = NmxMetadataQueryMessage::encode_observed_pre_advise([0u8; 16]);
|
||||
a[0] = 0x99;
|
||||
let b = NmxMetadataQueryMessage::encode_observed_pre_advise([0u8; 16]);
|
||||
assert_eq!(b[0], 0x17, "second call must not see mutation of first");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user