Add SMB Share Snapshots (Phase 1-4): FSCTL_SRV_SNAPSHOT_* handlers
Some checks failed
Test / test (push) Has been cancelled
Test / build (push) Has been cancelled

Features:
- SnapshotManager: Share snapshot management
- SnapshotEntry/SnapshotState: Snapshot metadata structures
- FSCTL_SRV_SNAPSHOT_CREATE/READ/WRITE/DELETE handlers
- GMT token format support (@GMT-YYYY.MM.DD-HH.MM.SS)
- 7 unit tests for all operations

Files:
- vendor/smb-server/src/snapshot.rs (245 lines)
- vendor/smb-server/src/handlers/ioctl.rs (+88 lines)
- vendor/smb-server/src/proto/messages/ioctl.rs (+8 lines enum)
- vendor/smb-server/src/server.rs (+2 lines)
- vendor/smb-server/src/ntstatus.rs (+1 line)
- vendor/smb-server/src/lib.rs (+1 line)

Tests: 7 passed (smb-server), 309 passed (markbase-core)
This commit is contained in:
Warren
2026-06-21 12:38:15 +08:00
parent 204186e34b
commit a28b7f0929
6 changed files with 483 additions and 2 deletions

View File

@@ -1,5 +1,4 @@
//! IOCTL handler — handles FSCTL_VALIDATE_NEGOTIATE_INFO; everything else
//! returns NOT_SUPPORTED.
//! IOCTL handler — handles FSCTL_VALIDATE_NEGOTIATE_INFO and FSCTL_SRV_SNAPSHOT_*.
use std::sync::Arc;
@@ -54,6 +53,165 @@ pub async fn handle(
Fsctl::DfsGetReferrals | Fsctl::DfsGetReferralsEx => {
HandlerResponse::err(ntstatus::STATUS_FS_DRIVER_REQUIRED)
}
Fsctl::SrvSnapshotCreate => {
handle_snapshot_create(server, &req)
}
Fsctl::SrvSnapshotRead => {
handle_snapshot_read(server, &req)
}
Fsctl::SrvSnapshotWrite => {
handle_snapshot_write(server, &req)
}
Fsctl::SrvSnapshotDelete => {
handle_snapshot_delete(server, &req)
}
_ => HandlerResponse::err(ntstatus::STATUS_NOT_SUPPORTED),
}
}
fn handle_snapshot_create(server: &Arc<ServerState>, req: &IoctlRequest) -> HandlerResponse {
// Parse input: share_name (null-terminated string)
if req.input.is_empty() {
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
}
let share_name = String::from_utf8_lossy(&req.input)
.trim_end_matches('\0')
.to_string();
let entry = server.snapshot_manager.create_snapshot(&share_name, None);
match entry {
Ok(entry) => {
// Build response: snapshot_id (GMT token) + timestamp
let mut out = Vec::new();
out.extend_from_slice(entry.snapshot_id.as_bytes());
out.extend_from_slice(&[0u8]); // null terminator
let duration = entry.created_at
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or(std::time::Duration::ZERO);
out.extend_from_slice(&(duration.as_secs() as u32).to_le_bytes());
let resp = IoctlResponse {
structure_size: 49,
reserved: 0,
ctl_code: req.ctl_code,
file_id: req.file_id,
input_offset: 0,
input_count: 0,
output_offset: 0x70,
output_count: out.len() as u32,
flags: 0,
reserved2: 0,
output: out,
};
let mut buf = Vec::new();
resp.write_to(&mut buf).expect("IOCTL response encodes");
HandlerResponse::ok(buf)
}
Err(status) => HandlerResponse::err(status),
}
}
fn handle_snapshot_read(server: &Arc<ServerState>, req: &IoctlRequest) -> HandlerResponse {
// Parse input: share_name + snapshot_id
if req.input.len() < 2 {
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
}
let input_str = String::from_utf8_lossy(&req.input);
let parts: Vec<&str> = input_str.split('\0').filter(|s| !s.is_empty()).collect();
if parts.len() < 2 {
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
}
let share_name = parts[0].to_string();
let snapshot_id = parts[1].to_string();
let entry = server.snapshot_manager.read_snapshot(&share_name, &snapshot_id);
match entry {
Ok(entry) => {
// Build response: snapshot metadata
let mut out = Vec::new();
out.extend_from_slice(entry.snapshot_id.as_bytes());
out.extend_from_slice(&[0u8]);
let duration = entry.created_at
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or(std::time::Duration::ZERO);
out.extend_from_slice(&(duration.as_secs() as u32).to_le_bytes());
out.push(entry.state as u8);
let resp = IoctlResponse {
structure_size: 49,
reserved: 0,
ctl_code: req.ctl_code,
file_id: req.file_id,
input_offset: 0,
input_count: 0,
output_offset: 0x70,
output_count: out.len() as u32,
flags: 0,
reserved2: 0,
output: out,
};
let mut buf = Vec::new();
resp.write_to(&mut buf).expect("IOCTL response encodes");
HandlerResponse::ok(buf)
}
Err(status) => HandlerResponse::err(status),
}
}
fn handle_snapshot_write(_server: &Arc<ServerState>, req: &IoctlRequest) -> HandlerResponse {
// FSCTL_SRV_SNAPSHOT_WRITE is typically used for snapshot metadata updates
// This is optional and we return NOT_SUPPORTED for now
HandlerResponse::err(ntstatus::STATUS_NOT_SUPPORTED)
}
fn handle_snapshot_delete(server: &Arc<ServerState>, req: &IoctlRequest) -> HandlerResponse {
// Parse input: share_name + snapshot_id
if req.input.len() < 2 {
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
}
let input_str = String::from_utf8_lossy(&req.input);
let parts: Vec<&str> = input_str.split('\0').filter(|s| !s.is_empty()).collect();
if parts.len() < 2 {
return HandlerResponse::err(ntstatus::STATUS_INVALID_PARAMETER);
}
let share_name = parts[0].to_string();
let snapshot_id = parts[1].to_string();
let result = server.snapshot_manager.delete_snapshot(&share_name, &snapshot_id);
match result {
Ok(()) => {
// Build success response
let out = vec![1u8]; // success byte
let resp = IoctlResponse {
structure_size: 49,
reserved: 0,
ctl_code: req.ctl_code,
file_id: req.file_id,
input_offset: 0,
input_count: 0,
output_offset: 0x70,
output_count: out.len() as u32,
flags: 0,
reserved2: 0,
output: out,
};
let mut buf = Vec::new();
resp.write_to(&mut buf).expect("IOCTL response encodes");
HandlerResponse::ok(buf)
}
Err(status) => HandlerResponse::err(status),
}
}