Skip to content

Commit 8e2e795

Browse files
committed
feat: implement Close, Read, and QueryInfo handlers for E2E file read
Add the server-side handlers needed for a complete file read flow: Create → QueryInfo → Read → Close. Protocol types: - Add accessor methods to SMBCloseRequest, SMBReadRequest, SMBQueryInfoRequest - Add constructors for SMBCloseResponse, SMBReadResponse, SMBQueryInfoResponse - Make close::flags and query_info::info_type modules public Trait extensions: - Add read_data() to ResourceHandle trait with implementations for SMBFileSystemHandle (seeks + reads) and SMBIPCHandle (stub) - Add read_data() to Open trait, delegating to the underlying handle - Add open_table_mut() to Session trait for close cleanup - Add remove_open() to Server trait for global open table cleanup - Fix SMBOpen::inner() todo!() → return None (terminal handler) Handlers (tree_connect.rs): - handle_close: returns file metadata if POSTQUERY_ATTRIB flag set, removes open from server (outer) then session (inner) tables - handle_read: reads data from the open's underlying handle, enforces minimum_count, returns SMBReadResponse - handle_query_info: supports FileBasicInformation (class 4), FileStandardInformation (class 5), FileNetworkOpenInformation (class 34) per MS-FSCC; returns InvalidInfoClass for unsupported types - Fix lock ordering in handle_create: server write before session write Lock ordering: all handlers acquire locks outer→inner (server → connection → session → open), readers before writers. NTStatus: add FileClosed, EndOfFile, InvalidInfoClass, InvalidDeviceRequest Tests: - 13 unit tests for request accessor and response constructor round-trips - 3 integration tests (smbclient E2E: file read, dir listing, missing file)
1 parent 1012d12 commit 8e2e795

12 files changed

Lines changed: 704 additions & 11 deletions

File tree

smb-core/src/nt_status.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ pub enum NTStatus {
1919
UserSessionDeleted = 0xC0000203,
2020
NetworkSessionExpired = 0xC000035C,
2121
FileNotAvailable = 0xC0000467,
22+
FileClosed = 0xC0000128,
23+
EndOfFile = 0xC0000011,
24+
InvalidInfoClass = 0xC0000003,
25+
InvalidDeviceRequest = 0xC0000010,
2226
UnknownError = 0xFFFFFFFF,
2327
}
2428

smb/src/protocol/body/close/mod.rs

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::protocol::body::create::file_attributes::SMBFileAttributes;
99
use crate::protocol::body::create::file_id::SMBFileId;
1010
use crate::protocol::body::filetime::FileTime;
1111

12-
mod flags;
12+
pub mod flags;
1313

1414
#[derive(
1515
Debug,
@@ -32,6 +32,16 @@ pub struct SMBCloseRequest {
3232
file_id: SMBFileId,
3333
}
3434

35+
impl SMBCloseRequest {
36+
pub fn flags(&self) -> SMBCloseFlags {
37+
self.flags
38+
}
39+
40+
pub fn file_id(&self) -> &SMBFileId {
41+
&self.file_id
42+
}
43+
}
44+
3545
#[derive(
3646
Debug,
3747
PartialEq,
@@ -63,4 +73,116 @@ pub struct SMBCloseResponse {
6373
end_of_file: u64,
6474
#[smb_direct(start(fixed = 56))]
6575
file_attributes: SMBFileAttributes,
76+
}
77+
78+
impl SMBCloseResponse {
79+
pub fn from_metadata(metadata: &crate::server::share::SMBFileMetadata, attributes: SMBFileAttributes) -> Self {
80+
Self {
81+
flags: SMBCloseFlags::POSTQUERY_ATTRIB,
82+
reserved: PhantomData,
83+
creation_time: metadata.creation_time.clone(),
84+
last_access_time: metadata.last_access_time.clone(),
85+
last_write_time: metadata.last_write_time.clone(),
86+
change_time: metadata.last_modification_time.clone(),
87+
allocation_size: metadata.allocated_size,
88+
end_of_file: metadata.actual_size,
89+
file_attributes: attributes,
90+
}
91+
}
92+
93+
pub fn empty() -> Self {
94+
Self {
95+
flags: SMBCloseFlags::empty(),
96+
reserved: PhantomData,
97+
creation_time: FileTime::zero(),
98+
last_access_time: FileTime::zero(),
99+
last_write_time: FileTime::zero(),
100+
change_time: FileTime::zero(),
101+
allocation_size: 0,
102+
end_of_file: 0,
103+
file_attributes: SMBFileAttributes::empty(),
104+
}
105+
}
106+
}
107+
108+
#[cfg(test)]
109+
mod tests {
110+
use super::*;
111+
use smb_core::{SMBByteSize, SMBToBytes, SMBFromBytes};
112+
113+
#[test]
114+
fn close_response_empty_has_zero_fields() {
115+
let resp = SMBCloseResponse::empty();
116+
assert_eq!(resp.flags, SMBCloseFlags::empty());
117+
assert_eq!(resp.allocation_size, 0);
118+
assert_eq!(resp.end_of_file, 0);
119+
assert_eq!(resp.file_attributes, SMBFileAttributes::empty());
120+
}
121+
122+
#[test]
123+
fn close_response_empty_serialization_round_trip() {
124+
let resp = SMBCloseResponse::empty();
125+
let bytes = resp.smb_to_bytes();
126+
assert_eq!(bytes.len(), resp.smb_byte_size());
127+
let (_, parsed) = SMBCloseResponse::smb_from_bytes(&bytes).unwrap();
128+
assert_eq!(resp, parsed);
129+
}
130+
131+
#[test]
132+
fn close_response_from_metadata_sets_postquery_flag() {
133+
use crate::server::share::SMBFileMetadata;
134+
let metadata = SMBFileMetadata {
135+
creation_time: FileTime::from_unix(1700000000),
136+
last_access_time: FileTime::from_unix(1700000100),
137+
last_write_time: FileTime::from_unix(1700000200),
138+
last_modification_time: FileTime::from_unix(1700000300),
139+
allocated_size: 4096,
140+
actual_size: 1024,
141+
};
142+
let resp = SMBCloseResponse::from_metadata(&metadata, SMBFileAttributes::NORMAL);
143+
assert!(resp.flags.contains(SMBCloseFlags::POSTQUERY_ATTRIB));
144+
assert_eq!(resp.allocation_size, 4096);
145+
assert_eq!(resp.end_of_file, 1024);
146+
assert_eq!(resp.file_attributes, SMBFileAttributes::NORMAL);
147+
}
148+
149+
#[test]
150+
fn close_response_from_metadata_serialization_round_trip() {
151+
use crate::server::share::SMBFileMetadata;
152+
let metadata = SMBFileMetadata {
153+
creation_time: FileTime::from_unix(1700000000),
154+
last_access_time: FileTime::from_unix(1700000100),
155+
last_write_time: FileTime::from_unix(1700000200),
156+
last_modification_time: FileTime::from_unix(1700000300),
157+
allocated_size: 8192,
158+
actual_size: 2048,
159+
};
160+
let resp = SMBCloseResponse::from_metadata(&metadata, SMBFileAttributes::ARCHIVE);
161+
let bytes = resp.smb_to_bytes();
162+
assert_eq!(bytes.len(), resp.smb_byte_size());
163+
let (_, parsed) = SMBCloseResponse::smb_from_bytes(&bytes).unwrap();
164+
assert_eq!(resp, parsed);
165+
}
166+
167+
#[test]
168+
fn close_request_accessors() {
169+
let file_id = SMBFileId { persistent: 42, volatile: 99 };
170+
let bytes = {
171+
let mut buf = Vec::new();
172+
// struct_size (u16) = 24
173+
buf.extend_from_slice(&24u16.to_le_bytes());
174+
// flags (u16) = POSTQUERY_ATTRIB = 0x0001
175+
buf.extend_from_slice(&1u16.to_le_bytes());
176+
// reserved (4 bytes)
177+
buf.extend_from_slice(&[0u8; 4]);
178+
// file_id: persistent (u64) + volatile (u64)
179+
buf.extend_from_slice(&42u64.to_le_bytes());
180+
buf.extend_from_slice(&99u64.to_le_bytes());
181+
buf
182+
};
183+
let (_, req) = SMBCloseRequest::smb_from_bytes(&bytes).unwrap();
184+
assert_eq!(req.file_id().persistent, file_id.persistent);
185+
assert_eq!(req.file_id().volatile, file_id.volatile);
186+
assert!(req.flags().contains(SMBCloseFlags::POSTQUERY_ATTRIB));
187+
}
66188
}

smb/src/protocol/body/query_info/mod.rs

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use crate::protocol::body::query_info::info_type::SMBInfoType;
1010
use crate::protocol::body::query_info::security_information::SMBSecurityInformation;
1111

1212
mod flags;
13-
mod info_type;
13+
pub mod info_type;
1414
mod security_information;
1515

1616
#[derive(
@@ -44,6 +44,24 @@ pub struct SMBQueryInfoRequest {
4444
buffer: Vec<u8>,
4545
}
4646

47+
impl SMBQueryInfoRequest {
48+
pub fn info_type(&self) -> SMBInfoType {
49+
self.info_type
50+
}
51+
52+
pub fn file_info_class(&self) -> u8 {
53+
self.file_info_class
54+
}
55+
56+
pub fn output_buffer_length(&self) -> u32 {
57+
self.output_buffer_length
58+
}
59+
60+
pub fn file_id(&self) -> &SMBFileId {
61+
&self.file_id
62+
}
63+
}
64+
4765
#[derive(
4866
Debug,
4967
PartialEq,
@@ -62,4 +80,78 @@ pub struct SMBQueryInfoResponse {
6280
// TODO make this a struct: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-smb2/3b1b3598-a898-44ca-bfac-2dcae065247f
6381
#[smb_buffer(order = 0, offset(inner(start = 2, num_type = "u16", subtract = 64)), length(inner(start = 4, num_type = "u32")))]
6482
data: Vec<u8>,
83+
}
84+
85+
impl SMBQueryInfoResponse {
86+
pub fn new(data: Vec<u8>) -> Self {
87+
Self {
88+
reserved: PhantomData,
89+
data,
90+
}
91+
}
92+
}
93+
94+
#[cfg(test)]
95+
mod tests {
96+
use super::*;
97+
use smb_core::{SMBByteSize, SMBToBytes, SMBFromBytes};
98+
99+
#[test]
100+
fn query_info_response_new_sets_data() {
101+
let data = vec![1, 2, 3, 4, 5, 6, 7, 8];
102+
let resp = SMBQueryInfoResponse::new(data.clone());
103+
assert_eq!(resp.data, data);
104+
}
105+
106+
#[test]
107+
fn query_info_response_serialization_round_trip() {
108+
let resp = SMBQueryInfoResponse::new(vec![0xAA; 40]);
109+
let bytes = resp.smb_to_bytes();
110+
assert_eq!(bytes.len(), resp.smb_byte_size());
111+
let (_, parsed) = SMBQueryInfoResponse::smb_from_bytes(&bytes).unwrap();
112+
assert_eq!(resp, parsed);
113+
}
114+
115+
#[test]
116+
fn query_info_response_empty_data_round_trip() {
117+
let resp = SMBQueryInfoResponse::new(vec![]);
118+
let bytes = resp.smb_to_bytes();
119+
let (_, parsed) = SMBQueryInfoResponse::smb_from_bytes(&bytes).unwrap();
120+
assert_eq!(resp, parsed);
121+
}
122+
123+
#[test]
124+
fn query_info_request_accessors() {
125+
let bytes = {
126+
let mut buf = Vec::new();
127+
// struct_size (u16) = 41
128+
buf.extend_from_slice(&41u16.to_le_bytes());
129+
// info_type (u8) = 0 (File)
130+
buf.push(0);
131+
// file_info_class (u8) = 4 (FileBasicInformation)
132+
buf.push(4);
133+
// output_buffer_length (u32) = 4096
134+
buf.extend_from_slice(&4096u32.to_le_bytes());
135+
// input_buffer_offset (u16) = 0
136+
buf.extend_from_slice(&0u16.to_le_bytes());
137+
// reserved (u16) = 0
138+
buf.extend_from_slice(&0u16.to_le_bytes());
139+
// input_buffer_length (u32) = 0
140+
buf.extend_from_slice(&0u32.to_le_bytes());
141+
// additional_information (u32) = 0
142+
buf.extend_from_slice(&0u32.to_le_bytes());
143+
// flags (u32) = 0
144+
buf.extend_from_slice(&0u32.to_le_bytes());
145+
// file_id: persistent (u64) + volatile (u64)
146+
buf.extend_from_slice(&55u64.to_le_bytes());
147+
buf.extend_from_slice(&77u64.to_le_bytes());
148+
buf
149+
};
150+
let (_, req) = SMBQueryInfoRequest::smb_from_bytes(&bytes).unwrap();
151+
assert_eq!(req.info_type(), SMBInfoType::File);
152+
assert_eq!(req.file_info_class(), 4);
153+
assert_eq!(req.output_buffer_length(), 4096);
154+
assert_eq!(req.file_id().persistent, 55);
155+
assert_eq!(req.file_id().volatile, 77);
156+
}
65157
}

smb/src/protocol/body/read/mod.rs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,24 @@ pub struct SMBReadRequest {
4242
channel_information: Vec<u8>,
4343
}
4444

45+
impl SMBReadRequest {
46+
pub fn file_id(&self) -> &SMBFileId {
47+
&self.file_id
48+
}
49+
50+
pub fn read_length(&self) -> u32 {
51+
self.read_length
52+
}
53+
54+
pub fn read_offset(&self) -> u64 {
55+
self.read_offset
56+
}
57+
58+
pub fn minimum_count(&self) -> u32 {
59+
self.minimum_count
60+
}
61+
}
62+
4563
#[derive(
4664
Debug,
4765
PartialEq,
@@ -63,4 +81,83 @@ pub struct SMBReadResponse {
6381
flags: SMBReadResponseFlags,
6482
#[smb_buffer(order = 0, offset(inner(start = 2, num_type = "u8", subtract = 64)), length(inner(start = 4, num_type = "u32")))]
6583
data: Vec<u8>,
84+
}
85+
86+
impl SMBReadResponse {
87+
pub fn new(data: Vec<u8>, data_remaining: u32) -> Self {
88+
Self {
89+
reserved: PhantomData,
90+
data_remaining,
91+
flags: SMBReadResponseFlags::None,
92+
data,
93+
}
94+
}
95+
}
96+
97+
#[cfg(test)]
98+
mod tests {
99+
use super::*;
100+
use smb_core::{SMBByteSize, SMBToBytes, SMBFromBytes};
101+
102+
#[test]
103+
fn read_response_new_sets_fields() {
104+
let data = vec![0xDE, 0xAD, 0xBE, 0xEF];
105+
let resp = SMBReadResponse::new(data.clone(), 100);
106+
assert_eq!(resp.data, data);
107+
assert_eq!(resp.data_remaining, 100);
108+
assert_eq!(resp.flags, SMBReadResponseFlags::None);
109+
}
110+
111+
#[test]
112+
fn read_response_serialization_round_trip() {
113+
let resp = SMBReadResponse::new(vec![1, 2, 3, 4, 5], 0);
114+
let bytes = resp.smb_to_bytes();
115+
assert_eq!(bytes.len(), resp.smb_byte_size());
116+
let (_, parsed) = SMBReadResponse::smb_from_bytes(&bytes).unwrap();
117+
assert_eq!(resp, parsed);
118+
}
119+
120+
#[test]
121+
fn read_response_empty_data() {
122+
let resp = SMBReadResponse::new(vec![], 0);
123+
let bytes = resp.smb_to_bytes();
124+
let (_, parsed) = SMBReadResponse::smb_from_bytes(&bytes).unwrap();
125+
assert_eq!(resp, parsed);
126+
}
127+
128+
#[test]
129+
fn read_request_accessors() {
130+
let bytes = {
131+
let mut buf = Vec::new();
132+
// struct_size (u16) = 49
133+
buf.extend_from_slice(&49u16.to_le_bytes());
134+
// padding (u8)
135+
buf.push(0);
136+
// flags (u8) = 0
137+
buf.push(0);
138+
// read_length (u32) = 1024
139+
buf.extend_from_slice(&1024u32.to_le_bytes());
140+
// read_offset (u64) = 512
141+
buf.extend_from_slice(&512u64.to_le_bytes());
142+
// file_id: persistent (u64) + volatile (u64)
143+
buf.extend_from_slice(&10u64.to_le_bytes());
144+
buf.extend_from_slice(&20u64.to_le_bytes());
145+
// minimum_count (u32) = 256
146+
buf.extend_from_slice(&256u32.to_le_bytes());
147+
// channel (u32) = 0
148+
buf.extend_from_slice(&0u32.to_le_bytes());
149+
// remaining_bytes (u32) = 0
150+
buf.extend_from_slice(&0u32.to_le_bytes());
151+
// channel_info_offset (u16) = 0, channel_info_length (u16) = 0
152+
buf.extend_from_slice(&0u16.to_le_bytes());
153+
buf.extend_from_slice(&0u16.to_le_bytes());
154+
buf
155+
};
156+
let (_, req) = SMBReadRequest::smb_from_bytes(&bytes).unwrap();
157+
assert_eq!(req.read_length(), 1024);
158+
assert_eq!(req.read_offset(), 512);
159+
assert_eq!(req.minimum_count(), 256);
160+
assert_eq!(req.file_id().persistent, 10);
161+
assert_eq!(req.file_id().volatile, 20);
162+
}
66163
}

smb/src/server/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ pub trait Server: Send + Sync {
5252
fn shares(&self) -> &HashMap<String, Arc<Self::Share>>;
5353
fn opens(&self) -> &HashMap<u32, Arc<RwLock<Self::Open>>>;
5454
fn add_open(&mut self, open: Arc<RwLock<Self::Open>>) -> impl Future<Output=u32>;
55+
fn remove_open(&mut self, global_id: u32);
5556
fn sessions(&self) -> &HashMap<u64, Arc<RwLock<Self::Session>>>;
5657
fn sessions_mut(&mut self) -> &mut HashMap<u64, Arc<RwLock<Self::Session>>>;
5758
fn guid(&self) -> Uuid;
@@ -198,6 +199,10 @@ impl<Addrs: Send + Sync, Listener: SMBSocket<Addrs>, Auth: AuthProvider, Share:
198199
0
199200
}
200201

202+
fn remove_open(&mut self, global_id: u32) {
203+
self.open_table.remove(&global_id);
204+
}
205+
201206
fn sessions(&self) -> &HashMap<u64, Arc<RwLock<Self::Session>>> {
202207
&self.session_table
203208
}

0 commit comments

Comments
 (0)