7eb528d35f
- Add VfsFile: Send supertrait for Mutex compatibility - Fix SmbServerCommand: struct → Subcommand enum with Start variant - Fix tracing_subscriber::init() → try_init() to avoid panic when logger already initialized - Fix CLI subcommand name: smb-server → smb-start (flatten naming) - Add #[command(name = "smb-start")] for CLI disambiguation - Fix unused variable warnings (smb_fs.rs, smb_server_backend.rs) - Remove unused VfsFile imports (webdav.rs, scp_handler.rs) - Integration test: Docker smbclient verified (list, upload, read)
670 lines
22 KiB
Rust
670 lines
22 KiB
Rust
//! SMB2 packet header (64 bytes) and error response.
|
|
//!
|
|
//! The SMB2 header has two variants that share the same 64-byte layout:
|
|
//! - **Sync header:** bytes 32-35 = Reserved (u32), bytes 36-39 = TreeId (u32)
|
|
//! - **Async header:** bytes 32-39 = AsyncId (u64)
|
|
//!
|
|
//! The choice is determined by the `SMB2_FLAGS_ASYNC_COMMAND` bit in the Flags field.
|
|
//!
|
|
//! Reference: MS-SMB2 sections 2.2.1, 2.2.1.1, 2.2.1.2, 2.2.2.
|
|
|
|
use crate::error::Result;
|
|
use crate::pack::{Pack, ReadCursor, Unpack, WriteCursor};
|
|
use crate::types::flags::HeaderFlags;
|
|
use crate::types::status::NtStatus;
|
|
use crate::types::{Command, CreditCharge, MessageId, SessionId, TreeId};
|
|
use crate::Error;
|
|
|
|
/// The 4-byte protocol identifier at the start of every SMB2 message.
|
|
pub const PROTOCOL_ID: [u8; 4] = [0xFE, b'S', b'M', b'B'];
|
|
|
|
/// SMB2 packet header (64 bytes).
|
|
///
|
|
/// Contains both sync and async variants. The `flags` field determines
|
|
/// which interpretation of bytes 32-39 is correct.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct Header {
|
|
/// Number of credits charged for this request.
|
|
pub credit_charge: CreditCharge,
|
|
/// In responses: NtStatus. In requests before SMB 3.x: Reserved.
|
|
/// In requests for SMB 3.x: ChannelSequence (u16) + Reserved (u16).
|
|
pub status: NtStatus,
|
|
/// The command code for this packet.
|
|
pub command: Command,
|
|
/// In requests: credits requested. In responses: credits granted.
|
|
pub credits: u16,
|
|
/// Flags indicating how to process the operation.
|
|
pub flags: HeaderFlags,
|
|
/// Offset to the next command in a compound chain (0 = last/only).
|
|
pub next_command: u32,
|
|
/// Unique message identifier for request/response correlation.
|
|
pub message_id: MessageId,
|
|
/// Sync-only: tree identifier. None if async.
|
|
pub tree_id: Option<TreeId>,
|
|
/// Async-only: async identifier. None if sync.
|
|
pub async_id: Option<u64>,
|
|
/// Session identifier.
|
|
pub session_id: SessionId,
|
|
/// 16-byte message signature.
|
|
pub signature: [u8; 16],
|
|
}
|
|
|
|
impl Header {
|
|
pub const STRUCTURE_SIZE: u16 = 64;
|
|
|
|
/// Total header size in bytes.
|
|
pub const SIZE: usize = 64;
|
|
|
|
/// Create a new request header for a given command.
|
|
pub fn new_request(command: Command) -> Self {
|
|
Self {
|
|
credit_charge: CreditCharge(0),
|
|
status: NtStatus::SUCCESS,
|
|
command,
|
|
credits: 1,
|
|
flags: HeaderFlags::default(),
|
|
next_command: 0,
|
|
message_id: MessageId::default(),
|
|
tree_id: Some(TreeId::default()),
|
|
async_id: None,
|
|
session_id: SessionId::default(),
|
|
signature: [0u8; 16],
|
|
}
|
|
}
|
|
|
|
/// Is this a response (vs request)?
|
|
pub fn is_response(&self) -> bool {
|
|
self.flags.is_response()
|
|
}
|
|
}
|
|
|
|
impl Pack for Header {
|
|
fn pack(&self, cursor: &mut WriteCursor) {
|
|
// ProtocolId (4 bytes)
|
|
cursor.write_bytes(&PROTOCOL_ID);
|
|
// StructureSize (2 bytes)
|
|
cursor.write_u16_le(Self::STRUCTURE_SIZE);
|
|
// CreditCharge (2 bytes)
|
|
cursor.write_u16_le(self.credit_charge.0);
|
|
// Status (4 bytes)
|
|
cursor.write_u32_le(self.status.0);
|
|
// Command (2 bytes)
|
|
cursor.write_u16_le(self.command.into());
|
|
// CreditRequest/CreditResponse (2 bytes)
|
|
cursor.write_u16_le(self.credits);
|
|
// Flags (4 bytes)
|
|
cursor.write_u32_le(self.flags.bits());
|
|
// NextCommand (4 bytes)
|
|
cursor.write_u32_le(self.next_command);
|
|
// MessageId (8 bytes)
|
|
cursor.write_u64_le(self.message_id.0);
|
|
|
|
// Bytes 32-39: async or sync variant
|
|
if self.flags.is_async() {
|
|
// AsyncId (8 bytes)
|
|
cursor.write_u64_le(self.async_id.unwrap_or(0));
|
|
} else {
|
|
// Reserved (4 bytes)
|
|
cursor.write_u32_le(0);
|
|
// TreeId (4 bytes)
|
|
cursor.write_u32_le(self.tree_id.map_or(0, |t| t.0));
|
|
}
|
|
|
|
// SessionId (8 bytes)
|
|
cursor.write_u64_le(self.session_id.0);
|
|
// Signature (16 bytes)
|
|
cursor.write_bytes(&self.signature);
|
|
}
|
|
}
|
|
|
|
impl Unpack for Header {
|
|
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
|
|
// ProtocolId (4 bytes)
|
|
let proto = cursor.read_bytes(4)?;
|
|
if proto != PROTOCOL_ID {
|
|
return Err(Error::invalid_data(format!(
|
|
"invalid SMB2 protocol ID: expected {:02X?}, got {:02X?}",
|
|
PROTOCOL_ID, proto
|
|
)));
|
|
}
|
|
|
|
// StructureSize (2 bytes)
|
|
let structure_size = cursor.read_u16_le()?;
|
|
if structure_size != Header::STRUCTURE_SIZE {
|
|
return Err(Error::invalid_data(format!(
|
|
"invalid SMB2 header structure size: expected {}, got {}",
|
|
Header::STRUCTURE_SIZE,
|
|
structure_size
|
|
)));
|
|
}
|
|
|
|
// CreditCharge (2 bytes)
|
|
let credit_charge = CreditCharge(cursor.read_u16_le()?);
|
|
|
|
// Status (4 bytes)
|
|
let status = NtStatus(cursor.read_u32_le()?);
|
|
|
|
// Command (2 bytes)
|
|
let command_raw = cursor.read_u16_le()?;
|
|
let command = Command::try_from(command_raw).map_err(|_| {
|
|
Error::invalid_data(format!("invalid SMB2 command code: 0x{:04X}", command_raw))
|
|
})?;
|
|
|
|
// CreditRequest/CreditResponse (2 bytes)
|
|
let credits = cursor.read_u16_le()?;
|
|
|
|
// Flags (4 bytes)
|
|
let flags = HeaderFlags::new(cursor.read_u32_le()?);
|
|
|
|
// NextCommand (4 bytes)
|
|
let next_command = cursor.read_u32_le()?;
|
|
|
|
// MessageId (8 bytes)
|
|
let message_id = MessageId(cursor.read_u64_le()?);
|
|
|
|
// Bytes 32-39: async or sync variant
|
|
let (tree_id, async_id) = if flags.is_async() {
|
|
let async_id = cursor.read_u64_le()?;
|
|
(None, Some(async_id))
|
|
} else {
|
|
let _reserved = cursor.read_u32_le()?;
|
|
let tree_id = TreeId(cursor.read_u32_le()?);
|
|
(Some(tree_id), None)
|
|
};
|
|
|
|
// SessionId (8 bytes)
|
|
let session_id = SessionId(cursor.read_u64_le()?);
|
|
|
|
// Signature (16 bytes)
|
|
let sig_bytes = cursor.read_bytes(16)?;
|
|
let mut signature = [0u8; 16];
|
|
signature.copy_from_slice(sig_bytes);
|
|
|
|
Ok(Header {
|
|
credit_charge,
|
|
status,
|
|
command,
|
|
credits,
|
|
flags,
|
|
next_command,
|
|
message_id,
|
|
tree_id,
|
|
async_id,
|
|
session_id,
|
|
signature,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// SMB2 ERROR Response body (spec section 2.2.2).
|
|
///
|
|
/// Sent by the server when a request fails. The structure is:
|
|
/// - StructureSize (2 bytes, must be 9)
|
|
/// - ErrorContextCount (1 byte)
|
|
/// - Reserved (1 byte)
|
|
/// - ByteCount (4 bytes)
|
|
/// - ErrorData (variable, ByteCount bytes)
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct ErrorResponse {
|
|
/// Number of error contexts (SMB 3.1.1 only, otherwise 0).
|
|
pub error_context_count: u8,
|
|
/// Variable-length error data.
|
|
pub error_data: Vec<u8>,
|
|
}
|
|
|
|
impl ErrorResponse {
|
|
pub const STRUCTURE_SIZE: u16 = 9;
|
|
}
|
|
|
|
impl Pack for ErrorResponse {
|
|
fn pack(&self, cursor: &mut WriteCursor) {
|
|
// StructureSize (2 bytes)
|
|
cursor.write_u16_le(Self::STRUCTURE_SIZE);
|
|
// ErrorContextCount (1 byte)
|
|
cursor.write_u8(self.error_context_count);
|
|
// Reserved (1 byte)
|
|
cursor.write_u8(0);
|
|
// ByteCount (4 bytes)
|
|
cursor.write_u32_le(self.error_data.len() as u32);
|
|
// ErrorData (variable)
|
|
cursor.write_bytes(&self.error_data);
|
|
}
|
|
}
|
|
|
|
impl Unpack for ErrorResponse {
|
|
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
|
|
// StructureSize (2 bytes)
|
|
let structure_size = cursor.read_u16_le()?;
|
|
if structure_size != Self::STRUCTURE_SIZE {
|
|
return Err(Error::invalid_data(format!(
|
|
"invalid ErrorResponse structure size: expected {}, got {}",
|
|
Self::STRUCTURE_SIZE,
|
|
structure_size
|
|
)));
|
|
}
|
|
|
|
// ErrorContextCount (1 byte)
|
|
let error_context_count = cursor.read_u8()?;
|
|
|
|
// Reserved (1 byte)
|
|
let _reserved = cursor.read_u8()?;
|
|
|
|
// ByteCount (4 bytes)
|
|
let byte_count = cursor.read_u32_le()? as usize;
|
|
|
|
// ErrorData (variable)
|
|
let error_data = if byte_count > 0 {
|
|
cursor.read_bytes_bounded(byte_count)?.to_vec()
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
|
|
Ok(ErrorResponse {
|
|
error_context_count,
|
|
error_data,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
// ── Header tests ────────────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn pack_request_header_produces_64_bytes_with_correct_magic() {
|
|
let header = Header::new_request(Command::Negotiate);
|
|
let mut cursor = WriteCursor::new();
|
|
header.pack(&mut cursor);
|
|
let bytes = cursor.into_inner();
|
|
|
|
assert_eq!(bytes.len(), Header::SIZE);
|
|
assert_eq!(&bytes[0..4], &PROTOCOL_ID);
|
|
}
|
|
|
|
#[test]
|
|
fn unpack_known_64_byte_buffer() {
|
|
// Build a known buffer manually: sync Negotiate request
|
|
let mut buf = [0u8; 64];
|
|
// ProtocolId
|
|
buf[0..4].copy_from_slice(&PROTOCOL_ID);
|
|
// StructureSize = 64
|
|
buf[4..6].copy_from_slice(&64u16.to_le_bytes());
|
|
// CreditCharge = 1
|
|
buf[6..8].copy_from_slice(&1u16.to_le_bytes());
|
|
// Status = SUCCESS (0)
|
|
buf[8..12].copy_from_slice(&0u32.to_le_bytes());
|
|
// Command = Negotiate (0)
|
|
buf[12..14].copy_from_slice(&0u16.to_le_bytes());
|
|
// Credits = 31
|
|
buf[14..16].copy_from_slice(&31u16.to_le_bytes());
|
|
// Flags = 0 (sync, request)
|
|
buf[16..20].copy_from_slice(&0u32.to_le_bytes());
|
|
// NextCommand = 0
|
|
buf[20..24].copy_from_slice(&0u32.to_le_bytes());
|
|
// MessageId = 42
|
|
buf[24..32].copy_from_slice(&42u64.to_le_bytes());
|
|
// Reserved = 0
|
|
buf[32..36].copy_from_slice(&0u32.to_le_bytes());
|
|
// TreeId = 7
|
|
buf[36..40].copy_from_slice(&7u32.to_le_bytes());
|
|
// SessionId = 0x1234
|
|
buf[40..48].copy_from_slice(&0x1234u64.to_le_bytes());
|
|
// Signature = all zeros
|
|
// (already zero)
|
|
|
|
let mut cursor = ReadCursor::new(&buf);
|
|
let header = Header::unpack(&mut cursor).unwrap();
|
|
|
|
assert_eq!(header.credit_charge, CreditCharge(1));
|
|
assert_eq!(header.status, NtStatus::SUCCESS);
|
|
assert_eq!(header.command, Command::Negotiate);
|
|
assert_eq!(header.credits, 31);
|
|
assert!(!header.flags.is_async());
|
|
assert!(!header.flags.is_response());
|
|
assert_eq!(header.next_command, 0);
|
|
assert_eq!(header.message_id, MessageId(42));
|
|
assert_eq!(header.tree_id, Some(TreeId(7)));
|
|
assert_eq!(header.async_id, None);
|
|
assert_eq!(header.session_id, SessionId(0x1234));
|
|
assert_eq!(header.signature, [0u8; 16]);
|
|
}
|
|
|
|
#[test]
|
|
fn roundtrip_sync_header() {
|
|
let original = Header {
|
|
credit_charge: CreditCharge(3),
|
|
status: NtStatus::ACCESS_DENIED,
|
|
command: Command::Read,
|
|
credits: 10,
|
|
flags: {
|
|
let mut f = HeaderFlags::default();
|
|
f.set_response();
|
|
f
|
|
},
|
|
next_command: 0,
|
|
message_id: MessageId(99),
|
|
tree_id: Some(TreeId(42)),
|
|
async_id: None,
|
|
session_id: SessionId(0xDEAD_BEEF),
|
|
signature: [
|
|
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
|
|
0x0F, 0x10,
|
|
],
|
|
};
|
|
|
|
let mut w = WriteCursor::new();
|
|
original.pack(&mut w);
|
|
let bytes = w.into_inner();
|
|
assert_eq!(bytes.len(), Header::SIZE);
|
|
|
|
let mut r = ReadCursor::new(&bytes);
|
|
let decoded = Header::unpack(&mut r).unwrap();
|
|
|
|
assert_eq!(decoded.credit_charge, original.credit_charge);
|
|
assert_eq!(decoded.status, original.status);
|
|
assert_eq!(decoded.command, original.command);
|
|
assert_eq!(decoded.credits, original.credits);
|
|
assert_eq!(decoded.flags.bits(), original.flags.bits());
|
|
assert_eq!(decoded.next_command, original.next_command);
|
|
assert_eq!(decoded.message_id, original.message_id);
|
|
assert_eq!(decoded.tree_id, original.tree_id);
|
|
assert_eq!(decoded.async_id, original.async_id);
|
|
assert_eq!(decoded.session_id, original.session_id);
|
|
assert_eq!(decoded.signature, original.signature);
|
|
}
|
|
|
|
#[test]
|
|
fn wrong_magic_bytes_returns_error() {
|
|
let mut buf = [0u8; 64];
|
|
// Wrong magic
|
|
buf[0..4].copy_from_slice(&[0xFF, b'X', b'Y', b'Z']);
|
|
buf[4..6].copy_from_slice(&64u16.to_le_bytes());
|
|
|
|
let mut cursor = ReadCursor::new(&buf);
|
|
let result = Header::unpack(&mut cursor);
|
|
assert!(result.is_err());
|
|
let err = result.unwrap_err().to_string();
|
|
assert!(err.contains("protocol ID"), "error was: {err}");
|
|
}
|
|
|
|
#[test]
|
|
fn wrong_structure_size_returns_error() {
|
|
let mut buf = [0u8; 64];
|
|
buf[0..4].copy_from_slice(&PROTOCOL_ID);
|
|
// Wrong structure size
|
|
buf[4..6].copy_from_slice(&32u16.to_le_bytes());
|
|
|
|
let mut cursor = ReadCursor::new(&buf);
|
|
let result = Header::unpack(&mut cursor);
|
|
assert!(result.is_err());
|
|
let err = result.unwrap_err().to_string();
|
|
assert!(err.contains("structure size"), "error was: {err}");
|
|
}
|
|
|
|
#[test]
|
|
fn async_header_pack_unpack() {
|
|
let mut flags = HeaderFlags::default();
|
|
flags.set_async();
|
|
flags.set_response();
|
|
|
|
let original = Header {
|
|
credit_charge: CreditCharge(0),
|
|
status: NtStatus::PENDING,
|
|
command: Command::ChangeNotify,
|
|
credits: 1,
|
|
flags,
|
|
next_command: 0,
|
|
message_id: MessageId(8),
|
|
tree_id: None,
|
|
async_id: Some(0x0000_0000_0000_0008),
|
|
session_id: SessionId(0x0000_0000_0853_27D7),
|
|
signature: [0u8; 16],
|
|
};
|
|
|
|
let mut w = WriteCursor::new();
|
|
original.pack(&mut w);
|
|
let bytes = w.into_inner();
|
|
assert_eq!(bytes.len(), Header::SIZE);
|
|
|
|
let mut r = ReadCursor::new(&bytes);
|
|
let decoded = Header::unpack(&mut r).unwrap();
|
|
|
|
assert!(decoded.flags.is_async());
|
|
assert_eq!(decoded.async_id, Some(8));
|
|
assert_eq!(decoded.tree_id, None);
|
|
assert_eq!(decoded.command, Command::ChangeNotify);
|
|
assert_eq!(decoded.status, NtStatus::PENDING);
|
|
assert_eq!(decoded.session_id, SessionId(0x0000_0000_0853_27D7));
|
|
}
|
|
|
|
#[test]
|
|
fn sync_header_has_tree_id_and_no_async_id() {
|
|
let header = Header::new_request(Command::Create);
|
|
|
|
let mut w = WriteCursor::new();
|
|
header.pack(&mut w);
|
|
let bytes = w.into_inner();
|
|
|
|
let mut r = ReadCursor::new(&bytes);
|
|
let decoded = Header::unpack(&mut r).unwrap();
|
|
|
|
assert!(!decoded.flags.is_async());
|
|
assert!(decoded.tree_id.is_some());
|
|
assert_eq!(decoded.async_id, None);
|
|
}
|
|
|
|
#[test]
|
|
fn signature_field_preserved() {
|
|
let sig = [
|
|
0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
|
|
0x99, 0x00,
|
|
];
|
|
let mut header = Header::new_request(Command::Echo);
|
|
header.signature = sig;
|
|
|
|
let mut w = WriteCursor::new();
|
|
header.pack(&mut w);
|
|
let bytes = w.into_inner();
|
|
|
|
let mut r = ReadCursor::new(&bytes);
|
|
let decoded = Header::unpack(&mut r).unwrap();
|
|
|
|
assert_eq!(decoded.signature, sig);
|
|
}
|
|
|
|
#[test]
|
|
fn new_request_produces_correct_defaults() {
|
|
let header = Header::new_request(Command::Write);
|
|
|
|
assert_eq!(header.command, Command::Write);
|
|
assert_eq!(header.credit_charge, CreditCharge(0));
|
|
assert_eq!(header.status, NtStatus::SUCCESS);
|
|
assert_eq!(header.credits, 1);
|
|
assert!(!header.flags.is_response());
|
|
assert!(!header.flags.is_async());
|
|
assert_eq!(header.next_command, 0);
|
|
assert_eq!(header.message_id, MessageId(0));
|
|
assert_eq!(header.tree_id, Some(TreeId(0)));
|
|
assert_eq!(header.async_id, None);
|
|
assert_eq!(header.session_id, SessionId(0));
|
|
assert_eq!(header.signature, [0u8; 16]);
|
|
assert!(!header.is_response());
|
|
}
|
|
|
|
// ── ErrorResponse tests ─────────────────────────────────────────
|
|
|
|
#[test]
|
|
fn error_response_pack_unpack_empty() {
|
|
let original = ErrorResponse {
|
|
error_context_count: 0,
|
|
error_data: Vec::new(),
|
|
};
|
|
|
|
let mut w = WriteCursor::new();
|
|
original.pack(&mut w);
|
|
let bytes = w.into_inner();
|
|
|
|
// StructureSize(2) + ErrorContextCount(1) + Reserved(1) + ByteCount(4) = 8
|
|
assert_eq!(bytes.len(), 8);
|
|
|
|
let mut r = ReadCursor::new(&bytes);
|
|
let decoded = ErrorResponse::unpack(&mut r).unwrap();
|
|
|
|
assert_eq!(decoded.error_context_count, 0);
|
|
assert!(decoded.error_data.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn error_response_pack_unpack_with_data() {
|
|
let data = vec![0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE];
|
|
let original = ErrorResponse {
|
|
error_context_count: 1,
|
|
error_data: data.clone(),
|
|
};
|
|
|
|
let mut w = WriteCursor::new();
|
|
original.pack(&mut w);
|
|
let bytes = w.into_inner();
|
|
|
|
// 8 bytes fixed + 6 bytes data
|
|
assert_eq!(bytes.len(), 14);
|
|
|
|
let mut r = ReadCursor::new(&bytes);
|
|
let decoded = ErrorResponse::unpack(&mut r).unwrap();
|
|
|
|
assert_eq!(decoded.error_context_count, 1);
|
|
assert_eq!(decoded.error_data, data);
|
|
}
|
|
|
|
#[test]
|
|
fn error_response_roundtrip() {
|
|
let original = ErrorResponse {
|
|
error_context_count: 2,
|
|
error_data: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
|
};
|
|
|
|
let mut w = WriteCursor::new();
|
|
original.pack(&mut w);
|
|
let bytes = w.into_inner();
|
|
|
|
let mut r = ReadCursor::new(&bytes);
|
|
let decoded = ErrorResponse::unpack(&mut r).unwrap();
|
|
|
|
assert_eq!(decoded.error_context_count, original.error_context_count);
|
|
assert_eq!(decoded.error_data, original.error_data);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod roundtrip_props {
|
|
use super::*;
|
|
use crate::msg::roundtrip_strategies::{
|
|
arb_command, arb_credit_charge, arb_header_flags, arb_message_id, arb_nt_status,
|
|
arb_session_id, arb_small_bytes, arb_tree_id,
|
|
};
|
|
use proptest::prelude::*;
|
|
|
|
/// Generate a `Header` whose `flags.is_async()` matches which of
|
|
/// `tree_id`/`async_id` is set. Any other combination wouldn't round-trip
|
|
/// (pack writes one or the other based on flags, and clears the other on
|
|
/// unpack), so we never generate it.
|
|
fn arb_header() -> impl Strategy<Value = Header> {
|
|
(
|
|
arb_credit_charge(),
|
|
arb_nt_status(),
|
|
arb_command(),
|
|
any::<u16>(),
|
|
arb_header_flags(),
|
|
any::<u32>(),
|
|
arb_message_id(),
|
|
any::<bool>(),
|
|
arb_tree_id(),
|
|
any::<u64>(),
|
|
arb_session_id(),
|
|
any::<[u8; 16]>(),
|
|
)
|
|
.prop_map(
|
|
|(
|
|
credit_charge,
|
|
status,
|
|
command,
|
|
credits,
|
|
raw_flags,
|
|
next_command,
|
|
message_id,
|
|
make_async,
|
|
tree_id,
|
|
async_id,
|
|
session_id,
|
|
signature,
|
|
)| {
|
|
// Force `flags.ASYNC_COMMAND` to match `make_async` so
|
|
// the pack path and the `Option<T>` fields agree.
|
|
let flags = if make_async {
|
|
let mut f = raw_flags;
|
|
f.set(HeaderFlags::ASYNC_COMMAND);
|
|
f
|
|
} else {
|
|
let mut f = raw_flags;
|
|
f.clear(HeaderFlags::ASYNC_COMMAND);
|
|
f
|
|
};
|
|
let (tree_id, async_id) = if make_async {
|
|
(None, Some(async_id))
|
|
} else {
|
|
(Some(tree_id), None)
|
|
};
|
|
Header {
|
|
credit_charge,
|
|
status,
|
|
command,
|
|
credits,
|
|
flags,
|
|
next_command,
|
|
message_id,
|
|
tree_id,
|
|
async_id,
|
|
session_id,
|
|
signature,
|
|
}
|
|
},
|
|
)
|
|
}
|
|
|
|
proptest! {
|
|
#[test]
|
|
fn header_pack_unpack(header in arb_header()) {
|
|
let mut w = WriteCursor::new();
|
|
header.pack(&mut w);
|
|
let bytes = w.into_inner();
|
|
prop_assert_eq!(bytes.len(), Header::SIZE);
|
|
|
|
let mut r = ReadCursor::new(&bytes);
|
|
let decoded = Header::unpack(&mut r).unwrap();
|
|
prop_assert_eq!(decoded, header);
|
|
prop_assert!(r.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn error_response_pack_unpack(
|
|
error_context_count in any::<u8>(),
|
|
error_data in arb_small_bytes(),
|
|
) {
|
|
let original = ErrorResponse {
|
|
error_context_count,
|
|
error_data,
|
|
};
|
|
let mut w = WriteCursor::new();
|
|
original.pack(&mut w);
|
|
let bytes = w.into_inner();
|
|
|
|
let mut r = ReadCursor::new(&bytes);
|
|
let decoded = ErrorResponse::unpack(&mut r).unwrap();
|
|
prop_assert_eq!(decoded, original);
|
|
prop_assert!(r.is_empty());
|
|
}
|
|
}
|
|
}
|