Compare commits
5 Commits
bb796ec6b9
...
5300b672cb
| Author | SHA1 | Date | |
|---|---|---|---|
| 5300b672cb | |||
| 637227f4e4 | |||
| d4f60929fa | |||
| e7863a3034 | |||
| 8ef1406ed3 |
@@ -3,7 +3,7 @@ use super::util;
|
||||
use super::{VfsAce, VfsAceFlag, VfsAceMask, VfsAceType, VfsAcl, VfsBackend, VfsDirEntry, VfsError, VfsFile, VfsPreviousVersion, VfsQuota, VfsQuotaUsage, VfsSnapshotInfo, VfsStat};
|
||||
use std::fs::{self, File, OpenOptions};
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||
use std::os::unix::fs::{FileExt, MetadataExt, PermissionsExt};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::SystemTime;
|
||||
|
||||
@@ -42,6 +42,14 @@ impl VfsFile for LocalFile {
|
||||
self.file.seek(pos).map_err(|e| VfsError::Io(e.to_string()))
|
||||
}
|
||||
|
||||
fn read_at(&mut self, buf: &mut [u8], offset: u64) -> Result<usize, VfsError> {
|
||||
self.file.read_at(buf, offset).map_err(|e| VfsError::Io(e.to_string()))
|
||||
}
|
||||
|
||||
fn write_at(&mut self, buf: &[u8], offset: u64) -> Result<usize, VfsError> {
|
||||
self.file.write_at(buf, offset).map_err(|e| VfsError::Io(e.to_string()))
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), VfsError> {
|
||||
self.file.flush().map_err(|e| VfsError::Io(e.to_string()))
|
||||
}
|
||||
|
||||
@@ -103,6 +103,20 @@ pub trait VfsFile: Send {
|
||||
fn stat(&mut self) -> Result<VfsStat, VfsError>;
|
||||
fn set_len(&mut self, size: u64) -> Result<(), VfsError>;
|
||||
|
||||
/// Read at `offset` without changing the seek position (like pread).
|
||||
/// Default implementation does seek + read.
|
||||
fn read_at(&mut self, buf: &mut [u8], offset: u64) -> Result<usize, VfsError> {
|
||||
self.seek(std::io::SeekFrom::Start(offset))?;
|
||||
self.read(buf)
|
||||
}
|
||||
|
||||
/// Write at `offset` without changing the seek position (like pwrite).
|
||||
/// Default implementation does seek + write.
|
||||
fn write_at(&mut self, buf: &[u8], offset: u64) -> Result<usize, VfsError> {
|
||||
self.seek(std::io::SeekFrom::Start(offset))?;
|
||||
self.write(buf)
|
||||
}
|
||||
|
||||
/// Write all bytes (convenience, default loops write() until done)
|
||||
fn write_all(&mut self, mut buf: &[u8]) -> Result<(), VfsError> {
|
||||
while !buf.is_empty() {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::time::SystemTime;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bytes::Bytes;
|
||||
@@ -160,7 +160,10 @@ impl ShareBackend for VfsShareBackend {
|
||||
|
||||
let file = self.vfs.open_file(&full_path, &flags).map_err(map_error)?;
|
||||
Ok(Box::new(VfsHandle::File {
|
||||
file: Mutex::new(file),
|
||||
inner: Mutex::new(FileAndBuf {
|
||||
file,
|
||||
read_buf: Vec::new(),
|
||||
}),
|
||||
path: full_path,
|
||||
vfs: self.vfs.clone(),
|
||||
}))
|
||||
@@ -194,9 +197,14 @@ impl ShareBackend for VfsShareBackend {
|
||||
}
|
||||
}
|
||||
|
||||
struct FileAndBuf {
|
||||
file: Box<dyn super::VfsFile + Send>,
|
||||
read_buf: Vec<u8>,
|
||||
}
|
||||
|
||||
enum VfsHandle {
|
||||
File {
|
||||
file: Mutex<Box<dyn super::VfsFile + Send>>,
|
||||
inner: Mutex<FileAndBuf>,
|
||||
path: PathBuf,
|
||||
vfs: Arc<dyn VfsBackend>,
|
||||
},
|
||||
@@ -210,14 +218,19 @@ enum VfsHandle {
|
||||
impl Handle for VfsHandle {
|
||||
async fn read(&self, offset: u64, len: u32) -> Result<Bytes, SmbError> {
|
||||
match self {
|
||||
Self::File { file, .. } => {
|
||||
let mut file = file.lock().unwrap();
|
||||
file.seek(std::io::SeekFrom::Start(offset))
|
||||
.map_err(vfs_error_to_io)?;
|
||||
let mut buf = vec![0u8; len as usize];
|
||||
let n = file.read(&mut buf).map_err(map_error)?;
|
||||
Self::File { inner, .. } => {
|
||||
let mut guard = inner.lock().await;
|
||||
let fb = &mut *guard;
|
||||
// Reuse read_buf to avoid per-read allocation
|
||||
let buf = &mut fb.read_buf;
|
||||
buf.clear();
|
||||
if buf.capacity() < len as usize {
|
||||
buf.reserve(len as usize - buf.capacity());
|
||||
}
|
||||
unsafe { buf.set_len(len as usize); }
|
||||
let n = fb.file.read_at(buf, offset).map_err(map_error)?;
|
||||
buf.truncate(n);
|
||||
Ok(Bytes::from(buf))
|
||||
Ok(Bytes::from(std::mem::take(buf)))
|
||||
}
|
||||
Self::Directory { .. } => Err(SmbError::NotSupported),
|
||||
}
|
||||
@@ -225,11 +238,9 @@ impl Handle for VfsHandle {
|
||||
|
||||
async fn write(&self, offset: u64, data: &[u8]) -> Result<u32, SmbError> {
|
||||
match self {
|
||||
Self::File { file, .. } => {
|
||||
let mut file = file.lock().unwrap();
|
||||
file.seek(std::io::SeekFrom::Start(offset))
|
||||
.map_err(vfs_error_to_io)?;
|
||||
let n = file.write(data).map_err(map_error)?;
|
||||
Self::File { inner, .. } => {
|
||||
let mut guard = inner.lock().await;
|
||||
let n = guard.file.write_at(data, offset).map_err(map_error)?;
|
||||
Ok(n as u32)
|
||||
}
|
||||
Self::Directory { .. } => Err(SmbError::NotSupported),
|
||||
@@ -238,9 +249,9 @@ impl Handle for VfsHandle {
|
||||
|
||||
async fn flush(&self) -> Result<(), SmbError> {
|
||||
match self {
|
||||
Self::File { file, .. } => {
|
||||
let mut file = file.lock().unwrap();
|
||||
file.flush().map_err(map_error)
|
||||
Self::File { inner, .. } => {
|
||||
let mut guard = inner.lock().await;
|
||||
guard.file.flush().map_err(map_error)
|
||||
}
|
||||
Self::Directory { .. } => Ok(()),
|
||||
}
|
||||
@@ -248,9 +259,9 @@ impl Handle for VfsHandle {
|
||||
|
||||
async fn stat(&self) -> Result<FileInfo, SmbError> {
|
||||
match self {
|
||||
Self::File { file, path, .. } => {
|
||||
let mut f = file.lock().unwrap();
|
||||
let vfs_stat = f.stat().map_err(map_error)?;
|
||||
Self::File { inner, path, .. } => {
|
||||
let mut guard = inner.lock().await;
|
||||
let vfs_stat = guard.file.stat().map_err(map_error)?;
|
||||
Ok(vfs_stat_to_file_info(&vfs_stat, "", path))
|
||||
}
|
||||
Self::Directory { vfs, path } => {
|
||||
@@ -277,26 +288,39 @@ impl Handle for VfsHandle {
|
||||
|
||||
async fn truncate(&self, len: u64) -> Result<(), SmbError> {
|
||||
match self {
|
||||
Self::File { file, .. } => {
|
||||
let mut file = file.lock().unwrap();
|
||||
file.set_len(len).map_err(map_error)
|
||||
Self::File { inner, .. } => {
|
||||
let mut guard = inner.lock().await;
|
||||
guard.file.set_len(len).map_err(map_error)
|
||||
}
|
||||
Self::Directory { .. } => Err(SmbError::NotSupported),
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_dir(&self, _pattern: Option<&str>) -> Result<Vec<DirEntry>, SmbError> {
|
||||
async fn list_dir(&self, pattern: Option<&str>) -> Result<Vec<DirEntry>, SmbError> {
|
||||
match self {
|
||||
Self::File { .. } => Err(SmbError::NotADirectory),
|
||||
Self::Directory { vfs, path } => {
|
||||
let entries = vfs.read_dir(path).map_err(map_error)?;
|
||||
let result = entries
|
||||
let mut result: Vec<DirEntry> = entries
|
||||
.into_iter()
|
||||
.filter(|entry| {
|
||||
let p = match pattern {
|
||||
None => return true,
|
||||
Some(p) => p,
|
||||
};
|
||||
if p == "*" || p == "*.*" || p.is_empty() {
|
||||
return true;
|
||||
}
|
||||
smb_match(&entry.name, p)
|
||||
})
|
||||
.map(|entry| {
|
||||
let info = vfs_stat_to_file_info(&entry.stat, &entry.name, path);
|
||||
DirEntry { info }
|
||||
})
|
||||
.collect();
|
||||
for (i, entry) in result.iter_mut().enumerate() {
|
||||
entry.info.file_index = (i + 1) as u64;
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -307,6 +331,36 @@ impl Handle for VfsHandle {
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple SMB wildcard match: `*` matches any sequence, `?` matches one char.
|
||||
fn smb_match(name: &str, pattern: &str) -> bool {
|
||||
let name = name.as_bytes();
|
||||
let pat = pattern.as_bytes();
|
||||
let mut ni = 0;
|
||||
let mut pi = 0;
|
||||
let mut star_idx: Option<usize> = None;
|
||||
let mut match_idx = 0;
|
||||
while ni < name.len() {
|
||||
if pi < pat.len() && (pat[pi] == b'?' || pat[pi] == name[ni]) {
|
||||
ni += 1;
|
||||
pi += 1;
|
||||
} else if pi < pat.len() && pat[pi] == b'*' {
|
||||
star_idx = Some(pi);
|
||||
match_idx = ni;
|
||||
pi += 1;
|
||||
} else if let Some(si) = star_idx {
|
||||
pi = si + 1;
|
||||
match_idx += 1;
|
||||
ni = match_idx;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
while pi < pat.len() && pat[pi] == b'*' {
|
||||
pi += 1;
|
||||
}
|
||||
pi == pat.len()
|
||||
}
|
||||
|
||||
fn filetime_to_systemtime(ft: u64) -> SystemTime {
|
||||
if ft < FILETIME_OFFSET {
|
||||
return SystemTime::UNIX_EPOCH;
|
||||
|
||||
Vendored
+3
-3
@@ -207,8 +207,8 @@ pub trait Handle: Send + Sync {
|
||||
/// Write `data` at `offset`. Returns bytes written.
|
||||
async fn write(&self, offset: u64, data: &[u8]) -> SmbResult<u32>;
|
||||
|
||||
/// Write owned `data` at `offset`. Backends that need ownership across a
|
||||
/// blocking boundary can override this to avoid an extra copy.
|
||||
/// Write owned `data` at `offset`. Backends needing ownership across a
|
||||
/// blocking boundary should override to avoid an extra copy.
|
||||
async fn write_owned(&self, offset: u64, data: Vec<u8>) -> SmbResult<u32> {
|
||||
self.write(offset, &data).await
|
||||
}
|
||||
@@ -253,7 +253,7 @@ impl ShareBackend for NotSupportedBackend {
|
||||
}
|
||||
fn capabilities(&self) -> BackendCapabilities {
|
||||
BackendCapabilities {
|
||||
is_read_only: true,
|
||||
is_read_only: false, // IPC$ share 允许 named pipe communication
|
||||
case_sensitive: false,
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+324
@@ -440,6 +440,13 @@ async fn dispatch_one(
|
||||
|
||||
let resp = handlers::dispatch_command(server, conn, &req_hdr, body_bytes).await;
|
||||
|
||||
debug!(
|
||||
command = ?req_hdr.command,
|
||||
status = resp.status,
|
||||
body_len = resp.body.len(),
|
||||
"SMB2 handler response"
|
||||
);
|
||||
|
||||
// If the handler asked for a preauth snapshot (3.1.1), take it now.
|
||||
if let Some(sid) = resp.take_preauth_snapshot_for_session {
|
||||
let snap = session_preauth
|
||||
@@ -574,6 +581,8 @@ async fn build_response_bytes(
|
||||
hdr.session_id = sid;
|
||||
}
|
||||
hdr.signature = [0u8; 16];
|
||||
// Grant at least 1 credit so clients (e.g. Samba smbclient) can proceed.
|
||||
hdr.credit_request_response = hdr.credit_request_response.max(1);
|
||||
|
||||
let request_was_signed = req_hdr.flags & SMB2_FLAGS_SIGNED != 0;
|
||||
// MS-SMB2 §3.3.5.5.3 step 12: SessionSetup SUCCESS must be signed for
|
||||
@@ -699,6 +708,10 @@ async fn handle_smb1_multi_protocol(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::conn::state::{Session, TreeConnect};
|
||||
use crate::proto::messages::create::{CreateResponse, FileId};
|
||||
use crate::proto::header::SMB2_MAGIC;
|
||||
use crate::Share;
|
||||
use uuid::Uuid;
|
||||
|
||||
fn test_conn() -> Arc<Connection> {
|
||||
@@ -752,4 +765,315 @@ mod tests {
|
||||
assert_eq!(got.snapshot(), expected);
|
||||
assert!(!conn.session_preauth.read().await.contains_key(&7));
|
||||
}
|
||||
|
||||
// ── Compound request response stitching ─────────────────────────────────
|
||||
|
||||
/// Build a minimal response frame of `body_len` bytes (64 header + body).
|
||||
fn make_response(body_len: usize) -> Vec<u8> {
|
||||
let mut buf = vec![0u8; SMB2_HEADER_LEN + body_len];
|
||||
buf[..4].copy_from_slice(&SMB2_MAGIC);
|
||||
buf
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stitch_responses_single() {
|
||||
let conn = test_conn();
|
||||
let r1 = make_response(100);
|
||||
let responses = vec![r1.clone()];
|
||||
let stitched = stitch_responses(&conn, responses).await;
|
||||
// Single response: no padding, NextCommand=0
|
||||
assert_eq!(stitched.len(), 100 + SMB2_HEADER_LEN);
|
||||
assert_eq!(&stitched[..4], &SMB2_MAGIC);
|
||||
// NextCommand at offset 20 should be 0
|
||||
let next = u32::from_le_bytes(stitched[20..24].try_into().unwrap());
|
||||
assert_eq!(next, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stitch_responses_aligned() {
|
||||
let conn = test_conn();
|
||||
// Two responses: 100 bytes + 80 bytes body
|
||||
let r1 = make_response(100);
|
||||
let r2 = make_response(80);
|
||||
let responses = vec![r1, r2];
|
||||
let stitched = stitch_responses(&conn, responses).await;
|
||||
|
||||
// First response: 64+100 = 164, aligned to 168 (next multiple of 8)
|
||||
let total1 = SMB2_HEADER_LEN + 100;
|
||||
let aligned1 = (total1 + 7) & !7;
|
||||
assert_eq!(aligned1, 168);
|
||||
|
||||
// First response's NextCommand should point to second
|
||||
let next1 = u32::from_le_bytes(stitched[20..24].try_into().unwrap());
|
||||
assert_eq!(next1 as usize, aligned1);
|
||||
|
||||
// Second response body starts at offset `aligned1`
|
||||
let next2 = u32::from_le_bytes(stitched[aligned1 + 20..aligned1 + 24].try_into().unwrap());
|
||||
assert_eq!(next2, 0); // Last response, no NextCommand
|
||||
|
||||
// Total length = aligned1 + 64(body header) + 80(body)
|
||||
assert_eq!(stitched.len(), aligned1 + SMB2_HEADER_LEN + 80);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stitch_responses_three_responses() {
|
||||
let conn = test_conn();
|
||||
let r1 = make_response(100);
|
||||
let r2 = make_response(80);
|
||||
let r3 = make_response(60);
|
||||
let responses = vec![r1, r2, r3];
|
||||
let stitched = stitch_responses(&conn, responses).await;
|
||||
|
||||
// First header at 0
|
||||
let total1 = SMB2_HEADER_LEN + 100;
|
||||
let aligned1 = (total1 + 7) & !7;
|
||||
let next1 = u32::from_le_bytes(stitched[20..24].try_into().unwrap());
|
||||
assert_eq!(next1 as usize, aligned1);
|
||||
|
||||
// Second header at aligned1
|
||||
let total2 = SMB2_HEADER_LEN + 80;
|
||||
let aligned2 = aligned1 + ((total2 + 7) & !7);
|
||||
let next2 = u32::from_le_bytes(stitched[aligned1 + 20..aligned1 + 24].try_into().unwrap());
|
||||
assert_eq!(next2 as usize, aligned2 - aligned1);
|
||||
|
||||
// Third header at aligned1 + aligned2_inner
|
||||
let next3 = u32::from_le_bytes(stitched[aligned2 + 20..aligned2 + 24].try_into().unwrap());
|
||||
assert_eq!(next3, 0); // Last
|
||||
|
||||
assert_eq!(stitched.len(), aligned2 + SMB2_HEADER_LEN + 60);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stitch_responses_empty_returns_empty() {
|
||||
let conn = test_conn();
|
||||
let stitched = stitch_responses(&conn, vec![]).await;
|
||||
assert!(stitched.is_empty());
|
||||
}
|
||||
|
||||
// ── FileId capture from CREATE response ─────────────────────────────────
|
||||
|
||||
/// Build CREATE response bytes with known FileId.
|
||||
fn make_create_response(persistent: u64, volatile: u64) -> Vec<u8> {
|
||||
let mut resp = Vec::new();
|
||||
let cr = CreateResponse {
|
||||
structure_size: 89,
|
||||
oplock_level: 0,
|
||||
flags: 0,
|
||||
create_action: 1,
|
||||
creation_time: 0,
|
||||
last_access_time: 0,
|
||||
last_write_time: 0,
|
||||
change_time: 0,
|
||||
allocation_size: 100,
|
||||
end_of_file: 100,
|
||||
file_attributes: 0,
|
||||
reserved2: 0,
|
||||
file_id: FileId::new(persistent, volatile),
|
||||
create_contexts_offset: 0,
|
||||
create_contexts_length: 0,
|
||||
create_contexts: vec![],
|
||||
};
|
||||
cr.write_to(&mut resp).unwrap();
|
||||
resp
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capture_create_file_id_found() {
|
||||
// Build a full response: 64-byte header + CREATE body
|
||||
let body = make_create_response(0xAAAABBBBCCCCDDDD, 0x1111222233334444);
|
||||
let mut response = vec![0u8; SMB2_HEADER_LEN];
|
||||
response[..4].copy_from_slice(&SMB2_MAGIC);
|
||||
response[12..14].copy_from_slice(&(Command::Create as u16).to_le_bytes());
|
||||
response.extend_from_slice(&body);
|
||||
|
||||
let file_id = capture_create_file_id(&response);
|
||||
// FileId::new(0xAAAABBBBCCCCDDDD, 0x1111222233334444)
|
||||
// LE: persistent=[0xDD,0xDD,0xCC,0xCC,0xBB,0xBB,0xAA,0xAA]
|
||||
// volatile=[0x44,0x44,0x33,0x33,0x22,0x22,0x11,0x11]
|
||||
assert_eq!(file_id, Some([0xDD, 0xDD, 0xCC, 0xCC, 0xBB, 0xBB, 0xAA, 0xAA,
|
||||
0x44, 0x44, 0x33, 0x33, 0x22, 0x22, 0x11, 0x11]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capture_create_file_id_not_create_command() {
|
||||
let response = vec![0u8; SMB2_HEADER_LEN];
|
||||
// Response has no file_id because it's not a CREATE
|
||||
assert!(capture_create_file_id(&response).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capture_create_file_id_too_short() {
|
||||
let mut response = vec![0u8; SMB2_HEADER_LEN + 10];
|
||||
response[12..14].copy_from_slice(&(Command::Create as u16).to_le_bytes());
|
||||
assert!(capture_create_file_id(&response).is_none());
|
||||
}
|
||||
|
||||
// ── Related context inheritance ─────────────────────────────────────────
|
||||
|
||||
fn build_frame(command: Command, flags: u32, next: u32, session: u64, tree: u32, file_id_bytes: &[u8; 16]) -> Vec<u8> {
|
||||
let mut f = vec![0u8; SMB2_HEADER_LEN];
|
||||
f[..4].copy_from_slice(&SMB2_MAGIC);
|
||||
f[4..6].copy_from_slice(&64u16.to_le_bytes()); // structure_size must be 64
|
||||
f[12..14].copy_from_slice(&(command as u16).to_le_bytes());
|
||||
f[16..20].copy_from_slice(&flags.to_le_bytes());
|
||||
f[20..24].copy_from_slice(&next.to_le_bytes());
|
||||
f[24..32].copy_from_slice(&0u64.to_le_bytes()); // message_id
|
||||
// tail: Sync { reserved: 0, tree_id }
|
||||
let tail_data = &tree.to_le_bytes();
|
||||
f[36..40].copy_from_slice(tail_data);
|
||||
f[40..48].copy_from_slice(&session.to_le_bytes());
|
||||
// Append file_id at body_offset for CLOSE (offset 8)
|
||||
let mut body = vec![0u8; 24]; // CLOSE body size
|
||||
body[8..24].copy_from_slice(file_id_bytes);
|
||||
f.extend_from_slice(&body);
|
||||
f
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_inherit_related_context_close() {
|
||||
// Use MAX values so inherit_related_context overrides them
|
||||
let mut frame = build_frame(Command::Close, SMB2_FLAGS_RELATED_OPERATIONS, 0, u64::MAX, u32::MAX, &[0xFF; 16]);
|
||||
let mut hdr = Smb2Header::parse(&frame).unwrap().0;
|
||||
|
||||
let prev_file_id = Some([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
||||
0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10]);
|
||||
|
||||
inherit_related_context(&mut frame, &mut hdr, 7, 3, prev_file_id);
|
||||
|
||||
// After inheritance: session_id should be 7
|
||||
let session = u64::from_le_bytes(frame[40..48].try_into().unwrap());
|
||||
assert_eq!(session, 7, "session_id should be inherited from 0xFFFF to 7");
|
||||
|
||||
// tree_id should be 3
|
||||
let tree = u32::from_le_bytes(frame[36..40].try_into().unwrap());
|
||||
assert_eq!(tree, 3, "tree_id should be inherited from 0xFFFFFFFF to 3");
|
||||
|
||||
// FileId should be inherited
|
||||
let file_id_field = &frame[SMB2_HEADER_LEN + 8..SMB2_HEADER_LEN + 24];
|
||||
assert_eq!(file_id_field, &prev_file_id.unwrap(), "FileId should be inherited from 0xFFFF to prev_file_id");
|
||||
}
|
||||
|
||||
// ── Full compound chain dispatch test ───────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_compound_chain_create_then_close() {
|
||||
use crate::server::SmbServer;
|
||||
use crate::tests::memfs::MemFsBackend;
|
||||
use crate::Access;
|
||||
|
||||
let server = SmbServer::builder()
|
||||
.listen("127.0.0.1:0".parse().unwrap())
|
||||
.user("alice", "password")
|
||||
.share(
|
||||
Share::new("home", MemFsBackend::new().with_file("test.txt", b"hello world"))
|
||||
.user("alice", Access::ReadWrite),
|
||||
)
|
||||
.build()
|
||||
.expect("build");
|
||||
|
||||
let state = server.state();
|
||||
let conn = Arc::new(Connection::new(
|
||||
state.config.server_guid,
|
||||
state.config.max_read_size,
|
||||
state.config.max_write_size,
|
||||
));
|
||||
state.active_connections.register(&conn).await;
|
||||
|
||||
let identity = Identity::User {
|
||||
user: "alice".to_string(),
|
||||
domain: String::new(),
|
||||
};
|
||||
let session = Session::new(1, identity, [0; 16], [0; 16], None, false, false, None);
|
||||
let session = Arc::new(tokio::sync::RwLock::new(session));
|
||||
let share = state.find_share("home").await.expect("share");
|
||||
let tree = Arc::new(tokio::sync::RwLock::new(TreeConnect::new(
|
||||
1,
|
||||
share,
|
||||
Access::ReadWrite,
|
||||
)));
|
||||
{
|
||||
let sess = session.read().await;
|
||||
sess.trees.write().await.insert(1, tree);
|
||||
}
|
||||
conn.sessions.write().await.insert(1, session);
|
||||
|
||||
// Build a compound CREATE+CLOSE frame
|
||||
// Sub-frame 1: CREATE "test.txt"
|
||||
let name_utf16: Vec<u8> = "test.txt".encode_utf16().flat_map(|c| c.to_le_bytes()).collect();
|
||||
let create_body_len = 56 + name_utf16.len();
|
||||
let create_frame_len = SMB2_HEADER_LEN + create_body_len;
|
||||
|
||||
let mut frame = vec![0u8; create_frame_len + SMB2_HEADER_LEN + 24]; // CREATE + CLOSE
|
||||
// ── CREATE sub-frame ──
|
||||
// Header
|
||||
frame[..4].copy_from_slice(&SMB2_MAGIC);
|
||||
frame[4..6].copy_from_slice(&64u16.to_le_bytes()); // structure_size
|
||||
frame[12..14].copy_from_slice(&(Command::Create as u16).to_le_bytes());
|
||||
frame[16..20].copy_from_slice(&SMB2_FLAGS_RELATED_OPERATIONS.to_le_bytes());
|
||||
frame[20..24].copy_from_slice(&(create_frame_len as u32).to_le_bytes()); // NextCommand
|
||||
frame[24..32].copy_from_slice(&0u64.to_le_bytes()); // message_id
|
||||
frame[36..40].copy_from_slice(&1u32.to_le_bytes()); // tree_id
|
||||
frame[40..48].copy_from_slice(&1u64.to_le_bytes()); // session_id
|
||||
|
||||
// CREATE body
|
||||
let body_start = SMB2_HEADER_LEN;
|
||||
frame[body_start..body_start+2].copy_from_slice(&57u16.to_le_bytes()); // structure_size
|
||||
frame[body_start+3] = 0; // security_flags
|
||||
frame[body_start+4..body_start+8].copy_from_slice(&2u32.to_le_bytes()); // impersonation_level
|
||||
// desired_access = 0x00120089 (READ_CONTROL|SYNCHRONIZE|READ_ATTR|READ_DATA)
|
||||
frame[body_start+24..body_start+28].copy_from_slice(&0x00120089u32.to_le_bytes());
|
||||
frame[body_start+32..body_start+36].copy_from_slice(&7u32.to_le_bytes()); // share_access
|
||||
frame[body_start+36..body_start+40].copy_from_slice(&1u32.to_le_bytes()); // create_disposition: OPEN
|
||||
frame[body_start+40..body_start+44].copy_from_slice(&0x00000040u32.to_le_bytes()); // create_options: FILE_NON_DIRECTORY_FILE
|
||||
// name_offset relative to header start: 64 + 56 = 120
|
||||
frame[body_start+44..body_start+46].copy_from_slice(&120u16.to_le_bytes());
|
||||
frame[body_start+46..body_start+48].copy_from_slice(&(name_utf16.len() as u16).to_le_bytes());
|
||||
frame[body_start+56..body_start+56+name_utf16.len()].copy_from_slice(&name_utf16);
|
||||
|
||||
// ── CLOSE sub-frame ──
|
||||
let close_start = create_frame_len;
|
||||
frame[close_start..close_start+4].copy_from_slice(&SMB2_MAGIC);
|
||||
frame[close_start+4..close_start+6].copy_from_slice(&64u16.to_le_bytes()); // structure_size
|
||||
frame[close_start+12..close_start+14].copy_from_slice(&(Command::Close as u16).to_le_bytes());
|
||||
frame[close_start+16..close_start+20].copy_from_slice(&(SMB2_FLAGS_RELATED_OPERATIONS).to_le_bytes());
|
||||
frame[close_start+20..close_start+24].copy_from_slice(&0u32.to_le_bytes()); // NextCommand = last
|
||||
frame[close_start+24..close_start+32].copy_from_slice(&0u64.to_le_bytes()); // message_id
|
||||
frame[close_start+36..close_start+40].copy_from_slice(&0u32.to_le_bytes()); // tree_id = 0 (inherited)
|
||||
frame[close_start+40..close_start+48].copy_from_slice(&0u64.to_le_bytes()); // session_id = 0 (inherited)
|
||||
// CLOSE body: FileId = 0xFFFF...FFFF (auto-inherit from CREATE)
|
||||
let close_body_start = close_start + SMB2_HEADER_LEN;
|
||||
frame[close_body_start..close_body_start+2].copy_from_slice(&24u16.to_le_bytes()); // structure_size
|
||||
fill_all_ones(&mut frame[close_body_start+8..close_body_start+24]); // FileId = FFFF...
|
||||
|
||||
let result = dispatch_frame(&state, &conn, &frame).await;
|
||||
assert!(result.is_some(), "dispatch_frame returned None");
|
||||
let response = result.unwrap();
|
||||
|
||||
// Response should contain two sub-responses
|
||||
assert!(response.len() >= SMB2_HEADER_LEN * 2 + 64,
|
||||
"response too short for compound chain: {} bytes", response.len());
|
||||
|
||||
// First response should be CREATE
|
||||
let next1 = u32::from_le_bytes(response[20..24].try_into().unwrap());
|
||||
assert!(next1 > 0, "first response should have NextCommand for compound chain");
|
||||
|
||||
// First response header should have SERVER_TO_REDIR
|
||||
let flags1 = u32::from_le_bytes(response[16..20].try_into().unwrap());
|
||||
assert_ne!(flags1 & SMB2_FLAGS_SERVER_TO_REDIR, 0);
|
||||
|
||||
// Second response should be CLOSE (at offset next1)
|
||||
let hdr2_start = next1 as usize;
|
||||
let flags2 = u32::from_le_bytes(response[hdr2_start+16..hdr2_start+20].try_into().unwrap());
|
||||
assert_ne!(flags2 & SMB2_FLAGS_SERVER_TO_REDIR, 0);
|
||||
|
||||
// Second response should be last (NextCommand = 0)
|
||||
let next2 = u32::from_le_bytes(response[hdr2_start+20..hdr2_start+24].try_into().unwrap());
|
||||
assert_eq!(next2, 0, "second (last) response should have NextCommand = 0");
|
||||
}
|
||||
|
||||
fn fill_all_ones(buf: &mut [u8]) {
|
||||
for b in buf.iter_mut() {
|
||||
*b = 0xFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+16
-10
@@ -306,16 +306,20 @@ impl ShareBackend for LocalFsBackend {
|
||||
}
|
||||
match opts.intent {
|
||||
OpenIntent::Open | OpenIntent::OpenOrCreate => {
|
||||
let dir_handle = if path.is_root() {
|
||||
Arc::clone(&self.root)
|
||||
} else {
|
||||
let root = Arc::clone(&self.root);
|
||||
let rel = rel.clone();
|
||||
let dir_handle = spawn_blocking(move || root.open_dir(&rel))
|
||||
Arc::new(spawn_blocking(move || root.open_dir(&rel))
|
||||
.await
|
||||
.map_err(join_to_io)
|
||||
.map_err(io_to_smb)?
|
||||
.map_err(io_to_smb)?;
|
||||
.map_err(io_to_smb)?)
|
||||
};
|
||||
return Ok(Box::new(LocalHandle::Dir {
|
||||
name: file_name_for(path),
|
||||
dir_handle: Arc::new(dir_handle),
|
||||
dir_handle,
|
||||
}));
|
||||
}
|
||||
OpenIntent::Create => return Err(SmbError::Exists),
|
||||
@@ -690,20 +694,17 @@ impl Handle for LocalHandle {
|
||||
LocalHandle::Dir { dir_handle, .. } => {
|
||||
let dir_handle = Arc::clone(dir_handle);
|
||||
let pat = pattern.map(|s| s.to_owned());
|
||||
spawn_blocking(move || -> io::Result<Vec<SmbDirEntry>> {
|
||||
let result: SmbResult<Vec<SmbDirEntry>> = spawn_blocking(move || -> io::Result<Vec<SmbDirEntry>> {
|
||||
let mut out = Vec::new();
|
||||
for entry in dir_handle.entries()? {
|
||||
let entry = entry?;
|
||||
let os_name = entry.file_name();
|
||||
let Some(name) = os_name.to_str().map(str::to_owned) else {
|
||||
// Skip non-UTF-8 names; SMB wire format is UTF-16
|
||||
// and we never want to emit invalid Unicode here.
|
||||
continue;
|
||||
};
|
||||
if let Some(p) = pat.as_deref() {
|
||||
// Empty / "*" / "*.*" all mean "match everything"
|
||||
// in DOS-speak.
|
||||
if !(p.is_empty() || p == "*" || p == "*.*" || glob_match(p, &name)) {
|
||||
let matches_glob = p.is_empty() || p == "*" || p == "*.*" || glob_match(p, &name);
|
||||
if !matches_glob {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -711,12 +712,17 @@ impl Handle for LocalHandle {
|
||||
let info = file_info_from_metadata(name, &md);
|
||||
out.push(SmbDirEntry { info });
|
||||
}
|
||||
// Assign unique file_index (1-based) so SMB2 FileId differs per entry
|
||||
for (i, entry) in out.iter_mut().enumerate() {
|
||||
entry.info.file_index = (i + 1) as u64;
|
||||
}
|
||||
Ok(out)
|
||||
})
|
||||
.await
|
||||
.map_err(join_to_io)
|
||||
.map_err(io_to_smb)?
|
||||
.map_err(io_to_smb)
|
||||
.map_err(io_to_smb);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-5
@@ -414,10 +414,13 @@ pub async fn handle(
|
||||
use crate::proto::messages::CreateContext;
|
||||
use crate::proto::messages::{
|
||||
AaplCreateContextRequest, AaplCreateContextResponse,
|
||||
SMB2_CRTCTX_AAPL_SERVER_QUERY, SMB2_CRTCTX_AAPL_SERVER_CAPS,
|
||||
SMB2_CRTCTX_AAPL_VOLUME_CAPS, SMB2_CRTCTX_AAPL_MODEL_INFO,
|
||||
SMB2_CRTCTX_AAPL_UNIX_BASED, SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR,
|
||||
SMB2_CRTCTX_AAPL_SERVER_QUERY,
|
||||
SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR,
|
||||
SMB2_CRTCTX_AAPL_UNIX_BASED,
|
||||
SMB2_CRTCTX_AAPL_SUPPORTS_NFS_ACE,
|
||||
SMB2_CRTCTX_AAPL_CASE_SENSITIVE,
|
||||
SMB2_CRTCTX_AAPL_SUPPORT_RESOLVE_ID,
|
||||
SMB2_CRTCTX_AAPL_FULL_SYNC,
|
||||
};
|
||||
|
||||
let contexts = CreateContext::parse_chain(&req.create_contexts).unwrap_or_default();
|
||||
@@ -426,8 +429,12 @@ pub async fn handle(
|
||||
if let Some(ctx) = aapl_ctx {
|
||||
if let Some(aapl_req) = AaplCreateContextRequest::from_bytes(&ctx.data) {
|
||||
if aapl_req.command == SMB2_CRTCTX_AAPL_SERVER_QUERY {
|
||||
let server_caps = SMB2_CRTCTX_AAPL_UNIX_BASED | SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR;
|
||||
let volume_caps = SMB2_CRTCTX_AAPL_CASE_SENSITIVE;
|
||||
let server_caps = SMB2_CRTCTX_AAPL_UNIX_BASED
|
||||
| SMB2_CRTCTX_AAPL_SUPPORTS_READ_DIR_ATTR
|
||||
| SMB2_CRTCTX_AAPL_SUPPORTS_NFS_ACE;
|
||||
let volume_caps = SMB2_CRTCTX_AAPL_CASE_SENSITIVE
|
||||
| SMB2_CRTCTX_AAPL_SUPPORT_RESOLVE_ID
|
||||
| SMB2_CRTCTX_AAPL_FULL_SYNC;
|
||||
let aapl_resp = AaplCreateContextResponse::new_server_query(
|
||||
aapl_req.request_bitmap,
|
||||
aapl_req.client_caps,
|
||||
|
||||
+29
-2
@@ -13,6 +13,13 @@ use crate::ntstatus;
|
||||
use crate::server::ServerState;
|
||||
use crate::utils::utf16le_to_string;
|
||||
|
||||
fn hex_dump(label: &str, data: &[u8], max: usize) {
|
||||
let show = data.len().min(max);
|
||||
let hex: Vec<String> = data[..show].iter().map(|b| format!("{:02x}", b)).collect();
|
||||
tracing::debug!("{}: len={} hex=[{}] {}", label, data.len(), hex.join(" "),
|
||||
if data.len() > max { format!("... (truncated {})", data.len()) } else { String::new() });
|
||||
}
|
||||
|
||||
pub async fn handle(
|
||||
_server: &Arc<ServerState>,
|
||||
conn: &Arc<Connection>,
|
||||
@@ -38,16 +45,24 @@ pub async fn handle(
|
||||
};
|
||||
|
||||
let pattern_str = utf16le_to_string(&req.file_name);
|
||||
let pattern: Option<String> = if pattern_str.is_empty() || pattern_str == "*" {
|
||||
let is_empty_pat = pattern_str.is_empty();
|
||||
let pattern: Option<String> = if is_empty_pat || pattern_str == "*" {
|
||||
None
|
||||
} else {
|
||||
Some(pattern_str)
|
||||
Some(pattern_str.clone())
|
||||
};
|
||||
|
||||
let restart = req.flags & QueryDirectoryRequest::FLAG_RESTART_SCANS != 0
|
||||
|| req.flags & QueryDirectoryRequest::FLAG_REOPEN != 0;
|
||||
let single_entry = req.flags & QueryDirectoryRequest::FLAG_RETURN_SINGLE_ENTRY != 0;
|
||||
|
||||
tracing::debug!(
|
||||
class = %req.file_information_class, output_buf_len = %req.output_buffer_length,
|
||||
flags = %req.flags, restart = %restart, single = %single_entry,
|
||||
pattern = %pattern_str, file_name_hex = ?req.file_name,
|
||||
"QueryDirectory request"
|
||||
);
|
||||
|
||||
// Populate or refresh the cursor.
|
||||
{
|
||||
let mut open = open_arc.write().await;
|
||||
@@ -55,6 +70,8 @@ pub async fn handle(
|
||||
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
|
||||
}
|
||||
if open.search_state.is_none() || restart {
|
||||
let handle_type = open.handle.as_ref().map(|h| std::any::type_name_of_val(&*h));
|
||||
tracing::debug!("handle type: {:?}", handle_type);
|
||||
let entries = match open.handle.as_ref() {
|
||||
Some(h) => h.list_dir(pattern.as_deref()).await,
|
||||
None => return HandlerResponse::err(ntstatus::STATUS_FILE_CLOSED),
|
||||
@@ -63,6 +80,7 @@ pub async fn handle(
|
||||
Ok(e) => e,
|
||||
Err(e) => return HandlerResponse::err(e.to_nt_status()),
|
||||
};
|
||||
tracing::debug!(">>> list_dir count={}", entries.len());
|
||||
open.search_state = Some(DirCursor {
|
||||
entries,
|
||||
next: 0,
|
||||
@@ -85,6 +103,7 @@ pub async fn handle(
|
||||
}
|
||||
let entry = &cursor.entries[cursor.next];
|
||||
let file_index = entry.info.file_index;
|
||||
tracing::debug!(name = %entry.info.name, file_index = %file_index, "encode_dir_entry file_index");
|
||||
let mut bytes = encode_dir_entry(class_byte, entry, file_index);
|
||||
if bytes.is_empty() {
|
||||
cursor.next += 1;
|
||||
@@ -119,10 +138,18 @@ pub async fn handle(
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Log entries processed
|
||||
tracing::debug!(next = cursor.next, total = cursor.entries.len(), buf_len = buf.len(), cap = cap, "QueryDirectory encoding done");
|
||||
}
|
||||
if buf.is_empty() {
|
||||
return HandlerResponse::err(ntstatus::STATUS_NO_MORE_FILES);
|
||||
}
|
||||
hex_dump("QueryDirectory buffer", &buf, 512);
|
||||
|
||||
// Set last entry's NextEntryOffset to 0 (no next entry)
|
||||
if let Some(prev_off) = last_offset_pos {
|
||||
buf[prev_off..prev_off + 4].copy_from_slice(&0u32.to_le_bytes());
|
||||
}
|
||||
|
||||
let resp = QueryDirectoryResponse {
|
||||
structure_size: 9,
|
||||
|
||||
+10
-11
@@ -91,16 +91,15 @@ pub async fn handle(
|
||||
if bytes.is_empty() && req.length > 0 {
|
||||
return HandlerResponse::err(ntstatus::STATUS_END_OF_FILE);
|
||||
}
|
||||
let resp = ReadResponse {
|
||||
structure_size: 17,
|
||||
data_offset: ReadResponse::STANDARD_DATA_OFFSET,
|
||||
reserved: 0,
|
||||
data_length: bytes.len() as u32,
|
||||
data_remaining: 0,
|
||||
flags: 0,
|
||||
data: bytes.to_vec(),
|
||||
};
|
||||
let mut buf = Vec::new();
|
||||
resp.write_to(&mut buf).expect("encode");
|
||||
// Build response directly to avoid Bytes→Vec<u8> copy and intermediate struct
|
||||
let data_len = bytes.len() as u32;
|
||||
let mut buf = Vec::with_capacity(16 + bytes.len());
|
||||
buf.extend_from_slice(&17u16.to_le_bytes()); // structure_size
|
||||
buf.push(ReadResponse::STANDARD_DATA_OFFSET); // data_offset
|
||||
buf.push(0); // reserved
|
||||
buf.extend_from_slice(&data_len.to_le_bytes()); // data_length
|
||||
buf.extend_from_slice(&0u32.to_le_bytes()); // data_remaining
|
||||
buf.extend_from_slice(&0u32.to_le_bytes()); // flags
|
||||
buf.extend_from_slice(&bytes); // data
|
||||
HandlerResponse::ok(buf)
|
||||
}
|
||||
|
||||
+25
-2
@@ -21,6 +21,11 @@ const FILE_GENERIC_READ: u32 = 0x0012_0089;
|
||||
const FILE_GENERIC_EXECUTE: u32 = 0x0012_00A0;
|
||||
const FILE_ALL_ACCESS: u32 = 0x001F_01FF;
|
||||
|
||||
const SMB2_SHAREFLAG_MANUAL_CACHING: u32 = 0x00000000;
|
||||
const SMB2_SHAREFLAG_ACCESS_BASED_DIRECTORY_ENUM: u32 = 0x00080000;
|
||||
|
||||
const SMB2_SHARE_CAP_DFS: u32 = 0x00000001;
|
||||
|
||||
pub async fn handle(
|
||||
server: &Arc<ServerState>,
|
||||
conn: &Arc<Connection>,
|
||||
@@ -119,6 +124,24 @@ pub async fn handle(
|
||||
tracing::info!(share = %share.name, uuid = %uuid, "Time Machine enabled");
|
||||
}
|
||||
|
||||
let share_flags = if share.is_ipc {
|
||||
0
|
||||
} else {
|
||||
SMB2_SHAREFLAG_MANUAL_CACHING | SMB2_SHAREFLAG_ACCESS_BASED_DIRECTORY_ENUM
|
||||
};
|
||||
|
||||
let capabilities = if share.is_ipc {
|
||||
0
|
||||
} else {
|
||||
0 // 普通磁盘 share,不是 DFS share
|
||||
};
|
||||
|
||||
let capabilities = if share.is_ipc {
|
||||
0
|
||||
} else {
|
||||
SMB2_SHARE_CAP_DFS // Basic DFS support for Finder
|
||||
};
|
||||
|
||||
let resp = TreeConnectResponse {
|
||||
structure_size: 16,
|
||||
share_type: if share.is_ipc {
|
||||
@@ -127,8 +150,8 @@ pub async fn handle(
|
||||
SHARE_TYPE_DISK
|
||||
},
|
||||
reserved: 0,
|
||||
share_flags: 0,
|
||||
capabilities: 0,
|
||||
share_flags,
|
||||
capabilities,
|
||||
maximal_access,
|
||||
};
|
||||
let mut buf = Vec::new();
|
||||
|
||||
Vendored
+2
-2
@@ -371,8 +371,8 @@ pub fn encode_dir_entry(class: u8, entry: &DirEntry, file_index: u64) -> Vec<u8>
|
||||
out.push(0); // ShortNameLength
|
||||
out.push(0); // Reserved1
|
||||
out.extend_from_slice(&[0u8; 24]); // ShortName
|
||||
out.extend_from_slice(&0u16.to_le_bytes()); // Reserved2
|
||||
out.extend_from_slice(&file_index.to_le_bytes()); // FileId
|
||||
out.extend_from_slice(&[0u8; 2]); // Reserved2 (alignment padding for FileId)
|
||||
out.extend_from_slice(&file_index.to_le_bytes()); // FileId (8 bytes)
|
||||
out.extend_from_slice(&name_u16);
|
||||
out
|
||||
}
|
||||
|
||||
Vendored
+1
-1
@@ -56,5 +56,5 @@ pub mod wire {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
mod dynamic_config;
|
||||
mod memfs;
|
||||
pub(crate) mod memfs;
|
||||
}
|
||||
|
||||
Vendored
+15
-5
@@ -112,17 +112,23 @@ impl OplockManager {
|
||||
new_share_access: u32,
|
||||
new_granted_access: Access,
|
||||
) -> Vec<OplockBreakNotification> {
|
||||
// Fast-path: no entries or single entry can't conflict with itself
|
||||
let entry_count = {
|
||||
let file_opens = self.file_opens.read().await;
|
||||
file_opens.get(path).map_or(0, |e| e.len())
|
||||
};
|
||||
if entry_count <= 1 {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut notifications = Vec::new();
|
||||
let mut file_opens = self.file_opens.write().await;
|
||||
|
||||
if let Some(entries) = file_opens.get_mut(path) {
|
||||
for entry in entries.iter_mut() {
|
||||
// Check if new open conflicts with existing oplock
|
||||
if !share_access_compatible(entry.share_access, new_share_access) {
|
||||
// Need to break the oplock
|
||||
let new_level = OplockLevel::Ii as u8; // Downgrade to Level II
|
||||
let new_level = OplockLevel::Ii as u8;
|
||||
|
||||
// Build notification (MS-SMB2 §2.2.23.1)
|
||||
notifications.push(OplockBreakNotification {
|
||||
structure_size: 24,
|
||||
oplock_level: new_level,
|
||||
@@ -131,7 +137,6 @@ impl OplockManager {
|
||||
file_id: entry.file_id,
|
||||
});
|
||||
|
||||
// Update entry's oplock level
|
||||
entry.oplock_level = new_level;
|
||||
}
|
||||
}
|
||||
@@ -266,6 +271,11 @@ impl LeaseManager {
|
||||
|
||||
/// Break lease when conflicting access occurs (MS-SMB2 §3.3.5.10).
|
||||
pub async fn break_lease(&self, requested_state: u32) -> Vec<LeaseBreakNotification> {
|
||||
// Fast-path: no leases to break
|
||||
if self.leases.read().await.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let mut leases = self.leases.write().await;
|
||||
let mut notifications = Vec::new();
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -78,7 +78,7 @@ impl ShareBindings {
|
||||
Self::new(
|
||||
"IPC$".to_string(),
|
||||
Arc::new(crate::backend::NotSupportedBackend),
|
||||
ShareMode::PublicReadOnly,
|
||||
ShareMode::Public, // 允许 named pipe communication (ReadWrite)
|
||||
HashMap::new(),
|
||||
true,
|
||||
false,
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ pub const FRUIT_ENC_PRIVATE: bool = false;
|
||||
|
||||
const APPLE_SLASH: u16 = 0xF026;
|
||||
const APPLE_COLON: u16 = 0xF02A;
|
||||
const APPLE_ASTERISK: u16 = 0xF02A;
|
||||
const APPLE_ASTERISK: u16 = 0xF02B;
|
||||
const APPLE_QUESTION: u16 = 0xF03F;
|
||||
const APPLE_QUOTE: u16 = 0xF022;
|
||||
const APPLE_LESS_THAN: u16 = 0xF03C;
|
||||
|
||||
Reference in New Issue
Block a user