//! 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)` 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 { 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)) } /// Parse a `12345678-1234-1234-1234-123456789012` style GUID string /// into wire-byte form. Inverse of the [`std::fmt::Display`] impl. /// /// Accepts the canonical dashed-hex form, optionally wrapped in /// `{...}` braces (the .NET `B` format). Case-insensitive. The /// first three hex groups are stored little-endian on the wire (per /// the module docstring) so the parser byte-swaps them after the /// raw hex pass. /// /// There is no .NET reference to mirror here — the Display impl is /// the spec, this is its inverse. /// /// # Errors /// Returns [`crate::error::RpcError::Decode`] if the input is not /// 32 hex chars (with 4 optional dashes and optional outer braces), /// or contains a non-hex character. pub fn parse_str(s: &str) -> Result { let trimmed = s.trim_start_matches('{').trim_end_matches('}'); // Strip dashes; everything else must be a hex digit. let mut bytes = [0u8; 16]; let mut nibble_count = 0usize; let mut acc: u8 = 0; for c in trimmed.chars() { if c == '-' { continue; } let digit = match c.to_digit(16) { Some(d) => d as u8, None => { return Err(crate::error::RpcError::Decode { offset: nibble_count / 2, reason: "non-hex character in guid", buffer_len: trimmed.len(), }); } }; if nibble_count >= 32 { return Err(crate::error::RpcError::Decode { offset: 16, reason: "guid hex too long", buffer_len: trimmed.len(), }); } if nibble_count % 2 == 0 { acc = digit << 4; } else { bytes[nibble_count / 2] = acc | digit; } nibble_count += 1; } if nibble_count != 32 { return Err(crate::error::RpcError::Decode { offset: nibble_count / 2, reason: "guid hex too short", buffer_len: trimmed.len(), }); } // Byte-swap the first three groups so the resulting bytes match // the wire layout the Display impl reads. bytes[0..4].reverse(); bytes[4..6].reverse(); bytes[6..8].reverse(); Ok(Self(bytes)) } /// 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" ); } #[test] fn parse_str_round_trips_display() { // The dashed-hex form from the display fixture above. let g = Guid::parse_str("b49f92f7-c748-4169-8eca-a0670b012746").unwrap(); assert_eq!( g.0, [ 0xF7, 0x92, 0x9F, 0xB4, 0x48, 0xC7, 0x69, 0x41, 0x8E, 0xCA, 0xA0, 0x67, 0x0B, 0x01, 0x27, 0x46, ] ); // Round-trip back via Display. assert_eq!(g.to_string(), "b49f92f7-c748-4169-8eca-a0670b012746"); } #[test] fn parse_str_accepts_braces() { // .NET "B" format wraps the dashed-hex form in `{}`. let g = Guid::parse_str("{b49f92f7-c748-4169-8eca-a0670b012746}").unwrap(); assert_eq!(g.to_string(), "b49f92f7-c748-4169-8eca-a0670b012746"); } #[test] fn parse_str_accepts_uppercase() { let g = Guid::parse_str("B49F92F7-C748-4169-8ECA-A0670B012746").unwrap(); assert_eq!(g.to_string(), "b49f92f7-c748-4169-8eca-a0670b012746"); } #[test] fn parse_str_accepts_no_dashes() { let g = Guid::parse_str("b49f92f7c74841698ecaa0670b012746").unwrap(); assert_eq!(g.to_string(), "b49f92f7-c748-4169-8eca-a0670b012746"); } #[test] fn parse_str_round_trips_zero() { let g = Guid::parse_str("00000000-0000-0000-0000-000000000000").unwrap(); assert_eq!(g, Guid::ZERO); } #[test] fn parse_str_rejects_too_short() { let err = Guid::parse_str("b49f92f7-c748-4169-8eca-a0670b0127").unwrap_err(); assert!(matches!( err, crate::error::RpcError::Decode { reason: "guid hex too short", .. } )); } #[test] fn parse_str_rejects_too_long() { let err = Guid::parse_str("b49f92f7-c748-4169-8eca-a0670b01274600").unwrap_err(); assert!(matches!( err, crate::error::RpcError::Decode { reason: "guid hex too long", .. } )); } #[test] fn parse_str_rejects_non_hex() { let err = Guid::parse_str("b49f92f7-c748-4169-8eca-a0670b01274z").unwrap_err(); assert!(matches!( err, crate::error::RpcError::Decode { reason: "non-hex character in guid", .. } )); } }