Skip to main content

rustsmb/cmds/
dir_cmds.rs

1//! Legacy path command handlers ([MS-CIFS] core/LANMAN set).
2
3use smb_server_proto::types::Status;
4use smb_server_proto_smb1::consts;
5use smb_server_proto_smb1::header::RespBody;
6use smb_server_proto_smb1::legacy as legacy_codec;
7use smb_server_vfs::SetOp;
8
9use crate::cmds::{IoCtx, share_vfs};
10use crate::dispatch::ReqView;
11
12fn read_path(req: &ReqView<'_>) -> String {
13    let mut rd = smb_server_proto::buf::Reader::new(req.data, 0);
14    if !req.data.is_empty() && req.data[0] == consts::BUFFER_FORMAT_DATA {
15        rd.skip(1);
16    }
17    rd.zstring(req.unicode(), req.bc_off_abs + consts::BYTE_COUNT_LEN)
18}
19
20fn read_two_paths(req: &ReqView<'_>) -> (String, String) {
21    let data = req.data;
22    let mut rd = smb_server_proto::buf::Reader::new(data, 0);
23    let base = req.bc_off_abs + consts::BYTE_COUNT_LEN;
24    if (!data.is_empty() && data[0] == consts::BUFFER_FORMAT_DATA)
25        || (unicode(req) && !base.is_multiple_of(consts::WORD_LEN) && !data.is_empty())
26    {
27        rd.skip(1);
28    }
29    let a = rd.zstring(unicode(req), base);
30    if !rd.at_end() {
31        if unicode(req) && (base + rd.pos()) & 1 != 0 {
32            rd.skip(1);
33        }
34        if rd.pos() < data.len() && data[rd.pos()] == consts::BUFFER_FORMAT_DATA {
35            rd.skip(1);
36        }
37    }
38    let b = rd.zstring(unicode(req), base + rd.pos());
39    (a, b)
40}
41
42fn unicode(req: &ReqView<'_>) -> bool {
43    req.unicode()
44}
45
46/// CREATE_DIRECTORY (0x00).
47pub async fn mkdir(
48    io: &mut IoCtx<'_>,
49    req: &ReqView<'_>,
50    bodies: &mut Vec<RespBody>,
51) -> Result<Status, Status> {
52    let vfs = share_vfs(io, req.hdr.tid);
53    vfs.mkdir(&read_path(req)).await.map_err(vfs_err)?;
54    bodies.push(RespBody::new(
55        consts::COM_CREATE_DIRECTORY,
56        Vec::new(),
57        Vec::new(),
58    ));
59    Ok(Status::SUCCESS)
60}
61
62/// DELETE_DIRECTORY (0x01).
63pub async fn rmdir(
64    io: &mut IoCtx<'_>,
65    req: &ReqView<'_>,
66    bodies: &mut Vec<RespBody>,
67) -> Result<Status, Status> {
68    let vfs = share_vfs(io, req.hdr.tid);
69    vfs.rmdir(&read_path(req)).await.map_err(vfs_err)?;
70    bodies.push(RespBody::new(
71        consts::COM_DELETE_DIRECTORY,
72        Vec::new(),
73        Vec::new(),
74    ));
75    Ok(Status::SUCCESS)
76}
77
78/// CHECK_DIRECTORY (0x10).
79pub async fn check_dir(
80    io: &mut IoCtx<'_>,
81    req: &ReqView<'_>,
82    bodies: &mut Vec<RespBody>,
83) -> Result<Status, Status> {
84    let vfs = share_vfs(io, req.hdr.tid);
85    vfs.check_dir(&read_path(req)).await.map_err(vfs_err)?;
86    bodies.push(RespBody::new(
87        consts::COM_CHECK_DIRECTORY,
88        Vec::new(),
89        Vec::new(),
90    ));
91    Ok(Status::SUCCESS)
92}
93
94/// DELETE (0x06): exact file or wildcard pattern.
95pub async fn delete(
96    io: &mut IoCtx<'_>,
97    req: &ReqView<'_>,
98    bodies: &mut Vec<RespBody>,
99) -> Result<Status, Status> {
100    let vfs = share_vfs(io, req.hdr.tid);
101    let pattern = read_path(req);
102    let (dir_rel, name_pat) = split_pattern_pub(&pattern);
103
104    if !name_pat.contains('*') && !name_pat.contains('?') {
105        let rel = join_rel(&dir_rel, &name_pat);
106        vfs.unlink(&rel).await.map_err(vfs_err)?;
107    } else {
108        let deleted = vfs
109            .delete_pattern(&dir_rel, &name_pat)
110            .await
111            .map_err(vfs_err)?;
112        if !deleted {
113            return Err(Status::NO_SUCH_FILE);
114        }
115    }
116    bodies.push(RespBody::new(consts::COM_DELETE, Vec::new(), Vec::new()));
117    Ok(Status::SUCCESS)
118}
119
120/// RENAME (0x07) — also used by NT_RENAME level 1.
121pub async fn rename(
122    io: &mut IoCtx<'_>,
123    req: &ReqView<'_>,
124    bodies: &mut Vec<RespBody>,
125) -> Result<Status, Status> {
126    let vfs = share_vfs(io, req.hdr.tid);
127    let (old, new) = read_two_paths(req);
128    if new.is_empty() {
129        return Err(Status::INVALID_PARAMETER);
130    }
131    vfs.rename(&old, &new).await.map_err(vfs_err)?;
132    bodies.push(RespBody::new(consts::COM_RENAME, Vec::new(), Vec::new()));
133    Ok(Status::SUCCESS)
134}
135
136/// QUERY_INFORMATION (0x08): attributes + DOS time + size.
137/// QUERY_INFORMATION (0x08): pack size/attributes and the DOS date/time word
138/// ([MS-DTYP] §2.3.4/5 bit-packing uses fixed calendar/field constants).
139#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))]
140pub async fn query_info_legacy(
141    io: &mut IoCtx<'_>,
142    req: &ReqView<'_>,
143    bodies: &mut Vec<RespBody>,
144) -> Result<Status, Status> {
145    let vfs = share_vfs(io, req.hdr.tid);
146    let path = read_path(req);
147    let m = vfs.stat(&path).await.map_err(vfs_err)?;
148    let (secs, _) = smb_server_proto::types::FileTime(m.times[2].0).to_unix();
149
150    // DOS date/time packing ([MS-DTYP] §2.3.4/5).
151    let secs = secs.max(0);
152    let days = secs / 86400;
153    let tod = secs % 86400;
154    let (year, month, day) = civil_from_days(days);
155    let hours = tod / 3600;
156    let minutes = (tod % 3600) / 60;
157    let seconds = tod % 60 / 2 * 2;
158    let dos_time = ((hours as u32) << 11) | ((minutes as u32) << 5) | (seconds as u32);
159    let dos_date = (((year - 1980).clamp(0, 127) as u32) << 9) | (month << 5) | day;
160
161    let attrs = if m.is_dir {
162        0x10u16
163    } else if m.attrs.0 & smb_server_proto::types::AttrFlags::READONLY != 0 {
164        0x21u16
165    } else {
166        0x20u16
167    };
168    let resp = legacy_codec::QueryInfoResp {
169        attrs,
170        dos_time: dos_date | dos_time,
171        size: m.eof as u32,
172    };
173    bodies.push(RespBody::new(
174        consts::COM_QUERY_INFORMATION,
175        resp.encode(),
176        Vec::new(),
177    ));
178    Ok(Status::SUCCESS)
179}
180
181/// SET_INFORMATION (0x09): apply the last-write time (Unix->FILETIME epoch math).
182#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))]
183pub async fn set_info_legacy(
184    io: &mut IoCtx<'_>,
185    req: &ReqView<'_>,
186    bodies: &mut Vec<RespBody>,
187) -> Result<Status, Status> {
188    let vfs = share_vfs(io, req.hdr.tid);
189    let path = read_path(req);
190    let w = req.words;
191    if w.len() >= 6 {
192        let utime = u32::from_le_bytes([w[2], w[3], w[4], w[5]]);
193        if utime != 0 && utime != u32::MAX {
194            let ft = smb_server_proto::types::FileTime((utime as u64 + 11_644_473_600) * 10_000_000);
195            vfs.set_info_path(
196                &path,
197                &SetOp::Basic {
198                    access: None,
199                    write: Some(ft),
200                },
201            )
202            .await
203            .map_err(vfs_err)?;
204        }
205    }
206    bodies.push(RespBody::new(
207        consts::COM_SET_INFORMATION,
208        Vec::new(),
209        Vec::new(),
210    ));
211    Ok(Status::SUCCESS)
212}
213
214// ---- helpers ----
215
216/// Split a find pattern into `(directory, filename-pattern)` parts.
217pub fn split_pattern_pub(pattern: &str) -> (String, String) {
218    let p = pattern.trim_start_matches(['\\', '/']);
219    match p.rfind(['\\', '/']) {
220        Some(i) => (p[..i].to_string(), p[i + 1..].to_string()),
221        None => (String::new(), p.to_string()),
222    }
223}
224
225pub(crate) fn join_rel(base: &str, name: &str) -> String {
226    let name = name.trim_start_matches(['\\', '/']);
227    if base.is_empty() {
228        name.to_string()
229    } else {
230        format!("{}\\{}", base.trim_end_matches(['\\', '/']), name)
231    }
232}
233
234pub(crate) fn vfs_err(e: smb_server_vfs::VfsError) -> Status {
235    use smb_server_vfs::VfsError as E;
236    match e {
237        E::NotFound => Status::OBJECT_PATH_NOT_FOUND,
238        E::AlreadyExists => Status::OBJECT_NAME_COLLISION,
239        E::AccessDenied => Status::ACCESS_DENIED,
240        E::DirectoryNotEmpty => Status::DIRECTORY_NOT_EMPTY,
241        E::InvalidArgument => Status::INVALID_PARAMETER,
242        E::NotSupported => Status::NOT_IMPLEMENTED,
243        E::StoppedOnSymlink { .. } => Status::STOPPED_ON_SYMLINK,
244        E::Io(_) => Status::UNSUCCESSFUL,
245    }
246}
247
248/// Gregorian civil date from a Unix day number (Howard Hinnant's `civil_from_days`
249/// algorithm; its era/day constants are canonical to the algorithm).
250#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))]
251fn civil_from_days(z: i64) -> (i64, u32, u32) {
252    let z = z + 719_468;
253    let era = z.div_euclid(146_097);
254    let doe = (z - era * 146_097) as u64;
255    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
256    let y = yoe as i64 + era * 400;
257    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
258    let mp = (5 * doy + 2) / 153;
259    let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
260    let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
261    (y + if m <= 2 { 1 } else { 0 }, m, d)
262}