Files
markbase/vendor/smb2/src/msg/query_info.rs
Warren 7eb528d35f
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled
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)
2026-06-20 19:42:29 +08:00

480 lines
16 KiB
Rust

//! 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);
}
}
}