use log::{debug, error, info, warn};
use crate::client::connection::{CompoundOp, Connection, Frame};
use crate::error::{DurableLoss, Error, Result};
use crate::msg::create::{
CreateDisposition, CreateRequest, CreateResponse, ImpersonationLevel, ShareAccess,
};
use crate::msg::create_context::{self, DurableGrant, DurableReconnectV2, DurableRequestV2};
use crate::msg::query_info::{InfoType, QueryInfoRequest, QueryInfoResponse};
use crate::pack::{Guid, ReadCursor, Unpack};
use crate::types::flags::FileAccessMask;
use crate::types::status::NtStatus;
use crate::types::{Command, CreditCharge, Dialect, FileId, OplockLevel};
use super::tree::Tree;
const FILE_NON_DIRECTORY_FILE: u32 = 0x0000_0040;
const FILE_ATTRIBUTE_NORMAL: u32 = 0x80;
const FILE_INTERNAL_INFORMATION: u8 = 6;
const FS_VOLUME_INFORMATION: u8 = 1;
const FS_VOLUME_SERIAL_OFFSET: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FileIdentity {
pub volume_serial: u32,
pub index_number: u64,
}
impl FileIdentity {
fn index_query() -> QueryInfoRequest {
QueryInfoRequest {
info_type: InfoType::File,
file_info_class: FILE_INTERNAL_INFORMATION,
output_buffer_length: 8,
additional_information: 0,
flags: 0,
file_id: FileId::SENTINEL,
input_buffer: vec![],
}
}
fn volume_query() -> QueryInfoRequest {
QueryInfoRequest {
info_type: InfoType::Filesystem,
file_info_class: FS_VOLUME_INFORMATION,
output_buffer_length: 128,
additional_information: 0,
flags: 0,
file_id: FileId::SENTINEL,
input_buffer: vec![],
}
}
fn payload(frame: &Frame) -> Option<Vec<u8>> {
if frame.header.status != NtStatus::SUCCESS {
return None;
}
Some(
QueryInfoResponse::unpack(&mut ReadCursor::new(&frame.body))
.ok()?
.output_buffer,
)
}
fn from_frames(index: Option<&Frame>, volume: Option<&Frame>) -> Option<Self> {
let index_number = {
let body = Self::payload(index?)?;
ReadCursor::new(&body).read_u64_le().ok()?
};
let volume_serial = volume
.and_then(Self::payload)
.and_then(|body| {
let slice = body.get(FS_VOLUME_SERIAL_OFFSET..FS_VOLUME_SERIAL_OFFSET + 4)?;
Some(u32::from_le_bytes(slice.try_into().ok()?))
})
.unwrap_or(0);
Some(Self {
volume_serial,
index_number,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DurableHandle {
pub file_id: FileId,
pub grant: DurableGrant,
identity: FileIdentity,
create_guid: Guid,
generation: u64,
}
impl DurableHandle {
pub fn identity(&self) -> FileIdentity {
self.identity
}
pub fn is_current(&self, conn: &Connection) -> bool {
self.generation == conn.generation()
}
}
#[derive(Debug, Clone, Copy)]
pub struct DurableOpen {
pub file_id: FileId,
pub size: u64,
pub durable: Option<DurableHandle>,
}
impl Tree {
pub async fn open_file_durable(
&self,
conn: &mut Connection,
path: &str,
) -> Result<DurableOpen> {
let create_guid = crate::client::connection::random_guid();
let durable_possible = conn.params().is_some_and(|p| p.dialect >= Dialect::Smb3_0);
let contexts = if durable_possible {
create_context::pack_contexts(&[DurableRequestV2 {
timeout_ms: 0,
persistent: false,
create_guid,
}
.context()])
} else {
Vec::new()
};
let create_req = CreateRequest {
requested_oplock_level: if durable_possible {
OplockLevel::Batch
} else {
OplockLevel::None
},
impersonation_level: ImpersonationLevel::Impersonation,
desired_access: FileAccessMask::new(
FileAccessMask::FILE_READ_DATA
| FileAccessMask::FILE_WRITE_DATA
| FileAccessMask::FILE_READ_ATTRIBUTES
| FileAccessMask::FILE_WRITE_ATTRIBUTES
| FileAccessMask::SYNCHRONIZE,
),
file_attributes: FILE_ATTRIBUTE_NORMAL,
share_access: ShareAccess(ShareAccess::FILE_SHARE_READ | ShareAccess::FILE_SHARE_WRITE),
create_disposition: CreateDisposition::FileOpenIf,
create_options: FILE_NON_DIRECTORY_FILE,
name: self.format_path(path),
create_contexts: contexts,
};
let (create, identity) = self.create_and_identify(conn, &create_req).await?;
let resp = CreateResponse::unpack(&mut ReadCursor::new(&create.body))?;
let answered = create_context::parse_contexts(&resp.create_contexts)?;
let grant = create_context::find(&answered, create_context::NAME_DH2Q)
.map(|c| DurableGrant::from_bytes(&c.data))
.transpose()?;
let durable = match (grant, identity) {
(Some(grant), Some(identity)) => {
info!(
"durable: {} opened with a durable handle, server holds it {} ms{}",
path,
grant.timeout_ms,
if grant.persistent {
" (persistent)"
} else {
""
}
);
conn.register_oplock(resp.file_id, self.tree_id);
Some(DurableHandle {
file_id: resp.file_id,
grant,
identity,
create_guid,
generation: conn.generation(),
})
}
(Some(_), None) => {
warn!(
"durable: {path} got a durable handle but the server would not say \
which file it is, so a reclaim could never be proven to be the same \
one; treating the handle as non-resumable"
);
None
}
(None, _) => {
debug!(
"durable: {path} opened without durability (server declined or does \
not support it); an interrupted write will restart"
);
None
}
};
Ok(DurableOpen {
file_id: resp.file_id,
size: resp.end_of_file,
durable,
})
}
pub async fn reclaim_durable_handle(
&self,
conn: &mut Connection,
handle: &DurableHandle,
path: &str,
) -> Result<DurableHandle> {
let lost = |reason| Error::DurableHandleLost {
path: path.to_string(),
reason,
};
let create_req = CreateRequest {
requested_oplock_level: OplockLevel::Batch,
impersonation_level: ImpersonationLevel::Impersonation,
desired_access: FileAccessMask::new(
FileAccessMask::FILE_READ_DATA
| FileAccessMask::FILE_WRITE_DATA
| FileAccessMask::FILE_READ_ATTRIBUTES
| FileAccessMask::FILE_WRITE_ATTRIBUTES
| FileAccessMask::SYNCHRONIZE,
),
file_attributes: FILE_ATTRIBUTE_NORMAL,
share_access: ShareAccess(ShareAccess::FILE_SHARE_READ | ShareAccess::FILE_SHARE_WRITE),
create_disposition: CreateDisposition::FileOpen,
create_options: FILE_NON_DIRECTORY_FILE,
name: self.format_path(path),
create_contexts: create_context::pack_contexts(&[DurableReconnectV2 {
file_id: handle.file_id,
create_guid: handle.create_guid,
persistent: handle.grant.persistent,
}
.context()]),
};
let (create, identity) = match self.create_and_identify(conn, &create_req).await {
Ok(pair) => pair,
Err(Error::Protocol { status, .. }) => {
debug!(
"durable: the server would not give {path} back ({status}); the open \
expired or the server restarted"
);
return Err(lost(DurableLoss::Expired));
}
Err(e) => return Err(e),
};
let resp = CreateResponse::unpack(&mut ReadCursor::new(&create.body))?;
let Some(identity) = identity else {
warn!(
"durable: {path} came back but the server would not say which file it \
is, so nothing about it can be proven; closing it and starting over"
);
let _ = self.close_handle(conn, resp.file_id).await;
return Err(lost(DurableLoss::IdentityUnavailable));
};
if identity != handle.identity {
error!(
"durable: REFUSING a reclaimed handle for {path} -- the server matched \
our CreateGuid but handed back a different file (asked for volume \
{:#x} file {:#x}, got volume {:#x} file {:#x}). Closing it; the \
transfer restarts rather than writing into the wrong file.",
handle.identity.volume_serial,
handle.identity.index_number,
identity.volume_serial,
identity.index_number,
);
let _ = self.close_handle(conn, resp.file_id).await;
return Err(lost(DurableLoss::IdentityMismatch));
}
info!(
"durable: {path} reclaimed on the new session; the transfer resumes rather \
than restarting"
);
conn.register_oplock(resp.file_id, self.tree_id);
Ok(DurableHandle {
file_id: resp.file_id,
generation: conn.generation(),
..*handle
})
}
async fn create_and_identify(
&self,
conn: &mut Connection,
create_req: &CreateRequest,
) -> Result<(Frame, Option<FileIdentity>)> {
let index_req = FileIdentity::index_query();
let volume_req = FileIdentity::volume_query();
let results = conn
.execute_compound(&[
CompoundOp {
command: Command::Create,
body: create_req,
tree_id: Some(self.tree_id),
credit_charge: CreditCharge(1),
},
CompoundOp {
command: Command::QueryInfo,
body: &index_req,
tree_id: Some(self.tree_id),
credit_charge: CreditCharge(1),
},
CompoundOp {
command: Command::QueryInfo,
body: &volume_req,
tree_id: Some(self.tree_id),
credit_charge: CreditCharge(1),
},
])
.await?;
let frames: Vec<Option<Frame>> = results.into_iter().map(|r| r.ok()).collect();
let create = frames
.first()
.and_then(|f| f.as_ref())
.ok_or_else(|| Error::invalid_data("compound CREATE produced no response"))?;
if create.header.status != NtStatus::SUCCESS {
return Err(Error::Protocol {
status: create.header.status,
command: Command::Create,
});
}
let identity = FileIdentity::from_frames(
frames.get(1).and_then(|f| f.as_ref()),
frames.get(2).and_then(|f| f.as_ref()),
);
let create = frames.into_iter().next().flatten().expect("checked above");
Ok((create, identity))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::client::test_helpers::{
build_close_response, build_compound_response_frame, build_create_error_response,
build_create_response_with_contexts, build_query_info_error_response,
build_query_info_response, setup_connection,
};
use crate::msg::create_context::CreateContext;
use crate::transport::MockTransport;
use crate::types::TreeId;
use std::sync::Arc;
const OURS: FileIdentity = FileIdentity {
volume_serial: 0xABCD,
index_number: 0x1234_5678,
};
const SOMEONE_ELSES: FileIdentity = FileIdentity {
volume_serial: 0xABCD,
index_number: 0x9999_9999,
};
const ANOTHER_VOLUME: FileIdentity = FileIdentity {
volume_serial: 0x1234,
index_number: 0x1234_5678,
};
pub(super) fn a_share() -> Tree {
Tree {
tree_id: TreeId(20),
share_name: "test".to_string(),
server: "test-server".to_string(),
is_dfs: false,
encrypt_data: false,
}
}
fn smb3(conn: &mut Connection) {
let mut params = conn.params().unwrap();
params.dialect = Dialect::Smb3_1_1;
conn.set_test_params(params);
}
fn index_body(id: FileIdentity) -> Vec<u8> {
id.index_number.to_le_bytes().to_vec()
}
fn volume_body(id: FileIdentity) -> Vec<u8> {
let mut out = vec![0u8; FS_VOLUME_SERIAL_OFFSET];
out.extend_from_slice(&id.volume_serial.to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&[0, 0]); out
}
fn granted(timeout_ms: u32) -> CreateContext {
let mut body = Vec::new();
body.extend_from_slice(&timeout_ms.to_le_bytes());
body.extend_from_slice(&0u32.to_le_bytes());
CreateContext::new(create_context::NAME_DH2Q, body)
}
fn a_file_id(n: u64) -> FileId {
FileId {
persistent: n,
volatile: n,
}
}
fn answer(
file_id: FileId,
contexts: &[CreateContext],
identity: Option<FileIdentity>,
) -> Vec<u8> {
answer_with(file_id, contexts, identity, true)
}
fn answer_with(
file_id: FileId,
contexts: &[CreateContext],
identity: Option<FileIdentity>,
with_volume: bool,
) -> Vec<u8> {
let (index, volume) = match identity {
Some(id) => (
build_query_info_response(index_body(id)),
if with_volume {
build_query_info_response(volume_body(id))
} else {
build_query_info_error_response(NtStatus::NOT_SUPPORTED)
},
),
None => (
build_query_info_error_response(NtStatus::NOT_SUPPORTED),
build_query_info_error_response(NtStatus::NOT_SUPPORTED),
),
};
build_compound_response_frame(&[
build_create_response_with_contexts(file_id, 0, contexts),
index,
volume,
])
}
fn sent_contexts(mock: &MockTransport, n: usize) -> Vec<CreateContext> {
let sent = mock.sent_message(n).unwrap();
let mut cursor = ReadCursor::new(&sent);
let _header = crate::msg::header::Header::unpack(&mut cursor).unwrap();
let req = CreateRequest::unpack(&mut cursor).unwrap();
create_context::parse_contexts(&req.create_contexts).unwrap()
}
fn sent_oplock(mock: &MockTransport, n: usize) -> OplockLevel {
let sent = mock.sent_message(n).unwrap();
let mut cursor = ReadCursor::new(&sent);
let _header = crate::msg::header::Header::unpack(&mut cursor).unwrap();
CreateRequest::unpack(&mut cursor)
.unwrap()
.requested_oplock_level
}
pub(super) fn durable_answer(file_id: FileId) -> Vec<u8> {
answer(file_id, &[granted(180_000)], Some(OURS))
}
async fn open_durably(mock: &Arc<MockTransport>, conn: &mut Connection) -> DurableOpen {
mock.queue_response(answer(a_file_id(1), &[granted(180_000)], Some(OURS)));
a_share()
.open_file_durable(conn, "big.iso")
.await
.expect("the open must succeed")
}
#[tokio::test]
async fn an_open_the_server_backs_with_both_proofs_is_resumable() {
let mock = Arc::new(MockTransport::new());
let mut conn = setup_connection(&mock);
smb3(&mut conn);
let open = open_durably(&mock, &mut conn).await;
let durable = open.durable.expect("both proofs were answered");
assert_eq!(durable.file_id, a_file_id(1));
assert_eq!(durable.grant.timeout_ms, 180_000);
assert_eq!(durable.identity(), OURS);
assert!(durable.is_current(&conn));
assert_eq!(
sent_oplock(&mock, 0),
OplockLevel::Batch,
"without a batch oplock the server ignores the durable request \
entirely (MS-SMB2 3.3.5.9.10)"
);
assert!(
create_context::find(&sent_contexts(&mock, 0), create_context::NAME_DH2Q).is_some()
);
}
#[tokio::test]
async fn an_open_the_server_declines_durability_on_still_returns_a_working_handle() {
let mock = Arc::new(MockTransport::new());
let mut conn = setup_connection(&mock);
smb3(&mut conn);
mock.queue_response(answer(a_file_id(1), &[], Some(OURS)));
let open = a_share()
.open_file_durable(&mut conn, "big.iso")
.await
.unwrap();
assert_eq!(open.file_id, a_file_id(1));
assert!(
open.durable.is_none(),
"no grant means no resume, and that must not be an error"
);
}
#[tokio::test]
async fn a_grant_the_server_will_not_back_with_an_identity_is_not_resumable() {
let mock = Arc::new(MockTransport::new());
let mut conn = setup_connection(&mock);
smb3(&mut conn);
mock.queue_response(answer(a_file_id(1), &[granted(180_000)], None));
let open = a_share()
.open_file_durable(&mut conn, "big.iso")
.await
.unwrap();
assert!(open.durable.is_none());
}
#[tokio::test]
async fn a_pre_smb3_server_is_never_asked_for_a_durable_handle() {
let mock = Arc::new(MockTransport::new());
let mut conn = setup_connection(&mock); mock.queue_response(answer(a_file_id(1), &[], Some(OURS)));
let open = a_share()
.open_file_durable(&mut conn, "big.iso")
.await
.unwrap();
assert!(open.durable.is_none());
assert_eq!(sent_oplock(&mock, 0), OplockLevel::None);
assert!(
create_context::find(&sent_contexts(&mock, 0), create_context::NAME_DH2Q).is_none()
);
}
#[tokio::test]
async fn a_reclaim_of_the_same_file_succeeds_and_carries_the_new_handle() {
let mock = Arc::new(MockTransport::new());
let mut conn = setup_connection(&mock);
smb3(&mut conn);
let durable = open_durably(&mock, &mut conn).await.durable.unwrap();
mock.queue_response(answer(a_file_id(2), &[], Some(OURS)));
let reclaimed = a_share()
.reclaim_durable_handle(&mut conn, &durable, "big.iso")
.await
.expect("same file, both proofs held");
assert_eq!(
reclaimed.file_id,
a_file_id(2),
"the caller must write through the new handle, not the dead one"
);
assert_eq!(reclaimed.identity(), OURS);
}
#[tokio::test]
async fn the_reconnect_context_replays_the_create_guid_and_travels_alone() {
let mock = Arc::new(MockTransport::new());
let mut conn = setup_connection(&mock);
smb3(&mut conn);
let durable = open_durably(&mock, &mut conn).await.durable.unwrap();
let opened = sent_contexts(&mock, 0);
let dh2q = create_context::find(&opened, create_context::NAME_DH2Q).unwrap();
let guid_at_open = dh2q.data[16..32].to_vec();
mock.queue_response(answer(a_file_id(2), &[], Some(OURS)));
a_share()
.reclaim_durable_handle(&mut conn, &durable, "big.iso")
.await
.unwrap();
let reclaimed = sent_contexts(&mock, 1);
assert_eq!(
reclaimed.len(),
1,
"the reconnect context must be the only one, or servers reject the \
reclaim outright: {reclaimed:?}"
);
let dh2c = create_context::find(&reclaimed, create_context::NAME_DH2C)
.expect("the reclaim must carry a DH2C context");
assert_eq!(
dh2c.data[16..32],
guid_at_open[..],
"the guid the server stored with the open is what proves the \
handle is ours"
);
assert_eq!(
dh2c.data[0..8],
durable.file_id.persistent.to_le_bytes()[..],
"and the old FileId is what it looks up"
);
}
#[tokio::test]
async fn a_reclaim_that_comes_back_as_a_different_file_is_refused_and_closed() {
let mock = Arc::new(MockTransport::new());
let mut conn = setup_connection(&mock);
smb3(&mut conn);
let durable = open_durably(&mock, &mut conn).await.durable.unwrap();
mock.queue_response(answer(a_file_id(2), &[], Some(SOMEONE_ELSES)));
mock.queue_response(build_close_response());
let outcome = a_share()
.reclaim_durable_handle(&mut conn, &durable, "big.iso")
.await;
assert!(
matches!(
outcome,
Err(Error::DurableHandleLost {
reason: DurableLoss::IdentityMismatch,
..
})
),
"expected a refusal naming the mismatch, got {outcome:?}"
);
assert_eq!(
mock.sent_count(),
3,
"the handle we refused must be closed, not leaked on the server: \
open, reclaim, close"
);
let closed = mock.sent_message(2).unwrap();
let mut cursor = ReadCursor::new(&closed);
let header = crate::msg::header::Header::unpack(&mut cursor).unwrap();
assert_eq!(header.command, Command::Close);
}
#[tokio::test]
async fn a_reclaim_with_no_identity_is_refused_and_closed() {
let mock = Arc::new(MockTransport::new());
let mut conn = setup_connection(&mock);
smb3(&mut conn);
let durable = open_durably(&mock, &mut conn).await.durable.unwrap();
mock.queue_response(answer(a_file_id(2), &[], None));
mock.queue_response(build_close_response());
let outcome = a_share()
.reclaim_durable_handle(&mut conn, &durable, "big.iso")
.await;
assert!(
matches!(
outcome,
Err(Error::DurableHandleLost {
reason: DurableLoss::IdentityUnavailable,
..
})
),
"got {outcome:?}"
);
assert_eq!(mock.sent_count(), 3, "the unprovable handle was closed");
}
#[tokio::test]
async fn a_reclaim_the_server_rejects_reports_the_open_as_expired() {
let mock = Arc::new(MockTransport::new());
let mut conn = setup_connection(&mock);
smb3(&mut conn);
let durable = open_durably(&mock, &mut conn).await.durable.unwrap();
mock.queue_response(build_compound_response_frame(&[
build_create_error_response(NtStatus::OBJECT_NAME_NOT_FOUND),
build_query_info_error_response(NtStatus::OBJECT_NAME_NOT_FOUND),
build_query_info_error_response(NtStatus::OBJECT_NAME_NOT_FOUND),
]));
let outcome = a_share()
.reclaim_durable_handle(&mut conn, &durable, "big.iso")
.await;
assert!(
matches!(
outcome,
Err(Error::DurableHandleLost {
reason: DurableLoss::Expired,
..
})
),
"got {outcome:?}"
);
assert_eq!(
mock.sent_count(),
2,
"nothing was opened, so there is nothing to close"
);
}
#[tokio::test]
async fn a_handle_knows_it_has_outlived_its_session() {
let mock = Arc::new(MockTransport::new());
let mut conn = setup_connection(&mock);
smb3(&mut conn);
let durable = open_durably(&mock, &mut conn).await.durable.unwrap();
assert!(durable.is_current(&conn), "generation 0 is what conn is on");
let after_a_reconnect = DurableHandle {
generation: 1,
..durable
};
assert!(!after_a_reconnect.is_current(&conn));
}
#[tokio::test]
async fn a_reclaim_on_a_different_volume_is_refused_even_at_the_same_index() {
let mock = Arc::new(MockTransport::new());
let mut conn = setup_connection(&mock);
smb3(&mut conn);
let durable = open_durably(&mock, &mut conn).await.durable.unwrap();
mock.queue_response(answer(a_file_id(2), &[], Some(ANOTHER_VOLUME)));
mock.queue_response(build_close_response());
let outcome = a_share()
.reclaim_durable_handle(&mut conn, &durable, "big.iso")
.await;
assert!(
matches!(
outcome,
Err(Error::DurableHandleLost {
reason: DurableLoss::IdentityMismatch,
..
})
),
"got {outcome:?}"
);
}
#[tokio::test]
async fn a_server_that_will_not_name_the_volume_still_gets_a_resume() {
let mock = Arc::new(MockTransport::new());
let mut conn = setup_connection(&mock);
smb3(&mut conn);
mock.queue_response(answer_with(
a_file_id(1),
&[granted(180_000)],
Some(OURS),
false,
));
let durable = a_share()
.open_file_durable(&mut conn, "big.iso")
.await
.unwrap()
.durable
.expect("the index number alone is enough to identify the file");
assert_eq!(durable.identity().volume_serial, 0, "not answered");
mock.queue_response(answer_with(a_file_id(2), &[], Some(OURS), false));
a_share()
.reclaim_durable_handle(&mut conn, &durable, "big.iso")
.await
.expect("same index number, same file");
}
#[tokio::test]
async fn a_volume_that_goes_missing_between_open_and_reclaim_is_refused() {
let mock = Arc::new(MockTransport::new());
let mut conn = setup_connection(&mock);
smb3(&mut conn);
let durable = open_durably(&mock, &mut conn).await.durable.unwrap();
assert_eq!(durable.identity().volume_serial, OURS.volume_serial);
mock.queue_response(answer_with(a_file_id(2), &[], Some(OURS), false));
mock.queue_response(build_close_response());
let outcome = a_share()
.reclaim_durable_handle(&mut conn, &durable, "big.iso")
.await;
assert!(
matches!(
outcome,
Err(Error::DurableHandleLost {
reason: DurableLoss::IdentityMismatch,
..
})
),
"got {outcome:?}"
);
}
}
#[cfg(test)]
mod oplock_break_tests {
use super::tests::*;
use super::*;
use crate::client::connection::{pack_message, NegotiatedParams};
use crate::msg::header::Header;
use crate::msg::oplock_break::OplockBreak;
use crate::pack::Unpack;
use crate::transport::MockTransport;
use crate::types::flags::Capabilities;
use crate::types::{MessageId, SessionId, TreeId};
use std::sync::Arc;
use std::time::{Duration, Instant};
const THE_TREE: TreeId = TreeId(20);
fn plain_connection(mock: &Arc<MockTransport>) -> Connection {
let mut conn =
Connection::from_transport(Box::new(mock.clone()), Box::new(mock.clone()), "test");
conn.set_test_params(NegotiatedParams {
dialect: Dialect::Smb3_1_1,
max_read_size: 65536,
max_write_size: 65536,
max_transact_size: 65536,
server_guid: Guid::ZERO,
signing_required: false,
capabilities: Capabilities::default(),
gmac_negotiated: false,
cipher: None,
compression_supported: false,
});
conn.set_session_id(SessionId(1));
conn.set_credits(512);
conn
}
fn with_msg_ids(mut frame: Vec<u8>, first: u64) -> Vec<u8> {
let mut offset = 0usize;
let mut id = first;
loop {
frame[offset + 24..offset + 32].copy_from_slice(&id.to_le_bytes());
let next =
u32::from_le_bytes(frame[offset + 20..offset + 24].try_into().unwrap()) as usize;
if next == 0 {
return frame;
}
offset += next;
id += 1;
}
}
fn a_break(file_id: FileId) -> Vec<u8> {
let mut h = Header::new_request(Command::OplockBreak);
h.flags.set_response();
h.message_id = MessageId::UNSOLICITED;
h.credits = 1;
pack_message(
&h,
&OplockBreak {
oplock_level: OplockLevel::LevelII,
file_id,
},
)
}
async fn wait_for(what: &str, mut cond: impl FnMut() -> bool) {
let deadline = Instant::now() + Duration::from_secs(10);
while !cond() {
assert!(Instant::now() < deadline, "timed out waiting for {what}");
tokio::time::sleep(Duration::from_millis(2)).await;
}
}
#[tokio::test]
async fn an_oplock_break_is_acknowledged_so_the_other_client_is_not_left_waiting() {
let mock = Arc::new(MockTransport::new());
let mut conn = plain_connection(&mock);
let handle_id = FileId {
persistent: 5,
volatile: 5,
};
mock.queue_response(with_msg_ids(durable_answer(handle_id), 0));
let open = a_share()
.open_file_durable(&mut conn, "big.iso")
.await
.unwrap();
assert!(open.durable.is_some());
let sent_after_open = mock.sent_count();
mock.queue_response(a_break(handle_id));
wait_for("the acknowledgment to be sent", || {
mock.sent_count() > sent_after_open
})
.await;
let sent = mock.sent_message(sent_after_open).unwrap();
let mut cursor = ReadCursor::new(&sent);
let header = Header::unpack(&mut cursor).unwrap();
assert_eq!(header.command, Command::OplockBreak);
assert_eq!(
header.tree_id,
Some(THE_TREE),
"the acknowledgment has to name the tree the open belongs to \
(MS-SMB2 2.2.24.1); on the wrong one the server rejects it and the \
other client waits anyway"
);
let ack = OplockBreak::unpack(&mut cursor).unwrap();
assert_eq!(ack.file_id, handle_id);
assert_eq!(
ack.oplock_level,
OplockLevel::None,
"we only ever took the oplock to get durability, and durability is \
already gone by the time a break arrives"
);
}
#[tokio::test]
async fn a_break_for_a_handle_we_never_oplocked_is_left_alone() {
let mock = Arc::new(MockTransport::new());
let conn = plain_connection(&mock);
mock.queue_response(a_break(FileId {
persistent: 999,
volatile: 999,
}));
tokio::time::sleep(Duration::from_millis(150)).await;
assert_eq!(mock.sent_count(), 0, "nothing should have been sent");
assert_eq!(conn.metrics().unsolicited_notifications_received, 1);
}
#[tokio::test]
async fn closing_a_handle_retires_its_oplock_bookkeeping() {
let mock = Arc::new(MockTransport::new());
let mut conn = plain_connection(&mock);
let handle_id = FileId {
persistent: 5,
volatile: 5,
};
mock.queue_response(with_msg_ids(durable_answer(handle_id), 0));
a_share()
.open_file_durable(&mut conn, "big.iso")
.await
.unwrap();
conn.set_next_message_id(0);
mock.queue_response(with_msg_ids(
crate::client::test_helpers::build_close_response(),
0,
));
a_share().close_handle(&mut conn, handle_id).await.unwrap();
let sent_after_close = mock.sent_count();
mock.queue_response(a_break(handle_id));
tokio::time::sleep(Duration::from_millis(150)).await;
assert_eq!(
mock.sent_count(),
sent_after_close,
"a break for a closed handle must not produce an acknowledgment"
);
}
}