Skip to main content

rustsmb/cmds/
file_cmds.rs

1//! File command handlers: NT_CREATE_ANDX, READ/WRITE_ANDX, CLOSE, FLUSH,
2//! SEEK, LOCKING_ANDX and QUERY_INFORMATION_DISK.
3
4use smb_server_proto::types::Status;
5use smb_server_proto_smb1::consts;
6use smb_server_proto_smb1::create as create_codec;
7use smb_server_proto_smb1::header::RespBody;
8use smb_server_proto_smb1::misc;
9use smb_server_proto_smb1::rw::{self, ReadReq, WriteReq};
10use smb_server_vfs::SetOp;
11
12use crate::cmds::{IoCtx, share_vfs};
13use crate::dispatch::ReqView;
14use crate::state::next_fid;
15
16/// QUERY_INFORMATION_DISK (0x80).
17pub async fn query_disk(
18    io: &mut IoCtx<'_>,
19    req: &ReqView<'_>,
20    bodies: &mut Vec<RespBody>,
21) -> Result<Status, Status> {
22    let vfs = share_vfs(io, req.hdr.tid);
23    let (total, free, bps, spc) = vfs.query_disk().await.map_err(vfs_err)?;
24    let info = misc::QueryDiskInfo {
25        total_units: total,
26        free_units: free,
27        bps,
28        spc,
29    };
30    bodies.push(RespBody::new(
31        consts::COM_QUERY_INFORMATION_DISK,
32        info.encode(),
33        Vec::new(),
34    ));
35    Ok(Status::SUCCESS)
36}
37
38/// NT_CREATE_ANDX ([MS-SMB] §2.2.4.9).
39pub async fn nt_create(
40    io: &mut IoCtx<'_>,
41    req: &ReqView<'_>,
42    bodies: &mut Vec<RespBody>,
43) -> Result<Status, Status> {
44    let is_ipc = io
45        .conn
46        .trees
47        .get(&req.hdr.tid)
48        .map(|n| n == "ipc$")
49        .unwrap_or(false);
50    if is_ipc {
51        return Err(Status::OBJECT_NAME_NOT_FOUND);
52    }
53    let creq = create_codec::NtCreateReq::parse(
54        req.words,
55        req.data,
56        req.unicode(),
57        req.bc_off_abs + smb_server_proto_smb1::consts::WORD_LEN,
58    )
59    .map_err(|_| Status::INVALID_PARAMETER)?;
60
61    let base_rel = if creq.root_fid != 0 && creq.root_fid <= u16::MAX as u32 {
62        io.conn
63            .handles
64            .get(&(creq.root_fid as u16))
65            .map(|h| h.path.clone())
66            .ok_or(Status::INVALID_HANDLE)?
67    } else {
68        String::new()
69    };
70    let rel = join_rel(&base_rel, &creq.name);
71
72    let want_dir =
73        creq.create_options & smb_server_proto_smb1::consts::create_options::FILE_DIRECTORY_FILE != 0;
74    let no_dir =
75        creq.create_options & smb_server_proto_smb1::consts::create_options::FILE_NON_DIRECTORY_FILE != 0;
76    let delete_on_close =
77        creq.create_options & smb_server_proto_smb1::consts::create_options::FILE_DELETE_ON_CLOSE != 0;
78
79    let vfs = share_vfs(io, req.hdr.tid);
80    let (mut open, meta, action) = vfs
81        .create(
82            &rel,
83            want_dir,
84            creq.desired_access,
85            creq.disposition,
86            creq.create_options,
87            creq.ext_attrs,
88        )
89        .await
90        .map_err(vfs_err)?;
91
92    if no_dir && open.is_dir {
93        return Err(Status::FILE_IS_A_DIRECTORY);
94    }
95    open.delete_on_close |= delete_on_close;
96
97    let fid = next_fid();
98    io.conn.handles.insert(fid, open);
99
100    let body = create_codec::build_response(
101        fid,
102        action,
103        meta.times,
104        meta.attrs.0,
105        meta.alloc,
106        meta.eof,
107        meta.is_dir,
108    );
109    tracing::debug!(
110        fid = format!("{:#06x}", fid),
111        eof = meta.eof,
112        dir = meta.is_dir,
113        "nt_create"
114    );
115    metrics::counter!("smb_creates_total").increment(1);
116    bodies.push(RespBody::new(consts::COM_NT_CREATE_ANDX, body, Vec::new()));
117    Ok(Status::SUCCESS)
118}
119
120/// READ_ANDX.
121pub async fn read_andx(
122    io: &mut IoCtx<'_>,
123    req: &ReqView<'_>,
124    bodies: &mut Vec<RespBody>,
125) -> Result<Status, Status> {
126    let rr = ReadReq::parse(req.words).map_err(|_| Status::INVALID_PARAMETER)?;
127    tracing::trace!(
128        fid = format!("{:#06x}", rr.fid),
129        offset = rr.offset,
130        want = rr.max_count,
131        "read_andx"
132    );
133    let vfs = share_vfs(io, req.hdr.tid);
134
135    let Some(h) = io.conn.handles.get_mut(&rr.fid).map(|b| &mut **b) else {
136        return Err(Status::INVALID_HANDLE);
137    };
138    if h.is_dir || !h.can_read {
139        return Err(Status::ACCESS_DENIED);
140    }
141    let data = vfs
142        .read(h, rr.offset, rr.max_count)
143        .await
144        .map_err(vfs_err)?;
145    metrics::counter!("smb_bytes_read_total").increment(data.len() as u64);
146
147    let (params, bytes) = rw::read_response(&data);
148    bodies.push(RespBody::new(consts::COM_READ_ANDX, params, bytes));
149    Ok(Status::SUCCESS)
150}
151
152/// WRITE_ANDX.
153pub async fn write_andx(
154    io: &mut IoCtx<'_>,
155    req: &ReqView<'_>,
156    bodies: &mut Vec<RespBody>,
157) -> Result<Status, Status> {
158    let wr = WriteReq::parse(req.words, req.frame)?;
159    let vfs = share_vfs(io, req.hdr.tid);
160
161    let Some(h) = io.conn.handles.get_mut(&wr.fid).map(|b| &mut **b) else {
162        return Err(Status::INVALID_HANDLE);
163    };
164    if h.is_dir || !h.can_write {
165        return Err(Status::ACCESS_DENIED);
166    }
167    let written = vfs
168        .write(h, wr.offset, &wr.payload, wr.write_through)
169        .await
170        .map_err(vfs_err)?;
171
172    bodies.push(RespBody::new(
173        consts::COM_WRITE_ANDX,
174        rw::write_response(written),
175        Vec::new(),
176    ));
177    Ok(Status::SUCCESS)
178}
179
180/// CLOSE (0x04): optional mtime update then close.
181pub async fn close(
182    io: &mut IoCtx<'_>,
183    req: &ReqView<'_>,
184    bodies: &mut Vec<RespBody>,
185) -> Result<Status, Status> {
186    let Some(cr) = misc::CloseReq::parse(req.words) else {
187        return Err(Status::INVALID_PARAMETER);
188    };
189    let vfs = share_vfs(io, req.hdr.tid);
190    let Some(mut h) = io.conn.handles.remove(&cr.fid) else {
191        return Err(Status::INVALID_HANDLE);
192    };
193    if cr.mtime != 0 && cr.mtime != u32::MAX {
194        // Legacy UTIME field: seconds since 1970 in the low word.
195        let ft = smb_server_proto::types::FileTime::from_unix(cr.mtime as i64, 0);
196        let _ = vfs
197            .set_info_open(
198                &mut h,
199                &SetOp::Basic {
200                    access: None,
201                    write: Some(ft),
202                },
203            )
204            .await;
205    }
206    vfs.close(h).await.map_err(vfs_err)?;
207    bodies.push(RespBody::new(consts::COM_CLOSE, Vec::new(), Vec::new()));
208    Ok(Status::SUCCESS)
209}
210
211/// FLUSH (0x05).
212pub async fn flush(
213    io: &mut IoCtx<'_>,
214    req: &ReqView<'_>,
215    bodies: &mut Vec<RespBody>,
216) -> Result<Status, Status> {
217    let vfs = share_vfs(io, req.hdr.tid);
218    match misc::flush_fid(req.words) {
219        None => return Err(Status::INVALID_PARAMETER),
220        Some(0xFFFF) => vfs.flush_all().await.map_err(vfs_err)?,
221        Some(fid) => {
222            if let Some(h) = io.conn.handles.get_mut(&fid) {
223                vfs.flush(h).await.map_err(vfs_err)?;
224            }
225        }
226    }
227    bodies.push(RespBody::new(consts::COM_FLUSH, Vec::new(), Vec::new()));
228    Ok(Status::SUCCESS)
229}
230
231/// SEEK (0x12).
232pub async fn seek(
233    io: &mut IoCtx<'_>,
234    req: &ReqView<'_>,
235    bodies: &mut Vec<RespBody>,
236) -> Result<Status, Status> {
237    let Some(sr) = misc::SeekReq::parse(req.words) else {
238        return Err(Status::INVALID_PARAMETER);
239    };
240    let vfs = share_vfs(io, req.hdr.tid);
241    let Some(h) = io.conn.handles.get_mut(&sr.fid) else {
242        return Err(Status::INVALID_HANDLE);
243    };
244    let pos = vfs.seek(h, sr.mode, sr.offset).await.map_err(vfs_err)?;
245    bodies.push(RespBody::new(
246        consts::COM_SEEK,
247        misc::SeekReq::build_response(pos),
248        Vec::new(),
249    ));
250    Ok(Status::SUCCESS)
251}
252
253/// LOCKING_ANDX / byte-range lock+unlock — accepted unenforced.
254pub async fn locking(
255    _io: &mut IoCtx<'_>,
256    req: &ReqView<'_>,
257    bodies: &mut Vec<RespBody>,
258) -> Result<Status, Status> {
259    let cmd = req.hdr.command;
260    bodies.push(RespBody::new(
261        cmd,
262        vec![smb_server_proto_smb1::consts::ANDX_NONE, 0, 0, 0],
263        Vec::new(),
264    ));
265    Ok(Status::SUCCESS)
266}
267
268// ---- helpers ----
269
270pub(crate) fn vfs_err(e: smb_server_vfs::VfsError) -> Status {
271    use smb_server_vfs::VfsError as E;
272    match e {
273        E::NotFound => Status::OBJECT_PATH_NOT_FOUND,
274        E::AlreadyExists => Status::OBJECT_NAME_COLLISION,
275        E::AccessDenied => Status::ACCESS_DENIED,
276        E::DirectoryNotEmpty => Status::DIRECTORY_NOT_EMPTY,
277        E::InvalidArgument => Status::INVALID_PARAMETER,
278        E::NotSupported => Status::NOT_IMPLEMENTED,
279        E::StoppedOnSymlink { .. } => Status::STOPPED_ON_SYMLINK,
280        E::Io(_) => Status::UNSUCCESSFUL,
281    }
282}
283
284fn join_rel(base: &str, name: &str) -> String {
285    let name = name.trim_start_matches(['\\', '/']);
286    if base.is_empty() {
287        name.to_string()
288    } else {
289        format!("{}\\{}", base.trim_end_matches(['\\', '/']), name)
290    }
291}