[M2] mxaccess-rpc: OXID + RemQI body codecs (wave 2)
Lands M2 wave 2 — two pure-Rust body-codec modules under crates/mxaccess-rpc, plus a small inline ORPC framing port and a crate-level type consolidation. Resolves F7+F8 from wave 1. New modules - guid.rs (4 tests) — hoisted from objref::Guid; shared by all of mxaccess-rpc. Resolves F7. - error.rs — hoisted RpcError union (ShortRead, UnexpectedPacketType, UnknownPacketType, InvalidFragmentLength, TruncatedBindBody, InvalidAuthTrailer, MissingAuthValue, Decode). Resolves F8. - orpc.rs (8 tests) — port of OrpcStructures.cs:1-141. ComVersion, OrpcThis (32-byte header), OrpcThat (8-byte header), MInterfacePointer (length-prefixed OBJREF), StdObjRef (40 bytes). - object_exporter.rs (~530 LoC, 20 tests) — port of ObjectExporterMessages.cs:1-141. IObjectExporter IID, opnums, ResolveOxid request encoder + ResolveOxidResult/Failure parsers. Owned-string protocol labels cleaned up via Cow upgrade rather than Box::leak (ComDualStringEntry::protocol is now Cow<'static, str>). - rem_unknown.rs (~340 LoC, 11 tests) — port of RemUnknownMessages.cs. IRemUnknown IID, RemQueryInterface request/response, RemQiResult. 4-byte NDR pad in REMQIRESULT preserved as pad_after_hresult per CLAUDE.md unknown-bytes rule. Test count delta: 277 -> 319 (+42; codec 215 unchanged, mxaccess-rpc 60 -> 102, codec parity 2 unchanged). Open followups touched: F7 + F8 resolved; F9, F10, F11 added. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
//! 16-byte GUID with .NET-compatible display.
|
||||
//!
|
||||
//! Hoisted from `objref::Guid` in M2 wave 2 — see `design/followups.md` F7.
|
||||
//! Both `objref` (for `iid`/`ipid`) and `pdu` (for `SyntaxId` IIDs) and the
|
||||
//! M2 wave 2 `orpc::OrpcThis::cid` / `object_exporter::*` / `rem_unknown::*`
|
||||
//! types share this single representation rather than each rolling their own.
|
||||
//!
|
||||
//! Stored as 16 wire bytes. The first three groups on the wire are
|
||||
//! little-endian (`Data1` u32 LE, `Data2` u16 LE, `Data3` u16 LE) followed by
|
||||
//! 8 big-endian `Data4` bytes — the byte layout produced by .NET
|
||||
//! `new Guid(ReadOnlySpan<byte>)` and consumed by `Guid.TryWriteBytes` (used
|
||||
//! across the .NET reference, e.g. `ComObjRef.cs:31,36`,
|
||||
//! `OrpcStructures.cs:48,127`, `RemUnknownMessages.cs:20,30`).
|
||||
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
|
||||
/// 16-byte GUID. See module docs for byte layout.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
pub struct Guid(pub [u8; 16]);
|
||||
|
||||
impl Guid {
|
||||
pub const ZERO: Guid = Guid([0u8; 16]);
|
||||
|
||||
pub const fn new(bytes: [u8; 16]) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
|
||||
pub const fn as_bytes(&self) -> &[u8; 16] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Parse a `Guid` from a 16-byte little-endian-leading wire slice. Mirrors
|
||||
/// the .NET `new Guid(span)` byte order.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`crate::error::RpcError::ShortRead`] if `bytes.len() < 16`.
|
||||
pub fn parse(bytes: &[u8]) -> Result<Self, crate::error::RpcError> {
|
||||
if bytes.len() < 16 {
|
||||
return Err(crate::error::RpcError::ShortRead {
|
||||
expected: 16,
|
||||
actual: bytes.len(),
|
||||
});
|
||||
}
|
||||
let mut out = [0u8; 16];
|
||||
out.copy_from_slice(&bytes[..16]);
|
||||
Ok(Self(out))
|
||||
}
|
||||
|
||||
/// Write the 16 wire bytes into `dst[..16]`. Mirrors .NET
|
||||
/// `Guid.TryWriteBytes(span)`.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns [`crate::error::RpcError::ShortRead`] if `dst.len() < 16`.
|
||||
pub fn write_to(&self, dst: &mut [u8]) -> Result<(), crate::error::RpcError> {
|
||||
if dst.len() < 16 {
|
||||
return Err(crate::error::RpcError::ShortRead {
|
||||
expected: 16,
|
||||
actual: dst.len(),
|
||||
});
|
||||
}
|
||||
dst[..16].copy_from_slice(&self.0);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Guid {
|
||||
/// Mirrors .NET `Guid.ToString("D")`: dashed hex, lowercase, e.g.
|
||||
/// `b49f92f7-c748-4169-8eca-a0670b012746`. The first three groups are
|
||||
/// little-endian on the wire so are byte-swapped on display.
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let b = &self.0;
|
||||
write!(
|
||||
f,
|
||||
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
|
||||
b[3],
|
||||
b[2],
|
||||
b[1],
|
||||
b[0],
|
||||
b[5],
|
||||
b[4],
|
||||
b[7],
|
||||
b[6],
|
||||
b[8],
|
||||
b[9],
|
||||
b[10],
|
||||
b[11],
|
||||
b[12],
|
||||
b[13],
|
||||
b[14],
|
||||
b[15],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; 16]> for Guid {
|
||||
fn from(bytes: [u8; 16]) -> Self {
|
||||
Self(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(
|
||||
clippy::unwrap_used,
|
||||
clippy::expect_used,
|
||||
clippy::indexing_slicing,
|
||||
clippy::panic
|
||||
)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn display_matches_dotnet_d_format() {
|
||||
// First 3 groups are byte-swapped on display (LE wire → BE display).
|
||||
let g = Guid::new([
|
||||
0xF7, 0x92, 0x9F, 0xB4, 0x48, 0xC7, 0x69, 0x41, 0x8E, 0xCA, 0xA0, 0x67, 0x0B, 0x01,
|
||||
0x27, 0x46,
|
||||
]);
|
||||
assert_eq!(g.to_string(), "b49f92f7-c748-4169-8eca-a0670b012746");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_round_trip() {
|
||||
let bytes = [0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
|
||||
let g = Guid::parse(&bytes).unwrap();
|
||||
let mut out = [0u8; 16];
|
||||
g.write_to(&mut out).unwrap();
|
||||
assert_eq!(out, bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_short_buffer_errors() {
|
||||
assert!(matches!(
|
||||
Guid::parse(&[0u8; 15]),
|
||||
Err(crate::error::RpcError::ShortRead { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_guid() {
|
||||
assert_eq!(
|
||||
Guid::ZERO.to_string(),
|
||||
"00000000-0000-0000-0000-000000000000"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user