SMB Server Phase 2: VFS backend build fix + integration test
- 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:
44
vendor/smb2/src/msg/CLAUDE.md
vendored
Normal file
44
vendor/smb2/src/msg/CLAUDE.md
vendored
Normal 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
27
vendor/smb2/src/msg/cancel.rs
vendored
Normal 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
355
vendor/smb2/src/msg/change_notify.rs
vendored
Normal 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
390
vendor/smb2/src/msg/close.rs
vendored
Normal 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
870
vendor/smb2/src/msg/create.rs
vendored
Normal 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
697
vendor/smb2/src/msg/dfs.rs
vendored
Normal 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
42
vendor/smb2/src/msg/echo.rs
vendored
Normal 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
254
vendor/smb2/src/msg/flush.rs
vendored
Normal 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
669
vendor/smb2/src/msg/header.rs
vendored
Normal 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
479
vendor/smb2/src/msg/ioctl.rs
vendored
Normal 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
445
vendor/smb2/src/msg/lock.rs
vendored
Normal 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
42
vendor/smb2/src/msg/logoff.rs
vendored
Normal 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
152
vendor/smb2/src/msg/mod.rs
vendored
Normal 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
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
262
vendor/smb2/src/msg/oplock_break.rs
vendored
Normal 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
476
vendor/smb2/src/msg/query_directory.rs
vendored
Normal 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
479
vendor/smb2/src/msg/query_info.rs
vendored
Normal 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
462
vendor/smb2/src/msg/read.rs
vendored
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
250
vendor/smb2/src/msg/roundtrip_strategies.rs
vendored
Normal file
250
vendor/smb2/src/msg/roundtrip_strategies.rs
vendored
Normal 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
481
vendor/smb2/src/msg/session_setup.rs
vendored
Normal 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
328
vendor/smb2/src/msg/set_info.rs
vendored
Normal 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
452
vendor/smb2/src/msg/transform.rs
vendored
Normal 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
477
vendor/smb2/src/msg/tree_connect.rs
vendored
Normal 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
43
vendor/smb2/src/msg/tree_disconnect.rs
vendored
Normal 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
446
vendor/smb2/src/msg/write.rs
vendored
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user