Skip to main content

smb_server_proto_smb1/
find.rs

1//! Directory enumeration entry encoding for FIND_FIRST2/FIND_NEXT2 replies
2//! ([MS-SMB] §2.2.8.1 information levels).
3
4use smb_server_proto::types::FileTime;
5
6/// Neutral directory-entry input for the encoder (kept free of storage-layer
7/// types so the protocol crate stays self-contained).
8#[derive(Debug, Clone)]
9pub struct FindEntry {
10    /// File name as presented to clients.
11    pub name: String,
12    /// Metadata snapshot.
13    pub meta: crate::query::QueryMeta,
14}
15
16/// Fixed portion size per level, matching the fields written below.
17#[cfg(test)]
18const fn fixed_size(level: u16) -> usize {
19    match level {
20        // NextEntryOffset + FileIndex + 4×FILETIME + EOF + Alloc + Attrs
21        // + NameLength ([MS-CIFS] §2.2.8.1.2).
22        crate::find_level::DIRECTORY => 64,
23        // …+ EaSize + Reserved ([MS-CIFS] §2.2.8.1.3).
24        crate::find_level::FULL => 68,
25        // …+ EaSize + ShortNameLen + Reserved + ShortName[12]
26        // ([MS-CIFS] §2.2.8.1.7).
27        crate::find_level::BOTH => 94,
28        // NextEntryOffset + FileIndex + NameLength ([MS-CIFS] §2.2.8.1.4).
29        crate::find_level::NAMES => 12,
30        // DOS form incl. resume key ([MS-CIFS] §2.2.8.1.1).
31        crate::find_level::STANDARD => 31,
32        _ => 94,
33    }
34}
35
36/// Encode one entry's fixed fields after its NextEntryOffset slot.
37fn push_fixed(buf: &mut Vec<u8>, level: u16, m: &crate::query::QueryMeta, name_len: usize) {
38    match level {
39        crate::find_level::STANDARD => {
40            // Resume-key slot doubles as FileIndex placeholder.
41            buf.extend_from_slice(&0u32.to_le_bytes());
42            for t in &m.times[..3] {
43                let ft = FileTime(*t);
44                let (secs, _) = ft.to_unix();
45                buf.extend_from_slice(&(secs as u32).to_le_bytes());
46            }
47            buf.extend_from_slice(&(m.eof as u32).to_le_bytes());
48            buf.extend_from_slice(&(m.alloc as u32).to_le_bytes());
49            buf.extend_from_slice(&m.attrs.0.to_le_bytes());
50            buf.push(name_len as u8);
51        }
52        crate::find_level::NAMES => {
53            buf.extend_from_slice(&0u32.to_le_bytes()); // FileIndex
54            buf.extend_from_slice(&(name_len as u32).to_le_bytes());
55        }
56        crate::find_level::DIRECTORY => {
57            buf.extend_from_slice(&0u32.to_le_bytes()); // FileIndex
58            for t in &m.times {
59                buf.extend_from_slice(&t.to_le_bytes());
60            }
61            buf.extend_from_slice(&m.eof.to_le_bytes());
62            buf.extend_from_slice(&m.alloc.to_le_bytes());
63            buf.extend_from_slice(&m.attrs.0.to_le_bytes());
64            buf.extend_from_slice(&(name_len as u32).to_le_bytes());
65        }
66        _ => {
67            // FULL / BOTH
68            buf.extend_from_slice(&0u32.to_le_bytes()); // FileIndex
69            for t in &m.times {
70                buf.extend_from_slice(&t.to_le_bytes());
71            }
72            buf.extend_from_slice(&m.eof.to_le_bytes());
73            buf.extend_from_slice(&m.alloc.to_le_bytes());
74            buf.extend_from_slice(&m.attrs.0.to_le_bytes());
75            buf.extend_from_slice(&(name_len as u32).to_le_bytes());
76            buf.extend_from_slice(&0u32.to_le_bytes()); // EaSize
77            if level == crate::find_level::BOTH {
78                buf.push(0); // ShortNameLength
79                buf.push(0); // Reserved
80                buf.extend_from_slice(&[0u8; 24]); // ShortName slot
81            }
82        }
83    }
84}
85
86/// Encode `entries` into the chained NextEntryOffset form used by
87/// FIND_FIRST2/NEXT2 responses. Returns the buffer plus the offset of the
88/// last entry (`LastNameOffset`).
89///
90/// The final entry carries `NextEntryOffset == 0`; intermediate strides are
91/// 4-byte aligned as clients require. Names are UTF-16LE when the client
92/// negotiated Unicode, else OEM bytes.
93pub fn encode_entries(
94    entries: &[FindEntry],
95    level: u16,
96    unicode: bool,
97) -> (Vec<u8>, Option<usize>) {
98    let mut buf: Vec<u8> = Vec::new();
99    let mut last_off = None;
100
101    for (i, e) in entries.iter().enumerate() {
102        let start = buf.len();
103        let m = &e.meta;
104        let name_bytes: Vec<u8> = if unicode {
105            let mut v = Vec::with_capacity(e.name.len() * 2);
106            for u in e.name.encode_utf16() {
107                v.extend_from_slice(&u.to_le_bytes());
108            }
109            v
110        } else {
111            e.name.as_bytes().to_vec()
112        };
113
114        // Reserve the NextEntryOffset slot; patched once the stride is known.
115        let next_pos = buf.len();
116        buf.extend_from_slice(&0u32.to_le_bytes());
117
118        push_fixed(&mut buf, level, m, name_bytes.len());
119
120        // DOS-standard records carry a trailing NUL after the name and are
121        // byte-packed; NT levels pad to a 4-byte boundary.
122        buf.extend_from_slice(&name_bytes);
123        if level == crate::find_level::STANDARD {
124            buf.push(0);
125        }
126
127        let stride = buf.len() - start;
128        if i + 1 < entries.len() {
129            let padded = (stride + 3) & !3usize;
130            while buf.len() < start + padded {
131                buf.push(0);
132            }
133            let next = (buf.len() - start) as u32;
134            buf[next_pos..next_pos + 4].copy_from_slice(&next.to_le_bytes());
135        }
136        last_off = Some(start);
137    }
138    (buf, last_off)
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use crate::query::QueryMeta;
145
146    fn mk(n: &str) -> FindEntry {
147        FindEntry {
148            name: n.into(),
149            meta: QueryMeta {
150                times: [1, 2, 3, 4],
151                attrs: smb_server_proto::types::AttrFlags(0x20),
152                eof: 19,
153                alloc: 4096,
154                is_dir: false,
155            },
156        }
157    }
158
159    fn next_at(buf: &[u8], off: usize) -> u32 {
160        u32::from_le_bytes(buf[off..off + 4].try_into().unwrap())
161    }
162
163    #[test]
164    fn both_level_chain_is_consistent() {
165        let entries = vec![mk("a.txt"), mk("b.txt")];
166        let (buf, _) = encode_entries(&entries, crate::find_level::BOTH, true);
167
168        // Walk the NextEntryOffset chain: strides must be >= fixed size and
169        // land on the next entry until the terminator (0) on the LAST entry.
170        let mut off = 0usize;
171        let mut count = 0;
172        loop {
173            assert!(off + 4 <= buf.len(), "chain walked past end");
174            let next = next_at(&buf, off);
175            if next == 0 {
176                break;
177            }
178            assert!(next >= 94, "stride smaller than BOTH header");
179            count += 1;
180            off += next as usize;
181        }
182        count += 1; // terminator entry itself
183        assert_eq!(count, entries.len(), "every entry must be reachable");
184    }
185
186    #[test]
187    fn final_entry_has_zero_next_and_names_decode() {
188        let entries = vec![mk("alpha"), mk("beta"), mk("gamma")];
189        let levels = [
190            crate::find_level::DIRECTORY,
191            crate::find_level::FULL,
192            crate::find_level::BOTH,
193            crate::find_level::NAMES,
194        ];
195        for level in levels {
196            let fixed = fixed_size(level);
197            let (buf, last) =
198                encode_entries(&entries, level, true);
199            let mut off = 0usize;
200            for e in &entries {
201                let next = next_at(&buf, off);
202                let expect_units: Vec<u8> = e.name.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
203                // FileNameLength position differs per level: NT levels with
204                // an EaSize field keep it before that trailing u32; the DOS
205                // form uses a single length byte.
206                let namelen = match level {
207                    crate::find_level::FULL => u32::from_le_bytes(
208                        buf[off + fixed - 8..off + fixed - 4].try_into().unwrap(),
209                    ),
210                    crate::find_level::BOTH => u32::from_le_bytes(
211                        buf[off + fixed - 34..off + fixed - 30].try_into().unwrap(),
212                    ),
213                    crate::find_level::STANDARD => buf[off + fixed - 1] as u32,
214                    _ => {
215                        u32::from_le_bytes(buf[off + fixed - 4..off + fixed].try_into().unwrap())
216                    }
217                };
218                assert_eq!(namelen as usize, expect_units.len(), "level {level:#x}");
219                assert_eq!(
220                    &buf[off + fixed..off + fixed + expect_units.len()],
221                    &expect_units[..],
222                    "level {level:#x}"
223                );
224                if off == last.unwrap() {
225                    assert_eq!(next, 0, "last entry must terminate the chain");
226                } else {
227                    assert_eq!(next as usize % 4, 0, "strides must stay aligned");
228                    off += next as usize;
229                }
230            }
231        }
232    }
233}