SMB Server Phase 2: VFS backend build fix + integration test
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

- 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)
This commit is contained in:
Warren
2026-06-20 19:42:29 +08:00
parent 45d050c0b3
commit 7eb528d35f
167 changed files with 59897 additions and 12 deletions

44
vendor/smb2/src/msg/CLAUDE.md vendored Normal file
View File

@@ -0,0 +1,44 @@
# Msg -- wire format message structs
One sub-module per SMB2 command. Each defines request and response structs with `Pack` and `Unpack` implementations.
## Key files
| File | Purpose |
|---|---|
| `mod.rs` | `trivial_message!` macro for 4-byte stub messages, module declarations |
| `header.rs` | 64-byte SMB2 header (sync + async variants), `PROTOCOL_ID` (`0xFE 'S' 'M' 'B'`) |
| `negotiate.rs` | Negotiate contexts (preauth integrity, encryption, signing, compression) |
| `create.rs` | CREATE request/response with create contexts |
| `transform.rs` | `TransformHeader` (encryption, protocol ID `0xFD`), `CompressionTransformHeader` (`0xFC`) |
19 command modules total: negotiate, session_setup, logoff, tree_connect, tree_disconnect, create, close, flush, read, write, lock, ioctl, query_directory, change_notify, query_info, set_info, echo, cancel, oplock_break. Plus `dfs.rs` for DFS referral request/response wire format (used by IOCTL FSCTL_DFS_GET_REFERRALS).
## Patterns
- **Pack/Unpack**: All structs implement `pack(&self, &mut WriteCursor)` and `unpack(&mut ReadCursor) -> Result<Self>`. Hand-rolled, no proc macros.
- **Offset calculation**: All offsets in SMB2 are relative to the start of the SMB2 header (not the body, not the transport frame). When packing variable-length fields, compute `header_size + fixed_body_size` as the base offset.
- **StructureSize validation**: `Unpack` implementations read `StructureSize` first and return an error if it doesn't match the expected value.
- **`trivial_message!` macro**: Generates Pack/Unpack for 4-byte stub messages (StructureSize=4 + Reserved=0). Used by echo, cancel, logoff, tree_disconnect.
## Compound messages
Built by `Connection::send_compound`. Each sub-request's header has a `NextCommand` field pointing to the next message (8-byte aligned). The last message has `NextCommand = 0`. Related operations use `FileId::SENTINEL` (`0xFFFFFFFF:0xFFFFFFFF`) so the server substitutes the handle from the first CREATE.
## Transform headers
- **Encryption** (`0xFD 'S' 'M' 'B'`): 52-byte `TransformHeader` wraps encrypted message(s). Contains nonce, auth tag (signature), original message size, session ID.
- **Compression** (`0xFC 'S' 'M' 'B'`): `CompressionTransformHeader` wraps LZ4-compressed messages. Contains original and compressed sizes, algorithm ID.
## Gotchas
- **TCP framing is big-endian**: The 4-byte transport header (1 zero byte + 3-byte length) uses big-endian byte order. Everything inside the SMB2 message is little-endian. This is the only big-endian value in the entire protocol.
- **StructureSize is "fixed"**: The spec says StructureSize is the size of the fixed-length portion of the struct. It does NOT include variable-length buffers. It's validated on unpack.
- **`#![allow(missing_docs)]`**: This module opts out of doc requirements because wire format field names are self-documenting from the spec.
- **Manual offset arithmetic requires careful bounds**: In `dfs.rs`, `parse_referral_entry` uses `ensure_remaining(buf, pos, N)` before raw `buf[pos..]` reads. Count the fixed fields carefully -- V2's body is **18** bytes (server_type+flags+proximity+ttl + three u16 offsets), not 16. An off-by-2 here lets a malformed `entry_size` slip past the initial guard and panic on the last offset read. Fuzz-caught in 0.7.2; regression test `resp_parse_v2_short_entry_returns_clean_error`.
## Fuzzing
Parse entry points are exposed via the `fuzzing` feature (`smb2::fuzzing`) and exercised by the `fuzz/` crate. See
`fuzz/README.md` (if present) or run `just fuzz fuzz_header_parse 300` for a local sweep. Every new parser touching
external bytes should get a fuzz target wrapper added in `src/fuzzing.rs` and a matching `fuzz/fuzz_targets/*.rs`.

27
vendor/smb2/src/msg/cancel.rs vendored Normal file
View File

@@ -0,0 +1,27 @@
//! SMB2 CANCEL request (spec section 2.2.30).
//!
//! The CANCEL request is fire-and-forget: the client sends it to cancel a
//! previously sent message, and there is no corresponding response message.
//! The MessageId of the request to cancel is set in the SMB2 header.
super::trivial_message! {
/// SMB2 CANCEL request (spec section 2.2.30).
///
/// Sent by the client to cancel a previously sent message on the same
/// transport connection. There is no response for this command.
/// Contains only StructureSize (2 bytes) and Reserved (2 bytes).
pub struct CancelRequest;
}
#[cfg(test)]
mod tests {
use super::*;
super::super::trivial_message_tests!(
CancelRequest,
cancel_request_known_bytes,
cancel_request_roundtrip,
cancel_request_wrong_structure_size,
cancel_request_too_short
);
}

355
vendor/smb2/src/msg/change_notify.rs vendored Normal file
View File

@@ -0,0 +1,355 @@
//! SMB2 CHANGE_NOTIFY Request and Response (MS-SMB2 sections 2.2.35, 2.2.36).
//!
//! The CHANGE_NOTIFY request registers for change notifications on a
//! directory. The response returns FILE_NOTIFY_INFORMATION entries
//! describing the changes that occurred.
use crate::error::Result;
use crate::pack::{Pack, ReadCursor, Unpack, WriteCursor};
use crate::types::FileId;
use crate::Error;
// ── Change Notify flags ────────────────────────────────────────────────
/// Watch the entire subtree (recursive).
pub const SMB2_WATCH_TREE: u16 = 0x0001;
// ── CompletionFilter values ────────────────────────────────────────────
/// Notify when a file name changes.
pub const FILE_NOTIFY_CHANGE_FILE_NAME: u32 = 0x0000_0001;
/// Notify when a directory name changes.
pub const FILE_NOTIFY_CHANGE_DIR_NAME: u32 = 0x0000_0002;
/// Notify when file attributes change.
pub const FILE_NOTIFY_CHANGE_ATTRIBUTES: u32 = 0x0000_0004;
/// Notify when the file size changes.
pub const FILE_NOTIFY_CHANGE_SIZE: u32 = 0x0000_0008;
/// Notify when the last write time changes.
pub const FILE_NOTIFY_CHANGE_LAST_WRITE: u32 = 0x0000_0010;
/// Notify when the last access time changes.
pub const FILE_NOTIFY_CHANGE_LAST_ACCESS: u32 = 0x0000_0020;
/// Notify when the creation time changes.
pub const FILE_NOTIFY_CHANGE_CREATION: u32 = 0x0000_0040;
/// Notify when extended attributes change.
pub const FILE_NOTIFY_CHANGE_EA: u32 = 0x0000_0080;
/// Notify when the security descriptor changes.
pub const FILE_NOTIFY_CHANGE_SECURITY: u32 = 0x0000_0100;
/// Notify when a stream name changes.
pub const FILE_NOTIFY_CHANGE_STREAM_NAME: u32 = 0x0000_0200;
/// Notify when a stream size changes.
pub const FILE_NOTIFY_CHANGE_STREAM_SIZE: u32 = 0x0000_0400;
/// Notify when stream data is written.
pub const FILE_NOTIFY_CHANGE_STREAM_WRITE: u32 = 0x0000_0800;
// ── ChangeNotifyRequest ────────────────────────────────────────────────
/// SMB2 CHANGE_NOTIFY Request (MS-SMB2 section 2.2.35).
///
/// Registers for directory change notifications. The structure is 32 bytes:
/// - StructureSize (2 bytes, must be 32)
/// - Flags (2 bytes)
/// - OutputBufferLength (4 bytes)
/// - FileId (16 bytes)
/// - CompletionFilter (4 bytes)
/// - Reserved (4 bytes)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChangeNotifyRequest {
/// Flags controlling the notification. Use `SMB2_WATCH_TREE` for recursive.
pub flags: u16,
/// Maximum size of the output buffer for notification data.
pub output_buffer_length: u32,
/// The directory handle to watch.
pub file_id: FileId,
/// Bitmask of change types to watch for.
pub completion_filter: u32,
}
impl ChangeNotifyRequest {
pub const STRUCTURE_SIZE: u16 = 32;
}
impl Pack for ChangeNotifyRequest {
fn pack(&self, cursor: &mut WriteCursor) {
// StructureSize (2 bytes)
cursor.write_u16_le(Self::STRUCTURE_SIZE);
// Flags (2 bytes)
cursor.write_u16_le(self.flags);
// OutputBufferLength (4 bytes)
cursor.write_u32_le(self.output_buffer_length);
// FileId (16 bytes)
cursor.write_u64_le(self.file_id.persistent);
cursor.write_u64_le(self.file_id.volatile);
// CompletionFilter (4 bytes)
cursor.write_u32_le(self.completion_filter);
// Reserved (4 bytes)
cursor.write_u32_le(0);
}
}
impl Unpack for ChangeNotifyRequest {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let structure_size = cursor.read_u16_le()?;
if structure_size != Self::STRUCTURE_SIZE {
return Err(Error::invalid_data(format!(
"invalid ChangeNotifyRequest structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
let flags = cursor.read_u16_le()?;
let output_buffer_length = cursor.read_u32_le()?;
let persistent = cursor.read_u64_le()?;
let volatile = cursor.read_u64_le()?;
let completion_filter = cursor.read_u32_le()?;
let _reserved = cursor.read_u32_le()?;
Ok(ChangeNotifyRequest {
flags,
output_buffer_length,
file_id: FileId {
persistent,
volatile,
},
completion_filter,
})
}
}
// ── ChangeNotifyResponse ───────────────────────────────────────────────
/// SMB2 CHANGE_NOTIFY Response (MS-SMB2 section 2.2.36).
///
/// Returns FILE_NOTIFY_INFORMATION entries describing directory changes.
/// The buffer contains raw FILE_NOTIFY_INFORMATION entries; parsing those
/// is left to the caller for now.
///
/// Layout:
/// - StructureSize (2 bytes, must be 9)
/// - OutputBufferOffset (2 bytes)
/// - OutputBufferLength (4 bytes)
/// - Buffer (variable, OutputBufferLength bytes)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChangeNotifyResponse {
/// Raw FILE_NOTIFY_INFORMATION data. Parsing individual entries is
/// deferred to a higher layer.
pub output_data: Vec<u8>,
}
impl ChangeNotifyResponse {
pub const STRUCTURE_SIZE: u16 = 9;
/// Fixed header size before the variable buffer (8 bytes).
const FIXED_SIZE: u32 = 8;
}
impl Pack for ChangeNotifyResponse {
fn pack(&self, cursor: &mut WriteCursor) {
let start = cursor.position();
// StructureSize (2 bytes)
cursor.write_u16_le(Self::STRUCTURE_SIZE);
let output_len = self.output_data.len() as u32;
// Offset is from the beginning of the SMB2 header per spec.
let output_offset = if output_len > 0 {
(start as u32) + Self::FIXED_SIZE
} else {
0
};
// OutputBufferOffset (2 bytes)
cursor.write_u16_le(output_offset as u16);
// OutputBufferLength (4 bytes)
cursor.write_u32_le(output_len);
// Buffer (variable)
cursor.write_bytes(&self.output_data);
}
}
impl Unpack for ChangeNotifyResponse {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let structure_size = cursor.read_u16_le()?;
if structure_size != Self::STRUCTURE_SIZE {
return Err(Error::invalid_data(format!(
"invalid ChangeNotifyResponse structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
let _output_buffer_offset = cursor.read_u16_le()?;
let output_buffer_length = cursor.read_u32_le()?;
let output_data = if output_buffer_length > 0 {
cursor
.read_bytes_bounded(output_buffer_length as usize)?
.to_vec()
} else {
Vec::new()
};
Ok(ChangeNotifyResponse { output_data })
}
}
#[cfg(test)]
mod tests {
use super::*;
// ── ChangeNotifyRequest tests ─────────────────────────────────────
#[test]
fn change_notify_request_roundtrip_recursive() {
let original = ChangeNotifyRequest {
flags: SMB2_WATCH_TREE,
output_buffer_length: 65536,
file_id: FileId {
persistent: 0x1122_3344_5566_7788,
volatile: 0xAABB_CCDD_EEFF_0011,
},
completion_filter: FILE_NOTIFY_CHANGE_FILE_NAME
| FILE_NOTIFY_CHANGE_DIR_NAME
| FILE_NOTIFY_CHANGE_LAST_WRITE,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// Fixed 32 bytes, no variable data
assert_eq!(bytes.len(), 32);
let mut r = ReadCursor::new(&bytes);
let decoded = ChangeNotifyRequest::unpack(&mut r).unwrap();
assert_eq!(decoded.flags, SMB2_WATCH_TREE);
assert_eq!(decoded.output_buffer_length, 65536);
assert_eq!(decoded.file_id, original.file_id);
assert_eq!(
decoded.completion_filter,
FILE_NOTIFY_CHANGE_FILE_NAME
| FILE_NOTIFY_CHANGE_DIR_NAME
| FILE_NOTIFY_CHANGE_LAST_WRITE
);
}
#[test]
fn change_notify_request_wrong_structure_size() {
let mut buf = [0u8; 32];
buf[0..2].copy_from_slice(&99u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = ChangeNotifyRequest::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
// ── ChangeNotifyResponse tests ────────────────────────────────────
#[test]
fn change_notify_response_roundtrip_with_data() {
let notify_data = vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
let original = ChangeNotifyResponse {
output_data: notify_data.clone(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// Fixed 8 bytes + 8 bytes data
assert_eq!(bytes.len(), 16);
let mut r = ReadCursor::new(&bytes);
let decoded = ChangeNotifyResponse::unpack(&mut r).unwrap();
assert_eq!(decoded.output_data, notify_data);
}
#[test]
fn change_notify_response_roundtrip_empty() {
let original = ChangeNotifyResponse {
output_data: Vec::new(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
assert_eq!(bytes.len(), 8);
let mut r = ReadCursor::new(&bytes);
let decoded = ChangeNotifyResponse::unpack(&mut r).unwrap();
assert!(decoded.output_data.is_empty());
}
#[test]
fn change_notify_response_wrong_structure_size() {
let mut buf = [0u8; 8];
buf[0..2].copy_from_slice(&42u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = ChangeNotifyResponse::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
}
#[cfg(test)]
mod roundtrip_props {
use super::*;
use crate::msg::roundtrip_strategies::{arb_bytes, arb_file_id};
use proptest::prelude::*;
proptest! {
#[test]
fn change_notify_request_pack_unpack(
flags in any::<u16>(),
output_buffer_length in any::<u32>(),
file_id in arb_file_id(),
completion_filter in any::<u32>(),
) {
let original = ChangeNotifyRequest {
flags,
output_buffer_length,
file_id,
completion_filter,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = ChangeNotifyRequest::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
prop_assert!(r.is_empty());
}
#[test]
fn change_notify_response_pack_unpack(output_data in arb_bytes()) {
let original = ChangeNotifyResponse { output_data };
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = ChangeNotifyResponse::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
prop_assert!(r.is_empty());
}
}
}

390
vendor/smb2/src/msg/close.rs vendored Normal file
View File

@@ -0,0 +1,390 @@
//! SMB2 CLOSE Request and Response (MS-SMB2 sections 2.2.15, 2.2.16).
//!
//! The CLOSE request closes a file handle previously opened via CREATE.
//! The response optionally returns file attributes if the
//! `SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB` flag was set.
use crate::error::Result;
use crate::pack::{FileTime, Pack, ReadCursor, Unpack, WriteCursor};
use crate::types::FileId;
use crate::Error;
/// Close flag: request that the server returns file attributes in the response.
pub const SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB: u16 = 0x0001;
/// SMB2 CLOSE Request (MS-SMB2 section 2.2.15).
///
/// Sent by the client to close a file handle. The structure is 24 bytes:
/// - StructureSize (2 bytes, must be 24)
/// - Flags (2 bytes)
/// - Reserved (4 bytes)
/// - FileId (16 bytes)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CloseRequest {
/// Flags indicating how to process the close.
/// Use `SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB` to request attributes.
pub flags: u16,
/// The file handle to close.
pub file_id: FileId,
}
impl CloseRequest {
pub const STRUCTURE_SIZE: u16 = 24;
}
impl Pack for CloseRequest {
fn pack(&self, cursor: &mut WriteCursor) {
// StructureSize (2 bytes)
cursor.write_u16_le(Self::STRUCTURE_SIZE);
// Flags (2 bytes)
cursor.write_u16_le(self.flags);
// Reserved (4 bytes)
cursor.write_u32_le(0);
// FileId (16 bytes): persistent + volatile
cursor.write_u64_le(self.file_id.persistent);
cursor.write_u64_le(self.file_id.volatile);
}
}
impl Unpack for CloseRequest {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let structure_size = cursor.read_u16_le()?;
if structure_size != Self::STRUCTURE_SIZE {
return Err(Error::invalid_data(format!(
"invalid CloseRequest structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
let flags = cursor.read_u16_le()?;
let _reserved = cursor.read_u32_le()?;
let persistent = cursor.read_u64_le()?;
let volatile = cursor.read_u64_le()?;
Ok(CloseRequest {
flags,
file_id: FileId {
persistent,
volatile,
},
})
}
}
/// SMB2 CLOSE Response (MS-SMB2 section 2.2.16).
///
/// Sent by the server to confirm a close. The structure is 60 bytes:
/// - StructureSize (2 bytes, must be 60)
/// - Flags (2 bytes)
/// - Reserved (4 bytes)
/// - CreationTime (8 bytes)
/// - LastAccessTime (8 bytes)
/// - LastWriteTime (8 bytes)
/// - ChangeTime (8 bytes)
/// - AllocationSize (8 bytes)
/// - EndOfFile (8 bytes)
/// - FileAttributes (4 bytes)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CloseResponse {
/// Flags echoed from the request. If `SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB`
/// is set, the attribute fields below contain valid data.
pub flags: u16,
/// File creation time.
pub creation_time: FileTime,
/// Last access time.
pub last_access_time: FileTime,
/// Last write time.
pub last_write_time: FileTime,
/// Change time.
pub change_time: FileTime,
/// Size of allocated data in bytes.
pub allocation_size: u64,
/// End-of-file position in bytes.
pub end_of_file: u64,
/// File attributes (see MS-FSCC section 2.6).
pub file_attributes: u32,
}
impl CloseResponse {
pub const STRUCTURE_SIZE: u16 = 60;
}
impl Pack for CloseResponse {
fn pack(&self, cursor: &mut WriteCursor) {
cursor.write_u16_le(Self::STRUCTURE_SIZE);
cursor.write_u16_le(self.flags);
cursor.write_u32_le(0); // Reserved
self.creation_time.pack(cursor);
self.last_access_time.pack(cursor);
self.last_write_time.pack(cursor);
self.change_time.pack(cursor);
cursor.write_u64_le(self.allocation_size);
cursor.write_u64_le(self.end_of_file);
cursor.write_u32_le(self.file_attributes);
}
}
impl Unpack for CloseResponse {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let structure_size = cursor.read_u16_le()?;
if structure_size != Self::STRUCTURE_SIZE {
return Err(Error::invalid_data(format!(
"invalid CloseResponse structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
let flags = cursor.read_u16_le()?;
let _reserved = cursor.read_u32_le()?;
let creation_time = FileTime::unpack(cursor)?;
let last_access_time = FileTime::unpack(cursor)?;
let last_write_time = FileTime::unpack(cursor)?;
let change_time = FileTime::unpack(cursor)?;
let allocation_size = cursor.read_u64_le()?;
let end_of_file = cursor.read_u64_le()?;
let file_attributes = cursor.read_u32_le()?;
Ok(CloseResponse {
flags,
creation_time,
last_access_time,
last_write_time,
change_time,
allocation_size,
end_of_file,
file_attributes,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
// ── CloseRequest tests ─────────────────────────────────────────
#[test]
fn close_request_roundtrip() {
let original = CloseRequest {
flags: SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB,
file_id: FileId {
persistent: 0x1122_3344_5566_7788,
volatile: 0xAABB_CCDD_EEFF_0011,
},
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// 2 + 2 + 4 + 16 = 24 bytes
assert_eq!(bytes.len(), 24);
let mut r = ReadCursor::new(&bytes);
let decoded = CloseRequest::unpack(&mut r).unwrap();
assert_eq!(decoded.flags, original.flags);
assert_eq!(decoded.file_id, original.file_id);
}
#[test]
fn close_request_known_bytes() {
let mut buf = [0u8; 24];
// StructureSize = 24
buf[0..2].copy_from_slice(&24u16.to_le_bytes());
// Flags = 0x0001
buf[2..4].copy_from_slice(&1u16.to_le_bytes());
// Reserved = 0
buf[4..8].copy_from_slice(&0u32.to_le_bytes());
// FileId persistent = 0x42
buf[8..16].copy_from_slice(&0x42u64.to_le_bytes());
// FileId volatile = 0x99
buf[16..24].copy_from_slice(&0x99u64.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let req = CloseRequest::unpack(&mut cursor).unwrap();
assert_eq!(req.flags, SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB);
assert_eq!(req.file_id.persistent, 0x42);
assert_eq!(req.file_id.volatile, 0x99);
}
#[test]
fn close_request_wrong_structure_size() {
let mut buf = [0u8; 24];
buf[0..2].copy_from_slice(&99u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = CloseRequest::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
// ── CloseResponse tests ────────────────────────────────────────
#[test]
fn close_response_roundtrip() {
let original = CloseResponse {
flags: SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB,
creation_time: FileTime(0x01D8_AAAA_BBBB_CCCC),
last_access_time: FileTime(0x01D8_DDDD_EEEE_FFFF),
last_write_time: FileTime(0x01D8_1111_2222_3333),
change_time: FileTime(0x01D8_4444_5555_6666),
allocation_size: 4096,
end_of_file: 2048,
file_attributes: 0x20, // FILE_ATTRIBUTE_ARCHIVE
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// 2 + 2 + 4 + 8*6 + 4 = 60 bytes
assert_eq!(bytes.len(), 60);
let mut r = ReadCursor::new(&bytes);
let decoded = CloseResponse::unpack(&mut r).unwrap();
assert_eq!(decoded.flags, original.flags);
assert_eq!(decoded.creation_time, original.creation_time);
assert_eq!(decoded.last_access_time, original.last_access_time);
assert_eq!(decoded.last_write_time, original.last_write_time);
assert_eq!(decoded.change_time, original.change_time);
assert_eq!(decoded.allocation_size, original.allocation_size);
assert_eq!(decoded.end_of_file, original.end_of_file);
assert_eq!(decoded.file_attributes, original.file_attributes);
}
#[test]
fn close_response_known_bytes() {
let mut buf = [0u8; 60];
// StructureSize = 60
buf[0..2].copy_from_slice(&60u16.to_le_bytes());
// Flags = 0x0001
buf[2..4].copy_from_slice(&1u16.to_le_bytes());
// Reserved = 0
buf[4..8].copy_from_slice(&0u32.to_le_bytes());
// CreationTime = 100
buf[8..16].copy_from_slice(&100u64.to_le_bytes());
// LastAccessTime = 200
buf[16..24].copy_from_slice(&200u64.to_le_bytes());
// LastWriteTime = 300
buf[24..32].copy_from_slice(&300u64.to_le_bytes());
// ChangeTime = 400
buf[32..40].copy_from_slice(&400u64.to_le_bytes());
// AllocationSize = 8192
buf[40..48].copy_from_slice(&8192u64.to_le_bytes());
// EndOfFile = 1024
buf[48..56].copy_from_slice(&1024u64.to_le_bytes());
// FileAttributes = 0x10 (directory)
buf[56..60].copy_from_slice(&0x10u32.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let resp = CloseResponse::unpack(&mut cursor).unwrap();
assert_eq!(resp.flags, SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB);
assert_eq!(resp.creation_time, FileTime(100));
assert_eq!(resp.last_access_time, FileTime(200));
assert_eq!(resp.last_write_time, FileTime(300));
assert_eq!(resp.change_time, FileTime(400));
assert_eq!(resp.allocation_size, 8192);
assert_eq!(resp.end_of_file, 1024);
assert_eq!(resp.file_attributes, 0x10);
}
#[test]
fn close_response_wrong_structure_size() {
let mut buf = [0u8; 60];
buf[0..2].copy_from_slice(&42u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = CloseResponse::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
#[test]
fn close_response_zero_flags_has_zeroed_attributes() {
let original = CloseResponse {
flags: 0,
creation_time: FileTime::ZERO,
last_access_time: FileTime::ZERO,
last_write_time: FileTime::ZERO,
change_time: FileTime::ZERO,
allocation_size: 0,
end_of_file: 0,
file_attributes: 0,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = CloseResponse::unpack(&mut r).unwrap();
assert_eq!(decoded.flags, 0);
assert_eq!(decoded.creation_time, FileTime::ZERO);
assert_eq!(decoded.file_attributes, 0);
}
}
#[cfg(test)]
mod roundtrip_props {
use super::*;
use crate::msg::roundtrip_strategies::{arb_file_id, arb_file_time};
use proptest::prelude::*;
proptest! {
#[test]
fn close_request_pack_unpack(
flags in any::<u16>(),
file_id in arb_file_id(),
) {
let original = CloseRequest { flags, file_id };
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = CloseRequest::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
prop_assert!(r.is_empty());
}
#[test]
fn close_response_pack_unpack(
flags in any::<u16>(),
creation_time in arb_file_time(),
last_access_time in arb_file_time(),
last_write_time in arb_file_time(),
change_time in arb_file_time(),
allocation_size in any::<u64>(),
end_of_file in any::<u64>(),
file_attributes in any::<u32>(),
) {
let original = CloseResponse {
flags,
creation_time,
last_access_time,
last_write_time,
change_time,
allocation_size,
end_of_file,
file_attributes,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = CloseResponse::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
prop_assert!(r.is_empty());
}
}
}

870
vendor/smb2/src/msg/create.rs vendored Normal file
View File

@@ -0,0 +1,870 @@
//! SMB2 CREATE request and response (spec sections 2.2.13, 2.2.14).
//!
//! The CREATE request opens or creates a file, named pipe, or printer.
//! The response carries the file handle ([`FileId`]) plus timestamps,
//! attributes, and optional create contexts.
use crate::error::Result;
use crate::msg::header::Header;
use crate::pack::{FileTime, Pack, ReadCursor, Unpack, WriteCursor};
use crate::types::flags::FileAccessMask;
use crate::types::{FileId, OplockLevel};
use crate::Error;
// ── Enums ────────────────────────────────────────────────────────────────
/// Impersonation level (MS-SMB2 2.2.13).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum ImpersonationLevel {
/// Anonymous impersonation.
Anonymous = 0,
/// Identification impersonation.
Identification = 1,
/// Impersonation level.
Impersonation = 2,
/// Delegate impersonation.
Delegate = 3,
}
impl TryFrom<u32> for ImpersonationLevel {
type Error = Error;
fn try_from(value: u32) -> Result<Self> {
match value {
0 => Ok(Self::Anonymous),
1 => Ok(Self::Identification),
2 => Ok(Self::Impersonation),
3 => Ok(Self::Delegate),
_ => Err(Error::invalid_data(format!(
"invalid ImpersonationLevel: {}",
value
))),
}
}
}
/// Share access flags (MS-SMB2 2.2.13).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ShareAccess(pub u32);
impl ShareAccess {
/// Allow other opens to read the file.
pub const FILE_SHARE_READ: u32 = 0x0000_0001;
/// Allow other opens to write the file.
pub const FILE_SHARE_WRITE: u32 = 0x0000_0002;
/// Allow other opens to delete the file.
pub const FILE_SHARE_DELETE: u32 = 0x0000_0004;
}
/// Create disposition (MS-SMB2 2.2.13).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum CreateDisposition {
/// If the file exists, supersede it. Otherwise, create.
FileSupersede = 0,
/// If the file exists, open it. Otherwise, fail.
FileOpen = 1,
/// If the file exists, fail. Otherwise, create.
FileCreate = 2,
/// If the file exists, open it. Otherwise, create.
FileOpenIf = 3,
/// If the file exists, overwrite it. Otherwise, fail.
FileOverwrite = 4,
/// If the file exists, overwrite it. Otherwise, create.
FileOverwriteIf = 5,
}
impl TryFrom<u32> for CreateDisposition {
type Error = Error;
fn try_from(value: u32) -> Result<Self> {
match value {
0 => Ok(Self::FileSupersede),
1 => Ok(Self::FileOpen),
2 => Ok(Self::FileCreate),
3 => Ok(Self::FileOpenIf),
4 => Ok(Self::FileOverwrite),
5 => Ok(Self::FileOverwriteIf),
_ => Err(Error::invalid_data(format!(
"invalid CreateDisposition: {}",
value
))),
}
}
}
/// Create action returned in the response (MS-SMB2 2.2.14).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum CreateAction {
/// An existing file was superseded.
FileSuperseded = 0,
/// An existing file was opened.
FileOpened = 1,
/// A new file was created.
FileCreated = 2,
/// An existing file was overwritten.
FileOverwritten = 3,
}
impl TryFrom<u32> for CreateAction {
type Error = Error;
fn try_from(value: u32) -> Result<Self> {
match value {
0 => Ok(Self::FileSuperseded),
1 => Ok(Self::FileOpened),
2 => Ok(Self::FileCreated),
3 => Ok(Self::FileOverwritten),
_ => Err(Error::invalid_data(format!(
"invalid CreateAction: {}",
value
))),
}
}
}
// ── CreateRequest ────────────────────────────────────────────────────────
/// SMB2 CREATE request (spec section 2.2.13).
///
/// Sent by the client to open or create a file on the server.
/// The buffer contains the filename encoded as UTF-16LE, optionally
/// followed by create context data.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateRequest {
/// Requested oplock level.
pub requested_oplock_level: OplockLevel,
/// Impersonation level.
pub impersonation_level: ImpersonationLevel,
/// Desired access rights.
pub desired_access: FileAccessMask,
/// File attributes for create/open.
pub file_attributes: u32,
/// Sharing mode.
pub share_access: ShareAccess,
/// Disposition: what to do if file exists/does not exist.
pub create_disposition: CreateDisposition,
/// Create options flags.
pub create_options: u32,
/// The filename to create or open.
pub name: String,
/// Raw create context bytes (unparsed).
pub create_contexts: Vec<u8>,
}
impl CreateRequest {
pub const STRUCTURE_SIZE: u16 = 57;
}
impl Pack for CreateRequest {
fn pack(&self, cursor: &mut WriteCursor) {
let start = cursor.position();
// StructureSize (2 bytes)
cursor.write_u16_le(Self::STRUCTURE_SIZE);
// SecurityFlags (1 byte) -- must be 0
cursor.write_u8(0);
// RequestedOplockLevel (1 byte)
cursor.write_u8(self.requested_oplock_level as u8);
// ImpersonationLevel (4 bytes)
cursor.write_u32_le(self.impersonation_level as u32);
// SmbCreateFlags (8 bytes) -- must be 0
cursor.write_u64_le(0);
// Reserved (8 bytes)
cursor.write_u64_le(0);
// DesiredAccess (4 bytes)
cursor.write_u32_le(self.desired_access.bits());
// FileAttributes (4 bytes)
cursor.write_u32_le(self.file_attributes);
// ShareAccess (4 bytes)
cursor.write_u32_le(self.share_access.0);
// CreateDisposition (4 bytes)
cursor.write_u32_le(self.create_disposition as u32);
// CreateOptions (4 bytes)
cursor.write_u32_le(self.create_options);
// NameOffset (2 bytes) -- placeholder, backpatch later
let name_offset_pos = cursor.position();
cursor.write_u16_le(0);
// NameLength (2 bytes) -- placeholder, backpatch later
let name_length_pos = cursor.position();
cursor.write_u16_le(0);
// CreateContextsOffset (4 bytes) -- placeholder
let ctx_offset_pos = cursor.position();
cursor.write_u32_le(0);
// CreateContextsLength (4 bytes) -- placeholder
let ctx_length_pos = cursor.position();
cursor.write_u32_le(0);
// Buffer: filename in UTF-16LE
// Offsets are from the beginning of the SMB2 header per spec.
let name_offset = Header::SIZE + (cursor.position() - start);
let name_start = cursor.position();
cursor.write_utf16_le(&self.name);
let name_byte_len = cursor.position() - name_start;
// Backpatch name offset and length
cursor.set_u16_le_at(name_offset_pos, name_offset as u16);
cursor.set_u16_le_at(name_length_pos, name_byte_len as u16);
// Create contexts (if any)
if !self.create_contexts.is_empty() {
// Align to 8-byte boundary before create contexts
cursor.align_to(8);
let ctx_offset = Header::SIZE + (cursor.position() - start);
cursor.write_bytes(&self.create_contexts);
let ctx_len = self.create_contexts.len();
cursor.set_u32_le_at(ctx_offset_pos, ctx_offset as u32);
cursor.set_u32_le_at(ctx_length_pos, ctx_len as u32);
} else if name_byte_len == 0 {
// Per spec, buffer must be at least 1 byte even if name is empty
cursor.write_u8(0);
}
}
}
impl Unpack for CreateRequest {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let start = cursor.position();
// StructureSize (2 bytes)
let structure_size = cursor.read_u16_le()?;
if structure_size != Self::STRUCTURE_SIZE {
return Err(Error::invalid_data(format!(
"invalid CreateRequest structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
// SecurityFlags (1 byte)
let _security_flags = cursor.read_u8()?;
// RequestedOplockLevel (1 byte)
let oplock_raw = cursor.read_u8()?;
let requested_oplock_level = OplockLevel::try_from(oplock_raw)?;
// ImpersonationLevel (4 bytes)
let imp_raw = cursor.read_u32_le()?;
let impersonation_level = ImpersonationLevel::try_from(imp_raw)?;
// SmbCreateFlags (8 bytes)
let _smb_create_flags = cursor.read_u64_le()?;
// Reserved (8 bytes)
let _reserved = cursor.read_u64_le()?;
// DesiredAccess (4 bytes)
let desired_access = FileAccessMask::new(cursor.read_u32_le()?);
// FileAttributes (4 bytes)
let file_attributes = cursor.read_u32_le()?;
// ShareAccess (4 bytes)
let share_access = ShareAccess(cursor.read_u32_le()?);
// CreateDisposition (4 bytes)
let disp_raw = cursor.read_u32_le()?;
let create_disposition = CreateDisposition::try_from(disp_raw)?;
// CreateOptions (4 bytes)
let create_options = cursor.read_u32_le()?;
// NameOffset (2 bytes)
let name_offset = cursor.read_u16_le()? as usize;
// NameLength (2 bytes)
let name_length = cursor.read_u16_le()? as usize;
// CreateContextsOffset (4 bytes)
let ctx_offset = cursor.read_u32_le()? as usize;
// CreateContextsLength (4 bytes)
let ctx_length = cursor.read_u32_le()? as usize;
// Read filename
// Offsets on the wire are from the beginning of the SMB2 header,
// so subtract Header::SIZE to get position within the body.
let name = if name_length > 0 {
let current = cursor.position();
let body_offset = name_offset.saturating_sub(Header::SIZE);
let target = start + body_offset;
if target > current {
cursor.skip(target - current)?;
}
cursor.read_utf16_le(name_length)?
} else {
String::new()
};
// Read create contexts
let create_contexts = if ctx_length > 0 {
let current = cursor.position();
let body_offset = ctx_offset.saturating_sub(Header::SIZE);
let target = start + body_offset;
if target > current {
cursor.skip(target - current)?;
}
cursor.read_bytes_bounded(ctx_length)?.to_vec()
} else {
Vec::new()
};
Ok(CreateRequest {
requested_oplock_level,
impersonation_level,
desired_access,
file_attributes,
share_access,
create_disposition,
create_options,
name,
create_contexts,
})
}
}
// ── CreateResponse ───────────────────────────────────────────────────────
/// SMB2 CREATE response (spec section 2.2.14).
///
/// Returned by the server with the file handle and metadata about
/// the created or opened file.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateResponse {
/// Oplock level granted by the server.
pub oplock_level: OplockLevel,
/// Flags (SMB 3.x only).
pub flags: u8,
/// Action taken by the server (opened, created, etc.).
pub create_action: CreateAction,
/// Time the file was created.
pub creation_time: FileTime,
/// Time the file was last accessed.
pub last_access_time: FileTime,
/// Time the file was last written.
pub last_write_time: FileTime,
/// Time the file metadata was last changed.
pub change_time: FileTime,
/// Allocation size of the file in bytes.
pub allocation_size: u64,
/// End-of-file position (actual file size in bytes).
pub end_of_file: u64,
/// File attributes.
pub file_attributes: u32,
/// The file handle.
pub file_id: FileId,
/// Raw create context bytes from the response.
pub create_contexts: Vec<u8>,
}
impl CreateResponse {
pub const STRUCTURE_SIZE: u16 = 89;
}
impl Pack for CreateResponse {
fn pack(&self, cursor: &mut WriteCursor) {
let start = cursor.position();
// StructureSize (2 bytes)
cursor.write_u16_le(Self::STRUCTURE_SIZE);
// OplockLevel (1 byte)
cursor.write_u8(self.oplock_level as u8);
// Flags (1 byte)
cursor.write_u8(self.flags);
// CreateAction (4 bytes)
cursor.write_u32_le(self.create_action as u32);
// CreationTime (8 bytes)
self.creation_time.pack(cursor);
// LastAccessTime (8 bytes)
self.last_access_time.pack(cursor);
// LastWriteTime (8 bytes)
self.last_write_time.pack(cursor);
// ChangeTime (8 bytes)
self.change_time.pack(cursor);
// AllocationSize (8 bytes)
cursor.write_u64_le(self.allocation_size);
// EndOfFile (8 bytes)
cursor.write_u64_le(self.end_of_file);
// FileAttributes (4 bytes)
cursor.write_u32_le(self.file_attributes);
// Reserved2 (4 bytes)
cursor.write_u32_le(0);
// FileId (16 bytes = persistent u64 + volatile u64)
cursor.write_u64_le(self.file_id.persistent);
cursor.write_u64_le(self.file_id.volatile);
// CreateContextsOffset (4 bytes) -- placeholder
let ctx_offset_pos = cursor.position();
cursor.write_u32_le(0);
// CreateContextsLength (4 bytes) -- placeholder
let ctx_length_pos = cursor.position();
cursor.write_u32_le(0);
// Create contexts (if any)
if !self.create_contexts.is_empty() {
cursor.align_to(8);
let ctx_offset = Header::SIZE + (cursor.position() - start);
cursor.write_bytes(&self.create_contexts);
let ctx_len = self.create_contexts.len();
cursor.set_u32_le_at(ctx_offset_pos, ctx_offset as u32);
cursor.set_u32_le_at(ctx_length_pos, ctx_len as u32);
}
}
}
impl Unpack for CreateResponse {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let start = cursor.position();
// StructureSize (2 bytes)
let structure_size = cursor.read_u16_le()?;
if structure_size != Self::STRUCTURE_SIZE {
return Err(Error::invalid_data(format!(
"invalid CreateResponse structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
// OplockLevel (1 byte)
let oplock_level = OplockLevel::try_from(cursor.read_u8()?)?;
// Flags (1 byte)
let flags = cursor.read_u8()?;
// CreateAction (4 bytes)
let create_action = CreateAction::try_from(cursor.read_u32_le()?)?;
// CreationTime (8 bytes)
let creation_time = FileTime::unpack(cursor)?;
// LastAccessTime (8 bytes)
let last_access_time = FileTime::unpack(cursor)?;
// LastWriteTime (8 bytes)
let last_write_time = FileTime::unpack(cursor)?;
// ChangeTime (8 bytes)
let change_time = FileTime::unpack(cursor)?;
// AllocationSize (8 bytes)
let allocation_size = cursor.read_u64_le()?;
// EndOfFile (8 bytes)
let end_of_file = cursor.read_u64_le()?;
// FileAttributes (4 bytes)
let file_attributes = cursor.read_u32_le()?;
// Reserved2 (4 bytes)
let _reserved2 = cursor.read_u32_le()?;
// FileId (16 bytes)
let persistent = cursor.read_u64_le()?;
let volatile = cursor.read_u64_le()?;
let file_id = FileId {
persistent,
volatile,
};
// CreateContextsOffset (4 bytes)
let ctx_offset = cursor.read_u32_le()? as usize;
// CreateContextsLength (4 bytes)
let ctx_length = cursor.read_u32_le()? as usize;
// Read create contexts
// Offset on the wire is from beginning of SMB2 header.
let create_contexts = if ctx_length > 0 {
let current = cursor.position();
let body_offset = ctx_offset.saturating_sub(Header::SIZE);
let target = start + body_offset;
if target > current {
cursor.skip(target - current)?;
}
cursor.read_bytes_bounded(ctx_length)?.to_vec()
} else {
Vec::new()
};
Ok(CreateResponse {
oplock_level,
flags,
create_action,
creation_time,
last_access_time,
last_write_time,
change_time,
allocation_size,
end_of_file,
file_attributes,
file_id,
create_contexts,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
// ── CreateRequest tests ──────────────────────────────────────────
#[test]
fn create_request_roundtrip_no_contexts() {
let original = CreateRequest {
requested_oplock_level: OplockLevel::Exclusive,
impersonation_level: ImpersonationLevel::Impersonation,
desired_access: FileAccessMask::new(
FileAccessMask::GENERIC_READ | FileAccessMask::FILE_READ_ATTRIBUTES,
),
file_attributes: 0x80, // FILE_ATTRIBUTE_NORMAL
share_access: ShareAccess(ShareAccess::FILE_SHARE_READ | ShareAccess::FILE_SHARE_WRITE),
create_disposition: CreateDisposition::FileOpenIf,
create_options: 0,
name: "test\\file.txt".to_string(),
create_contexts: Vec::new(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = CreateRequest::unpack(&mut r).unwrap();
assert_eq!(
decoded.requested_oplock_level,
original.requested_oplock_level
);
assert_eq!(decoded.impersonation_level, original.impersonation_level);
assert_eq!(decoded.desired_access, original.desired_access);
assert_eq!(decoded.file_attributes, original.file_attributes);
assert_eq!(decoded.share_access, original.share_access);
assert_eq!(decoded.create_disposition, original.create_disposition);
assert_eq!(decoded.create_options, original.create_options);
assert_eq!(decoded.name, original.name);
assert!(decoded.create_contexts.is_empty());
}
#[test]
fn create_request_roundtrip_with_create_contexts() {
// Simulate a raw create context blob (for example, a
// SMB2_CREATE_QUERY_MAXIMAL_ACCESS_REQUEST context).
let fake_ctx = vec![
0x00, 0x00, 0x00, 0x00, // NextEntryOffset = 0 (last entry)
0x10, 0x00, // NameOffset = 16
0x04, 0x00, // NameLength = 4
0x00, 0x00, // Reserved
0x18, 0x00, // DataOffset = 24
0x04, 0x00, 0x00, 0x00, // DataLength = 4
b'M', b'x', b'A', b'c', // Name = "MxAc"
0x00, 0x00, 0x00, 0x00, // padding
0x01, 0x02, 0x03, 0x04, // Data (4 bytes)
];
let original = CreateRequest {
requested_oplock_level: OplockLevel::Batch,
impersonation_level: ImpersonationLevel::Delegate,
desired_access: FileAccessMask::new(FileAccessMask::GENERIC_ALL),
file_attributes: 0x20, // FILE_ATTRIBUTE_ARCHIVE
share_access: ShareAccess(ShareAccess::FILE_SHARE_DELETE),
create_disposition: CreateDisposition::FileCreate,
create_options: 0x0000_0040, // FILE_NON_DIRECTORY_FILE
name: "share\\docs\\report.docx".to_string(),
create_contexts: fake_ctx.clone(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = CreateRequest::unpack(&mut r).unwrap();
assert_eq!(decoded.requested_oplock_level, OplockLevel::Batch);
assert_eq!(decoded.impersonation_level, ImpersonationLevel::Delegate);
assert_eq!(decoded.name, "share\\docs\\report.docx");
assert_eq!(decoded.create_contexts, fake_ctx);
}
#[test]
fn create_request_structure_size_field() {
let req = CreateRequest {
requested_oplock_level: OplockLevel::None,
impersonation_level: ImpersonationLevel::Anonymous,
desired_access: FileAccessMask::default(),
file_attributes: 0,
share_access: ShareAccess::default(),
create_disposition: CreateDisposition::FileOpen,
create_options: 0,
name: "x".to_string(),
create_contexts: Vec::new(),
};
let mut w = WriteCursor::new();
req.pack(&mut w);
let bytes = w.into_inner();
// First two bytes are StructureSize = 57
assert_eq!(bytes[0], 57);
assert_eq!(bytes[1], 0);
}
#[test]
fn create_request_wrong_structure_size() {
let mut buf = vec![0u8; 64];
// Set wrong structure size
buf[0..2].copy_from_slice(&99u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = CreateRequest::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
// ── CreateResponse tests ─────────────────────────────────────────
#[test]
fn create_response_roundtrip() {
let original = CreateResponse {
oplock_level: OplockLevel::LevelII,
flags: 0,
create_action: CreateAction::FileOpened,
creation_time: FileTime(133_485_408_000_000_000),
last_access_time: FileTime(133_485_408_100_000_000),
last_write_time: FileTime(133_485_408_200_000_000),
change_time: FileTime(133_485_408_300_000_000),
allocation_size: 4096,
end_of_file: 1234,
file_attributes: 0x20, // FILE_ATTRIBUTE_ARCHIVE
file_id: FileId {
persistent: 0x1111_2222_3333_4444,
volatile: 0x5555_6666_7777_8888,
},
create_contexts: Vec::new(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = CreateResponse::unpack(&mut r).unwrap();
assert_eq!(decoded.oplock_level, original.oplock_level);
assert_eq!(decoded.flags, original.flags);
assert_eq!(decoded.create_action, original.create_action);
assert_eq!(decoded.creation_time, original.creation_time);
assert_eq!(decoded.last_access_time, original.last_access_time);
assert_eq!(decoded.last_write_time, original.last_write_time);
assert_eq!(decoded.change_time, original.change_time);
assert_eq!(decoded.allocation_size, original.allocation_size);
assert_eq!(decoded.end_of_file, original.end_of_file);
assert_eq!(decoded.file_attributes, original.file_attributes);
assert_eq!(decoded.file_id, original.file_id);
assert!(decoded.create_contexts.is_empty());
}
#[test]
fn create_response_with_contexts() {
let ctx_data = vec![0xAA, 0xBB, 0xCC, 0xDD];
let original = CreateResponse {
oplock_level: OplockLevel::None,
flags: 0x01,
create_action: CreateAction::FileCreated,
creation_time: FileTime(100),
last_access_time: FileTime(200),
last_write_time: FileTime(300),
change_time: FileTime(400),
allocation_size: 0,
end_of_file: 0,
file_attributes: 0,
file_id: FileId {
persistent: 1,
volatile: 2,
},
create_contexts: ctx_data.clone(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = CreateResponse::unpack(&mut r).unwrap();
assert_eq!(decoded.create_action, CreateAction::FileCreated);
assert_eq!(decoded.file_id.persistent, 1);
assert_eq!(decoded.file_id.volatile, 2);
assert_eq!(decoded.create_contexts, ctx_data);
}
#[test]
fn create_response_structure_size_field() {
let resp = CreateResponse {
oplock_level: OplockLevel::None,
flags: 0,
create_action: CreateAction::FileOpened,
creation_time: FileTime::ZERO,
last_access_time: FileTime::ZERO,
last_write_time: FileTime::ZERO,
change_time: FileTime::ZERO,
allocation_size: 0,
end_of_file: 0,
file_attributes: 0,
file_id: FileId::default(),
create_contexts: Vec::new(),
};
let mut w = WriteCursor::new();
resp.pack(&mut w);
let bytes = w.into_inner();
// First two bytes are StructureSize = 89
assert_eq!(bytes[0], 89);
assert_eq!(bytes[1], 0);
}
#[test]
fn create_response_wrong_structure_size() {
let mut buf = vec![0u8; 96];
buf[0..2].copy_from_slice(&42u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = CreateResponse::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
// ── Enum conversion tests ────────────────────────────────────────
#[test]
fn oplock_level_roundtrip() {
for &level in &[
OplockLevel::None,
OplockLevel::LevelII,
OplockLevel::Exclusive,
OplockLevel::Batch,
OplockLevel::Lease,
] {
let raw = level as u8;
let decoded = OplockLevel::try_from(raw).unwrap();
assert_eq!(decoded, level);
}
}
#[test]
fn oplock_level_invalid() {
assert!(OplockLevel::try_from(0x42).is_err());
}
#[test]
fn impersonation_level_roundtrip() {
for &level in &[
ImpersonationLevel::Anonymous,
ImpersonationLevel::Identification,
ImpersonationLevel::Impersonation,
ImpersonationLevel::Delegate,
] {
let raw = level as u32;
let decoded = ImpersonationLevel::try_from(raw).unwrap();
assert_eq!(decoded, level);
}
}
#[test]
fn create_disposition_roundtrip() {
for &disp in &[
CreateDisposition::FileSupersede,
CreateDisposition::FileOpen,
CreateDisposition::FileCreate,
CreateDisposition::FileOpenIf,
CreateDisposition::FileOverwrite,
CreateDisposition::FileOverwriteIf,
] {
let raw = disp as u32;
let decoded = CreateDisposition::try_from(raw).unwrap();
assert_eq!(decoded, disp);
}
}
#[test]
fn create_action_roundtrip() {
for &action in &[
CreateAction::FileSuperseded,
CreateAction::FileOpened,
CreateAction::FileCreated,
CreateAction::FileOverwritten,
] {
let raw = action as u32;
let decoded = CreateAction::try_from(raw).unwrap();
assert_eq!(decoded, action);
}
}
}
#[cfg(test)]
mod roundtrip_props {
use super::*;
use crate::msg::roundtrip_strategies::{
arb_create_action, arb_create_disposition, arb_file_access_mask, arb_file_id,
arb_file_time, arb_impersonation_level, arb_oplock_level, arb_share_access,
arb_small_bytes, arb_utf16_string,
};
use proptest::prelude::*;
proptest! {
#[test]
fn create_request_pack_unpack(
requested_oplock_level in arb_oplock_level(),
impersonation_level in arb_impersonation_level(),
desired_access in arb_file_access_mask(),
file_attributes in any::<u32>(),
share_access in arb_share_access(),
create_disposition in arb_create_disposition(),
create_options in any::<u32>(),
name in arb_utf16_string(128),
create_contexts in arb_small_bytes(),
) {
let original = CreateRequest {
requested_oplock_level,
impersonation_level,
desired_access,
file_attributes,
share_access,
create_disposition,
create_options,
name,
create_contexts,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = CreateRequest::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
// Note: pack may write a trailing 1-byte pad when name is empty
// and there are no create contexts. Unpack only advances through
// fields it reads, so the cursor may have 1 trailing byte in
// that corner case. That's fine for symmetry on struct contents.
}
#[test]
fn create_response_pack_unpack(
oplock_level in arb_oplock_level(),
flags in any::<u8>(),
create_action in arb_create_action(),
creation_time in arb_file_time(),
last_access_time in arb_file_time(),
last_write_time in arb_file_time(),
change_time in arb_file_time(),
allocation_size in any::<u64>(),
end_of_file in any::<u64>(),
file_attributes in any::<u32>(),
file_id in arb_file_id(),
create_contexts in arb_small_bytes(),
) {
let original = CreateResponse {
oplock_level,
flags,
create_action,
creation_time,
last_access_time,
last_write_time,
change_time,
allocation_size,
end_of_file,
file_attributes,
file_id,
create_contexts,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = CreateResponse::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
}
}
}

697
vendor/smb2/src/msg/dfs.rs vendored Normal file
View File

@@ -0,0 +1,697 @@
//! DFS referral request and response wire format (MS-DFSC sections 2.2.2, 2.2.4).
//!
//! These types are packed into the input/output buffers of an IOCTL request
//! with `ctl_code = FSCTL_DFS_GET_REFERRALS`.
use crate::error::Result;
use crate::pack::{Pack, ReadCursor, Unpack, WriteCursor};
use crate::Error;
// ── ReqGetDfsReferral ─────────────────────────────────────────────────
/// REQ_GET_DFS_REFERRAL (MS-DFSC 2.2.2).
///
/// Sent as the input buffer of an `FSCTL_DFS_GET_REFERRALS` IOCTL request.
/// Contains the maximum referral version the client understands and the
/// DFS path to resolve.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReqGetDfsReferral {
/// Highest DFS referral version understood by the client (typically 4).
pub max_referral_level: u16,
/// The DFS path to resolve (case-insensitive UNC path).
pub request_file_name: String,
}
impl Pack for ReqGetDfsReferral {
fn pack(&self, cursor: &mut WriteCursor) {
// MaxReferralLevel (2 bytes, LE)
cursor.write_u16_le(self.max_referral_level);
// RequestFileName (null-terminated UTF-16LE)
cursor.write_utf16_le(&self.request_file_name);
// Null terminator (2 bytes)
cursor.write_u16_le(0);
}
}
impl Unpack for ReqGetDfsReferral {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let max_referral_level = cursor.read_u16_le()?;
// Read the rest as null-terminated UTF-16LE.
let request_file_name = read_null_terminated_utf16(cursor)?;
Ok(ReqGetDfsReferral {
max_referral_level,
request_file_name,
})
}
}
// ── RespGetDfsReferral ────────────────────────────────────────────────
/// RESP_GET_DFS_REFERRAL (MS-DFSC 2.2.4).
///
/// Returned in the output buffer of an IOCTL response for
/// `FSCTL_DFS_GET_REFERRALS`. Contains the number of bytes of the path
/// consumed by the server, header flags, and a list of referral entries.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RespGetDfsReferral {
/// Number of bytes (not characters) of the path prefix that matched.
pub path_consumed: u16,
/// Header flags (ReferralServers | StorageServers | TargetFailback).
pub header_flags: u32,
/// The list of referral entries (V2, V3, or V4).
pub entries: Vec<DfsReferralEntry>,
}
/// A single DFS referral entry (V2-V4 flattened).
///
/// V1 is not supported (extremely rare in practice). Each entry describes
/// one target server/share that the client can use to access the DFS path.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DfsReferralEntry {
/// Referral entry version (2, 3, or 4).
pub version: u16,
/// Server type: 0 = non-root/link target, 1 = root target.
pub server_type: u16,
/// Referral entry flags (version-specific).
pub referral_entry_flags: u16,
/// Time-to-live in seconds for caching this referral.
pub ttl: u32,
/// The DFS path prefix that matched.
pub dfs_path: String,
/// The DFS alternate path (usually identical to dfs_path).
pub dfs_alternate_path: String,
/// The target UNC path (for example, `\\server\share`).
pub network_address: String,
}
impl Unpack for RespGetDfsReferral {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let path_consumed = cursor.read_u16_le()?;
let number_of_referrals = cursor.read_u16_le()?;
let header_flags = cursor.read_u32_le()?;
// The remaining data contains all referral entries followed by a
// string buffer. We need the full remaining slice to resolve
// offsets that are relative to each entry's start.
let entry_data = cursor.read_bytes(cursor.remaining())?;
let mut entries = Vec::with_capacity(number_of_referrals as usize);
let mut offset = 0usize;
for _ in 0..number_of_referrals {
if offset + 4 > entry_data.len() {
return Err(Error::invalid_data(
"DFS referral entry truncated (version/size header)",
));
}
let version = u16::from_le_bytes([entry_data[offset], entry_data[offset + 1]]);
let entry_size =
u16::from_le_bytes([entry_data[offset + 2], entry_data[offset + 3]]) as usize;
if entry_size < 4 {
return Err(Error::invalid_data(format!(
"DFS referral entry size too small: {entry_size}"
)));
}
let entry_start = offset;
// The entry_size includes the version and size fields themselves.
let entry_end = entry_start + entry_size;
if entry_end > entry_data.len() {
return Err(Error::invalid_data(format!(
"DFS referral entry extends past buffer: entry_end={entry_end}, buf={}",
entry_data.len()
)));
}
// All strings referenced by offsets live from entry_start onward
// in the full buffer (not truncated to entry_size, because the
// strings are in the trailing string buffer).
let entry = parse_referral_entry(version, entry_data, entry_start)?;
entries.push(entry);
offset = entry_end;
}
Ok(RespGetDfsReferral {
path_consumed,
header_flags,
entries,
})
}
}
/// Parse a single referral entry starting at `entry_start` within `buf`.
///
/// String offsets in V2/V3/V4 are relative to the start of the entry
/// (which includes the 4-byte version+size prefix).
fn parse_referral_entry(version: u16, buf: &[u8], entry_start: usize) -> Result<DfsReferralEntry> {
// Skip version (2) + size (2) -- already read by caller.
let mut pos = entry_start + 4;
match version {
2 => {
// V2: server_type(2) + flags(2) + proximity(4) + ttl(4) +
// dfs_path_offset(2) + dfs_alternate_path_offset(2) + network_address_offset(2)
// = 18 bytes of fixed entry body after the 4-byte version/size prefix.
ensure_remaining(buf, pos, 18)?;
let server_type = read_u16(buf, pos);
pos += 2;
let referral_entry_flags = read_u16(buf, pos);
pos += 2;
let _proximity = read_u32(buf, pos);
pos += 4;
let ttl = read_u32(buf, pos);
pos += 4;
let dfs_path_offset = read_u16(buf, pos) as usize;
pos += 2;
let dfs_alternate_path_offset = read_u16(buf, pos) as usize;
pos += 2;
let network_address_offset = read_u16(buf, pos) as usize;
let dfs_path = read_offset_string(buf, entry_start, dfs_path_offset)?;
let dfs_alternate_path =
read_offset_string(buf, entry_start, dfs_alternate_path_offset)?;
let network_address = read_offset_string(buf, entry_start, network_address_offset)?;
Ok(DfsReferralEntry {
version,
server_type,
referral_entry_flags,
ttl,
dfs_path,
dfs_alternate_path,
network_address,
})
}
3 | 4 => {
// V3/V4 share the same layout for the common (non-NameListReferral) case.
// server_type(2) + flags(2) + ttl(4) +
// dfs_path_offset(2) + dfs_alternate_path_offset(2) + network_address_offset(2)
// V3/V4: + service_site_guid(16) when NameListReferral=0
ensure_remaining(buf, pos, 14)?;
let server_type = read_u16(buf, pos);
pos += 2;
let referral_entry_flags = read_u16(buf, pos);
pos += 2;
let ttl = read_u32(buf, pos);
pos += 4;
let dfs_path_offset = read_u16(buf, pos) as usize;
pos += 2;
let dfs_alternate_path_offset = read_u16(buf, pos) as usize;
pos += 2;
let network_address_offset = read_u16(buf, pos) as usize;
// Skip the rest of the fixed entry (service_site_guid for V3/V4).
let dfs_path = read_offset_string(buf, entry_start, dfs_path_offset)?;
let dfs_alternate_path =
read_offset_string(buf, entry_start, dfs_alternate_path_offset)?;
let network_address = read_offset_string(buf, entry_start, network_address_offset)?;
Ok(DfsReferralEntry {
version,
server_type,
referral_entry_flags,
ttl,
dfs_path,
dfs_alternate_path,
network_address,
})
}
_ => Err(Error::invalid_data(format!(
"unsupported DFS referral version: {version} (only V2-V4 are supported)"
))),
}
}
// ── Helper functions ──────────────────────────────────────────────────
/// Read a null-terminated UTF-16LE string from a `ReadCursor`.
fn read_null_terminated_utf16(cursor: &mut ReadCursor<'_>) -> Result<String> {
let mut code_units: Vec<u16> = Vec::new();
loop {
let cu = cursor.read_u16_le()?;
if cu == 0 {
break;
}
code_units.push(cu);
}
String::from_utf16(&code_units)
.map_err(|_| Error::invalid_data("invalid UTF-16LE in DFS request file name"))
}
/// Read a null-terminated UTF-16LE string from a raw byte buffer at a given absolute offset.
fn read_null_terminated_utf16_at(buf: &[u8], offset: usize) -> Result<String> {
let mut code_units: Vec<u16> = Vec::new();
let mut pos = offset;
loop {
if pos + 2 > buf.len() {
return Err(Error::invalid_data(
"DFS referral string extends past buffer",
));
}
let cu = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
pos += 2;
if cu == 0 {
break;
}
code_units.push(cu);
}
String::from_utf16(&code_units)
.map_err(|_| Error::invalid_data("invalid UTF-16LE in DFS referral string"))
}
/// Read a null-terminated UTF-16LE string at an offset relative to an entry start.
fn read_offset_string(buf: &[u8], entry_start: usize, offset: usize) -> Result<String> {
let abs = entry_start + offset;
read_null_terminated_utf16_at(buf, abs)
}
/// Inline LE u16 read from a byte buffer.
fn read_u16(buf: &[u8], pos: usize) -> u16 {
u16::from_le_bytes([buf[pos], buf[pos + 1]])
}
/// Inline LE u32 read from a byte buffer.
fn read_u32(buf: &[u8], pos: usize) -> u32 {
u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]])
}
/// Check that at least `need` bytes are available at `pos` in `buf`.
fn ensure_remaining(buf: &[u8], pos: usize, need: usize) -> Result<()> {
if pos + need > buf.len() {
Err(Error::invalid_data(format!(
"DFS referral entry truncated: need {need} bytes at offset {pos}, buf len {}",
buf.len()
)))
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
// ── Request tests ─────────────────────────────────────────────────
#[test]
fn req_pack_known_bytes() {
// Test vector from smb-rs: ReqGetDfsReferral { max_referral_level: 4,
// request_file_name: r"\ADC.aviv.local\dfs\Docs" }
let expected = hex_to_bytes(
"04005c004100440043002e0061007600690076002e006c006f00630061006c005c006400660073005c0044006f00630073000000",
);
let req = ReqGetDfsReferral {
max_referral_level: 4,
request_file_name: r"\ADC.aviv.local\dfs\Docs".to_string(),
};
let mut cursor = WriteCursor::new();
req.pack(&mut cursor);
assert_eq!(cursor.into_inner(), expected);
}
#[test]
fn req_pack_roundtrip() {
let original = ReqGetDfsReferral {
max_referral_level: 4,
request_file_name: r"\server\share\path".to_string(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = ReqGetDfsReferral::unpack(&mut r).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn req_pack_empty_path() {
let req = ReqGetDfsReferral {
max_referral_level: 3,
request_file_name: String::new(),
};
let mut w = WriteCursor::new();
req.pack(&mut w);
let bytes = w.into_inner();
// max_referral_level (2) + null terminator (2) = 4 bytes
assert_eq!(bytes.len(), 4);
assert_eq!(bytes, [0x03, 0x00, 0x00, 0x00]);
let mut r = ReadCursor::new(&bytes);
let decoded = ReqGetDfsReferral::unpack(&mut r).unwrap();
assert_eq!(decoded, req);
}
#[test]
fn req_unpack_truncated() {
// Only 1 byte -- not enough for max_referral_level.
let bytes = [0x04];
let mut r = ReadCursor::new(&bytes);
assert!(ReqGetDfsReferral::unpack(&mut r).is_err());
}
// ── Response tests ────────────────────────────────────────────────
#[test]
fn resp_parse_v4_referral() {
// Test vector from smb-rs: two V4 entries.
let hex = "300002000200000004002200000004000807000044007600\
a800000000000000000000000000000000000400220000000000\
0807000022005400a8000000000000000000000000000000\
00005c004100440043002e0061007600690076002e006c00\
6f00630061006c005c006400660073005c0044006f006300\
730000005c004100440043002e0061007600690076002e00\
6c006f00630061006c005c006400660073005c0044006f00\
6300730000005c004100440043005c005300680061007200\
650073005c0044006f006300730000005c00460053005200\
56005c005300680061007200650073005c004d0079005300\
6800610072006500000000";
let data = hex_to_bytes(hex);
let mut cursor = ReadCursor::new(&data);
let resp = RespGetDfsReferral::unpack(&mut cursor).unwrap();
assert_eq!(resp.path_consumed, 48);
// header_flags = 0x00000002 (StorageServers)
assert_eq!(resp.header_flags, 0x0000_0002);
assert_eq!(resp.entries.len(), 2);
let e0 = &resp.entries[0];
assert_eq!(e0.version, 4);
assert_eq!(e0.server_type, 0); // non-root
assert_eq!(e0.ttl, 1800);
assert_eq!(e0.dfs_path, r"\ADC.aviv.local\dfs\Docs");
assert_eq!(e0.dfs_alternate_path, r"\ADC.aviv.local\dfs\Docs");
assert_eq!(e0.network_address, r"\ADC\Shares\Docs");
let e1 = &resp.entries[1];
assert_eq!(e1.version, 4);
assert_eq!(e1.server_type, 0);
assert_eq!(e1.ttl, 1800);
assert_eq!(e1.dfs_path, r"\ADC.aviv.local\dfs\Docs");
assert_eq!(e1.dfs_alternate_path, r"\ADC.aviv.local\dfs\Docs");
assert_eq!(e1.network_address, r"\FSRV\Shares\MyShare");
}
#[test]
fn resp_parse_v3_referral() {
// Manually constructed V3 response: one entry.
// Header: path_consumed=20, num_referrals=1, flags=0x03
// Entry: version=3, size=34 (fixed part), server_type=1, flags=0,
// ttl=600, offsets point to strings after the entry.
let dfs_path = encode_null_utf16(r"\dom\share");
let alt_path = encode_null_utf16(r"\dom\share");
let net_addr = encode_null_utf16(r"\srv\share");
let entry_fixed_size: u16 = 34; // 4 + 2+2+4 + 2+2+2 + 16 = 34
let dfs_path_offset = entry_fixed_size;
let alt_path_offset = dfs_path_offset + dfs_path.len() as u16;
let net_addr_offset = alt_path_offset + alt_path.len() as u16;
let mut buf = Vec::new();
// Response header
buf.extend_from_slice(&20u16.to_le_bytes()); // path_consumed
buf.extend_from_slice(&1u16.to_le_bytes()); // number_of_referrals
buf.extend_from_slice(&3u32.to_le_bytes()); // header_flags
// Entry header
buf.extend_from_slice(&3u16.to_le_bytes()); // version
buf.extend_from_slice(&entry_fixed_size.to_le_bytes()); // size (fixed part)
buf.extend_from_slice(&1u16.to_le_bytes()); // server_type (root)
buf.extend_from_slice(&0u16.to_le_bytes()); // referral_entry_flags
buf.extend_from_slice(&600u32.to_le_bytes()); // ttl
buf.extend_from_slice(&dfs_path_offset.to_le_bytes());
buf.extend_from_slice(&alt_path_offset.to_le_bytes());
buf.extend_from_slice(&net_addr_offset.to_le_bytes());
buf.extend_from_slice(&[0u8; 16]); // service_site_guid
// String buffer
buf.extend_from_slice(&dfs_path);
buf.extend_from_slice(&alt_path);
buf.extend_from_slice(&net_addr);
let mut cursor = ReadCursor::new(&buf);
let resp = RespGetDfsReferral::unpack(&mut cursor).unwrap();
assert_eq!(resp.path_consumed, 20);
assert_eq!(resp.header_flags, 3);
assert_eq!(resp.entries.len(), 1);
let e = &resp.entries[0];
assert_eq!(e.version, 3);
assert_eq!(e.server_type, 1);
assert_eq!(e.ttl, 600);
assert_eq!(e.dfs_path, r"\dom\share");
assert_eq!(e.dfs_alternate_path, r"\dom\share");
assert_eq!(e.network_address, r"\srv\share");
}
#[test]
fn resp_parse_v2_referral() {
// Manually constructed V2 response: one entry.
let dfs_path = encode_null_utf16(r"\domain\dfs");
let alt_path = encode_null_utf16(r"\domain\dfs");
let net_addr = encode_null_utf16(r"\server\data");
let entry_fixed_size: u16 = 22; // 4 + 2+2+4+4 + 2+2+2 = 22
let dfs_path_offset = entry_fixed_size;
let alt_path_offset = dfs_path_offset + dfs_path.len() as u16;
let net_addr_offset = alt_path_offset + alt_path.len() as u16;
let mut buf = Vec::new();
// Response header
buf.extend_from_slice(&24u16.to_le_bytes()); // path_consumed
buf.extend_from_slice(&1u16.to_le_bytes()); // number_of_referrals
buf.extend_from_slice(&1u32.to_le_bytes()); // header_flags (ReferralServers)
// Entry
buf.extend_from_slice(&2u16.to_le_bytes()); // version
buf.extend_from_slice(&entry_fixed_size.to_le_bytes()); // size
buf.extend_from_slice(&0u16.to_le_bytes()); // server_type
buf.extend_from_slice(&0u16.to_le_bytes()); // flags
buf.extend_from_slice(&0u32.to_le_bytes()); // proximity
buf.extend_from_slice(&300u32.to_le_bytes()); // ttl
buf.extend_from_slice(&dfs_path_offset.to_le_bytes());
buf.extend_from_slice(&alt_path_offset.to_le_bytes());
buf.extend_from_slice(&net_addr_offset.to_le_bytes());
// String buffer
buf.extend_from_slice(&dfs_path);
buf.extend_from_slice(&alt_path);
buf.extend_from_slice(&net_addr);
let mut cursor = ReadCursor::new(&buf);
let resp = RespGetDfsReferral::unpack(&mut cursor).unwrap();
assert_eq!(resp.path_consumed, 24);
assert_eq!(resp.header_flags, 1);
assert_eq!(resp.entries.len(), 1);
let e = &resp.entries[0];
assert_eq!(e.version, 2);
assert_eq!(e.server_type, 0);
assert_eq!(e.ttl, 300);
assert_eq!(e.dfs_path, r"\domain\dfs");
assert_eq!(e.dfs_alternate_path, r"\domain\dfs");
assert_eq!(e.network_address, r"\server\data");
}
#[test]
fn resp_parse_empty() {
// Zero referral entries.
let mut buf = Vec::new();
buf.extend_from_slice(&0u16.to_le_bytes()); // path_consumed
buf.extend_from_slice(&0u16.to_le_bytes()); // number_of_referrals
buf.extend_from_slice(&0u32.to_le_bytes()); // header_flags
let mut cursor = ReadCursor::new(&buf);
let resp = RespGetDfsReferral::unpack(&mut cursor).unwrap();
assert_eq!(resp.path_consumed, 0);
assert_eq!(resp.header_flags, 0);
assert!(resp.entries.is_empty());
}
#[test]
fn resp_parse_multiple_entries() {
// Two V2 entries with different targets.
// Layout: [entry1 fixed][entry2 fixed][strings for entry1][strings for entry2]
// Offsets are relative to each entry's start.
let dfs_path = encode_null_utf16(r"\ns\link");
let alt_path = encode_null_utf16(r"\ns\link");
let net_addr_1 = encode_null_utf16(r"\srv1\data");
let net_addr_2 = encode_null_utf16(r"\srv2\data");
let entry_fixed_size: u16 = 22;
let total_fixed: u16 = entry_fixed_size * 2; // both entries' fixed parts
// Entry 1 string offsets (relative to entry 1 start = 0 in entry_data).
// Strings start after both entries' fixed parts.
let e1_dfs_offset = total_fixed; // 44
let e1_alt_offset = e1_dfs_offset + dfs_path.len() as u16;
let e1_net_offset = e1_alt_offset + alt_path.len() as u16;
let e1_strings_end = e1_net_offset + net_addr_1.len() as u16;
// Entry 2 string offsets (relative to entry 2 start = 22 in entry_data).
let e2_dfs_offset = e1_strings_end - entry_fixed_size; // offset from entry 2 start
let e2_alt_offset = e2_dfs_offset + dfs_path.len() as u16;
let e2_net_offset = e2_alt_offset + alt_path.len() as u16;
let mut buf = Vec::new();
// Response header
buf.extend_from_slice(&16u16.to_le_bytes()); // path_consumed
buf.extend_from_slice(&2u16.to_le_bytes()); // number_of_referrals
buf.extend_from_slice(&0u32.to_le_bytes()); // header_flags
// Entry 1 fixed part
buf.extend_from_slice(&2u16.to_le_bytes()); // version
buf.extend_from_slice(&entry_fixed_size.to_le_bytes()); // size
buf.extend_from_slice(&0u16.to_le_bytes()); // server_type
buf.extend_from_slice(&0u16.to_le_bytes()); // flags
buf.extend_from_slice(&0u32.to_le_bytes()); // proximity
buf.extend_from_slice(&120u32.to_le_bytes()); // ttl
buf.extend_from_slice(&e1_dfs_offset.to_le_bytes());
buf.extend_from_slice(&e1_alt_offset.to_le_bytes());
buf.extend_from_slice(&e1_net_offset.to_le_bytes());
// Entry 2 fixed part
buf.extend_from_slice(&2u16.to_le_bytes());
buf.extend_from_slice(&entry_fixed_size.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes()); // server_type = root
buf.extend_from_slice(&0u16.to_le_bytes());
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(&240u32.to_le_bytes());
buf.extend_from_slice(&e2_dfs_offset.to_le_bytes());
buf.extend_from_slice(&e2_alt_offset.to_le_bytes());
buf.extend_from_slice(&e2_net_offset.to_le_bytes());
// String buffer for entry 1
buf.extend_from_slice(&dfs_path);
buf.extend_from_slice(&alt_path);
buf.extend_from_slice(&net_addr_1);
// String buffer for entry 2
buf.extend_from_slice(&dfs_path);
buf.extend_from_slice(&alt_path);
buf.extend_from_slice(&net_addr_2);
let mut cursor = ReadCursor::new(&buf);
let resp = RespGetDfsReferral::unpack(&mut cursor).unwrap();
assert_eq!(resp.entries.len(), 2);
assert_eq!(resp.entries[0].ttl, 120);
assert_eq!(resp.entries[0].network_address, r"\srv1\data");
assert_eq!(resp.entries[1].ttl, 240);
assert_eq!(resp.entries[1].server_type, 1);
assert_eq!(resp.entries[1].network_address, r"\srv2\data");
}
#[test]
fn resp_parse_unsupported_version() {
let mut buf = Vec::new();
// Response header
buf.extend_from_slice(&0u16.to_le_bytes());
buf.extend_from_slice(&1u16.to_le_bytes()); // 1 entry
buf.extend_from_slice(&0u32.to_le_bytes());
// Entry with version 1 (unsupported)
buf.extend_from_slice(&1u16.to_le_bytes()); // version
buf.extend_from_slice(&8u16.to_le_bytes()); // size
buf.extend_from_slice(&[0u8; 4]); // padding to reach size
let mut cursor = ReadCursor::new(&buf);
let result = RespGetDfsReferral::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("unsupported DFS referral version"),
"error was: {err}"
);
}
#[test]
fn resp_parse_truncated_header() {
// Only 4 bytes -- missing header_flags.
let buf = [0x00, 0x00, 0x01, 0x00];
let mut cursor = ReadCursor::new(&buf);
assert!(RespGetDfsReferral::unpack(&mut cursor).is_err());
}
/// Regression: fuzz-found crash. A V2 entry that claims `entry_size = 16`
/// used to panic inside the entry-body read. The V2 body needs 18 bytes
/// (server_type+flags+proximity+ttl + three u16 offsets), but the guard
/// only ensured 16 bytes were available, so the final offset read would
/// slip past the buffer. See fuzz target
/// `fuzz_dfs_referral_response_parse` crash
/// `a6933afd5a1ccec7166d914caed66154416a2fcb`.
#[test]
fn resp_parse_v2_short_entry_returns_clean_error() {
let crash_input: [u8; 28] = [
0x10, 0x00, 0x01, 0x00, 0x22, 0x23, 0x00, 0x03, // header
0x02, 0x00, 0x10, 0x00, 0x01, 0x00, 0x22, 0x23, // v2 entry start (size=16)
0x00, 0x03, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, // body bytes
0x00, 0x00, 0x00, 0x00, // tail
];
let mut cursor = ReadCursor::new(&crash_input);
let result = RespGetDfsReferral::unpack(&mut cursor);
assert!(result.is_err(), "expected clean error, got {result:?}");
}
// ── Test helpers ──────────────────────────────────────────────────
/// Decode a hex string (no spaces, no 0x prefix) into bytes.
fn hex_to_bytes(hex: &str) -> Vec<u8> {
let hex: String = hex.chars().filter(|c| !c.is_whitespace()).collect();
(0..hex.len())
.step_by(2)
.map(|i| u8::from_str_radix(&hex[i..i + 2], 16).unwrap())
.collect()
}
/// Encode a string as null-terminated UTF-16LE bytes.
fn encode_null_utf16(s: &str) -> Vec<u8> {
let mut out = Vec::new();
for cu in s.encode_utf16() {
out.extend_from_slice(&cu.to_le_bytes());
}
out.extend_from_slice(&[0x00, 0x00]); // null terminator
out
}
}
#[cfg(test)]
mod roundtrip_props {
use super::*;
use crate::msg::roundtrip_strategies::arb_utf16_string;
use proptest::prelude::*;
/// Generate a UTF-16 string without interior null (U+0000). The encoder
/// terminates with a 0x0000 code unit, so an interior null would end
/// the string early on decode.
fn arb_utf16_no_nul(max: usize) -> impl Strategy<Value = String> {
arb_utf16_string(max).prop_filter("string must not contain interior U+0000", |s| {
!s.contains('\0')
})
}
proptest! {
#[test]
fn req_get_dfs_referral_pack_unpack(
max_referral_level in any::<u16>(),
request_file_name in arb_utf16_no_nul(128),
) {
let original = ReqGetDfsReferral {
max_referral_level,
request_file_name,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = ReqGetDfsReferral::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
prop_assert!(r.is_empty());
}
}
}

42
vendor/smb2/src/msg/echo.rs vendored Normal file
View File

@@ -0,0 +1,42 @@
//! SMB2 ECHO request and response (spec sections 2.2.28, 2.2.29).
//!
//! Echo messages are used to check whether a server is processing requests.
//! Both request and response contain only a StructureSize field and a
//! reserved field, for a total of 4 bytes each.
super::trivial_message! {
/// SMB2 ECHO request (spec section 2.2.28).
///
/// Sent by the client to determine whether a server is processing requests.
/// Contains only StructureSize (2 bytes) and Reserved (2 bytes).
pub struct EchoRequest;
}
super::trivial_message! {
/// SMB2 ECHO response (spec section 2.2.29).
///
/// Sent by the server to confirm that an ECHO request was processed.
/// Contains only StructureSize (2 bytes) and Reserved (2 bytes).
pub struct EchoResponse;
}
#[cfg(test)]
mod tests {
use super::*;
super::super::trivial_message_tests!(
EchoRequest,
echo_request_known_bytes,
echo_request_roundtrip,
echo_request_wrong_structure_size,
echo_request_too_short
);
super::super::trivial_message_tests!(
EchoResponse,
echo_response_known_bytes,
echo_response_roundtrip,
echo_response_wrong_structure_size,
echo_response_too_short
);
}

254
vendor/smb2/src/msg/flush.rs vendored Normal file
View File

@@ -0,0 +1,254 @@
//! SMB2 FLUSH request and response (spec sections 2.2.17, 2.2.18).
//!
//! Flush messages request that the server flush all cached file information
//! for a specified open to persistent storage. If the open refers to a
//! named pipe, the operation completes once all written data has been
//! consumed by a reader.
use crate::error::Result;
use crate::pack::{Pack, ReadCursor, Unpack, WriteCursor};
use crate::types::FileId;
use crate::Error;
/// SMB2 FLUSH request (spec section 2.2.17).
///
/// Sent by the client to request that the server flush cached data for a file.
///
/// Wire layout (24 bytes):
/// - StructureSize (2 bytes): must be 24
/// - Reserved1 (2 bytes): must be 0
/// - Reserved2 (4 bytes): must be 0
/// - FileId (16 bytes): persistent (8 bytes) + volatile (8 bytes)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FlushRequest {
pub file_id: FileId,
}
impl FlushRequest {
pub const STRUCTURE_SIZE: u16 = 24;
}
impl Pack for FlushRequest {
fn pack(&self, cursor: &mut WriteCursor) {
// StructureSize (2 bytes)
cursor.write_u16_le(Self::STRUCTURE_SIZE);
// Reserved1 (2 bytes)
cursor.write_u16_le(0);
// Reserved2 (4 bytes)
cursor.write_u32_le(0);
// FileId: Persistent (8 bytes) + Volatile (8 bytes)
cursor.write_u64_le(self.file_id.persistent);
cursor.write_u64_le(self.file_id.volatile);
}
}
impl Unpack for FlushRequest {
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 FlushRequest structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
// Reserved1 (2 bytes)
let _reserved1 = cursor.read_u16_le()?;
// Reserved2 (4 bytes)
let _reserved2 = cursor.read_u32_le()?;
// FileId: Persistent (8 bytes) + Volatile (8 bytes)
let persistent = cursor.read_u64_le()?;
let volatile = cursor.read_u64_le()?;
Ok(FlushRequest {
file_id: FileId {
persistent,
volatile,
},
})
}
}
super::trivial_message! {
/// SMB2 FLUSH response (spec section 2.2.18).
///
/// Sent by the server to confirm that a FLUSH request was processed.
/// Contains only StructureSize (2 bytes) and Reserved (2 bytes).
pub struct FlushResponse;
}
#[cfg(test)]
mod tests {
use super::*;
// ── FlushRequest tests ─────────────────────────────────────────
#[test]
fn flush_request_pack_produces_24_bytes() {
let req = FlushRequest {
file_id: FileId::default(),
};
let mut cursor = WriteCursor::new();
req.pack(&mut cursor);
let bytes = cursor.into_inner();
assert_eq!(bytes.len(), 24);
}
#[test]
fn flush_request_known_bytes() {
let req = FlushRequest {
file_id: FileId {
persistent: 0x0102_0304_0506_0708,
volatile: 0x090A_0B0C_0D0E_0F10,
},
};
let mut cursor = WriteCursor::new();
req.pack(&mut cursor);
let bytes = cursor.into_inner();
#[rustfmt::skip]
let expected: [u8; 24] = [
// StructureSize = 24
0x18, 0x00,
// Reserved1 = 0
0x00, 0x00,
// Reserved2 = 0
0x00, 0x00, 0x00, 0x00,
// FileId.Persistent (LE)
0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01,
// FileId.Volatile (LE)
0x10, 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09,
];
assert_eq!(bytes, expected);
}
#[test]
fn flush_request_unpack_known_bytes() {
#[rustfmt::skip]
let bytes: [u8; 24] = [
// StructureSize = 24
0x18, 0x00,
// Reserved1 = 0
0x00, 0x00,
// Reserved2 = 0
0x00, 0x00, 0x00, 0x00,
// FileId.Persistent = 0xDEADBEEFCAFEBABE
0xBE, 0xBA, 0xFE, 0xCA, 0xEF, 0xBE, 0xAD, 0xDE,
// FileId.Volatile = 0x1234567890ABCDEF
0xEF, 0xCD, 0xAB, 0x90, 0x78, 0x56, 0x34, 0x12,
];
let mut cursor = ReadCursor::new(&bytes);
let req = FlushRequest::unpack(&mut cursor).unwrap();
assert_eq!(req.file_id.persistent, 0xDEAD_BEEF_CAFE_BABE);
assert_eq!(req.file_id.volatile, 0x1234_5678_90AB_CDEF);
assert!(cursor.is_empty());
}
#[test]
fn flush_request_roundtrip() {
let original = FlushRequest {
file_id: FileId {
persistent: 0xAAAA_BBBB_CCCC_DDDD,
volatile: 0x1111_2222_3333_4444,
},
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = FlushRequest::unpack(&mut r).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn flush_request_roundtrip_sentinel_file_id() {
let original = FlushRequest {
file_id: FileId::SENTINEL,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = FlushRequest::unpack(&mut r).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn flush_request_wrong_structure_size() {
let mut bytes = [0u8; 24];
// Wrong structure size = 4 instead of 24
bytes[0..2].copy_from_slice(&4u16.to_le_bytes());
let mut cursor = ReadCursor::new(&bytes);
let result = FlushRequest::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
#[test]
fn flush_request_too_short() {
let bytes = [0x18, 0x00, 0x00, 0x00];
let mut cursor = ReadCursor::new(&bytes);
let result = FlushRequest::unpack(&mut cursor);
assert!(result.is_err());
}
#[test]
fn flush_request_ignores_reserved_values() {
#[rustfmt::skip]
let bytes: [u8; 24] = [
// StructureSize = 24
0x18, 0x00,
// Reserved1 = 0xFFFF (non-zero, should be ignored)
0xFF, 0xFF,
// Reserved2 = 0xFFFFFFFF (non-zero, should be ignored)
0xFF, 0xFF, 0xFF, 0xFF,
// FileId.Persistent = 0
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// FileId.Volatile = 0
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
];
let mut cursor = ReadCursor::new(&bytes);
let req = FlushRequest::unpack(&mut cursor).unwrap();
assert_eq!(req.file_id, FileId::default());
}
// ── FlushResponse tests ────────────────────────────────────────
super::super::trivial_message_tests!(
FlushResponse,
flush_response_known_bytes,
flush_response_roundtrip,
flush_response_wrong_structure_size,
flush_response_too_short
);
}
#[cfg(test)]
mod roundtrip_props {
use super::*;
use crate::msg::roundtrip_strategies::arb_file_id;
use proptest::prelude::*;
proptest! {
#[test]
fn flush_request_pack_unpack(file_id in arb_file_id()) {
let original = FlushRequest { file_id };
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = FlushRequest::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
prop_assert!(r.is_empty());
}
}
}

669
vendor/smb2/src/msg/header.rs vendored Normal file
View File

@@ -0,0 +1,669 @@
//! 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());
}
}
}

479
vendor/smb2/src/msg/ioctl.rs vendored Normal file
View File

@@ -0,0 +1,479 @@
//! SMB2 IOCTL Request and Response (MS-SMB2 sections 2.2.31, 2.2.32).
//!
//! The IOCTL request sends a control code to a server, optionally with input
//! data. The response returns output data from the control operation.
use crate::error::Result;
use crate::pack::{Pack, ReadCursor, Unpack, WriteCursor};
use crate::types::FileId;
use crate::Error;
// ── IOCTL flags ────────────────────────────────────────────────────────
/// The request is a file system control (FSCTL) request.
pub const SMB2_0_IOCTL_IS_FSCTL: u32 = 0x0000_0001;
// ── Common CtlCode values ──────────────────────────────────────────────
/// Named pipe transceive operation.
pub const FSCTL_PIPE_TRANSCEIVE: u32 = 0x0011_C017;
/// Server-side copy chunk (read handle).
pub const FSCTL_SRV_COPYCHUNK: u32 = 0x0014_40F2;
/// Server-side copy chunk (write handle).
pub const FSCTL_SRV_COPYCHUNK_WRITE: u32 = 0x0014_80F2;
/// DFS referral request.
pub const FSCTL_DFS_GET_REFERRALS: u32 = 0x0006_0194;
/// Validate negotiate info (SMB 3.x).
pub const FSCTL_VALIDATE_NEGOTIATE_INFO: u32 = 0x0014_0204;
// ── IoctlRequest ───────────────────────────────────────────────────────
/// SMB2 IOCTL Request (MS-SMB2 section 2.2.31).
///
/// Sent by the client to issue a device or file system control command.
/// The fixed part is 56 bytes (StructureSize = 57 indicates 1 byte of
/// variable data is included in the fixed size, per SMB2 convention).
///
/// Layout:
/// - StructureSize (2 bytes, must be 57)
/// - Reserved (2 bytes)
/// - CtlCode (4 bytes)
/// - FileId (16 bytes)
/// - InputOffset (4 bytes)
/// - InputCount (4 bytes)
/// - MaxInputResponse (4 bytes)
/// - OutputOffset (4 bytes)
/// - OutputCount (4 bytes)
/// - MaxOutputResponse (4 bytes)
/// - Flags (4 bytes)
/// - Reserved2 (4 bytes)
/// - Buffer (variable, InputCount bytes)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IoctlRequest {
/// The control code for the operation.
pub ctl_code: u32,
/// The file handle for the operation.
pub file_id: FileId,
/// Maximum number of input bytes the server can return.
pub max_input_response: u32,
/// Maximum number of output bytes the server can return.
pub max_output_response: u32,
/// Flags for the request (for example, `SMB2_0_IOCTL_IS_FSCTL`).
pub flags: u32,
/// Input data buffer.
pub input_data: Vec<u8>,
}
impl IoctlRequest {
pub const STRUCTURE_SIZE: u16 = 57;
/// Fixed header size before the variable buffer (56 bytes).
const FIXED_SIZE: u32 = 56;
}
impl Pack for IoctlRequest {
fn pack(&self, cursor: &mut WriteCursor) {
let start = cursor.position();
// StructureSize (2 bytes)
cursor.write_u16_le(Self::STRUCTURE_SIZE);
// Reserved (2 bytes)
cursor.write_u16_le(0);
// CtlCode (4 bytes)
cursor.write_u32_le(self.ctl_code);
// FileId (16 bytes)
cursor.write_u64_le(self.file_id.persistent);
cursor.write_u64_le(self.file_id.volatile);
let input_count = self.input_data.len() as u32;
// Offset is from the beginning of the SMB2 header per spec.
// `start` is the cursor position at the beginning of the body;
// in a standalone request this equals Header::SIZE, in a compound
// it includes the preceding sub-requests.
let input_offset = if input_count > 0 {
(start as u32) + Self::FIXED_SIZE
} else {
0
};
// InputOffset (4 bytes)
cursor.write_u32_le(input_offset);
// InputCount (4 bytes)
cursor.write_u32_le(input_count);
// MaxInputResponse (4 bytes)
cursor.write_u32_le(self.max_input_response);
// OutputOffset (4 bytes) -- no output data in the request
cursor.write_u32_le(0);
// OutputCount (4 bytes) -- no output data in the request
cursor.write_u32_le(0);
// MaxOutputResponse (4 bytes)
cursor.write_u32_le(self.max_output_response);
// Flags (4 bytes)
cursor.write_u32_le(self.flags);
// Reserved2 (4 bytes)
cursor.write_u32_le(0);
// Buffer (variable)
cursor.write_bytes(&self.input_data);
}
}
impl Unpack for IoctlRequest {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let structure_size = cursor.read_u16_le()?;
if structure_size != Self::STRUCTURE_SIZE {
return Err(Error::invalid_data(format!(
"invalid IoctlRequest structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
let _reserved = cursor.read_u16_le()?;
let ctl_code = cursor.read_u32_le()?;
let persistent = cursor.read_u64_le()?;
let volatile = cursor.read_u64_le()?;
let _input_offset = cursor.read_u32_le()?;
let input_count = cursor.read_u32_le()?;
let max_input_response = cursor.read_u32_le()?;
let _output_offset = cursor.read_u32_le()?;
let _output_count = cursor.read_u32_le()?;
let max_output_response = cursor.read_u32_le()?;
let flags = cursor.read_u32_le()?;
let _reserved2 = cursor.read_u32_le()?;
let input_data = if input_count > 0 {
cursor.read_bytes_bounded(input_count as usize)?.to_vec()
} else {
Vec::new()
};
Ok(IoctlRequest {
ctl_code,
file_id: FileId {
persistent,
volatile,
},
max_input_response,
max_output_response,
flags,
input_data,
})
}
}
// ── IoctlResponse ──────────────────────────────────────────────────────
/// SMB2 IOCTL Response (MS-SMB2 section 2.2.32).
///
/// Sent by the server to return the results of an IOCTL operation.
///
/// Layout:
/// - StructureSize (2 bytes, must be 49)
/// - Reserved (2 bytes)
/// - CtlCode (4 bytes)
/// - FileId (16 bytes)
/// - InputOffset (4 bytes)
/// - InputCount (4 bytes)
/// - OutputOffset (4 bytes)
/// - OutputCount (4 bytes)
/// - Flags (4 bytes)
/// - Reserved2 (4 bytes)
/// - Buffer (variable -- may contain both input and output data)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IoctlResponse {
/// The control code echoed from the request.
pub ctl_code: u32,
/// The file handle echoed from the request.
pub file_id: FileId,
/// Flags echoed from the request.
pub flags: u32,
/// Output data buffer returned by the server.
pub output_data: Vec<u8>,
}
impl IoctlResponse {
pub const STRUCTURE_SIZE: u16 = 49;
/// Fixed header size before the variable buffer (48 bytes).
const FIXED_SIZE: u32 = 48;
}
impl Pack for IoctlResponse {
fn pack(&self, cursor: &mut WriteCursor) {
let start = cursor.position();
// StructureSize (2 bytes)
cursor.write_u16_le(Self::STRUCTURE_SIZE);
// Reserved (2 bytes)
cursor.write_u16_le(0);
// CtlCode (4 bytes)
cursor.write_u32_le(self.ctl_code);
// FileId (16 bytes)
cursor.write_u64_le(self.file_id.persistent);
cursor.write_u64_le(self.file_id.volatile);
let output_count = self.output_data.len() as u32;
// Offset is from the beginning of the SMB2 header per spec.
let output_offset = if output_count > 0 {
(start as u32) + Self::FIXED_SIZE
} else {
0
};
// InputOffset (4 bytes) -- no input data in the response
cursor.write_u32_le(0);
// InputCount (4 bytes)
cursor.write_u32_le(0);
// OutputOffset (4 bytes)
cursor.write_u32_le(output_offset);
// OutputCount (4 bytes)
cursor.write_u32_le(output_count);
// Flags (4 bytes)
cursor.write_u32_le(self.flags);
// Reserved2 (4 bytes)
cursor.write_u32_le(0);
// Buffer (variable)
cursor.write_bytes(&self.output_data);
}
}
impl Unpack for IoctlResponse {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let structure_size = cursor.read_u16_le()?;
if structure_size != Self::STRUCTURE_SIZE {
return Err(Error::invalid_data(format!(
"invalid IoctlResponse structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
let _reserved = cursor.read_u16_le()?;
let ctl_code = cursor.read_u32_le()?;
let persistent = cursor.read_u64_le()?;
let volatile = cursor.read_u64_le()?;
let _input_offset = cursor.read_u32_le()?;
let _input_count = cursor.read_u32_le()?;
let _output_offset = cursor.read_u32_le()?;
let output_count = cursor.read_u32_le()?;
let flags = cursor.read_u32_le()?;
let _reserved2 = cursor.read_u32_le()?;
let output_data = if output_count > 0 {
cursor.read_bytes_bounded(output_count as usize)?.to_vec()
} else {
Vec::new()
};
Ok(IoctlResponse {
ctl_code,
file_id: FileId {
persistent,
volatile,
},
flags,
output_data,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
// ── IoctlRequest tests ────────────────────────────────────────────
#[test]
fn ioctl_request_roundtrip_with_input_data() {
let original = IoctlRequest {
ctl_code: FSCTL_PIPE_TRANSCEIVE,
file_id: FileId {
persistent: 0x1122_3344_5566_7788,
volatile: 0xAABB_CCDD_EEFF_0011,
},
max_input_response: 0,
max_output_response: 4096,
flags: SMB2_0_IOCTL_IS_FSCTL,
input_data: vec![0x01, 0x02, 0x03, 0x04, 0x05],
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// Fixed 56 bytes + 5 bytes input data
assert_eq!(bytes.len(), 61);
let mut r = ReadCursor::new(&bytes);
let decoded = IoctlRequest::unpack(&mut r).unwrap();
assert_eq!(decoded.ctl_code, FSCTL_PIPE_TRANSCEIVE);
assert_eq!(decoded.file_id, original.file_id);
assert_eq!(decoded.max_input_response, 0);
assert_eq!(decoded.max_output_response, 4096);
assert_eq!(decoded.flags, SMB2_0_IOCTL_IS_FSCTL);
assert_eq!(decoded.input_data, vec![0x01, 0x02, 0x03, 0x04, 0x05]);
}
#[test]
fn ioctl_request_roundtrip_no_input_data() {
let original = IoctlRequest {
ctl_code: FSCTL_VALIDATE_NEGOTIATE_INFO,
file_id: FileId::SENTINEL,
max_input_response: 0,
max_output_response: 256,
flags: SMB2_0_IOCTL_IS_FSCTL,
input_data: Vec::new(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
assert_eq!(bytes.len(), 56);
let mut r = ReadCursor::new(&bytes);
let decoded = IoctlRequest::unpack(&mut r).unwrap();
assert_eq!(decoded.ctl_code, FSCTL_VALIDATE_NEGOTIATE_INFO);
assert_eq!(decoded.file_id, FileId::SENTINEL);
assert!(decoded.input_data.is_empty());
}
#[test]
fn ioctl_request_wrong_structure_size() {
let mut buf = [0u8; 56];
buf[0..2].copy_from_slice(&99u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = IoctlRequest::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
// ── IoctlResponse tests ───────────────────────────────────────────
#[test]
fn ioctl_response_roundtrip_with_output_data() {
let original = IoctlResponse {
ctl_code: FSCTL_PIPE_TRANSCEIVE,
file_id: FileId {
persistent: 0x42,
volatile: 0x99,
},
flags: SMB2_0_IOCTL_IS_FSCTL,
output_data: vec![0xDE, 0xAD, 0xBE, 0xEF],
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// Fixed 48 bytes + 4 bytes output data
assert_eq!(bytes.len(), 52);
let mut r = ReadCursor::new(&bytes);
let decoded = IoctlResponse::unpack(&mut r).unwrap();
assert_eq!(decoded.ctl_code, FSCTL_PIPE_TRANSCEIVE);
assert_eq!(decoded.file_id, original.file_id);
assert_eq!(decoded.flags, SMB2_0_IOCTL_IS_FSCTL);
assert_eq!(decoded.output_data, vec![0xDE, 0xAD, 0xBE, 0xEF]);
}
#[test]
fn ioctl_response_roundtrip_no_output_data() {
let original = IoctlResponse {
ctl_code: FSCTL_SRV_COPYCHUNK,
file_id: FileId::default(),
flags: 0,
output_data: Vec::new(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
assert_eq!(bytes.len(), 48);
let mut r = ReadCursor::new(&bytes);
let decoded = IoctlResponse::unpack(&mut r).unwrap();
assert_eq!(decoded.ctl_code, FSCTL_SRV_COPYCHUNK);
assert!(decoded.output_data.is_empty());
}
#[test]
fn ioctl_response_wrong_structure_size() {
let mut buf = [0u8; 48];
buf[0..2].copy_from_slice(&42u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = IoctlResponse::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
}
#[cfg(test)]
mod roundtrip_props {
use super::*;
use crate::msg::roundtrip_strategies::{arb_bytes, arb_file_id};
use proptest::prelude::*;
proptest! {
#[test]
fn ioctl_request_pack_unpack(
ctl_code in any::<u32>(),
file_id in arb_file_id(),
max_input_response in any::<u32>(),
max_output_response in any::<u32>(),
flags in any::<u32>(),
input_data in arb_bytes(),
) {
let original = IoctlRequest {
ctl_code,
file_id,
max_input_response,
max_output_response,
flags,
input_data,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = IoctlRequest::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
prop_assert!(r.is_empty());
}
#[test]
fn ioctl_response_pack_unpack(
ctl_code in any::<u32>(),
file_id in arb_file_id(),
flags in any::<u32>(),
output_data in arb_bytes(),
) {
let original = IoctlResponse {
ctl_code,
file_id,
flags,
output_data,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = IoctlResponse::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
prop_assert!(r.is_empty());
}
}
}

445
vendor/smb2/src/msg/lock.rs vendored Normal file
View File

@@ -0,0 +1,445 @@
//! SMB2 LOCK Request and Response (MS-SMB2 sections 2.2.26, 2.2.27).
//!
//! The LOCK request locks or unlocks byte ranges within a file.
//! Multiple ranges can be locked/unlocked in a single request.
use crate::error::Result;
use crate::pack::{Pack, ReadCursor, Unpack, WriteCursor};
use crate::types::FileId;
use crate::Error;
/// Lock flag: shared lock (allows other readers).
pub const SMB2_LOCKFLAG_SHARED_LOCK: u32 = 0x0000_0001;
/// Lock flag: exclusive lock (no other readers or writers).
pub const SMB2_LOCKFLAG_EXCLUSIVE_LOCK: u32 = 0x0000_0002;
/// Lock flag: unlock a previously locked range.
pub const SMB2_LOCKFLAG_UNLOCK: u32 = 0x0000_0004;
/// Lock flag: fail immediately if the lock conflicts.
pub const SMB2_LOCKFLAG_FAIL_IMMEDIATELY: u32 = 0x0000_0010;
/// A single lock element describing a byte range to lock or unlock.
///
/// Each element is 24 bytes on the wire:
/// - Offset (8 bytes)
/// - Length (8 bytes)
/// - Flags (4 bytes)
/// - Reserved (4 bytes)
///
/// Reference: MS-SMB2 section 2.2.26.1.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LockElement {
/// Starting offset in bytes from where the range begins.
pub offset: u64,
/// Length of the range in bytes.
pub length: u64,
/// Flags describing how the range is locked or unlocked.
pub flags: u32,
}
impl LockElement {
/// Wire size of a single lock element.
pub const SIZE: usize = 24;
}
impl Pack for LockElement {
fn pack(&self, cursor: &mut WriteCursor) {
cursor.write_u64_le(self.offset);
cursor.write_u64_le(self.length);
cursor.write_u32_le(self.flags);
cursor.write_u32_le(0); // Reserved
}
}
impl Unpack for LockElement {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let offset = cursor.read_u64_le()?;
let length = cursor.read_u64_le()?;
let flags = cursor.read_u32_le()?;
let _reserved = cursor.read_u32_le()?;
Ok(LockElement {
offset,
length,
flags,
})
}
}
/// SMB2 LOCK Request (MS-SMB2 section 2.2.26).
///
/// Sent by the client to lock or unlock byte ranges. The fixed portion
/// is 48 bytes (StructureSize=48, which includes one `LockElement`):
/// - StructureSize (2 bytes, must be 48)
/// - LockCount (2 bytes)
/// - LockSequenceNumber/Index (4 bytes)
/// - FileId (16 bytes)
/// - Locks (variable, LockCount x 24 bytes each)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LockRequest {
/// Combined lock sequence number (4 bits) and index (28 bits).
/// In SMB 2.0.2 this field is reserved (0).
pub lock_sequence: u32,
/// File handle to lock ranges on.
pub file_id: FileId,
/// Array of lock elements. Must contain at least one element.
pub locks: Vec<LockElement>,
}
impl LockRequest {
pub const STRUCTURE_SIZE: u16 = 48;
}
impl Pack for LockRequest {
fn pack(&self, cursor: &mut WriteCursor) {
cursor.write_u16_le(Self::STRUCTURE_SIZE);
cursor.write_u16_le(self.locks.len() as u16); // LockCount
cursor.write_u32_le(self.lock_sequence);
cursor.write_u64_le(self.file_id.persistent);
cursor.write_u64_le(self.file_id.volatile);
for lock in &self.locks {
lock.pack(cursor);
}
}
}
impl Unpack for LockRequest {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let structure_size = cursor.read_u16_le()?;
if structure_size != Self::STRUCTURE_SIZE {
return Err(Error::invalid_data(format!(
"invalid LockRequest structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
let lock_count = cursor.read_u16_le()?;
let lock_sequence = cursor.read_u32_le()?;
let persistent = cursor.read_u64_le()?;
let volatile = cursor.read_u64_le()?;
let mut locks = Vec::with_capacity(lock_count as usize);
for _ in 0..lock_count {
locks.push(LockElement::unpack(cursor)?);
}
Ok(LockRequest {
lock_sequence,
file_id: FileId {
persistent,
volatile,
},
locks,
})
}
}
/// SMB2 LOCK Response (MS-SMB2 section 2.2.27).
///
/// Sent by the server to confirm a lock operation. The structure is 4 bytes:
/// - StructureSize (2 bytes, must be 4)
/// - Reserved (2 bytes)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LockResponse;
impl LockResponse {
pub const STRUCTURE_SIZE: u16 = 4;
}
impl Pack for LockResponse {
fn pack(&self, cursor: &mut WriteCursor) {
cursor.write_u16_le(Self::STRUCTURE_SIZE);
cursor.write_u16_le(0); // Reserved
}
}
impl Unpack for LockResponse {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let structure_size = cursor.read_u16_le()?;
if structure_size != Self::STRUCTURE_SIZE {
return Err(Error::invalid_data(format!(
"invalid LockResponse structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
let _reserved = cursor.read_u16_le()?;
Ok(LockResponse)
}
}
#[cfg(test)]
mod tests {
use super::*;
// ── LockElement tests ──────────────────────────────────────────
#[test]
fn lock_element_roundtrip() {
let original = LockElement {
offset: 0x1000,
length: 0x2000,
flags: SMB2_LOCKFLAG_EXCLUSIVE_LOCK | SMB2_LOCKFLAG_FAIL_IMMEDIATELY,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
assert_eq!(bytes.len(), LockElement::SIZE);
let mut r = ReadCursor::new(&bytes);
let decoded = LockElement::unpack(&mut r).unwrap();
assert_eq!(decoded, original);
}
// ── LockRequest tests ──────────────────────────────────────────
#[test]
fn lock_request_single_lock_roundtrip() {
let original = LockRequest {
lock_sequence: 0,
file_id: FileId {
persistent: 0xDEAD,
volatile: 0xBEEF,
},
locks: vec![LockElement {
offset: 0,
length: 4096,
flags: SMB2_LOCKFLAG_SHARED_LOCK,
}],
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// Fixed: 24 bytes + 1 lock element (24 bytes) = 48 bytes
assert_eq!(bytes.len(), 48);
let mut r = ReadCursor::new(&bytes);
let decoded = LockRequest::unpack(&mut r).unwrap();
assert_eq!(decoded.lock_sequence, original.lock_sequence);
assert_eq!(decoded.file_id, original.file_id);
assert_eq!(decoded.locks.len(), 1);
assert_eq!(decoded.locks[0], original.locks[0]);
}
#[test]
fn lock_request_multiple_locks_roundtrip() {
let original = LockRequest {
lock_sequence: 0x1234_5678,
file_id: FileId {
persistent: 0x1111,
volatile: 0x2222,
},
locks: vec![
LockElement {
offset: 0,
length: 1024,
flags: SMB2_LOCKFLAG_EXCLUSIVE_LOCK | SMB2_LOCKFLAG_FAIL_IMMEDIATELY,
},
LockElement {
offset: 4096,
length: 2048,
flags: SMB2_LOCKFLAG_SHARED_LOCK,
},
LockElement {
offset: 8192,
length: 512,
flags: SMB2_LOCKFLAG_UNLOCK,
},
],
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// Fixed: 24 bytes + 3 lock elements (3 * 24) = 96 bytes
assert_eq!(bytes.len(), 96);
let mut r = ReadCursor::new(&bytes);
let decoded = LockRequest::unpack(&mut r).unwrap();
assert_eq!(decoded.lock_sequence, original.lock_sequence);
assert_eq!(decoded.file_id, original.file_id);
assert_eq!(decoded.locks.len(), 3);
assert_eq!(decoded.locks[0], original.locks[0]);
assert_eq!(decoded.locks[1], original.locks[1]);
assert_eq!(decoded.locks[2], original.locks[2]);
}
#[test]
fn lock_request_known_bytes() {
let mut buf = Vec::new();
// StructureSize = 48
buf.extend_from_slice(&48u16.to_le_bytes());
// LockCount = 1
buf.extend_from_slice(&1u16.to_le_bytes());
// LockSequence = 0
buf.extend_from_slice(&0u32.to_le_bytes());
// FileId persistent = 0x10
buf.extend_from_slice(&0x10u64.to_le_bytes());
// FileId volatile = 0x20
buf.extend_from_slice(&0x20u64.to_le_bytes());
// LockElement: offset = 0, length = 100, flags = SHARED (1), reserved = 0
buf.extend_from_slice(&0u64.to_le_bytes());
buf.extend_from_slice(&100u64.to_le_bytes());
buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(&0u32.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let req = LockRequest::unpack(&mut cursor).unwrap();
assert_eq!(req.file_id.persistent, 0x10);
assert_eq!(req.file_id.volatile, 0x20);
assert_eq!(req.locks.len(), 1);
assert_eq!(req.locks[0].offset, 0);
assert_eq!(req.locks[0].length, 100);
assert_eq!(req.locks[0].flags, SMB2_LOCKFLAG_SHARED_LOCK);
}
#[test]
fn lock_request_wrong_structure_size() {
let mut buf = [0u8; 48];
buf[0..2].copy_from_slice(&99u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = LockRequest::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
// ── LockResponse tests ─────────────────────────────────────────
#[test]
fn lock_response_roundtrip() {
let original = LockResponse;
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// 2 + 2 = 4 bytes
assert_eq!(bytes.len(), 4);
let mut r = ReadCursor::new(&bytes);
let _decoded = LockResponse::unpack(&mut r).unwrap();
}
#[test]
fn lock_response_known_bytes() {
let mut buf = [0u8; 4];
buf[0..2].copy_from_slice(&4u16.to_le_bytes());
buf[2..4].copy_from_slice(&0u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let _resp = LockResponse::unpack(&mut cursor).unwrap();
}
#[test]
fn lock_response_wrong_structure_size() {
let mut buf = [0u8; 4];
buf[0..2].copy_from_slice(&8u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = LockResponse::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
#[test]
fn lock_flags_combinations() {
// Verify flag constants are distinct and correct
assert_eq!(SMB2_LOCKFLAG_SHARED_LOCK, 0x01);
assert_eq!(SMB2_LOCKFLAG_EXCLUSIVE_LOCK, 0x02);
assert_eq!(SMB2_LOCKFLAG_UNLOCK, 0x04);
assert_eq!(SMB2_LOCKFLAG_FAIL_IMMEDIATELY, 0x10);
// Shared + fail immediately
let combined = SMB2_LOCKFLAG_SHARED_LOCK | SMB2_LOCKFLAG_FAIL_IMMEDIATELY;
assert_eq!(combined, 0x11);
// Exclusive + fail immediately
let combined = SMB2_LOCKFLAG_EXCLUSIVE_LOCK | SMB2_LOCKFLAG_FAIL_IMMEDIATELY;
assert_eq!(combined, 0x12);
}
}
#[cfg(test)]
mod roundtrip_props {
use super::*;
use crate::msg::roundtrip_strategies::arb_file_id;
use proptest::prelude::*;
fn arb_lock_element() -> impl Strategy<Value = LockElement> {
(any::<u64>(), any::<u64>(), any::<u32>()).prop_map(|(offset, length, flags)| LockElement {
offset,
length,
flags,
})
}
proptest! {
#[test]
fn lock_element_pack_unpack(elem in arb_lock_element()) {
let mut w = WriteCursor::new();
elem.pack(&mut w);
let bytes = w.into_inner();
prop_assert_eq!(bytes.len(), LockElement::SIZE);
let mut r = ReadCursor::new(&bytes);
let decoded = LockElement::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, elem);
prop_assert!(r.is_empty());
}
#[test]
fn lock_request_pack_unpack(
lock_sequence in any::<u32>(),
file_id in arb_file_id(),
// MS-SMB2: LockCount must be >= 1, so generate 1..=8.
locks in prop::collection::vec(arb_lock_element(), 1..=8),
) {
let original = LockRequest {
lock_sequence,
file_id,
locks,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = LockRequest::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
prop_assert!(r.is_empty());
}
#[test]
fn lock_response_pack_unpack(_ in any::<bool>()) {
// LockResponse is a unit struct; there's nothing to vary, but
// running it through the proptest harness keeps the coverage
// map uniform.
let original = LockResponse;
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = LockResponse::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
prop_assert!(r.is_empty());
}
}
}

42
vendor/smb2/src/msg/logoff.rs vendored Normal file
View File

@@ -0,0 +1,42 @@
//! SMB2 LOGOFF request and response (spec sections 2.2.7, 2.2.8).
//!
//! Logoff messages request and confirm termination of a session.
//! Both request and response contain only a StructureSize field and a
//! reserved field, for a total of 4 bytes each.
super::trivial_message! {
/// SMB2 LOGOFF request (spec section 2.2.7).
///
/// Sent by the client to request termination of a particular session.
/// Contains only StructureSize (2 bytes) and Reserved (2 bytes).
pub struct LogoffRequest;
}
super::trivial_message! {
/// SMB2 LOGOFF response (spec section 2.2.8).
///
/// Sent by the server to confirm that a LOGOFF request was processed.
/// Contains only StructureSize (2 bytes) and Reserved (2 bytes).
pub struct LogoffResponse;
}
#[cfg(test)]
mod tests {
use super::*;
super::super::trivial_message_tests!(
LogoffRequest,
logoff_request_known_bytes,
logoff_request_roundtrip,
logoff_request_wrong_structure_size,
logoff_request_too_short
);
super::super::trivial_message_tests!(
LogoffResponse,
logoff_response_known_bytes,
logoff_response_roundtrip,
logoff_response_wrong_structure_size,
logoff_response_too_short
);
}

152
vendor/smb2/src/msg/mod.rs vendored Normal file
View File

@@ -0,0 +1,152 @@
//! Wire format message structs for SMB2/3.
//!
//! Each sub-module corresponds to one SMB2 command type with its
//! request and response structures.
//!
//! Most users don't need this module directly -- use [`SmbClient`](crate::SmbClient)
//! for high-level file operations.
// Wire format internals, comments would be pretty redundant. Public API docs are enforced at the crate level.
#![allow(missing_docs)]
/// Generates a trivial 4-byte SMB2 stub message (StructureSize + Reserved).
///
/// Many SMB2 commands (echo, cancel, logoff, tree_disconnect) have request
/// and/or response structs that are identical: 2-byte StructureSize (always 4)
/// plus 2-byte Reserved. This macro generates the struct definition and its
/// `Pack`/`Unpack` impls from a single declaration.
///
/// # Usage
///
/// ```ignore
/// trivial_message! {
/// /// Doc comment for the struct.
/// pub struct EchoRequest;
/// }
/// ```
macro_rules! trivial_message {
(
$(#[$meta:meta])*
pub struct $name:ident;
) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct $name;
impl $name {
pub const STRUCTURE_SIZE: u16 = 4;
}
impl crate::pack::Pack for $name {
fn pack(&self, cursor: &mut crate::pack::WriteCursor) {
// StructureSize (2 bytes)
cursor.write_u16_le(Self::STRUCTURE_SIZE);
// Reserved (2 bytes)
cursor.write_u16_le(0);
}
}
impl crate::pack::Unpack for $name {
fn unpack(cursor: &mut crate::pack::ReadCursor<'_>) -> crate::error::Result<Self> {
// StructureSize (2 bytes)
let structure_size = cursor.read_u16_le()?;
if structure_size != Self::STRUCTURE_SIZE {
return Err(crate::Error::invalid_data(format!(
"invalid {} structure size: expected {}, got {}",
stringify!($name),
Self::STRUCTURE_SIZE,
structure_size
)));
}
// Reserved (2 bytes)
let _reserved = cursor.read_u16_le()?;
Ok($name)
}
}
};
}
pub(crate) use trivial_message;
/// Generates a minimal test suite for a trivial 4-byte message type.
///
/// Tests: known bytes, pack-unpack roundtrip, wrong structure size, and
/// truncated input. These four tests cover all interesting behavior for
/// types produced by [`trivial_message!`].
#[cfg(test)]
macro_rules! trivial_message_tests {
($type:ident, $known:ident, $roundtrip:ident, $wrong_size:ident, $short:ident) => {
#[test]
fn $known() {
let msg = $type;
let mut cursor = crate::pack::WriteCursor::new();
crate::pack::Pack::pack(&msg, &mut cursor);
let bytes = cursor.into_inner();
// StructureSize=4 (LE), Reserved=0
assert_eq!(bytes, [0x04, 0x00, 0x00, 0x00]);
}
#[test]
fn $roundtrip() {
let original = $type;
let mut w = crate::pack::WriteCursor::new();
crate::pack::Pack::pack(&original, &mut w);
let bytes = w.into_inner();
let mut r = crate::pack::ReadCursor::new(&bytes);
let decoded = <$type as crate::pack::Unpack>::unpack(&mut r).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn $wrong_size() {
let bytes = [0x08, 0x00, 0x00, 0x00];
let mut cursor = crate::pack::ReadCursor::new(&bytes);
let result = <$type as crate::pack::Unpack>::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
#[test]
fn $short() {
let bytes = [0x04, 0x00];
let mut cursor = crate::pack::ReadCursor::new(&bytes);
let result = <$type as crate::pack::Unpack>::unpack(&mut cursor);
assert!(result.is_err());
}
};
}
#[cfg(test)]
pub(crate) use trivial_message_tests;
#[cfg(test)]
pub(crate) mod roundtrip_strategies;
pub mod cancel;
pub mod change_notify;
pub mod close;
pub mod create;
pub mod dfs;
pub mod echo;
pub mod flush;
pub mod header;
pub mod ioctl;
pub mod lock;
pub mod logoff;
pub mod negotiate;
pub mod oplock_break;
pub mod query_directory;
pub mod query_info;
pub mod read;
pub mod session_setup;
pub mod set_info;
pub mod transform;
pub mod tree_connect;
pub mod tree_disconnect;
pub mod write;
pub use header::{ErrorResponse, Header, PROTOCOL_ID};

1228
vendor/smb2/src/msg/negotiate.rs vendored Normal file

File diff suppressed because it is too large Load Diff

262
vendor/smb2/src/msg/oplock_break.rs vendored Normal file
View File

@@ -0,0 +1,262 @@
//! SMB2 Oplock Break Notification, Acknowledgment, and Response
//! (MS-SMB2 sections 2.2.23, 2.2.24, 2.2.25).
//!
//! All three oplock break messages share an identical 24-byte wire format:
//! - StructureSize (2 bytes, must be 24)
//! - OplockLevel (1 byte)
//! - Reserved (1 byte)
//! - Reserved2 (4 bytes)
//! - FileId (16 bytes)
//!
//! We define one shared struct and provide type aliases for each role.
//!
//! Note: Lease break notification/acknowledgment/response (sections 2.2.23.2,
//! 2.2.24.2, 2.2.25.2) use a different structure with LeaseKey, LeaseState,
//! etc. Lease break handling is deferred to a future implementation.
use crate::error::Result;
use crate::pack::{Pack, ReadCursor, Unpack, WriteCursor};
use crate::types::{FileId, OplockLevel};
use crate::Error;
// ── OplockBreak (shared struct) ────────────────────────────────────────
/// Shared wire format for oplock break notification, acknowledgment, and
/// response messages (MS-SMB2 sections 2.2.23, 2.2.24, 2.2.25).
///
/// All three messages have an identical 24-byte layout. The message's role
/// (notification vs acknowledgment vs response) is determined by the header's
/// command code and flags, not by this structure.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OplockBreak {
/// The oplock level.
pub oplock_level: OplockLevel,
/// The file handle associated with the oplock.
pub file_id: FileId,
}
impl OplockBreak {
pub const STRUCTURE_SIZE: u16 = 24;
}
impl Pack for OplockBreak {
fn pack(&self, cursor: &mut WriteCursor) {
// StructureSize (2 bytes)
cursor.write_u16_le(Self::STRUCTURE_SIZE);
// OplockLevel (1 byte)
cursor.write_u8(self.oplock_level as u8);
// Reserved (1 byte)
cursor.write_u8(0);
// Reserved2 (4 bytes)
cursor.write_u32_le(0);
// FileId (16 bytes)
cursor.write_u64_le(self.file_id.persistent);
cursor.write_u64_le(self.file_id.volatile);
}
}
impl Unpack for OplockBreak {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let structure_size = cursor.read_u16_le()?;
if structure_size != Self::STRUCTURE_SIZE {
return Err(Error::invalid_data(format!(
"invalid OplockBreak structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
let oplock_level = OplockLevel::try_from(cursor.read_u8()?)?;
let _reserved = cursor.read_u8()?;
let _reserved2 = cursor.read_u32_le()?;
let persistent = cursor.read_u64_le()?;
let volatile = cursor.read_u64_le()?;
Ok(OplockBreak {
oplock_level,
file_id: FileId {
persistent,
volatile,
},
})
}
}
/// Oplock break notification (server to client, MS-SMB2 section 2.2.23).
///
/// Arrives with `MessageId = 0xFFFFFFFFFFFFFFFF` (unsolicited).
pub type OplockBreakNotification = OplockBreak;
/// Oplock break acknowledgment (client to server, MS-SMB2 section 2.2.24).
pub type OplockBreakAcknowledgment = OplockBreak;
/// Oplock break response (server to client after ack, MS-SMB2 section 2.2.25).
pub type OplockBreakResponse = OplockBreak;
#[cfg(test)]
mod tests {
use super::*;
// ── OplockBreakNotification tests ─────────────────────────────────
#[test]
fn oplock_break_notification_roundtrip() {
let original = OplockBreakNotification {
oplock_level: OplockLevel::LevelII,
file_id: FileId {
persistent: 0x1122_3344_5566_7788,
volatile: 0xAABB_CCDD_EEFF_0011,
},
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// Fixed 24 bytes
assert_eq!(bytes.len(), 24);
let mut r = ReadCursor::new(&bytes);
let decoded = OplockBreakNotification::unpack(&mut r).unwrap();
assert_eq!(decoded.oplock_level, OplockLevel::LevelII);
assert_eq!(decoded.file_id, original.file_id);
}
#[test]
fn oplock_break_notification_exclusive_level() {
let original = OplockBreakNotification {
oplock_level: OplockLevel::Exclusive,
file_id: FileId {
persistent: 0x42,
volatile: 0x99,
},
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = OplockBreakNotification::unpack(&mut r).unwrap();
assert_eq!(decoded.oplock_level, OplockLevel::Exclusive);
assert_eq!(decoded.file_id.persistent, 0x42);
assert_eq!(decoded.file_id.volatile, 0x99);
}
// ── OplockBreakAcknowledgment tests ───────────────────────────────
#[test]
fn oplock_break_acknowledgment_roundtrip() {
let original = OplockBreakAcknowledgment {
oplock_level: OplockLevel::None,
file_id: FileId {
persistent: 0xDEAD,
volatile: 0xBEEF,
},
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
assert_eq!(bytes.len(), 24);
let mut r = ReadCursor::new(&bytes);
let decoded = OplockBreakAcknowledgment::unpack(&mut r).unwrap();
assert_eq!(decoded.oplock_level, OplockLevel::None);
assert_eq!(decoded.file_id, original.file_id);
}
// ── OplockBreakResponse tests ─────────────────────────────────────
#[test]
fn oplock_break_response_roundtrip() {
let original = OplockBreakResponse {
oplock_level: OplockLevel::Batch,
file_id: FileId {
persistent: 0xCAFE,
volatile: 0xFACE,
},
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
assert_eq!(bytes.len(), 24);
let mut r = ReadCursor::new(&bytes);
let decoded = OplockBreakResponse::unpack(&mut r).unwrap();
assert_eq!(decoded.oplock_level, OplockLevel::Batch);
assert_eq!(decoded.file_id, original.file_id);
}
// ── Error tests ───────────────────────────────────────────────────
#[test]
fn oplock_break_wrong_structure_size() {
let mut buf = [0u8; 24];
buf[0..2].copy_from_slice(&99u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = OplockBreak::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
// Roundtrip property tests live in `roundtrip_props` at file end.
#[test]
fn oplock_break_reserved_fields_ignored() {
let mut buf = [0u8; 24];
// StructureSize = 24
buf[0..2].copy_from_slice(&24u16.to_le_bytes());
// OplockLevel = LEVEL_II
buf[2] = OplockLevel::LevelII as u8;
// Reserved = 0xFF (should be ignored)
buf[3] = 0xFF;
// Reserved2 = 0xDEADBEEF (should be ignored)
buf[4..8].copy_from_slice(&0xDEAD_BEEFu32.to_le_bytes());
// FileId persistent = 1
buf[8..16].copy_from_slice(&1u64.to_le_bytes());
// FileId volatile = 2
buf[16..24].copy_from_slice(&2u64.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let decoded = OplockBreak::unpack(&mut cursor).unwrap();
assert_eq!(decoded.oplock_level, OplockLevel::LevelII);
assert_eq!(decoded.file_id.persistent, 1);
assert_eq!(decoded.file_id.volatile, 2);
}
}
#[cfg(test)]
mod roundtrip_props {
use super::*;
use crate::msg::roundtrip_strategies::{arb_file_id, arb_oplock_level};
use proptest::prelude::*;
proptest! {
#[test]
fn oplock_break_pack_unpack(
oplock_level in arb_oplock_level(),
file_id in arb_file_id(),
) {
let original = OplockBreak { oplock_level, file_id };
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = OplockBreak::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
prop_assert!(r.is_empty());
}
}
}

476
vendor/smb2/src/msg/query_directory.rs vendored Normal file
View File

@@ -0,0 +1,476 @@
//! SMB2 QUERY_DIRECTORY request and response (spec sections 2.2.33, 2.2.34).
//!
//! Used by the client to enumerate directory contents. The request specifies
//! a search pattern (typically `"*"`) and the response contains directory
//! entries in the requested information class format.
use crate::error::Result;
use crate::msg::header::Header;
use crate::pack::{Pack, ReadCursor, Unpack, WriteCursor};
use crate::types::FileId;
use crate::Error;
// ── Enums / flags ────────────────────────────────────────────────────────
/// File information class for directory queries (MS-SMB2 2.2.33).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum FileInformationClass {
/// Basic directory information.
FileDirectoryInformation = 0x01,
/// Full directory information.
FileFullDirectoryInformation = 0x02,
/// Both short and long name information.
FileBothDirectoryInformation = 0x03,
/// File names only.
FileNamesInformation = 0x0C,
/// Both short and long name information with file IDs.
FileIdBothDirectoryInformation = 0x25,
/// Full directory information with file IDs.
FileIdFullDirectoryInformation = 0x26,
}
impl TryFrom<u8> for FileInformationClass {
type Error = Error;
fn try_from(value: u8) -> Result<Self> {
match value {
0x01 => Ok(Self::FileDirectoryInformation),
0x02 => Ok(Self::FileFullDirectoryInformation),
0x03 => Ok(Self::FileBothDirectoryInformation),
0x0C => Ok(Self::FileNamesInformation),
0x25 => Ok(Self::FileIdBothDirectoryInformation),
0x26 => Ok(Self::FileIdFullDirectoryInformation),
_ => Err(Error::invalid_data(format!(
"invalid FileInformationClass: 0x{:02X}",
value
))),
}
}
}
/// Query directory flags (MS-SMB2 2.2.33).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct QueryDirectoryFlags(pub u8);
impl QueryDirectoryFlags {
/// Restart the enumeration from the beginning.
pub const RESTART_SCANS: u8 = 0x01;
/// Return only a single entry.
pub const RETURN_SINGLE_ENTRY: u8 = 0x02;
/// Resume from the specified file index.
pub const INDEX_SPECIFIED: u8 = 0x04;
/// Reopen the directory and change the search pattern.
pub const REOPEN: u8 = 0x10;
}
// ── QueryDirectoryRequest ────────────────────────────────────────────────
/// SMB2 QUERY_DIRECTORY request (spec section 2.2.33).
///
/// Sent by the client to enumerate files in a directory.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryDirectoryRequest {
/// The type of information to return for each directory entry.
pub file_information_class: FileInformationClass,
/// Flags controlling the query behavior.
pub flags: QueryDirectoryFlags,
/// Byte offset within the directory to resume enumeration from.
pub file_index: u32,
/// Handle to the directory being queried.
pub file_id: FileId,
/// Maximum number of bytes the server can return.
pub output_buffer_length: u32,
/// Search pattern (for example, `"*"` for all files).
pub file_name: String,
}
impl QueryDirectoryRequest {
pub const STRUCTURE_SIZE: u16 = 33;
}
impl Pack for QueryDirectoryRequest {
fn pack(&self, cursor: &mut WriteCursor) {
let start = cursor.position();
// StructureSize (2 bytes)
cursor.write_u16_le(Self::STRUCTURE_SIZE);
// FileInformationClass (1 byte)
cursor.write_u8(self.file_information_class as u8);
// Flags (1 byte)
cursor.write_u8(self.flags.0);
// FileIndex (4 bytes)
cursor.write_u32_le(self.file_index);
// FileId (16 bytes)
cursor.write_u64_le(self.file_id.persistent);
cursor.write_u64_le(self.file_id.volatile);
// FileNameOffset (2 bytes) -- placeholder
let name_offset_pos = cursor.position();
cursor.write_u16_le(0);
// FileNameLength (2 bytes) -- placeholder
let name_length_pos = cursor.position();
cursor.write_u16_le(0);
// OutputBufferLength (4 bytes)
cursor.write_u32_le(self.output_buffer_length);
if self.file_name.is_empty() {
// No search pattern: FileNameOffset and FileNameLength stay 0
// per spec section 2.2.33. Write 1 padding byte to satisfy
// StructureSize=33 (32 fixed + 1 byte buffer minimum).
cursor.write_u8(0);
} else {
// Buffer: filename pattern in UTF-16LE.
// Offset is from the beginning of the SMB2 header per spec.
let name_offset = Header::SIZE + (cursor.position() - start);
let name_start = cursor.position();
cursor.write_utf16_le(&self.file_name);
let name_byte_len = cursor.position() - name_start;
// Backpatch
cursor.set_u16_le_at(name_offset_pos, name_offset as u16);
cursor.set_u16_le_at(name_length_pos, name_byte_len as u16);
}
}
}
impl Unpack for QueryDirectoryRequest {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let start = cursor.position();
// StructureSize (2 bytes)
let structure_size = cursor.read_u16_le()?;
if structure_size != Self::STRUCTURE_SIZE {
return Err(Error::invalid_data(format!(
"invalid QueryDirectoryRequest structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
// FileInformationClass (1 byte)
let info_class = FileInformationClass::try_from(cursor.read_u8()?)?;
// Flags (1 byte)
let flags = QueryDirectoryFlags(cursor.read_u8()?);
// FileIndex (4 bytes)
let file_index = cursor.read_u32_le()?;
// FileId (16 bytes)
let persistent = cursor.read_u64_le()?;
let volatile = cursor.read_u64_le()?;
let file_id = FileId {
persistent,
volatile,
};
// FileNameOffset (2 bytes)
let name_offset = cursor.read_u16_le()? as usize;
// FileNameLength (2 bytes)
let name_length = cursor.read_u16_le()? as usize;
// OutputBufferLength (4 bytes)
let output_buffer_length = cursor.read_u32_le()?;
// Read filename
// Offset on the wire is from beginning of SMB2 header.
let file_name = if name_length > 0 {
let current = cursor.position();
let body_offset = name_offset.saturating_sub(Header::SIZE);
let target = start + body_offset;
if target > current {
cursor.skip(target - current)?;
}
cursor.read_utf16_le(name_length)?
} else {
String::new()
};
Ok(QueryDirectoryRequest {
file_information_class: info_class,
flags,
file_index,
file_id,
output_buffer_length,
file_name,
})
}
}
// ── QueryDirectoryResponse ───────────────────────────────────────────────
/// SMB2 QUERY_DIRECTORY response (spec section 2.2.34).
///
/// Contains directory enumeration data as raw bytes. The format depends
/// on the `FileInformationClass` from the request.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryDirectoryResponse {
/// Raw output buffer containing directory entries.
pub output_buffer: Vec<u8>,
}
impl QueryDirectoryResponse {
pub const STRUCTURE_SIZE: u16 = 9;
}
impl Pack for QueryDirectoryResponse {
fn pack(&self, cursor: &mut WriteCursor) {
let start = cursor.position();
// StructureSize (2 bytes)
cursor.write_u16_le(Self::STRUCTURE_SIZE);
// OutputBufferOffset (2 bytes) -- placeholder
let offset_pos = cursor.position();
cursor.write_u16_le(0);
// OutputBufferLength (4 bytes)
cursor.write_u32_le(self.output_buffer.len() as u32);
// Buffer
if !self.output_buffer.is_empty() {
// Offset is from the beginning of the SMB2 header per spec.
let buf_offset = Header::SIZE + (cursor.position() - start);
cursor.write_bytes(&self.output_buffer);
cursor.set_u16_le_at(offset_pos, buf_offset as u16);
}
}
}
impl Unpack for QueryDirectoryResponse {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let start = cursor.position();
// StructureSize (2 bytes)
let structure_size = cursor.read_u16_le()?;
if structure_size != Self::STRUCTURE_SIZE {
return Err(Error::invalid_data(format!(
"invalid QueryDirectoryResponse structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
// OutputBufferOffset (2 bytes)
let buf_offset = cursor.read_u16_le()? as usize;
// OutputBufferLength (4 bytes)
let buf_length = cursor.read_u32_le()? as usize;
// Read buffer
// Offset on the wire is from beginning of SMB2 header.
let output_buffer = if buf_length > 0 {
let current = cursor.position();
let body_offset = buf_offset.saturating_sub(Header::SIZE);
let target = start + body_offset;
if target > current {
cursor.skip(target - current)?;
}
cursor.read_bytes_bounded(buf_length)?.to_vec()
} else {
Vec::new()
};
Ok(QueryDirectoryResponse { output_buffer })
}
}
#[cfg(test)]
mod tests {
use super::*;
// ── QueryDirectoryRequest tests ──────────────────────────────────
#[test]
fn query_directory_request_roundtrip_star_pattern() {
let original = QueryDirectoryRequest {
file_information_class: FileInformationClass::FileBothDirectoryInformation,
flags: QueryDirectoryFlags(QueryDirectoryFlags::RESTART_SCANS),
file_index: 0,
file_id: FileId {
persistent: 0xAAAA_BBBB_CCCC_DDDD,
volatile: 0x1111_2222_3333_4444,
},
output_buffer_length: 65536,
file_name: "*".to_string(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = QueryDirectoryRequest::unpack(&mut r).unwrap();
assert_eq!(
decoded.file_information_class,
FileInformationClass::FileBothDirectoryInformation
);
assert_eq!(decoded.flags.0, QueryDirectoryFlags::RESTART_SCANS);
assert_eq!(decoded.file_index, 0);
assert_eq!(decoded.file_id, original.file_id);
assert_eq!(decoded.output_buffer_length, 65536);
assert_eq!(decoded.file_name, "*");
}
#[test]
fn query_directory_request_structure_size() {
let req = QueryDirectoryRequest {
file_information_class: FileInformationClass::FileDirectoryInformation,
flags: QueryDirectoryFlags::default(),
file_index: 0,
file_id: FileId::default(),
output_buffer_length: 1024,
file_name: "*".to_string(),
};
let mut w = WriteCursor::new();
req.pack(&mut w);
let bytes = w.into_inner();
assert_eq!(bytes[0], 33);
assert_eq!(bytes[1], 0);
}
#[test]
fn query_directory_request_wrong_structure_size() {
let mut buf = vec![0u8; 40];
buf[0..2].copy_from_slice(&99u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = QueryDirectoryRequest::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
// ── QueryDirectoryResponse tests ─────────────────────────────────
#[test]
fn query_directory_response_roundtrip_with_buffer() {
// Simulate raw directory entry data
let raw_entries = vec![
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
0x0F, 0x10,
];
let original = QueryDirectoryResponse {
output_buffer: raw_entries.clone(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = QueryDirectoryResponse::unpack(&mut r).unwrap();
assert_eq!(decoded.output_buffer, raw_entries);
}
#[test]
fn query_directory_response_empty_buffer() {
let original = QueryDirectoryResponse {
output_buffer: Vec::new(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// StructureSize(2) + Offset(2) + Length(4) = 8 bytes
assert_eq!(bytes.len(), 8);
let mut r = ReadCursor::new(&bytes);
let decoded = QueryDirectoryResponse::unpack(&mut r).unwrap();
assert!(decoded.output_buffer.is_empty());
}
#[test]
fn query_directory_response_structure_size() {
let resp = QueryDirectoryResponse {
output_buffer: vec![0xFF],
};
let mut w = WriteCursor::new();
resp.pack(&mut w);
let bytes = w.into_inner();
assert_eq!(bytes[0], 9);
assert_eq!(bytes[1], 0);
}
#[test]
fn query_directory_response_wrong_structure_size() {
let mut buf = vec![0u8; 16];
buf[0..2].copy_from_slice(&42u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = QueryDirectoryResponse::unpack(&mut cursor);
assert!(result.is_err());
}
// ── Enum tests ───────────────────────────────────────────────────
#[test]
fn file_information_class_roundtrip() {
for &class in &[
FileInformationClass::FileDirectoryInformation,
FileInformationClass::FileFullDirectoryInformation,
FileInformationClass::FileBothDirectoryInformation,
FileInformationClass::FileNamesInformation,
FileInformationClass::FileIdFullDirectoryInformation,
FileInformationClass::FileIdBothDirectoryInformation,
] {
let raw = class as u8;
let decoded = FileInformationClass::try_from(raw).unwrap();
assert_eq!(decoded, class);
}
}
#[test]
fn file_information_class_invalid() {
assert!(FileInformationClass::try_from(0xFF).is_err());
}
}
#[cfg(test)]
mod roundtrip_props {
use super::*;
use crate::msg::roundtrip_strategies::{
arb_bytes, arb_file_id, arb_file_information_class, arb_utf16_string,
};
use proptest::prelude::*;
proptest! {
#[test]
fn query_directory_request_pack_unpack(
file_information_class in arb_file_information_class(),
flags_raw in any::<u8>(),
file_index in any::<u32>(),
file_id in arb_file_id(),
output_buffer_length in any::<u32>(),
// Search pattern is UTF-16LE on the wire. Allow empty + typical.
file_name in arb_utf16_string(128),
) {
let original = QueryDirectoryRequest {
file_information_class,
flags: QueryDirectoryFlags(flags_raw),
file_index,
file_id,
output_buffer_length,
file_name,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = QueryDirectoryRequest::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
}
#[test]
fn query_directory_response_pack_unpack(output_buffer in arb_bytes()) {
let original = QueryDirectoryResponse { output_buffer };
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = QueryDirectoryResponse::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
}
}
}

479
vendor/smb2/src/msg/query_info.rs vendored Normal file
View File

@@ -0,0 +1,479 @@
//! SMB2 QUERY_INFO request and response (spec sections 2.2.37, 2.2.38).
//!
//! Used to query file, filesystem, security, or quota information.
//! The response buffer is stored as raw bytes -- parsing into specific
//! information classes is deferred.
use crate::error::Result;
use crate::msg::header::Header;
use crate::pack::{Pack, ReadCursor, Unpack, WriteCursor};
use crate::types::FileId;
use crate::Error;
// ── Enums ────────────────────────────────────────────────────────────────
/// Info type for query/set info operations (MS-SMB2 2.2.37).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum InfoType {
/// Query file information.
File = 0x01,
/// Query filesystem information.
Filesystem = 0x02,
/// Query security information.
Security = 0x03,
/// Query quota information.
Quota = 0x04,
}
impl TryFrom<u8> for InfoType {
type Error = Error;
fn try_from(value: u8) -> Result<Self> {
match value {
0x01 => Ok(Self::File),
0x02 => Ok(Self::Filesystem),
0x03 => Ok(Self::Security),
0x04 => Ok(Self::Quota),
_ => Err(Error::invalid_data(format!(
"invalid InfoType: 0x{:02X}",
value
))),
}
}
}
// ── QueryInfoRequest ─────────────────────────────────────────────────────
/// SMB2 QUERY_INFO request (spec section 2.2.37).
///
/// Sent by the client to query information about a file, filesystem,
/// security descriptor, or quota.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryInfoRequest {
/// The type of information being queried.
pub info_type: InfoType,
/// The file information class (interpretation depends on `info_type`).
pub file_info_class: u8,
/// Maximum number of output bytes the server may return.
pub output_buffer_length: u32,
/// Additional information flags (for example, security information flags).
pub additional_information: u32,
/// Query flags.
pub flags: u32,
/// Handle to the file or directory being queried.
pub file_id: FileId,
/// Optional input buffer (for example, for quota queries).
pub input_buffer: Vec<u8>,
}
impl QueryInfoRequest {
pub const STRUCTURE_SIZE: u16 = 41;
}
impl Pack for QueryInfoRequest {
fn pack(&self, cursor: &mut WriteCursor) {
let start = cursor.position();
// StructureSize (2 bytes)
cursor.write_u16_le(Self::STRUCTURE_SIZE);
// InfoType (1 byte)
cursor.write_u8(self.info_type as u8);
// FileInfoClass (1 byte)
cursor.write_u8(self.file_info_class);
// OutputBufferLength (4 bytes)
cursor.write_u32_le(self.output_buffer_length);
// InputBufferOffset (2 bytes) -- placeholder
let input_offset_pos = cursor.position();
cursor.write_u16_le(0);
// Reserved (2 bytes)
cursor.write_u16_le(0);
// InputBufferLength (4 bytes)
cursor.write_u32_le(self.input_buffer.len() as u32);
// AdditionalInformation (4 bytes)
cursor.write_u32_le(self.additional_information);
// Flags (4 bytes)
cursor.write_u32_le(self.flags);
// FileId (16 bytes)
cursor.write_u64_le(self.file_id.persistent);
cursor.write_u64_le(self.file_id.volatile);
// Buffer (variable)
if !self.input_buffer.is_empty() {
// Offset is from the beginning of the SMB2 header per spec.
let buf_offset = Header::SIZE + (cursor.position() - start);
cursor.write_bytes(&self.input_buffer);
cursor.set_u16_le_at(input_offset_pos, buf_offset as u16);
}
}
}
impl Unpack for QueryInfoRequest {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let start = cursor.position();
// StructureSize (2 bytes)
let structure_size = cursor.read_u16_le()?;
if structure_size != Self::STRUCTURE_SIZE {
return Err(Error::invalid_data(format!(
"invalid QueryInfoRequest structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
// InfoType (1 byte)
let info_type = InfoType::try_from(cursor.read_u8()?)?;
// FileInfoClass (1 byte)
let file_info_class = cursor.read_u8()?;
// OutputBufferLength (4 bytes)
let output_buffer_length = cursor.read_u32_le()?;
// InputBufferOffset (2 bytes)
let input_offset = cursor.read_u16_le()? as usize;
// Reserved (2 bytes)
let _reserved = cursor.read_u16_le()?;
// InputBufferLength (4 bytes)
let input_length = cursor.read_u32_le()? as usize;
// AdditionalInformation (4 bytes)
let additional_information = cursor.read_u32_le()?;
// Flags (4 bytes)
let flags = cursor.read_u32_le()?;
// FileId (16 bytes)
let persistent = cursor.read_u64_le()?;
let volatile = cursor.read_u64_le()?;
let file_id = FileId {
persistent,
volatile,
};
// Read input buffer
// Offset on the wire is from beginning of SMB2 header.
let input_buffer = if input_length > 0 {
let current = cursor.position();
let body_offset = input_offset.saturating_sub(Header::SIZE);
let target = start + body_offset;
if target > current {
cursor.skip(target - current)?;
}
cursor.read_bytes_bounded(input_length)?.to_vec()
} else {
Vec::new()
};
Ok(QueryInfoRequest {
info_type,
file_info_class,
output_buffer_length,
additional_information,
flags,
file_id,
input_buffer,
})
}
}
// ── QueryInfoResponse ────────────────────────────────────────────────────
/// SMB2 QUERY_INFO response (spec section 2.2.38).
///
/// Contains the queried information as raw bytes. The format depends
/// on the `InfoType` and `FileInfoClass` from the request.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryInfoResponse {
/// Raw output buffer containing the queried information.
pub output_buffer: Vec<u8>,
}
impl QueryInfoResponse {
pub const STRUCTURE_SIZE: u16 = 9;
}
impl Pack for QueryInfoResponse {
fn pack(&self, cursor: &mut WriteCursor) {
let start = cursor.position();
// StructureSize (2 bytes)
cursor.write_u16_le(Self::STRUCTURE_SIZE);
// OutputBufferOffset (2 bytes) -- placeholder
let offset_pos = cursor.position();
cursor.write_u16_le(0);
// OutputBufferLength (4 bytes)
cursor.write_u32_le(self.output_buffer.len() as u32);
// Buffer
if !self.output_buffer.is_empty() {
// Offset is from the beginning of the SMB2 header per spec.
let buf_offset = Header::SIZE + (cursor.position() - start);
cursor.write_bytes(&self.output_buffer);
cursor.set_u16_le_at(offset_pos, buf_offset as u16);
}
}
}
impl Unpack for QueryInfoResponse {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let start = cursor.position();
// StructureSize (2 bytes)
let structure_size = cursor.read_u16_le()?;
if structure_size != Self::STRUCTURE_SIZE {
return Err(Error::invalid_data(format!(
"invalid QueryInfoResponse structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
// OutputBufferOffset (2 bytes)
let buf_offset = cursor.read_u16_le()? as usize;
// OutputBufferLength (4 bytes)
let buf_length = cursor.read_u32_le()? as usize;
// Read buffer
// Offset on the wire is from beginning of SMB2 header.
let output_buffer = if buf_length > 0 {
let current = cursor.position();
let body_offset = buf_offset.saturating_sub(Header::SIZE);
let target = start + body_offset;
if target > current {
cursor.skip(target - current)?;
}
cursor.read_bytes_bounded(buf_length)?.to_vec()
} else {
Vec::new()
};
Ok(QueryInfoResponse { output_buffer })
}
}
#[cfg(test)]
mod tests {
use super::*;
// ── QueryInfoRequest tests ───────────────────────────────────────
#[test]
fn query_info_request_roundtrip_file_info() {
let original = QueryInfoRequest {
info_type: InfoType::File,
file_info_class: 0x12, // FileAllInformation
output_buffer_length: 4096,
additional_information: 0,
flags: 0,
file_id: FileId {
persistent: 0xDEAD_BEEF_CAFE_BABE,
volatile: 0x1234_5678_9ABC_DEF0,
},
input_buffer: Vec::new(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = QueryInfoRequest::unpack(&mut r).unwrap();
assert_eq!(decoded.info_type, InfoType::File);
assert_eq!(decoded.file_info_class, 0x12);
assert_eq!(decoded.output_buffer_length, 4096);
assert_eq!(decoded.additional_information, 0);
assert_eq!(decoded.flags, 0);
assert_eq!(decoded.file_id, original.file_id);
assert!(decoded.input_buffer.is_empty());
}
#[test]
fn query_info_request_with_input_buffer() {
let input = vec![0x01, 0x02, 0x03, 0x04];
let original = QueryInfoRequest {
info_type: InfoType::Quota,
file_info_class: 0x20,
output_buffer_length: 8192,
additional_information: 0x04, // SACL_SECURITY_INFORMATION
flags: 0,
file_id: FileId {
persistent: 1,
volatile: 2,
},
input_buffer: input.clone(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = QueryInfoRequest::unpack(&mut r).unwrap();
assert_eq!(decoded.info_type, InfoType::Quota);
assert_eq!(decoded.input_buffer, input);
}
#[test]
fn query_info_request_structure_size() {
let req = QueryInfoRequest {
info_type: InfoType::File,
file_info_class: 0,
output_buffer_length: 0,
additional_information: 0,
flags: 0,
file_id: FileId::default(),
input_buffer: Vec::new(),
};
let mut w = WriteCursor::new();
req.pack(&mut w);
let bytes = w.into_inner();
assert_eq!(bytes[0], 41);
assert_eq!(bytes[1], 0);
}
#[test]
fn query_info_request_wrong_structure_size() {
let mut buf = vec![0u8; 48];
buf[0..2].copy_from_slice(&99u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = QueryInfoRequest::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
// ── QueryInfoResponse tests ──────────────────────────────────────
#[test]
fn query_info_response_roundtrip_with_data() {
let info_data = vec![
0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xA0, 0xB0, 0xC0,
];
let original = QueryInfoResponse {
output_buffer: info_data.clone(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = QueryInfoResponse::unpack(&mut r).unwrap();
assert_eq!(decoded.output_buffer, info_data);
}
#[test]
fn query_info_response_empty() {
let original = QueryInfoResponse {
output_buffer: Vec::new(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// StructureSize(2) + Offset(2) + Length(4) = 8
assert_eq!(bytes.len(), 8);
let mut r = ReadCursor::new(&bytes);
let decoded = QueryInfoResponse::unpack(&mut r).unwrap();
assert!(decoded.output_buffer.is_empty());
}
#[test]
fn query_info_response_structure_size() {
let resp = QueryInfoResponse {
output_buffer: vec![0xFF],
};
let mut w = WriteCursor::new();
resp.pack(&mut w);
let bytes = w.into_inner();
assert_eq!(bytes[0], 9);
assert_eq!(bytes[1], 0);
}
#[test]
fn query_info_response_wrong_structure_size() {
let mut buf = vec![0u8; 16];
buf[0..2].copy_from_slice(&42u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = QueryInfoResponse::unpack(&mut cursor);
assert!(result.is_err());
}
// ── Enum tests ───────────────────────────────────────────────────
#[test]
fn info_type_roundtrip() {
for &it in &[
InfoType::File,
InfoType::Filesystem,
InfoType::Security,
InfoType::Quota,
] {
let raw = it as u8;
let decoded = InfoType::try_from(raw).unwrap();
assert_eq!(decoded, it);
}
}
#[test]
fn info_type_invalid() {
assert!(InfoType::try_from(0x00).is_err());
assert!(InfoType::try_from(0x05).is_err());
}
}
#[cfg(test)]
mod roundtrip_props {
use super::*;
use crate::msg::roundtrip_strategies::{arb_bytes, arb_file_id, arb_info_type};
use proptest::prelude::*;
proptest! {
#[test]
fn query_info_request_pack_unpack(
info_type in arb_info_type(),
file_info_class in any::<u8>(),
output_buffer_length in any::<u32>(),
additional_information in any::<u32>(),
flags in any::<u32>(),
file_id in arb_file_id(),
input_buffer in arb_bytes(),
) {
let original = QueryInfoRequest {
info_type,
file_info_class,
output_buffer_length,
additional_information,
flags,
file_id,
input_buffer,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = QueryInfoRequest::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
}
#[test]
fn query_info_response_pack_unpack(output_buffer in arb_bytes()) {
let original = QueryInfoResponse { output_buffer };
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = QueryInfoResponse::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
}
}
}

462
vendor/smb2/src/msg/read.rs vendored Normal file
View File

@@ -0,0 +1,462 @@
//! SMB2 READ Request and Response (MS-SMB2 sections 2.2.19, 2.2.20).
//!
//! The READ request reads data from a file or named pipe.
//! The response carries the read data in a variable-length buffer.
use crate::error::Result;
use crate::pack::{Pack, ReadCursor, Unpack, WriteCursor};
use crate::types::FileId;
use crate::Error;
/// Read flag: read data directly from underlying storage (SMB 3.0.2+).
pub const SMB2_READFLAG_READ_UNBUFFERED: u8 = 0x01;
/// Read flag: request compressed response (SMB 3.1.1).
pub const SMB2_READFLAG_REQUEST_COMPRESSED: u8 = 0x02;
/// Channel value: no channel information.
pub const SMB2_CHANNEL_NONE: u32 = 0x0000_0000;
/// SMB2 READ Request (MS-SMB2 section 2.2.19).
///
/// Sent by the client to read data from a file. The fixed portion is 49 bytes
/// (StructureSize says 49 regardless of the variable buffer length):
/// - StructureSize (2 bytes, must be 49)
/// - Padding (1 byte)
/// - Flags (1 byte)
/// - Length (4 bytes)
/// - Offset (8 bytes)
/// - FileId (16 bytes)
/// - MinimumCount (4 bytes)
/// - Channel (4 bytes)
/// - RemainingBytes (4 bytes)
/// - ReadChannelInfoOffset (2 bytes)
/// - ReadChannelInfoLength (2 bytes)
/// - Buffer (variable, typically empty for basic reads)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadRequest {
/// Requested data placement offset in the response.
pub padding: u8,
/// Flags for the read operation.
pub flags: u8,
/// Number of bytes to read.
pub length: u32,
/// File offset to start reading from.
pub offset: u64,
/// File handle to read from.
pub file_id: FileId,
/// Minimum number of bytes for a successful read.
pub minimum_count: u32,
/// Channel for RDMA operations (typically `SMB2_CHANNEL_NONE`).
pub channel: u32,
/// Remaining bytes in a multi-part read.
pub remaining_bytes: u32,
/// Variable-length read channel info buffer.
pub read_channel_info: Vec<u8>,
}
impl ReadRequest {
pub const STRUCTURE_SIZE: u16 = 49;
}
impl Pack for ReadRequest {
fn pack(&self, cursor: &mut WriteCursor) {
cursor.write_u16_le(Self::STRUCTURE_SIZE);
cursor.write_u8(self.padding);
cursor.write_u8(self.flags);
cursor.write_u32_le(self.length);
cursor.write_u64_le(self.offset);
cursor.write_u64_le(self.file_id.persistent);
cursor.write_u64_le(self.file_id.volatile);
cursor.write_u32_le(self.minimum_count);
cursor.write_u32_le(self.channel);
cursor.write_u32_le(self.remaining_bytes);
// ReadChannelInfoOffset/Length: relative to start of SMB2 header.
// For packing the body alone, we store offset as 0 when empty.
if self.read_channel_info.is_empty() {
cursor.write_u16_le(0);
cursor.write_u16_le(0);
} else {
// Offset from the SMB2 header = header (64) + fixed body (48) = 112.
// The fixed body before Buffer is 48 bytes (StructureSize 49 minus
// the 1 byte of Buffer that's counted in StructureSize).
cursor.write_u16_le(0); // Caller must backpatch if needed
cursor.write_u16_le(self.read_channel_info.len() as u16);
}
// Buffer: at minimum 1 byte per the StructureSize=49 contract,
// but we write the actual channel info if present.
if self.read_channel_info.is_empty() {
// Write a single padding byte so the fixed part is 49 bytes
// (StructureSize includes this 1-byte minimum buffer).
cursor.write_u8(0);
} else {
cursor.write_bytes(&self.read_channel_info);
}
}
}
impl Unpack for ReadRequest {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let structure_size = cursor.read_u16_le()?;
if structure_size != Self::STRUCTURE_SIZE {
return Err(Error::invalid_data(format!(
"invalid ReadRequest structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
let padding = cursor.read_u8()?;
let flags = cursor.read_u8()?;
let length = cursor.read_u32_le()?;
let offset = cursor.read_u64_le()?;
let persistent = cursor.read_u64_le()?;
let volatile = cursor.read_u64_le()?;
let minimum_count = cursor.read_u32_le()?;
let channel = cursor.read_u32_le()?;
let remaining_bytes = cursor.read_u32_le()?;
let _read_channel_info_offset = cursor.read_u16_le()?;
let read_channel_info_length = cursor.read_u16_le()?;
// The buffer is at least 1 byte (per StructureSize=49).
// Read channel info from the buffer based on the length field.
let read_channel_info = if read_channel_info_length > 0 {
cursor
.read_bytes(read_channel_info_length as usize)?
.to_vec()
} else {
// Skip the minimum 1-byte buffer
cursor.skip(1)?;
Vec::new()
};
Ok(ReadRequest {
padding,
flags,
length,
offset,
file_id: FileId {
persistent,
volatile,
},
minimum_count,
channel,
remaining_bytes,
read_channel_info,
})
}
}
/// SMB2 READ Response (MS-SMB2 section 2.2.20).
///
/// Sent by the server with the requested data. The fixed portion is 17 bytes:
/// - StructureSize (2 bytes, must be 17)
/// - DataOffset (1 byte)
/// - Reserved (1 byte)
/// - DataLength (4 bytes)
/// - DataRemaining (4 bytes)
/// - Reserved2 (4 bytes)
/// - Buffer (variable, DataLength bytes)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadResponse {
/// Offset from the start of the SMB2 header to the data.
pub data_offset: u8,
/// Number of remaining bytes on the channel.
pub data_remaining: u32,
/// Flags/Reserved2 field (used in SMB 3.1.1, otherwise 0).
pub flags: u32,
/// The data that was read.
pub data: Vec<u8>,
}
impl ReadResponse {
pub const STRUCTURE_SIZE: u16 = 17;
}
impl Pack for ReadResponse {
fn pack(&self, cursor: &mut WriteCursor) {
cursor.write_u16_le(Self::STRUCTURE_SIZE);
cursor.write_u8(self.data_offset);
cursor.write_u8(0); // Reserved
cursor.write_u32_le(self.data.len() as u32);
cursor.write_u32_le(self.data_remaining);
cursor.write_u32_le(self.flags); // Reserved2/Flags
cursor.write_bytes(&self.data);
}
}
impl Unpack for ReadResponse {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let structure_size = cursor.read_u16_le()?;
if structure_size != Self::STRUCTURE_SIZE {
return Err(Error::invalid_data(format!(
"invalid ReadResponse structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
let data_offset = cursor.read_u8()?;
let _reserved = cursor.read_u8()?;
let data_length = cursor.read_u32_le()?;
let data_remaining = cursor.read_u32_le()?;
let flags = cursor.read_u32_le()?;
let data = if data_length > 0 {
cursor.read_bytes_bounded(data_length as usize)?.to_vec()
} else {
Vec::new()
};
Ok(ReadResponse {
data_offset,
data_remaining,
flags,
data,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
// ── ReadRequest tests ──────────────────────────────────────────
#[test]
fn read_request_roundtrip() {
let original = ReadRequest {
padding: 0x50,
flags: SMB2_READFLAG_READ_UNBUFFERED,
length: 65536,
offset: 0x1000,
file_id: FileId {
persistent: 0xAAAA_BBBB_CCCC_DDDD,
volatile: 0x1111_2222_3333_4444,
},
minimum_count: 1024,
channel: SMB2_CHANNEL_NONE,
remaining_bytes: 0,
read_channel_info: Vec::new(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// Fixed: 48 bytes + 1-byte minimum buffer = 49 bytes
assert_eq!(bytes.len(), 49);
let mut r = ReadCursor::new(&bytes);
let decoded = ReadRequest::unpack(&mut r).unwrap();
assert_eq!(decoded.padding, original.padding);
assert_eq!(decoded.flags, original.flags);
assert_eq!(decoded.length, original.length);
assert_eq!(decoded.offset, original.offset);
assert_eq!(decoded.file_id, original.file_id);
assert_eq!(decoded.minimum_count, original.minimum_count);
assert_eq!(decoded.channel, original.channel);
assert_eq!(decoded.remaining_bytes, original.remaining_bytes);
assert!(decoded.read_channel_info.is_empty());
}
#[test]
fn read_request_with_channel_info_roundtrip() {
let channel_data = vec![0xDE, 0xAD, 0xBE, 0xEF];
let original = ReadRequest {
padding: 0,
flags: 0,
length: 4096,
offset: 0,
file_id: FileId {
persistent: 1,
volatile: 2,
},
minimum_count: 0,
channel: 0x0000_0001, // SMB2_CHANNEL_RDMA_V1
remaining_bytes: 4096,
read_channel_info: channel_data.clone(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// Fixed: 48 bytes + 4-byte channel info = 52 bytes
assert_eq!(bytes.len(), 52);
let mut r = ReadCursor::new(&bytes);
let decoded = ReadRequest::unpack(&mut r).unwrap();
assert_eq!(decoded.read_channel_info, channel_data);
assert_eq!(decoded.channel, 0x0000_0001);
}
#[test]
fn read_request_wrong_structure_size() {
let mut buf = [0u8; 49];
buf[0..2].copy_from_slice(&50u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = ReadRequest::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
// ── ReadResponse tests ─────────────────────────────────────────
#[test]
fn read_response_roundtrip() {
let original = ReadResponse {
data_offset: 0x50, // typical: 64 (header) + 16 (body fixed) = 80 = 0x50
data_remaining: 0,
flags: 0,
data: vec![0x01, 0x02, 0x03, 0x04, 0x05],
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// Fixed: 16 bytes + 5 bytes data = 21 bytes
assert_eq!(bytes.len(), 21);
let mut r = ReadCursor::new(&bytes);
let decoded = ReadResponse::unpack(&mut r).unwrap();
assert_eq!(decoded.data_offset, original.data_offset);
assert_eq!(decoded.data_remaining, original.data_remaining);
assert_eq!(decoded.flags, original.flags);
assert_eq!(decoded.data, original.data);
}
#[test]
fn read_response_empty_data() {
let original = ReadResponse {
data_offset: 0,
data_remaining: 0,
flags: 0,
data: Vec::new(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// Fixed: 16 bytes, no data
assert_eq!(bytes.len(), 16);
let mut r = ReadCursor::new(&bytes);
let decoded = ReadResponse::unpack(&mut r).unwrap();
assert!(decoded.data.is_empty());
}
#[test]
fn read_response_known_bytes() {
let mut buf = Vec::new();
// StructureSize = 17
buf.extend_from_slice(&17u16.to_le_bytes());
// DataOffset = 0x50
buf.push(0x50);
// Reserved = 0
buf.push(0x00);
// DataLength = 3
buf.extend_from_slice(&3u32.to_le_bytes());
// DataRemaining = 0
buf.extend_from_slice(&0u32.to_le_bytes());
// Reserved2/Flags = 0
buf.extend_from_slice(&0u32.to_le_bytes());
// Buffer = [0xAA, 0xBB, 0xCC]
buf.extend_from_slice(&[0xAA, 0xBB, 0xCC]);
let mut cursor = ReadCursor::new(&buf);
let resp = ReadResponse::unpack(&mut cursor).unwrap();
assert_eq!(resp.data_offset, 0x50);
assert_eq!(resp.data, vec![0xAA, 0xBB, 0xCC]);
assert_eq!(resp.data_remaining, 0);
assert_eq!(resp.flags, 0);
}
#[test]
fn read_response_wrong_structure_size() {
let mut buf = [0u8; 16];
buf[0..2].copy_from_slice(&99u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = ReadResponse::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
}
#[cfg(test)]
mod roundtrip_props {
use super::*;
use crate::msg::roundtrip_strategies::{arb_bytes, arb_file_id, arb_small_bytes};
use proptest::prelude::*;
proptest! {
#[test]
fn read_request_pack_unpack(
padding in any::<u8>(),
flags in any::<u8>(),
length in any::<u32>(),
offset in any::<u64>(),
file_id in arb_file_id(),
minimum_count in any::<u32>(),
channel in any::<u32>(),
remaining_bytes in any::<u32>(),
read_channel_info in arb_small_bytes(),
) {
let original = ReadRequest {
padding,
flags,
length,
offset,
file_id,
minimum_count,
channel,
remaining_bytes,
read_channel_info,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = ReadRequest::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
prop_assert!(r.is_empty());
}
#[test]
fn read_response_pack_unpack(
data_offset in any::<u8>(),
data_remaining in any::<u32>(),
flags in any::<u32>(),
data in arb_bytes(),
) {
let original = ReadResponse {
data_offset,
data_remaining,
flags,
data,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = ReadResponse::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
prop_assert!(r.is_empty());
}
}
}

View File

@@ -0,0 +1,250 @@
//! Shared proptest strategies for wire-format roundtrip tests.
//!
//! Each strategy generates a value that a real encoder could emit. The goal
//! is not to stress-test the decoder against malformed input (that's fuzzing)
//! but to exercise encode/decode symmetry on well-formed inputs.
//!
//! Rules followed here:
//! - Typed enums always yield valid variants (no invalid discriminants).
//! - `Vec<u8>` lengths stay moderate (at most a few KB) to keep tests fast.
//! - Internally-dependent sizes (for example, a length field that must match a
//! sibling `Vec`) are produced via `prop_map` so generated instances are
//! always consistent.
// Note: `#[cfg(test)]` is applied at the module declaration in `src/msg/mod.rs`
// (`#[cfg(test)] pub(crate) mod roundtrip_strategies;`). We don't repeat it
// here; clippy's `duplicated_attributes` lint rejects that.
#![allow(dead_code)] // Helpers might be unused while tests are being added.
use proptest::prelude::*;
use crate::pack::{FileTime, Guid};
use crate::types::flags::{
Capabilities, FileAccessMask, HeaderFlags, SecurityMode, ShareCapabilities, ShareFlags,
};
use crate::types::status::NtStatus;
use crate::types::{
Command, CreditCharge, Dialect, FileId, MessageId, OplockLevel, SessionId, TreeId,
};
/// Max size (in bytes) used for generated `Vec<u8>` buffers across tests.
/// Kept small so a 256-case proptest run stays well under a second.
pub const MAX_BUFFER_BYTES: usize = 1024;
/// Moderate buffer for structs that usually carry small bodies.
pub const MAX_SMALL_BUFFER_BYTES: usize = 256;
/// Generate a `Vec<u8>` up to `max` bytes long (including zero).
pub fn bytes_up_to(max: usize) -> impl Strategy<Value = Vec<u8>> {
prop::collection::vec(any::<u8>(), 0..=max)
}
/// A standard moderate-length byte buffer.
pub fn arb_bytes() -> impl Strategy<Value = Vec<u8>> {
bytes_up_to(MAX_BUFFER_BYTES)
}
/// A smaller byte buffer, for sub-fields or tightly-nested structures.
pub fn arb_small_bytes() -> impl Strategy<Value = Vec<u8>> {
bytes_up_to(MAX_SMALL_BUFFER_BYTES)
}
/// Generate a valid UTF-16-encodable String, up to `max_chars` chars.
///
/// Excludes unpaired surrogates (U+D800..=U+DFFF) because UTF-16 decoding
/// would reject any surrogate that isn't part of a valid pair. We use the
/// BMP-minus-surrogates range plus occasional supplementary characters, so
/// both one-code-unit and two-code-unit forms are covered.
pub fn arb_utf16_string(max_chars: usize) -> impl Strategy<Value = String> {
prop::collection::vec(
prop::char::range('\u{0000}', '\u{D7FF}')
.prop_union(prop::char::range('\u{E000}', '\u{FFFF}'))
.or(prop::char::range('\u{1_0000}', '\u{10_FFFF}')),
0..=max_chars,
)
.prop_map(|chars| chars.into_iter().collect())
}
// ── Primitive newtype strategies ────────────────────────────────────
pub fn arb_session_id() -> impl Strategy<Value = SessionId> {
any::<u64>().prop_map(SessionId)
}
pub fn arb_message_id() -> impl Strategy<Value = MessageId> {
any::<u64>().prop_map(MessageId)
}
pub fn arb_tree_id() -> impl Strategy<Value = TreeId> {
any::<u32>().prop_map(TreeId)
}
pub fn arb_credit_charge() -> impl Strategy<Value = CreditCharge> {
any::<u16>().prop_map(CreditCharge)
}
pub fn arb_file_id() -> impl Strategy<Value = FileId> {
(any::<u64>(), any::<u64>()).prop_map(|(persistent, volatile)| FileId {
persistent,
volatile,
})
}
pub fn arb_file_time() -> impl Strategy<Value = FileTime> {
any::<u64>().prop_map(FileTime)
}
pub fn arb_guid() -> impl Strategy<Value = Guid> {
(any::<u32>(), any::<u16>(), any::<u16>(), any::<[u8; 8]>()).prop_map(
|(data1, data2, data3, data4)| Guid {
data1,
data2,
data3,
data4,
},
)
}
pub fn arb_nt_status() -> impl Strategy<Value = NtStatus> {
any::<u32>().prop_map(NtStatus)
}
// ── Flags ────────────────────────────────────────────────────────────
pub fn arb_header_flags() -> impl Strategy<Value = HeaderFlags> {
any::<u32>().prop_map(HeaderFlags::new)
}
pub fn arb_security_mode() -> impl Strategy<Value = SecurityMode> {
any::<u16>().prop_map(SecurityMode::new)
}
pub fn arb_capabilities() -> impl Strategy<Value = Capabilities> {
any::<u32>().prop_map(Capabilities::new)
}
pub fn arb_share_flags() -> impl Strategy<Value = ShareFlags> {
any::<u32>().prop_map(ShareFlags::new)
}
pub fn arb_share_capabilities() -> impl Strategy<Value = ShareCapabilities> {
any::<u32>().prop_map(ShareCapabilities::new)
}
pub fn arb_file_access_mask() -> impl Strategy<Value = FileAccessMask> {
any::<u32>().prop_map(FileAccessMask::new)
}
// ── Typed enums: only valid variants ────────────────────────────────
pub fn arb_oplock_level() -> impl Strategy<Value = OplockLevel> {
prop_oneof![
Just(OplockLevel::None),
Just(OplockLevel::LevelII),
Just(OplockLevel::Exclusive),
Just(OplockLevel::Batch),
Just(OplockLevel::Lease),
]
}
pub fn arb_dialect() -> impl Strategy<Value = Dialect> {
prop_oneof![
Just(Dialect::Smb2_0_2),
Just(Dialect::Smb2_1),
Just(Dialect::Smb3_0),
Just(Dialect::Smb3_0_2),
Just(Dialect::Smb3_1_1),
]
}
pub fn arb_share_type() -> impl Strategy<Value = crate::msg::tree_connect::ShareType> {
use crate::msg::tree_connect::ShareType;
prop_oneof![
Just(ShareType::Disk),
Just(ShareType::Pipe),
Just(ShareType::Print),
]
}
pub fn arb_impersonation_level() -> impl Strategy<Value = crate::msg::create::ImpersonationLevel> {
use crate::msg::create::ImpersonationLevel;
prop_oneof![
Just(ImpersonationLevel::Anonymous),
Just(ImpersonationLevel::Identification),
Just(ImpersonationLevel::Impersonation),
Just(ImpersonationLevel::Delegate),
]
}
pub fn arb_create_disposition() -> impl Strategy<Value = crate::msg::create::CreateDisposition> {
use crate::msg::create::CreateDisposition;
prop_oneof![
Just(CreateDisposition::FileSupersede),
Just(CreateDisposition::FileOpen),
Just(CreateDisposition::FileCreate),
Just(CreateDisposition::FileOpenIf),
Just(CreateDisposition::FileOverwrite),
Just(CreateDisposition::FileOverwriteIf),
]
}
pub fn arb_create_action() -> impl Strategy<Value = crate::msg::create::CreateAction> {
use crate::msg::create::CreateAction;
prop_oneof![
Just(CreateAction::FileSuperseded),
Just(CreateAction::FileOpened),
Just(CreateAction::FileCreated),
Just(CreateAction::FileOverwritten),
]
}
pub fn arb_share_access() -> impl Strategy<Value = crate::msg::create::ShareAccess> {
any::<u32>().prop_map(crate::msg::create::ShareAccess)
}
pub fn arb_info_type() -> impl Strategy<Value = crate::msg::query_info::InfoType> {
use crate::msg::query_info::InfoType;
prop_oneof![
Just(InfoType::File),
Just(InfoType::Filesystem),
Just(InfoType::Security),
Just(InfoType::Quota),
]
}
pub fn arb_file_information_class(
) -> impl Strategy<Value = crate::msg::query_directory::FileInformationClass> {
use crate::msg::query_directory::FileInformationClass;
prop_oneof![
Just(FileInformationClass::FileDirectoryInformation),
Just(FileInformationClass::FileFullDirectoryInformation),
Just(FileInformationClass::FileBothDirectoryInformation),
Just(FileInformationClass::FileNamesInformation),
Just(FileInformationClass::FileIdBothDirectoryInformation),
Just(FileInformationClass::FileIdFullDirectoryInformation),
]
}
pub fn arb_command() -> impl Strategy<Value = Command> {
prop_oneof![
Just(Command::Negotiate),
Just(Command::SessionSetup),
Just(Command::Logoff),
Just(Command::TreeConnect),
Just(Command::TreeDisconnect),
Just(Command::Create),
Just(Command::Close),
Just(Command::Flush),
Just(Command::Read),
Just(Command::Write),
Just(Command::Lock),
Just(Command::Ioctl),
Just(Command::Cancel),
Just(Command::Echo),
Just(Command::QueryDirectory),
Just(Command::ChangeNotify),
Just(Command::QueryInfo),
Just(Command::SetInfo),
Just(Command::OplockBreak),
]
}

481
vendor/smb2/src/msg/session_setup.rs vendored Normal file
View File

@@ -0,0 +1,481 @@
//! SMB2 SESSION_SETUP request and response (spec sections 2.2.5, 2.2.6).
//!
//! Session setup messages are used to establish an authenticated session
//! between the client and the server. The request carries a security token
//! (for example, SPNEGO/NTLM) and the response carries the server's reply token
//! along with session flags.
use crate::error::Result;
use crate::msg::header::Header;
use crate::pack::{Pack, ReadCursor, Unpack, WriteCursor};
use crate::types::flags::{Capabilities, SecurityMode};
use crate::Error;
// ── Session setup request flags ────────────────────────────────────────
/// Flags for the SESSION_SETUP request (1 byte, spec section 2.2.5).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SessionSetupRequestFlags(pub u8);
impl SessionSetupRequestFlags {
/// Bind an existing session to a new connection (SMB 3.x only).
pub const BINDING: u8 = 0x01;
/// Returns `true` if the binding flag is set.
#[inline]
pub fn is_binding(&self) -> bool {
self.0 & Self::BINDING != 0
}
}
// ── Session flags (response) ───────────────────────────────────────────
/// Session flags returned in the SESSION_SETUP response (spec section 2.2.6).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SessionFlags(pub u16);
impl SessionFlags {
/// The client has been authenticated as a guest user.
pub const IS_GUEST: u16 = 0x0001;
/// The client has been authenticated as an anonymous user.
pub const IS_NULL: u16 = 0x0002;
/// The server requires encryption of messages on this session (SMB 3.x only).
pub const ENCRYPT_DATA: u16 = 0x0004;
/// Returns `true` if the guest flag is set.
#[inline]
pub fn is_guest(&self) -> bool {
self.0 & Self::IS_GUEST != 0
}
/// Returns `true` if the null session flag is set.
#[inline]
pub fn is_null(&self) -> bool {
self.0 & Self::IS_NULL != 0
}
/// Returns `true` if the encrypt-data flag is set.
#[inline]
pub fn encrypt_data(&self) -> bool {
self.0 & Self::ENCRYPT_DATA != 0
}
}
// ── SessionSetupRequest ────────────────────────────────────────────────
/// SMB2 SESSION_SETUP request (spec section 2.2.5).
///
/// Sent by the client to establish an authenticated session. The security
/// buffer carries a GSS/SPNEGO token (or other auth protocol token).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionSetupRequest {
/// Flags controlling the request (for example, session binding).
pub flags: SessionSetupRequestFlags,
/// Security mode indicating signing requirements.
pub security_mode: SecurityMode,
/// Client capabilities.
pub capabilities: Capabilities,
/// Channel field (reserved, must be 0).
pub channel: u32,
/// Previously established session identifier for reconnection.
pub previous_session_id: u64,
/// Security buffer containing the authentication token.
pub security_buffer: Vec<u8>,
}
impl SessionSetupRequest {
pub const STRUCTURE_SIZE: u16 = 25;
}
impl Pack for SessionSetupRequest {
fn pack(&self, cursor: &mut WriteCursor) {
// StructureSize (2 bytes)
cursor.write_u16_le(Self::STRUCTURE_SIZE);
// Flags (1 byte)
cursor.write_u8(self.flags.0);
// SecurityMode (1 byte)
cursor.write_u8(self.security_mode.bits() as u8);
// Capabilities (4 bytes)
cursor.write_u32_le(self.capabilities.bits());
// Channel (4 bytes)
cursor.write_u32_le(self.channel);
// SecurityBufferOffset (2 bytes) -- offset from start of SMB2 header
let offset = (Header::SIZE + 24) as u16; // 24 = bytes before the buffer in this struct
cursor.write_u16_le(offset);
// SecurityBufferLength (2 bytes)
cursor.write_u16_le(self.security_buffer.len() as u16);
// PreviousSessionId (8 bytes)
cursor.write_u64_le(self.previous_session_id);
// Buffer (variable)
cursor.write_bytes(&self.security_buffer);
}
}
impl Unpack for SessionSetupRequest {
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 SessionSetupRequest structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
// Flags (1 byte)
let flags = SessionSetupRequestFlags(cursor.read_u8()?);
// SecurityMode (1 byte)
let security_mode = SecurityMode::new(cursor.read_u8()? as u16);
// Capabilities (4 bytes)
let capabilities = Capabilities::new(cursor.read_u32_le()?);
// Channel (4 bytes)
let channel = cursor.read_u32_le()?;
// SecurityBufferOffset (2 bytes) -- we ignore, read sequentially
let _offset = cursor.read_u16_le()?;
// SecurityBufferLength (2 bytes)
let buffer_length = cursor.read_u16_le()? as usize;
// PreviousSessionId (8 bytes)
let previous_session_id = cursor.read_u64_le()?;
// Buffer (variable)
let security_buffer = if buffer_length > 0 {
cursor.read_bytes_bounded(buffer_length)?.to_vec()
} else {
Vec::new()
};
Ok(SessionSetupRequest {
flags,
security_mode,
capabilities,
channel,
previous_session_id,
security_buffer,
})
}
}
// ── SessionSetupResponse ───────────────────────────────────────────────
/// SMB2 SESSION_SETUP response (spec section 2.2.6).
///
/// Sent by the server in response to a SESSION_SETUP request. Contains
/// session flags and a security buffer with the server's auth token.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionSetupResponse {
/// Flags indicating additional information about the session.
pub session_flags: SessionFlags,
/// Security buffer containing the server's authentication token.
pub security_buffer: Vec<u8>,
}
impl SessionSetupResponse {
pub const STRUCTURE_SIZE: u16 = 9;
}
impl Pack for SessionSetupResponse {
fn pack(&self, cursor: &mut WriteCursor) {
// StructureSize (2 bytes)
cursor.write_u16_le(Self::STRUCTURE_SIZE);
// SessionFlags (2 bytes)
cursor.write_u16_le(self.session_flags.0);
// SecurityBufferOffset (2 bytes) -- offset from start of SMB2 header
let offset = (Header::SIZE + 8) as u16; // 8 = fixed part of response struct
cursor.write_u16_le(offset);
// SecurityBufferLength (2 bytes)
cursor.write_u16_le(self.security_buffer.len() as u16);
// Buffer (variable)
cursor.write_bytes(&self.security_buffer);
}
}
impl Unpack for SessionSetupResponse {
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 SessionSetupResponse structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
// SessionFlags (2 bytes)
let session_flags = SessionFlags(cursor.read_u16_le()?);
// SecurityBufferOffset (2 bytes)
let _offset = cursor.read_u16_le()?;
// SecurityBufferLength (2 bytes)
let buffer_length = cursor.read_u16_le()? as usize;
// Buffer (variable)
let security_buffer = if buffer_length > 0 {
cursor.read_bytes_bounded(buffer_length)?.to_vec()
} else {
Vec::new()
};
Ok(SessionSetupResponse {
session_flags,
security_buffer,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
// ── SessionSetupRequest tests ──────────────────────────────────
#[test]
fn session_setup_request_roundtrip() {
let token = vec![0x60, 0x28, 0x06, 0x06, 0x2b, 0x06, 0x01, 0x05];
let original = SessionSetupRequest {
flags: SessionSetupRequestFlags(0),
security_mode: SecurityMode::new(SecurityMode::SIGNING_ENABLED),
capabilities: Capabilities::new(Capabilities::DFS),
channel: 0,
previous_session_id: 0,
security_buffer: token.clone(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = SessionSetupRequest::unpack(&mut r).unwrap();
assert_eq!(decoded.flags, original.flags);
assert_eq!(decoded.security_mode.bits(), original.security_mode.bits());
assert_eq!(decoded.capabilities.bits(), original.capabilities.bits());
assert_eq!(decoded.channel, 0);
assert_eq!(decoded.previous_session_id, 0);
assert_eq!(decoded.security_buffer, token);
}
#[test]
fn session_setup_request_with_binding_flag() {
let original = SessionSetupRequest {
flags: SessionSetupRequestFlags(SessionSetupRequestFlags::BINDING),
security_mode: SecurityMode::new(
SecurityMode::SIGNING_ENABLED | SecurityMode::SIGNING_REQUIRED,
),
capabilities: Capabilities::default(),
channel: 0,
previous_session_id: 0xDEAD_BEEF_CAFE_BABE,
security_buffer: vec![0xAA, 0xBB],
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = SessionSetupRequest::unpack(&mut r).unwrap();
assert!(decoded.flags.is_binding());
assert!(decoded.security_mode.signing_enabled());
assert!(decoded.security_mode.signing_required());
assert_eq!(decoded.previous_session_id, 0xDEAD_BEEF_CAFE_BABE);
assert_eq!(decoded.security_buffer, vec![0xAA, 0xBB]);
}
#[test]
fn session_setup_request_empty_buffer() {
let original = SessionSetupRequest {
flags: SessionSetupRequestFlags(0),
security_mode: SecurityMode::default(),
capabilities: Capabilities::default(),
channel: 0,
previous_session_id: 0,
security_buffer: Vec::new(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = SessionSetupRequest::unpack(&mut r).unwrap();
assert!(decoded.security_buffer.is_empty());
}
#[test]
fn session_setup_request_structure_size_field() {
let req = SessionSetupRequest {
flags: SessionSetupRequestFlags(0),
security_mode: SecurityMode::default(),
capabilities: Capabilities::default(),
channel: 0,
previous_session_id: 0,
security_buffer: vec![0x01],
};
let mut w = WriteCursor::new();
req.pack(&mut w);
let bytes = w.into_inner();
// First 2 bytes are structure size = 25
assert_eq!(bytes[0], 25);
assert_eq!(bytes[1], 0);
}
#[test]
fn session_setup_request_wrong_structure_size() {
let mut buf = [0u8; 26];
buf[0..2].copy_from_slice(&99u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = SessionSetupRequest::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
// ── SessionSetupResponse tests ─────────────────────────────────
#[test]
fn session_setup_response_roundtrip() {
let token = vec![0xA1, 0x81, 0xB0, 0x30, 0x81, 0xAD];
let original = SessionSetupResponse {
session_flags: SessionFlags(0),
security_buffer: token.clone(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = SessionSetupResponse::unpack(&mut r).unwrap();
assert_eq!(decoded.session_flags, original.session_flags);
assert_eq!(decoded.security_buffer, token);
}
#[test]
fn session_setup_response_with_flags() {
let original = SessionSetupResponse {
session_flags: SessionFlags(SessionFlags::IS_GUEST | SessionFlags::ENCRYPT_DATA),
security_buffer: vec![0x01, 0x02, 0x03],
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = SessionSetupResponse::unpack(&mut r).unwrap();
assert!(decoded.session_flags.is_guest());
assert!(!decoded.session_flags.is_null());
assert!(decoded.session_flags.encrypt_data());
assert_eq!(decoded.security_buffer, vec![0x01, 0x02, 0x03]);
}
#[test]
fn session_setup_response_null_session() {
let original = SessionSetupResponse {
session_flags: SessionFlags(SessionFlags::IS_NULL),
security_buffer: Vec::new(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = SessionSetupResponse::unpack(&mut r).unwrap();
assert!(decoded.session_flags.is_null());
assert!(!decoded.session_flags.is_guest());
assert!(decoded.security_buffer.is_empty());
}
#[test]
fn session_setup_response_structure_size_field() {
let resp = SessionSetupResponse {
session_flags: SessionFlags(0),
security_buffer: Vec::new(),
};
let mut w = WriteCursor::new();
resp.pack(&mut w);
let bytes = w.into_inner();
// First 2 bytes are structure size = 9
assert_eq!(bytes[0], 9);
assert_eq!(bytes[1], 0);
}
#[test]
fn session_setup_response_wrong_structure_size() {
let mut buf = [0u8; 10];
buf[0..2].copy_from_slice(&99u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = SessionSetupResponse::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
}
#[cfg(test)]
mod roundtrip_props {
use super::*;
use crate::msg::roundtrip_strategies::{arb_capabilities, arb_small_bytes};
use proptest::prelude::*;
proptest! {
#[test]
fn session_setup_request_pack_unpack(
flags_raw in any::<u8>(),
// SESSION_SETUP packs SecurityMode as a single byte, so only the
// low 8 bits survive the roundtrip. Generate u8 values to avoid
// producing inputs the encoder would never emit from a real caller.
security_mode_raw in any::<u8>(),
capabilities in arb_capabilities(),
channel in any::<u32>(),
previous_session_id in any::<u64>(),
security_buffer in arb_small_bytes(),
) {
let original = SessionSetupRequest {
flags: SessionSetupRequestFlags(flags_raw),
security_mode: SecurityMode::new(security_mode_raw as u16),
capabilities,
channel,
previous_session_id,
security_buffer,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = SessionSetupRequest::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
prop_assert!(r.is_empty());
}
#[test]
fn session_setup_response_pack_unpack(
session_flags_raw in any::<u16>(),
security_buffer in arb_small_bytes(),
) {
let original = SessionSetupResponse {
session_flags: SessionFlags(session_flags_raw),
security_buffer,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = SessionSetupResponse::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
prop_assert!(r.is_empty());
}
}
}

328
vendor/smb2/src/msg/set_info.rs vendored Normal file
View File

@@ -0,0 +1,328 @@
//! SMB2 SET_INFO request and response (spec sections 2.2.39, 2.2.40).
//!
//! Used to set file, filesystem, security, or quota information.
//! The request buffer contains the information to set, stored as raw bytes.
//! The response is a minimal 2-byte structure.
use crate::error::Result;
use crate::msg::header::Header;
use crate::pack::{Pack, ReadCursor, Unpack, WriteCursor};
use crate::types::FileId;
use crate::Error;
// Re-use InfoType from query_info
pub use super::query_info::InfoType;
// ── SetInfoRequest ───────────────────────────────────────────────────────
/// SMB2 SET_INFO request (spec section 2.2.39).
///
/// Sent by the client to set information on a file, filesystem,
/// security descriptor, or quota.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SetInfoRequest {
/// The type of information being set.
pub info_type: InfoType,
/// The file information class (interpretation depends on `info_type`).
pub file_info_class: u8,
/// Additional information flags (for example, security information flags).
pub additional_information: u32,
/// Handle to the file or directory.
pub file_id: FileId,
/// Raw buffer containing the information to set.
pub buffer: Vec<u8>,
}
impl SetInfoRequest {
pub const STRUCTURE_SIZE: u16 = 33;
}
impl Pack for SetInfoRequest {
fn pack(&self, cursor: &mut WriteCursor) {
let start = cursor.position();
// StructureSize (2 bytes)
cursor.write_u16_le(Self::STRUCTURE_SIZE);
// InfoType (1 byte)
cursor.write_u8(self.info_type as u8);
// FileInfoClass (1 byte)
cursor.write_u8(self.file_info_class);
// BufferLength (4 bytes)
cursor.write_u32_le(self.buffer.len() as u32);
// BufferOffset (2 bytes) -- placeholder
let offset_pos = cursor.position();
cursor.write_u16_le(0);
// Reserved (2 bytes)
cursor.write_u16_le(0);
// AdditionalInformation (4 bytes)
cursor.write_u32_le(self.additional_information);
// FileId (16 bytes)
cursor.write_u64_le(self.file_id.persistent);
cursor.write_u64_le(self.file_id.volatile);
// Buffer (variable)
if !self.buffer.is_empty() {
// Offset is from the beginning of the SMB2 header per spec.
let buf_offset = Header::SIZE + (cursor.position() - start);
cursor.write_bytes(&self.buffer);
cursor.set_u16_le_at(offset_pos, buf_offset as u16);
}
}
}
impl Unpack for SetInfoRequest {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let start = cursor.position();
// StructureSize (2 bytes)
let structure_size = cursor.read_u16_le()?;
if structure_size != Self::STRUCTURE_SIZE {
return Err(Error::invalid_data(format!(
"invalid SetInfoRequest structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
// InfoType (1 byte)
let info_type = InfoType::try_from(cursor.read_u8()?)?;
// FileInfoClass (1 byte)
let file_info_class = cursor.read_u8()?;
// BufferLength (4 bytes)
let buffer_length = cursor.read_u32_le()? as usize;
// BufferOffset (2 bytes)
let buf_offset = cursor.read_u16_le()? as usize;
// Reserved (2 bytes)
let _reserved = cursor.read_u16_le()?;
// AdditionalInformation (4 bytes)
let additional_information = cursor.read_u32_le()?;
// FileId (16 bytes)
let persistent = cursor.read_u64_le()?;
let volatile = cursor.read_u64_le()?;
let file_id = FileId {
persistent,
volatile,
};
// Read buffer
// Offset on the wire is from beginning of SMB2 header.
let buffer = if buffer_length > 0 {
let current = cursor.position();
let body_offset = buf_offset.saturating_sub(Header::SIZE);
let target = start + body_offset;
if target > current {
cursor.skip(target - current)?;
}
cursor.read_bytes_bounded(buffer_length)?.to_vec()
} else {
Vec::new()
};
Ok(SetInfoRequest {
info_type,
file_info_class,
additional_information,
file_id,
buffer,
})
}
}
// ── SetInfoResponse ──────────────────────────────────────────────────────
/// SMB2 SET_INFO response (spec section 2.2.40).
///
/// A minimal response indicating that the set operation succeeded.
/// Contains only the 2-byte StructureSize field.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SetInfoResponse;
impl SetInfoResponse {
pub const STRUCTURE_SIZE: u16 = 2;
}
impl Pack for SetInfoResponse {
fn pack(&self, cursor: &mut WriteCursor) {
// StructureSize (2 bytes)
cursor.write_u16_le(Self::STRUCTURE_SIZE);
}
}
impl Unpack for SetInfoResponse {
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 SetInfoResponse structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
Ok(SetInfoResponse)
}
}
#[cfg(test)]
mod tests {
use super::*;
// ── SetInfoRequest tests ─────────────────────────────────────────
#[test]
fn set_info_request_roundtrip_with_buffer() {
let info_data = vec![0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04];
let original = SetInfoRequest {
info_type: InfoType::File,
file_info_class: 0x04, // FileBasicInformation
additional_information: 0,
file_id: FileId {
persistent: 0xAAAA_BBBB_CCCC_DDDD,
volatile: 0x1111_2222_3333_4444,
},
buffer: info_data.clone(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = SetInfoRequest::unpack(&mut r).unwrap();
assert_eq!(decoded.info_type, InfoType::File);
assert_eq!(decoded.file_info_class, 0x04);
assert_eq!(decoded.additional_information, 0);
assert_eq!(decoded.file_id, original.file_id);
assert_eq!(decoded.buffer, info_data);
}
#[test]
fn set_info_request_security_info() {
let sd_data = vec![0x01, 0x00, 0x04, 0x80, 0x00, 0x00, 0x00, 0x00];
let original = SetInfoRequest {
info_type: InfoType::Security,
file_info_class: 0,
additional_information: 0x04, // DACL_SECURITY_INFORMATION
file_id: FileId {
persistent: 42,
volatile: 99,
},
buffer: sd_data.clone(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = SetInfoRequest::unpack(&mut r).unwrap();
assert_eq!(decoded.info_type, InfoType::Security);
assert_eq!(decoded.additional_information, 0x04);
assert_eq!(decoded.buffer, sd_data);
}
#[test]
fn set_info_request_structure_size() {
let req = SetInfoRequest {
info_type: InfoType::File,
file_info_class: 0,
additional_information: 0,
file_id: FileId::default(),
buffer: vec![0x01],
};
let mut w = WriteCursor::new();
req.pack(&mut w);
let bytes = w.into_inner();
assert_eq!(bytes[0], 33);
assert_eq!(bytes[1], 0);
}
#[test]
fn set_info_request_wrong_structure_size() {
let mut buf = vec![0u8; 48];
buf[0..2].copy_from_slice(&99u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = SetInfoRequest::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
// ── SetInfoResponse tests ────────────────────────────────────────
#[test]
fn set_info_response_roundtrip() {
let original = SetInfoResponse;
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// Only 2 bytes
assert_eq!(bytes.len(), 2);
assert_eq!(bytes, [0x02, 0x00]);
let mut r = ReadCursor::new(&bytes);
let decoded = SetInfoResponse::unpack(&mut r).unwrap();
assert_eq!(decoded, SetInfoResponse);
}
#[test]
fn set_info_response_wrong_structure_size() {
let bytes = [0x04, 0x00];
let mut cursor = ReadCursor::new(&bytes);
let result = SetInfoResponse::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
#[test]
fn set_info_response_too_short() {
let bytes = [0x02];
let mut cursor = ReadCursor::new(&bytes);
let result = SetInfoResponse::unpack(&mut cursor);
assert!(result.is_err());
}
}
#[cfg(test)]
mod roundtrip_props {
use super::*;
use crate::msg::roundtrip_strategies::{arb_bytes, arb_file_id, arb_info_type};
use proptest::prelude::*;
proptest! {
#[test]
fn set_info_request_pack_unpack(
info_type in arb_info_type(),
file_info_class in any::<u8>(),
additional_information in any::<u32>(),
file_id in arb_file_id(),
buffer in arb_bytes(),
) {
let original = SetInfoRequest {
info_type,
file_info_class,
additional_information,
file_id,
buffer,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = SetInfoRequest::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
}
}
}

452
vendor/smb2/src/msg/transform.rs vendored Normal file
View File

@@ -0,0 +1,452 @@
//! SMB2 TRANSFORM_HEADER and COMPRESSION_TRANSFORM_HEADER
//! (MS-SMB2 sections 2.2.41, 2.2.42).
//!
//! These headers wrap (encrypted or compressed) SMB2 messages. They are NOT
//! SMB2 messages themselves -- they precede the actual message data.
use crate::error::Result;
use crate::pack::{Pack, ReadCursor, Unpack, WriteCursor};
use crate::types::SessionId;
use crate::Error;
// ── Transform header protocol IDs ──────────────────────────────────────
/// Protocol identifier for the encryption transform header (0xFD 'S' 'M' 'B').
/// Note: this is NOT the normal SMB2 protocol ID (0xFE).
pub const TRANSFORM_PROTOCOL_ID: [u8; 4] = [0xFD, b'S', b'M', b'B'];
/// Protocol identifier for the compression transform header (0xFC 'S' 'M' 'B').
pub const COMPRESSION_PROTOCOL_ID: [u8; 4] = [0xFC, b'S', b'M', b'B'];
// ── Transform header flags ────────────────────────────────────────────
/// The message is encrypted.
pub const SMB2_TRANSFORM_HEADER_FLAG_ENCRYPTED: u16 = 0x0001;
// ── CompressionAlgorithm values ────────────────────────────────────────
/// No compression.
pub const COMPRESSION_ALGORITHM_NONE: u16 = 0x0000;
/// LZNT1 compression.
pub const COMPRESSION_ALGORITHM_LZNT1: u16 = 0x0001;
/// LZ77 compression.
pub const COMPRESSION_ALGORITHM_LZ77: u16 = 0x0002;
/// LZ77 with Huffman encoding.
pub const COMPRESSION_ALGORITHM_LZ77_HUFFMAN: u16 = 0x0003;
/// Pattern_V1 compression.
pub const COMPRESSION_ALGORITHM_PATTERN_V1: u16 = 0x0004;
/// LZ4 compression.
pub const COMPRESSION_ALGORITHM_LZ4: u16 = 0x0005;
// ── Compression flags ──────────────────────────────────────────────────
/// No compression flags.
pub const SMB2_COMPRESSION_FLAG_NONE: u16 = 0x0000;
/// Chained compression (multiple segments).
pub const SMB2_COMPRESSION_FLAG_CHAINED: u16 = 0x0001;
// ── TransformHeader ────────────────────────────────────────────────────
/// SMB2 TRANSFORM_HEADER (MS-SMB2 section 2.2.41).
///
/// An encryption wrapper that precedes an encrypted SMB2 message.
/// The total header is 52 bytes:
/// - ProtocolId (4 bytes, must be 0xFD 'S' 'M' 'B')
/// - Signature (16 bytes)
/// - Nonce (16 bytes -- first 11 bytes used for AES-CCM, first 12 for AES-GCM)
/// - OriginalMessageSize (4 bytes)
/// - Reserved (2 bytes)
/// - Flags (2 bytes)
/// - SessionId (8 bytes)
///
/// The encrypted message data follows immediately after this header.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TransformHeader {
/// 16-byte AES signature over the encrypted message.
pub signature: [u8; 16],
/// 16-byte nonce. Only the first 11 bytes are used for AES-CCM,
/// and the first 12 bytes for AES-GCM. The remaining bytes must be zero.
pub nonce: [u8; 16],
/// Size of the original (unencrypted) SMB2 message in bytes.
pub original_message_size: u32,
/// Flags for the transform header. Use
/// `SMB2_TRANSFORM_HEADER_FLAG_ENCRYPTED`.
pub flags: u16,
/// Session identifier for the encrypted message.
pub session_id: SessionId,
}
impl TransformHeader {
/// Total header size in bytes (52).
pub const SIZE: usize = 52;
}
impl Pack for TransformHeader {
fn pack(&self, cursor: &mut WriteCursor) {
// ProtocolId (4 bytes)
cursor.write_bytes(&TRANSFORM_PROTOCOL_ID);
// Signature (16 bytes)
cursor.write_bytes(&self.signature);
// Nonce (16 bytes)
cursor.write_bytes(&self.nonce);
// OriginalMessageSize (4 bytes)
cursor.write_u32_le(self.original_message_size);
// Reserved (2 bytes)
cursor.write_u16_le(0);
// Flags (2 bytes)
cursor.write_u16_le(self.flags);
// SessionId (8 bytes)
cursor.write_u64_le(self.session_id.0);
}
}
impl Unpack for TransformHeader {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
// ProtocolId (4 bytes)
let proto = cursor.read_bytes(4)?;
if proto != TRANSFORM_PROTOCOL_ID {
return Err(Error::invalid_data(format!(
"invalid transform header protocol ID: expected {:02X?}, got {:02X?}",
TRANSFORM_PROTOCOL_ID, proto
)));
}
// Signature (16 bytes)
let sig_bytes = cursor.read_bytes(16)?;
let mut signature = [0u8; 16];
signature.copy_from_slice(sig_bytes);
// Nonce (16 bytes)
let nonce_bytes = cursor.read_bytes(16)?;
let mut nonce = [0u8; 16];
nonce.copy_from_slice(nonce_bytes);
// OriginalMessageSize (4 bytes)
let original_message_size = cursor.read_u32_le()?;
// Reserved (2 bytes)
let _reserved = cursor.read_u16_le()?;
// Flags (2 bytes)
let flags = cursor.read_u16_le()?;
// SessionId (8 bytes)
let session_id = SessionId(cursor.read_u64_le()?);
Ok(TransformHeader {
signature,
nonce,
original_message_size,
flags,
session_id,
})
}
}
// ── CompressionTransformHeader ─────────────────────────────────────────
/// SMB2 COMPRESSION_TRANSFORM_HEADER (MS-SMB2 section 2.2.42).
///
/// A compression wrapper that precedes a compressed SMB2 message.
/// This implements the unchained variant (Flags = 0) only. The total
/// header is 16 bytes:
/// - ProtocolId (4 bytes, must be 0xFC 'S' 'M' 'B')
/// - OriginalCompressedSegmentSize (4 bytes)
/// - CompressionAlgorithm (2 bytes)
/// - Flags (2 bytes)
/// - Offset (4 bytes) -- offset from the end of this header to the
/// start of compressed data
///
/// Note: The chained variant (Flags = SMB2_COMPRESSION_FLAG_CHAINED)
/// interprets the last 4 bytes as Length instead of Offset. Chained
/// compression is deferred to a future implementation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompressionTransformHeader {
/// Size of the original uncompressed data segment.
pub original_compressed_segment_size: u32,
/// The compression algorithm used.
pub compression_algorithm: u16,
/// Compression flags. Currently only unchained (0x0000) is supported.
pub flags: u16,
/// For unchained: offset from end of this header to the start of
/// compressed data. For chained: length of the original uncompressed
/// segment (chained is not yet implemented).
pub offset_or_length: u32,
}
impl CompressionTransformHeader {
/// Total header size in bytes (16).
pub const SIZE: usize = 16;
}
impl Pack for CompressionTransformHeader {
fn pack(&self, cursor: &mut WriteCursor) {
// ProtocolId (4 bytes)
cursor.write_bytes(&COMPRESSION_PROTOCOL_ID);
// OriginalCompressedSegmentSize (4 bytes)
cursor.write_u32_le(self.original_compressed_segment_size);
// CompressionAlgorithm (2 bytes)
cursor.write_u16_le(self.compression_algorithm);
// Flags (2 bytes)
cursor.write_u16_le(self.flags);
// Offset/Length (4 bytes)
cursor.write_u32_le(self.offset_or_length);
}
}
impl Unpack for CompressionTransformHeader {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
// ProtocolId (4 bytes)
let proto = cursor.read_bytes(4)?;
if proto != COMPRESSION_PROTOCOL_ID {
return Err(Error::invalid_data(format!(
"invalid compression transform header protocol ID: expected {:02X?}, got {:02X?}",
COMPRESSION_PROTOCOL_ID, proto
)));
}
// OriginalCompressedSegmentSize (4 bytes)
let original_compressed_segment_size = cursor.read_u32_le()?;
// CompressionAlgorithm (2 bytes)
let compression_algorithm = cursor.read_u16_le()?;
// Flags (2 bytes)
let flags = cursor.read_u16_le()?;
// Offset/Length (4 bytes)
let offset_or_length = cursor.read_u32_le()?;
Ok(CompressionTransformHeader {
original_compressed_segment_size,
compression_algorithm,
flags,
offset_or_length,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
// ── TransformHeader tests ─────────────────────────────────────────
#[test]
fn transform_header_roundtrip() {
let mut nonce = [0u8; 16];
nonce[0..12].copy_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
let original = TransformHeader {
signature: [
0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88,
0x99, 0x00,
],
nonce,
original_message_size: 1024,
flags: SMB2_TRANSFORM_HEADER_FLAG_ENCRYPTED,
session_id: SessionId(0xDEAD_BEEF_CAFE_FACE),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
assert_eq!(bytes.len(), TransformHeader::SIZE);
let mut r = ReadCursor::new(&bytes);
let decoded = TransformHeader::unpack(&mut r).unwrap();
assert_eq!(decoded.signature, original.signature);
assert_eq!(decoded.nonce, original.nonce);
assert_eq!(decoded.original_message_size, 1024);
assert_eq!(decoded.flags, SMB2_TRANSFORM_HEADER_FLAG_ENCRYPTED);
assert_eq!(decoded.session_id, SessionId(0xDEAD_BEEF_CAFE_FACE));
}
#[test]
fn transform_header_protocol_id_is_0xfd() {
let original = TransformHeader {
signature: [0u8; 16],
nonce: [0u8; 16],
original_message_size: 0,
flags: 0,
session_id: SessionId(0),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// First 4 bytes must be 0xFD 'S' 'M' 'B', NOT 0xFE
assert_eq!(bytes[0], 0xFD);
assert_eq!(bytes[1], b'S');
assert_eq!(bytes[2], b'M');
assert_eq!(bytes[3], b'B');
assert_ne!(bytes[0], 0xFE, "transform header must use 0xFD, not 0xFE");
}
#[test]
fn transform_header_wrong_protocol_id() {
let mut buf = [0u8; TransformHeader::SIZE];
// Use the normal SMB2 protocol ID (0xFE) instead of 0xFD
buf[0..4].copy_from_slice(&[0xFE, b'S', b'M', b'B']);
let mut cursor = ReadCursor::new(&buf);
let result = TransformHeader::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("protocol ID"), "error was: {err}");
}
// ── CompressionTransformHeader tests ──────────────────────────────
#[test]
fn compression_transform_header_roundtrip_unchained() {
let original = CompressionTransformHeader {
original_compressed_segment_size: 4096,
compression_algorithm: COMPRESSION_ALGORITHM_LZ77,
flags: SMB2_COMPRESSION_FLAG_NONE,
offset_or_length: 64,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
assert_eq!(bytes.len(), CompressionTransformHeader::SIZE);
let mut r = ReadCursor::new(&bytes);
let decoded = CompressionTransformHeader::unpack(&mut r).unwrap();
assert_eq!(decoded.original_compressed_segment_size, 4096);
assert_eq!(decoded.compression_algorithm, COMPRESSION_ALGORITHM_LZ77);
assert_eq!(decoded.flags, SMB2_COMPRESSION_FLAG_NONE);
assert_eq!(decoded.offset_or_length, 64);
}
#[test]
fn compression_transform_header_protocol_id_is_0xfc() {
let original = CompressionTransformHeader {
original_compressed_segment_size: 0,
compression_algorithm: COMPRESSION_ALGORITHM_NONE,
flags: SMB2_COMPRESSION_FLAG_NONE,
offset_or_length: 0,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// First 4 bytes must be 0xFC 'S' 'M' 'B'
assert_eq!(bytes[0], 0xFC);
assert_eq!(bytes[1], b'S');
assert_eq!(bytes[2], b'M');
assert_eq!(bytes[3], b'B');
assert_ne!(
bytes[0], 0xFE,
"compression transform header must use 0xFC, not 0xFE"
);
}
#[test]
fn compression_transform_header_wrong_protocol_id() {
let mut buf = [0u8; CompressionTransformHeader::SIZE];
// Use wrong protocol ID
buf[0..4].copy_from_slice(&[0xFE, b'S', b'M', b'B']);
let mut cursor = ReadCursor::new(&buf);
let result = CompressionTransformHeader::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("protocol ID"), "error was: {err}");
}
#[test]
fn compression_transform_header_lz77_huffman() {
let original = CompressionTransformHeader {
original_compressed_segment_size: 8192,
compression_algorithm: COMPRESSION_ALGORITHM_LZ77_HUFFMAN,
flags: SMB2_COMPRESSION_FLAG_NONE,
offset_or_length: 128,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = CompressionTransformHeader::unpack(&mut r).unwrap();
assert_eq!(
decoded.compression_algorithm,
COMPRESSION_ALGORITHM_LZ77_HUFFMAN
);
assert_eq!(decoded.original_compressed_segment_size, 8192);
}
}
#[cfg(test)]
mod roundtrip_props {
use super::*;
use crate::msg::roundtrip_strategies::arb_session_id;
use proptest::prelude::*;
proptest! {
#[test]
fn transform_header_pack_unpack(
signature in any::<[u8; 16]>(),
nonce in any::<[u8; 16]>(),
original_message_size in any::<u32>(),
flags in any::<u16>(),
session_id in arb_session_id(),
) {
let original = TransformHeader {
signature,
nonce,
original_message_size,
flags,
session_id,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
prop_assert_eq!(bytes.len(), TransformHeader::SIZE);
let mut r = ReadCursor::new(&bytes);
let decoded = TransformHeader::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
prop_assert!(r.is_empty());
}
#[test]
fn compression_transform_header_pack_unpack(
original_compressed_segment_size in any::<u32>(),
compression_algorithm in any::<u16>(),
flags in any::<u16>(),
offset_or_length in any::<u32>(),
) {
let original = CompressionTransformHeader {
original_compressed_segment_size,
compression_algorithm,
flags,
offset_or_length,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
prop_assert_eq!(bytes.len(), CompressionTransformHeader::SIZE);
let mut r = ReadCursor::new(&bytes);
let decoded = CompressionTransformHeader::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
prop_assert!(r.is_empty());
}
}
}

477
vendor/smb2/src/msg/tree_connect.rs vendored Normal file
View File

@@ -0,0 +1,477 @@
//! SMB2 TREE_CONNECT request and response (spec sections 2.2.9, 2.2.10).
//!
//! Tree connect messages establish access to a share on the server.
//! The request contains a UTF-16LE encoded share path (for example,
//! `\\server\share`), and the response contains share metadata such as
//! the share type, flags, capabilities, and maximal access rights.
use crate::error::Result;
use crate::msg::header::Header;
use crate::pack::{Pack, ReadCursor, Unpack, WriteCursor};
use crate::types::flags::{ShareCapabilities, ShareFlags};
use crate::Error;
// ── Share type ─────────────────────────────────────────────────────────
/// Type of share being accessed (spec section 2.2.10).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ShareType {
/// Physical disk share.
Disk = 0x01,
/// Named pipe share.
Pipe = 0x02,
/// Printer share.
Print = 0x03,
}
impl ShareType {
/// Try to convert a raw `u8` to a `ShareType`.
pub fn try_from_u8(val: u8) -> Result<Self> {
match val {
0x01 => Ok(ShareType::Disk),
0x02 => Ok(ShareType::Pipe),
0x03 => Ok(ShareType::Print),
other => Err(Error::invalid_data(format!(
"invalid share type: 0x{:02X}",
other
))),
}
}
}
// ── Tree connect request flags ─────────────────────────────────────────
/// Flags for the TREE_CONNECT request (spec section 2.2.9, SMB 3.1.1 only).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct TreeConnectRequestFlags(pub u16);
impl TreeConnectRequestFlags {
/// Client has previously connected to the specified cluster share.
pub const CLUSTER_RECONNECT: u16 = 0x0001;
/// Client can handle synchronous share redirects.
pub const REDIRECT_TO_OWNER: u16 = 0x0002;
/// Tree connect request extension is present.
pub const EXTENSION_PRESENT: u16 = 0x0004;
}
// ── TreeConnectRequest ─────────────────────────────────────────────────
/// SMB2 TREE_CONNECT request (spec section 2.2.9).
///
/// Sent by the client to request access to a particular share on the
/// server. The path is a Unicode string in the form `\\server\share`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeConnectRequest {
/// Flags controlling the request (SMB 3.1.1 only, otherwise 0).
pub flags: TreeConnectRequestFlags,
/// Full share path name in UTF-8 (encoded as UTF-16LE on the wire).
pub path: String,
}
impl TreeConnectRequest {
pub const STRUCTURE_SIZE: u16 = 9;
}
impl Pack for TreeConnectRequest {
fn pack(&self, cursor: &mut WriteCursor) {
// StructureSize (2 bytes)
cursor.write_u16_le(Self::STRUCTURE_SIZE);
// Flags/Reserved (2 bytes)
cursor.write_u16_le(self.flags.0);
// Compute path length in UTF-16LE bytes
let path_u16: Vec<u16> = self.path.encode_utf16().collect();
let path_byte_len = path_u16.len() * 2;
// PathOffset (2 bytes) -- offset from start of SMB2 header
let offset = (Header::SIZE + 8) as u16; // 8 = fixed part of this struct
cursor.write_u16_le(offset);
// PathLength (2 bytes)
cursor.write_u16_le(path_byte_len as u16);
// Buffer: path in UTF-16LE
cursor.write_utf16_le(&self.path);
}
}
impl Unpack for TreeConnectRequest {
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 TreeConnectRequest structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
// Flags/Reserved (2 bytes)
let flags = TreeConnectRequestFlags(cursor.read_u16_le()?);
// PathOffset (2 bytes) -- we ignore, read sequentially
let _offset = cursor.read_u16_le()?;
// PathLength (2 bytes)
let path_length = cursor.read_u16_le()? as usize;
// Buffer: path in UTF-16LE
if path_length > ReadCursor::MAX_UNPACK_BUFFER {
return Err(Error::invalid_data(format!(
"buffer size {} exceeds maximum {} bytes",
path_length,
ReadCursor::MAX_UNPACK_BUFFER
)));
}
let path = cursor.read_utf16_le(path_length)?;
Ok(TreeConnectRequest { flags, path })
}
}
// ── TreeConnectResponse ────────────────────────────────────────────────
/// SMB2 TREE_CONNECT response (spec section 2.2.10).
///
/// Sent by the server when a TREE_CONNECT request is processed
/// successfully. Contains share metadata.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TreeConnectResponse {
/// The type of share being accessed (disk, pipe, or print).
pub share_type: ShareType,
/// Properties for this share.
pub share_flags: ShareFlags,
/// Capabilities for this share.
pub capabilities: ShareCapabilities,
/// Maximum access rights for the connecting user.
pub maximal_access: u32,
}
impl TreeConnectResponse {
pub const STRUCTURE_SIZE: u16 = 16;
}
impl Pack for TreeConnectResponse {
fn pack(&self, cursor: &mut WriteCursor) {
// StructureSize (2 bytes)
cursor.write_u16_le(Self::STRUCTURE_SIZE);
// ShareType (1 byte)
cursor.write_u8(self.share_type as u8);
// Reserved (1 byte)
cursor.write_u8(0);
// ShareFlags (4 bytes)
cursor.write_u32_le(self.share_flags.bits());
// Capabilities (4 bytes)
cursor.write_u32_le(self.capabilities.bits());
// MaximalAccess (4 bytes)
cursor.write_u32_le(self.maximal_access);
}
}
impl Unpack for TreeConnectResponse {
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 TreeConnectResponse structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
// ShareType (1 byte)
let share_type = ShareType::try_from_u8(cursor.read_u8()?)?;
// Reserved (1 byte)
let _reserved = cursor.read_u8()?;
// ShareFlags (4 bytes)
let share_flags = ShareFlags::new(cursor.read_u32_le()?);
// Capabilities (4 bytes)
let capabilities = ShareCapabilities::new(cursor.read_u32_le()?);
// MaximalAccess (4 bytes)
let maximal_access = cursor.read_u32_le()?;
Ok(TreeConnectResponse {
share_type,
share_flags,
capabilities,
maximal_access,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
// ── TreeConnectRequest tests ───────────────────────────────────
#[test]
fn tree_connect_request_roundtrip() {
let original = TreeConnectRequest {
flags: TreeConnectRequestFlags::default(),
path: r"\\server\share".to_string(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = TreeConnectRequest::unpack(&mut r).unwrap();
assert_eq!(decoded.flags, original.flags);
assert_eq!(decoded.path, original.path);
}
#[test]
fn tree_connect_request_with_utf16_path() {
let path = r"\\myserver.example.com\IPC$";
let original = TreeConnectRequest {
flags: TreeConnectRequestFlags::default(),
path: path.to_string(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = TreeConnectRequest::unpack(&mut r).unwrap();
assert_eq!(decoded.path, path);
}
#[test]
fn tree_connect_request_structure_size_field() {
let req = TreeConnectRequest {
flags: TreeConnectRequestFlags::default(),
path: r"\\s\d".to_string(),
};
let mut w = WriteCursor::new();
req.pack(&mut w);
let bytes = w.into_inner();
// First 2 bytes are structure size = 9
assert_eq!(u16::from_le_bytes([bytes[0], bytes[1]]), 9);
}
#[test]
fn tree_connect_request_wrong_structure_size() {
let mut buf = [0u8; 20];
buf[0..2].copy_from_slice(&99u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = TreeConnectRequest::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
#[test]
fn tree_connect_request_with_flags() {
let original = TreeConnectRequest {
flags: TreeConnectRequestFlags(TreeConnectRequestFlags::CLUSTER_RECONNECT),
path: r"\\s\d".to_string(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = TreeConnectRequest::unpack(&mut r).unwrap();
assert_eq!(decoded.flags.0, TreeConnectRequestFlags::CLUSTER_RECONNECT);
}
// ── TreeConnectResponse tests ──────────────────────────────────
#[test]
fn tree_connect_response_roundtrip_disk() {
let original = TreeConnectResponse {
share_type: ShareType::Disk,
share_flags: ShareFlags::new(ShareFlags::DFS | ShareFlags::ACCESS_BASED_DIRECTORY_ENUM),
capabilities: ShareCapabilities::new(ShareCapabilities::DFS),
maximal_access: 0x001F_01FF,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = TreeConnectResponse::unpack(&mut r).unwrap();
assert_eq!(decoded.share_type, ShareType::Disk);
assert_eq!(decoded.share_flags.bits(), original.share_flags.bits());
assert_eq!(decoded.capabilities.bits(), original.capabilities.bits());
assert_eq!(decoded.maximal_access, 0x001F_01FF);
}
#[test]
fn tree_connect_response_roundtrip_pipe() {
let original = TreeConnectResponse {
share_type: ShareType::Pipe,
share_flags: ShareFlags::default(),
capabilities: ShareCapabilities::default(),
maximal_access: 0x0012_019F,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = TreeConnectResponse::unpack(&mut r).unwrap();
assert_eq!(decoded.share_type, ShareType::Pipe);
assert_eq!(decoded.maximal_access, 0x0012_019F);
}
#[test]
fn tree_connect_response_roundtrip_print() {
let original = TreeConnectResponse {
share_type: ShareType::Print,
share_flags: ShareFlags::new(ShareFlags::ENCRYPT_DATA),
capabilities: ShareCapabilities::new(
ShareCapabilities::CONTINUOUS_AVAILABILITY | ShareCapabilities::CLUSTER,
),
maximal_access: 0,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = TreeConnectResponse::unpack(&mut r).unwrap();
assert_eq!(decoded.share_type, ShareType::Print);
assert!(decoded.share_flags.contains(ShareFlags::ENCRYPT_DATA));
assert!(decoded
.capabilities
.contains(ShareCapabilities::CONTINUOUS_AVAILABILITY));
assert!(decoded.capabilities.contains(ShareCapabilities::CLUSTER));
}
#[test]
fn tree_connect_response_structure_size_field() {
let resp = TreeConnectResponse {
share_type: ShareType::Disk,
share_flags: ShareFlags::default(),
capabilities: ShareCapabilities::default(),
maximal_access: 0,
};
let mut w = WriteCursor::new();
resp.pack(&mut w);
let bytes = w.into_inner();
// First 2 bytes are structure size = 16
assert_eq!(u16::from_le_bytes([bytes[0], bytes[1]]), 16);
// Total packed size: 2 + 1 + 1 + 4 + 4 + 4 = 16
assert_eq!(bytes.len(), 16);
}
#[test]
fn tree_connect_response_wrong_structure_size() {
let mut buf = [0u8; 16];
buf[0..2].copy_from_slice(&99u16.to_le_bytes());
buf[2] = 0x01; // valid share type
let mut cursor = ReadCursor::new(&buf);
let result = TreeConnectResponse::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
#[test]
fn tree_connect_response_invalid_share_type() {
let mut buf = [0u8; 16];
buf[0..2].copy_from_slice(&16u16.to_le_bytes());
buf[2] = 0xFF; // invalid share type
let mut cursor = ReadCursor::new(&buf);
let result = TreeConnectResponse::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("share type"), "error was: {err}");
}
// Roundtrip property tests live in `roundtrip_props` at file end.
#[test]
fn tree_connect_response_known_bytes() {
// Known bytes from smb-rs test: share_type=Disk, share_flags=0x00000800,
// capabilities=0, maximal_access=0x001f01ff
let bytes: Vec<u8> = vec![
0x10, 0x00, // StructureSize = 16
0x01, // ShareType = Disk
0x00, // Reserved
0x00, 0x08, 0x00, 0x00, // ShareFlags = 0x00000800
0x00, 0x00, 0x00, 0x00, // Capabilities = 0
0xFF, 0x01, 0x1F, 0x00, // MaximalAccess = 0x001f01ff
];
let mut r = ReadCursor::new(&bytes);
let decoded = TreeConnectResponse::unpack(&mut r).unwrap();
assert_eq!(decoded.share_type, ShareType::Disk);
assert!(decoded
.share_flags
.contains(ShareFlags::ACCESS_BASED_DIRECTORY_ENUM));
assert_eq!(decoded.maximal_access, 0x001F_01FF);
}
}
#[cfg(test)]
mod roundtrip_props {
use super::*;
use crate::msg::roundtrip_strategies::{
arb_share_capabilities, arb_share_flags, arb_share_type, arb_utf16_string,
};
use proptest::prelude::*;
proptest! {
#[test]
fn tree_connect_request_pack_unpack(
flags_raw in any::<u16>(),
// Path is sent as UTF-16LE. Generate strings that survive that
// encoding cleanly (no unpaired surrogates).
path in arb_utf16_string(128),
) {
let original = TreeConnectRequest {
flags: TreeConnectRequestFlags(flags_raw),
path,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = TreeConnectRequest::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
prop_assert!(r.is_empty());
}
#[test]
fn tree_connect_response_pack_unpack(
share_type in arb_share_type(),
share_flags in arb_share_flags(),
capabilities in arb_share_capabilities(),
maximal_access in any::<u32>(),
) {
let original = TreeConnectResponse {
share_type,
share_flags,
capabilities,
maximal_access,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = TreeConnectResponse::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
prop_assert!(r.is_empty());
}
}
}

43
vendor/smb2/src/msg/tree_disconnect.rs vendored Normal file
View File

@@ -0,0 +1,43 @@
//! SMB2 TREE_DISCONNECT request and response (spec sections 2.2.11, 2.2.12).
//!
//! Tree disconnect messages request and confirm disconnection from a share.
//! Both request and response contain only a StructureSize field and a
//! reserved field, for a total of 4 bytes each.
super::trivial_message! {
/// SMB2 TREE_DISCONNECT request (spec section 2.2.11).
///
/// Sent by the client to request that the tree connect specified in the
/// TreeId within the SMB2 header be disconnected.
/// Contains only StructureSize (2 bytes) and Reserved (2 bytes).
pub struct TreeDisconnectRequest;
}
super::trivial_message! {
/// SMB2 TREE_DISCONNECT response (spec section 2.2.12).
///
/// Sent by the server to confirm that a TREE_DISCONNECT request was processed.
/// Contains only StructureSize (2 bytes) and Reserved (2 bytes).
pub struct TreeDisconnectResponse;
}
#[cfg(test)]
mod tests {
use super::*;
super::super::trivial_message_tests!(
TreeDisconnectRequest,
tree_disconnect_request_known_bytes,
tree_disconnect_request_roundtrip,
tree_disconnect_request_wrong_structure_size,
tree_disconnect_request_too_short
);
super::super::trivial_message_tests!(
TreeDisconnectResponse,
tree_disconnect_response_known_bytes,
tree_disconnect_response_roundtrip,
tree_disconnect_response_wrong_structure_size,
tree_disconnect_response_too_short
);
}

446
vendor/smb2/src/msg/write.rs vendored Normal file
View File

@@ -0,0 +1,446 @@
//! SMB2 WRITE Request and Response (MS-SMB2 sections 2.2.21, 2.2.22).
//!
//! The WRITE request writes data to a file or named pipe.
//! The response reports how many bytes were written.
use crate::error::Result;
use crate::pack::{Pack, ReadCursor, Unpack, WriteCursor};
use crate::types::FileId;
use crate::Error;
/// Write flag: server performs write-through (SMB 2.1+).
pub const SMB2_WRITEFLAG_WRITE_THROUGH: u32 = 0x0000_0001;
/// Write flag: file buffering is not performed (SMB 3.0.2+).
pub const SMB2_WRITEFLAG_WRITE_UNBUFFERED: u32 = 0x0000_0002;
/// SMB2 WRITE Request (MS-SMB2 section 2.2.21).
///
/// Sent by the client to write data to a file. The fixed portion is 49 bytes
/// (StructureSize says 49 regardless of the variable buffer length):
/// - StructureSize (2 bytes, must be 49)
/// - DataOffset (2 bytes)
/// - Length (4 bytes)
/// - Offset (8 bytes)
/// - FileId (16 bytes)
/// - Channel (4 bytes)
/// - RemainingBytes (4 bytes)
/// - WriteChannelInfoOffset (2 bytes)
/// - WriteChannelInfoLength (2 bytes)
/// - Flags (4 bytes)
/// - Buffer (variable, Length bytes)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WriteRequest {
/// Offset from the beginning of the SMB2 header to the write data.
pub data_offset: u16,
/// File offset to start writing at.
pub offset: u64,
/// File handle to write to.
pub file_id: FileId,
/// Channel for RDMA operations (typically 0 = SMB2_CHANNEL_NONE).
pub channel: u32,
/// Remaining bytes in a multi-part write.
pub remaining_bytes: u32,
/// Write channel info offset (typically 0).
pub write_channel_info_offset: u16,
/// Write channel info length (typically 0).
pub write_channel_info_length: u16,
/// Flags for the write operation.
pub flags: u32,
/// The data to write.
pub data: Vec<u8>,
}
impl WriteRequest {
pub const STRUCTURE_SIZE: u16 = 49;
}
impl Pack for WriteRequest {
fn pack(&self, cursor: &mut WriteCursor) {
cursor.write_u16_le(Self::STRUCTURE_SIZE);
cursor.write_u16_le(self.data_offset);
cursor.write_u32_le(self.data.len() as u32); // Length
cursor.write_u64_le(self.offset);
cursor.write_u64_le(self.file_id.persistent);
cursor.write_u64_le(self.file_id.volatile);
cursor.write_u32_le(self.channel);
cursor.write_u32_le(self.remaining_bytes);
cursor.write_u16_le(self.write_channel_info_offset);
cursor.write_u16_le(self.write_channel_info_length);
cursor.write_u32_le(self.flags);
// Buffer: write the data (may be empty for zero-length writes).
// Per StructureSize=49 contract, at least 1 byte is implied.
if self.data.is_empty() {
cursor.write_u8(0);
} else {
cursor.write_bytes(&self.data);
}
}
}
impl Unpack for WriteRequest {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let structure_size = cursor.read_u16_le()?;
if structure_size != Self::STRUCTURE_SIZE {
return Err(Error::invalid_data(format!(
"invalid WriteRequest structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
let data_offset = cursor.read_u16_le()?;
let length = cursor.read_u32_le()?;
let offset = cursor.read_u64_le()?;
let persistent = cursor.read_u64_le()?;
let volatile = cursor.read_u64_le()?;
let channel = cursor.read_u32_le()?;
let remaining_bytes = cursor.read_u32_le()?;
let write_channel_info_offset = cursor.read_u16_le()?;
let write_channel_info_length = cursor.read_u16_le()?;
let flags = cursor.read_u32_le()?;
let data = if length > 0 {
cursor.read_bytes_bounded(length as usize)?.to_vec()
} else {
// Skip the minimum 1-byte buffer
cursor.skip(1)?;
Vec::new()
};
Ok(WriteRequest {
data_offset,
offset,
file_id: FileId {
persistent,
volatile,
},
channel,
remaining_bytes,
write_channel_info_offset,
write_channel_info_length,
flags,
data,
})
}
}
/// SMB2 WRITE Response (MS-SMB2 section 2.2.22).
///
/// Sent by the server to confirm a write. The structure is 17 bytes:
/// - StructureSize (2 bytes, must be 17)
/// - Reserved (2 bytes)
/// - Count (4 bytes)
/// - Remaining (4 bytes)
/// - WriteChannelInfoOffset (2 bytes)
/// - WriteChannelInfoLength (2 bytes)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WriteResponse {
/// Number of bytes written.
pub count: u32,
/// Reserved remaining field (must be 0).
pub remaining: u32,
/// Reserved write channel info offset (must be 0).
pub write_channel_info_offset: u16,
/// Reserved write channel info length (must be 0).
pub write_channel_info_length: u16,
}
impl WriteResponse {
pub const STRUCTURE_SIZE: u16 = 17;
}
impl Pack for WriteResponse {
fn pack(&self, cursor: &mut WriteCursor) {
cursor.write_u16_le(Self::STRUCTURE_SIZE);
cursor.write_u16_le(0); // Reserved
cursor.write_u32_le(self.count);
cursor.write_u32_le(self.remaining);
cursor.write_u16_le(self.write_channel_info_offset);
cursor.write_u16_le(self.write_channel_info_length);
}
}
impl Unpack for WriteResponse {
fn unpack(cursor: &mut ReadCursor<'_>) -> Result<Self> {
let structure_size = cursor.read_u16_le()?;
if structure_size != Self::STRUCTURE_SIZE {
return Err(Error::invalid_data(format!(
"invalid WriteResponse structure size: expected {}, got {}",
Self::STRUCTURE_SIZE,
structure_size
)));
}
let _reserved = cursor.read_u16_le()?;
let count = cursor.read_u32_le()?;
let remaining = cursor.read_u32_le()?;
let write_channel_info_offset = cursor.read_u16_le()?;
let write_channel_info_length = cursor.read_u16_le()?;
Ok(WriteResponse {
count,
remaining,
write_channel_info_offset,
write_channel_info_length,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
// ── WriteRequest tests ─────────────────────────────────────────
#[test]
fn write_request_roundtrip() {
let original = WriteRequest {
data_offset: 0x70, // 64 (header) + 48 (fixed body) = 112 = 0x70
offset: 0x2000,
file_id: FileId {
persistent: 0xAAAA_BBBB_CCCC_DDDD,
volatile: 0x1111_2222_3333_4444,
},
channel: 0,
remaining_bytes: 0,
write_channel_info_offset: 0,
write_channel_info_length: 0,
flags: SMB2_WRITEFLAG_WRITE_THROUGH,
data: vec![0x48, 0x65, 0x6C, 0x6C, 0x6F], // "Hello"
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// Fixed: 48 bytes + 5 bytes data = 53 bytes
assert_eq!(bytes.len(), 53);
let mut r = ReadCursor::new(&bytes);
let decoded = WriteRequest::unpack(&mut r).unwrap();
assert_eq!(decoded.data_offset, original.data_offset);
assert_eq!(decoded.offset, original.offset);
assert_eq!(decoded.file_id, original.file_id);
assert_eq!(decoded.channel, original.channel);
assert_eq!(decoded.remaining_bytes, original.remaining_bytes);
assert_eq!(decoded.flags, original.flags);
assert_eq!(decoded.data, original.data);
}
#[test]
fn write_request_empty_data_roundtrip() {
let original = WriteRequest {
data_offset: 0x70,
offset: 0,
file_id: FileId {
persistent: 1,
volatile: 2,
},
channel: 0,
remaining_bytes: 0,
write_channel_info_offset: 0,
write_channel_info_length: 0,
flags: 0,
data: Vec::new(),
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// Fixed: 48 bytes + 1-byte minimum buffer = 49 bytes
assert_eq!(bytes.len(), 49);
let mut r = ReadCursor::new(&bytes);
let decoded = WriteRequest::unpack(&mut r).unwrap();
assert!(decoded.data.is_empty());
assert_eq!(decoded.file_id, original.file_id);
}
#[test]
fn write_request_wrong_structure_size() {
let mut buf = [0u8; 49];
buf[0..2].copy_from_slice(&48u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = WriteRequest::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
#[test]
fn write_request_known_bytes() {
let mut buf = Vec::new();
// StructureSize = 49
buf.extend_from_slice(&49u16.to_le_bytes());
// DataOffset = 0x70
buf.extend_from_slice(&0x70u16.to_le_bytes());
// Length = 2
buf.extend_from_slice(&2u32.to_le_bytes());
// Offset = 0
buf.extend_from_slice(&0u64.to_le_bytes());
// FileId persistent = 0x10
buf.extend_from_slice(&0x10u64.to_le_bytes());
// FileId volatile = 0x20
buf.extend_from_slice(&0x20u64.to_le_bytes());
// Channel = 0
buf.extend_from_slice(&0u32.to_le_bytes());
// RemainingBytes = 0
buf.extend_from_slice(&0u32.to_le_bytes());
// WriteChannelInfoOffset = 0
buf.extend_from_slice(&0u16.to_le_bytes());
// WriteChannelInfoLength = 0
buf.extend_from_slice(&0u16.to_le_bytes());
// Flags = WRITE_THROUGH
buf.extend_from_slice(&1u32.to_le_bytes());
// Buffer = [0xAA, 0xBB]
buf.extend_from_slice(&[0xAA, 0xBB]);
let mut cursor = ReadCursor::new(&buf);
let req = WriteRequest::unpack(&mut cursor).unwrap();
assert_eq!(req.data_offset, 0x70);
assert_eq!(req.file_id.persistent, 0x10);
assert_eq!(req.file_id.volatile, 0x20);
assert_eq!(req.flags, SMB2_WRITEFLAG_WRITE_THROUGH);
assert_eq!(req.data, vec![0xAA, 0xBB]);
}
// ── WriteResponse tests ────────────────────────────────────────
#[test]
fn write_response_roundtrip() {
let original = WriteResponse {
count: 65536,
remaining: 0,
write_channel_info_offset: 0,
write_channel_info_length: 0,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
// 2 + 2 + 4 + 4 + 2 + 2 = 16 bytes
assert_eq!(bytes.len(), 16);
let mut r = ReadCursor::new(&bytes);
let decoded = WriteResponse::unpack(&mut r).unwrap();
assert_eq!(decoded.count, original.count);
assert_eq!(decoded.remaining, original.remaining);
assert_eq!(
decoded.write_channel_info_offset,
original.write_channel_info_offset
);
assert_eq!(
decoded.write_channel_info_length,
original.write_channel_info_length
);
}
#[test]
fn write_response_known_bytes() {
let mut buf = Vec::new();
// StructureSize = 17
buf.extend_from_slice(&17u16.to_le_bytes());
// Reserved = 0
buf.extend_from_slice(&0u16.to_le_bytes());
// Count = 1024
buf.extend_from_slice(&1024u32.to_le_bytes());
// Remaining = 0
buf.extend_from_slice(&0u32.to_le_bytes());
// WriteChannelInfoOffset = 0
buf.extend_from_slice(&0u16.to_le_bytes());
// WriteChannelInfoLength = 0
buf.extend_from_slice(&0u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let resp = WriteResponse::unpack(&mut cursor).unwrap();
assert_eq!(resp.count, 1024);
assert_eq!(resp.remaining, 0);
}
#[test]
fn write_response_wrong_structure_size() {
let mut buf = [0u8; 16];
buf[0..2].copy_from_slice(&16u16.to_le_bytes());
let mut cursor = ReadCursor::new(&buf);
let result = WriteResponse::unpack(&mut cursor);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(err.contains("structure size"), "error was: {err}");
}
}
#[cfg(test)]
mod roundtrip_props {
use super::*;
use crate::msg::roundtrip_strategies::{arb_bytes, arb_file_id};
use proptest::prelude::*;
proptest! {
#[test]
fn write_request_pack_unpack(
data_offset in any::<u16>(),
offset in any::<u64>(),
file_id in arb_file_id(),
channel in any::<u32>(),
remaining_bytes in any::<u32>(),
write_channel_info_offset in any::<u16>(),
write_channel_info_length in any::<u16>(),
flags in any::<u32>(),
data in arb_bytes(),
) {
let original = WriteRequest {
data_offset,
offset,
file_id,
channel,
remaining_bytes,
write_channel_info_offset,
write_channel_info_length,
flags,
data,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = WriteRequest::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
prop_assert!(r.is_empty());
}
#[test]
fn write_response_pack_unpack(
count in any::<u32>(),
remaining in any::<u32>(),
write_channel_info_offset in any::<u16>(),
write_channel_info_length in any::<u16>(),
) {
let original = WriteResponse {
count,
remaining,
write_channel_info_offset,
write_channel_info_length,
};
let mut w = WriteCursor::new();
original.pack(&mut w);
let bytes = w.into_inner();
let mut r = ReadCursor::new(&bytes);
let decoded = WriteResponse::unpack(&mut r).unwrap();
prop_assert_eq!(decoded, original);
prop_assert!(r.is_empty());
}
}
}