Skip to main content

smb2_client/
msg.rs

1//! SMB2 request bodies and response parsers (MS-SMB2 §2.2). Offsets in the on-wire
2//! `*Offset` fields are measured from the start of the SMB2 header (i.e. `64 + body_off`).
3
4use crate::{Result, SmbError};
5
6fn utf16le(s: &str) -> Vec<u8> {
7    s.encode_utf16().flat_map(u16::to_le_bytes).collect()
8}
9fn u16(b: &[u8], o: usize) -> u16 {
10    u16::from_le_bytes([b[o], b[o + 1]])
11}
12fn u32(b: &[u8], o: usize) -> u32 {
13    u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]])
14}
15
16// ---- NEGOTIATE (§2.2.3) ---------------------------------------------------
17
18/// Offer dialect 2.1.0 with a random client GUID.
19pub fn negotiate(client_guid: &[u8; 16]) -> Vec<u8> {
20    // Offer SMB 2.0.2 (Server 2008/R2) and 2.1.0. The server picks the highest it supports and
21    // negotiates *down*, so this reaches 2008 through 2025 (2012/2016/2019/2022/2025 all accept
22    // 2.1.0 — validated live against Server 2025). Both sign with HMAC-SHA256.
23    //
24    // SMB 3.0.x (AES-CMAC) support exists in header.rs (sign_v3 / kdf_signing_key) and the
25    // client branches on the negotiated dialect, but 3.x is not offered yet — it's only needed
26    // for servers hardened to refuse SMB2 entirely, and the CMAC path isn't validated.
27    let dialects: [u16; 2] = [0x0202, 0x0210];
28    let mut b = Vec::new();
29    b.extend_from_slice(&36u16.to_le_bytes()); // StructureSize
30    b.extend_from_slice(&(dialects.len() as u16).to_le_bytes()); // DialectCount
31    b.extend_from_slice(&0x0001u16.to_le_bytes()); // SecurityMode = SIGNING_ENABLED
32    b.extend_from_slice(&0u16.to_le_bytes()); // Reserved
33    b.extend_from_slice(&0u32.to_le_bytes()); // Capabilities
34    b.extend_from_slice(client_guid);
35    b.extend_from_slice(&0u64.to_le_bytes()); // ClientStartTime
36    for dialect in dialects {
37        b.extend_from_slice(&dialect.to_le_bytes());
38    }
39    b
40}
41
42// ---- SESSION_SETUP (§2.2.5 / §2.2.6) --------------------------------------
43
44/// The security buffer holds a raw NTLMSSP token.
45pub fn session_setup(token: &[u8]) -> Vec<u8> {
46    let mut b = Vec::new();
47    b.extend_from_slice(&25u16.to_le_bytes()); // StructureSize
48    b.push(0); // Flags
49    b.push(0x01); // SecurityMode = SIGNING_ENABLED
50    b.extend_from_slice(&0u32.to_le_bytes()); // Capabilities
51    b.extend_from_slice(&0u32.to_le_bytes()); // Channel
52    let sec_off = 64u16 + 24; // header + fixed part
53    b.extend_from_slice(&sec_off.to_le_bytes()); // SecurityBufferOffset
54    b.extend_from_slice(&(token.len() as u16).to_le_bytes()); // SecurityBufferLength
55    b.extend_from_slice(&0u64.to_le_bytes()); // PreviousSessionId
56    b.extend_from_slice(token);
57    b
58}
59
60/// Extract the security buffer (server NTLM token) from a SESSION_SETUP response.
61pub fn session_setup_token(msg: &[u8]) -> Result<Vec<u8>> {
62    // body starts at 64; StructureSize(2), SessionFlags(2), SecBufOffset(2), SecBufLength(2)
63    let body = msg.get(64..).ok_or(SmbError::Truncated)?;
64    let off = u16(body, 4) as usize; // from SMB header start
65    let len = u16(body, 6) as usize;
66    msg.get(off..off + len)
67        .map(|s| s.to_vec())
68        .ok_or(SmbError::Truncated)
69}
70
71// ---- TREE_CONNECT (§2.2.9) ------------------------------------------------
72
73pub fn tree_connect(path: &str) -> Vec<u8> {
74    let name = utf16le(path);
75    let mut b = Vec::new();
76    b.extend_from_slice(&9u16.to_le_bytes()); // StructureSize
77    b.extend_from_slice(&0u16.to_le_bytes()); // Reserved/Flags
78    let path_off = 64u16 + 8;
79    b.extend_from_slice(&path_off.to_le_bytes()); // PathOffset
80    b.extend_from_slice(&(name.len() as u16).to_le_bytes()); // PathLength
81    b.extend_from_slice(&name);
82    b
83}
84
85// ---- CREATE (§2.2.13 / §2.2.14) -------------------------------------------
86
87/// Open a named pipe (e.g. "samr") on the IPC$ tree.
88pub fn create_pipe(name: &str) -> Vec<u8> {
89    let n = utf16le(name);
90    let mut b = Vec::new();
91    b.extend_from_slice(&57u16.to_le_bytes()); // StructureSize
92    b.push(0); // SecurityFlags
93    b.push(0); // RequestedOplockLevel
94    b.extend_from_slice(&2u32.to_le_bytes()); // ImpersonationLevel = Impersonation
95    b.extend_from_slice(&0u64.to_le_bytes()); // SmbCreateFlags
96    b.extend_from_slice(&0u64.to_le_bytes()); // Reserved
97    b.extend_from_slice(&0x0012_019Fu32.to_le_bytes()); // DesiredAccess: read+write data/EA/attrs (WRITE needs FILE_WRITE_DATA for a fire-and-forget AUTH3)
98    b.extend_from_slice(&0u32.to_le_bytes()); // FileAttributes
99    b.extend_from_slice(&0x0000_0007u32.to_le_bytes()); // ShareAccess = R|W|D
100    b.extend_from_slice(&0x0000_0001u32.to_le_bytes()); // CreateDisposition = OPEN
101    b.extend_from_slice(&0u32.to_le_bytes()); // CreateOptions
102    let name_off = 64u16 + 56;
103    b.extend_from_slice(&name_off.to_le_bytes()); // NameOffset
104    b.extend_from_slice(&(n.len() as u16).to_le_bytes()); // NameLength
105    b.extend_from_slice(&0u32.to_le_bytes()); // CreateContextsOffset
106    b.extend_from_slice(&0u32.to_le_bytes()); // CreateContextsLength
107    b.extend_from_slice(&n);
108    b
109}
110
111/// Generic disk-file CREATE (§2.2.13). `path` is relative to the connected share root (no
112/// leading backslash). Callers pass the access mask, share mode, disposition, and options.
113pub fn create_file(path: &str, access: u32, share: u32, disposition: u32, options: u32) -> Vec<u8> {
114    let n = utf16le(path);
115    let mut b = Vec::new();
116    b.extend_from_slice(&57u16.to_le_bytes()); // StructureSize
117    b.push(0); // SecurityFlags
118    b.push(0); // RequestedOplockLevel
119    b.extend_from_slice(&2u32.to_le_bytes()); // ImpersonationLevel = Impersonation
120    b.extend_from_slice(&0u64.to_le_bytes()); // SmbCreateFlags
121    b.extend_from_slice(&0u64.to_le_bytes()); // Reserved
122    b.extend_from_slice(&access.to_le_bytes()); // DesiredAccess
123    b.extend_from_slice(&0u32.to_le_bytes()); // FileAttributes (ignored on OPEN)
124    b.extend_from_slice(&share.to_le_bytes()); // ShareAccess
125    b.extend_from_slice(&disposition.to_le_bytes()); // CreateDisposition
126    b.extend_from_slice(&options.to_le_bytes()); // CreateOptions
127    // NameOffset always points at the buffer position, and the variable buffer
128    // is always present (≥1 byte). Opening the share root (empty name) needs
129    // NameLength=0 but a NameOffset that still addresses a real byte in the
130    // message plus that mandatory padding byte — Windows returns
131    // STATUS_INVALID_PARAMETER for a 57-byte body whose name buffer is absent.
132    let name_off = 64u16 + 56;
133    b.extend_from_slice(&name_off.to_le_bytes()); // NameOffset
134    b.extend_from_slice(&(n.len() as u16).to_le_bytes()); // NameLength
135    b.extend_from_slice(&0u32.to_le_bytes()); // CreateContextsOffset
136    b.extend_from_slice(&0u32.to_le_bytes()); // CreateContextsLength
137    if n.is_empty() {
138        b.push(0); // mandatory 1-byte Buffer when there is no name
139    } else {
140        b.extend_from_slice(&n);
141    }
142    b
143}
144
145/// SMB2 READ (§2.2.19): read `length` bytes at `offset` from the open file.
146pub fn read_req(file_id: &[u8; 16], offset: u64, length: u32) -> Vec<u8> {
147    let mut b = Vec::new();
148    b.extend_from_slice(&49u16.to_le_bytes()); // StructureSize
149    b.push(0); // Padding
150    b.push(0); // Flags
151    b.extend_from_slice(&length.to_le_bytes()); // Length
152    b.extend_from_slice(&offset.to_le_bytes()); // Offset
153    b.extend_from_slice(file_id);
154    b.extend_from_slice(&0u32.to_le_bytes()); // MinimumCount
155    b.extend_from_slice(&0u32.to_le_bytes()); // Channel
156    b.extend_from_slice(&0u32.to_le_bytes()); // RemainingBytes
157    b.extend_from_slice(&0u16.to_le_bytes()); // ReadChannelInfoOffset
158    b.extend_from_slice(&0u16.to_le_bytes()); // ReadChannelInfoLength
159    b.push(0); // Buffer (min 1 byte)
160    b
161}
162
163/// Extract the data returned by a READ response (§2.2.20).
164pub fn read_output(msg: &[u8]) -> Result<Vec<u8>> {
165    let body = msg.get(64..).ok_or(SmbError::Truncated)?;
166    let data_off = *body.get(2).ok_or(SmbError::Truncated)? as usize; // DataOffset, from header start
167    let data_len = u32(body, 4) as usize;
168    msg.get(data_off..data_off + data_len)
169        .map(|s| s.to_vec())
170        .ok_or(SmbError::Truncated)
171}
172
173/// SMB2 WRITE (§2.2.21): write `data` to the open handle at `offset`.
174pub fn write_req(file_id: &[u8; 16], offset: u64, data: &[u8]) -> Vec<u8> {
175    let mut b = Vec::new();
176    b.extend_from_slice(&49u16.to_le_bytes()); // StructureSize
177    b.extend_from_slice(&(64u16 + 48).to_le_bytes()); // DataOffset (header + 48-byte body)
178    b.extend_from_slice(&(data.len() as u32).to_le_bytes()); // Length
179    b.extend_from_slice(&offset.to_le_bytes()); // Offset
180    b.extend_from_slice(file_id);
181    b.extend_from_slice(&0u32.to_le_bytes()); // Channel
182    b.extend_from_slice(&0u32.to_le_bytes()); // RemainingBytes
183    b.extend_from_slice(&0u16.to_le_bytes()); // WriteChannelInfoOffset
184    b.extend_from_slice(&0u16.to_le_bytes()); // WriteChannelInfoLength
185    b.extend_from_slice(&0u32.to_le_bytes()); // Flags
186    b.extend_from_slice(data);
187    b
188}
189
190/// SMB2 CLOSE (§2.2.15).
191pub fn close_req(file_id: &[u8; 16]) -> Vec<u8> {
192    let mut b = Vec::new();
193    b.extend_from_slice(&24u16.to_le_bytes()); // StructureSize
194    b.extend_from_slice(&0u16.to_le_bytes()); // Flags
195    b.extend_from_slice(&0u32.to_le_bytes()); // Reserved
196    b.extend_from_slice(file_id);
197    b
198}
199
200/// FileId (16 bytes) from a CREATE response.
201pub fn create_file_id(msg: &[u8]) -> Result<[u8; 16]> {
202    // FileId sits at body offset 64 → absolute 128.
203    msg.get(128..144)
204        .map(|s| s.try_into().unwrap())
205        .ok_or(SmbError::Truncated)
206}
207
208/// One entry from a directory enumeration (FileDirectoryInformation, class 1).
209#[derive(Clone, Debug, PartialEq, Eq)]
210pub struct DirEntry {
211    pub name: String,
212    pub is_dir: bool,
213    pub size: u64,
214}
215
216/// SMB2 QUERY_DIRECTORY (§2.2.33): enumerate an open directory handle using
217/// FileDirectoryInformation (class 1). `pattern` is the search wildcard
218/// (typically `*`); on continuation calls the server ignores it and resumes
219/// from where the handle left off, so passing `*` every time is correct.
220pub fn query_directory_req(file_id: &[u8; 16], pattern: &str, output_len: u32) -> Vec<u8> {
221    const FILE_DIRECTORY_INFORMATION: u8 = 0x01;
222    let n = utf16le(pattern);
223    let mut b = Vec::new();
224    b.extend_from_slice(&33u16.to_le_bytes()); // StructureSize (fixed 33)
225    b.push(FILE_DIRECTORY_INFORMATION); // FileInformationClass
226    b.push(0); // Flags (0: resume from handle position)
227    b.extend_from_slice(&0u32.to_le_bytes()); // FileIndex
228    b.extend_from_slice(file_id);
229    let name_off = 64u16 + 32; // header + 32-byte fixed body
230    b.extend_from_slice(&name_off.to_le_bytes()); // FileNameOffset
231    b.extend_from_slice(&(n.len() as u16).to_le_bytes()); // FileNameLength
232    b.extend_from_slice(&output_len.to_le_bytes()); // OutputBufferLength
233    if n.is_empty() {
234        b.push(0); // Buffer min 1 byte
235    } else {
236        b.extend_from_slice(&n);
237    }
238    b
239}
240
241/// Parse a QUERY_DIRECTORY response (§2.2.34) carrying FileDirectoryInformation
242/// entries. Bounds-checked and loop-bounded: a hostile server cannot drive an
243/// out-of-range read or a non-terminating walk (a NextEntryOffset that fails to
244/// advance, or an entry claiming a name longer than the buffer, ends parsing).
245pub fn parse_directory_info(msg: &[u8]) -> Result<Vec<DirEntry>> {
246    const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10;
247    let body = msg.get(64..).ok_or(SmbError::Truncated)?;
248    // Response fixed part: StructureSize(2), OutputBufferOffset(2), OutputBufferLength(4).
249    // Guard its 8 bytes before the direct-indexing u16/u32 helpers touch them —
250    // a truncated response must return empty, not panic.
251    if body.len() < 8 {
252        return Ok(Vec::new());
253    }
254    let out_off = u16(body, 2) as usize; // from header start
255    let out_len = u32(body, 4) as usize;
256    let buf = msg
257        .get(out_off..out_off.checked_add(out_len).ok_or(SmbError::Truncated)?)
258        .ok_or(SmbError::Truncated)?;
259
260    let mut entries = Vec::new();
261    let mut pos = 0usize;
262    // Cap iterations well above any real directory to bound a malformed chain.
263    for _ in 0..100_000 {
264        let rec = match buf.get(pos..) {
265            Some(r) if r.len() >= 64 => r,
266            _ => break,
267        };
268        let next = u32(rec, 0) as usize; // NextEntryOffset
269        let attrs = u32(rec, 56); // FileAttributes
270        let name_len = u32(rec, 60) as usize; // FileNameLength (bytes)
271                                              // FileName starts at fixed offset 64 within the record.
272        if let Some(name_bytes) = rec.get(64..64usize.saturating_add(name_len)) {
273            let units: Vec<u16> = name_bytes
274                .chunks_exact(2)
275                .map(|c| u16::from_le_bytes([c[0], c[1]]))
276                .collect();
277            let name = String::from_utf16_lossy(&units);
278            if name != "." && name != ".." && !name.is_empty() {
279                entries.push(DirEntry {
280                    name,
281                    is_dir: attrs & FILE_ATTRIBUTE_DIRECTORY != 0,
282                    size: u64::from_le_bytes(
283                        rec.get(40..48)
284                            .and_then(|s| s.try_into().ok())
285                            .unwrap_or([0; 8]),
286                    ),
287                });
288            }
289        } else {
290            break; // name overruns the record → stop, don't read OOB
291        }
292        if next == 0 {
293            break; // last entry
294        }
295        // NextEntryOffset must strictly advance, else a hostile 0-cycle loops forever.
296        pos = match pos.checked_add(next) {
297            Some(p) if p > pos => p,
298            _ => break,
299        };
300    }
301    Ok(entries)
302}
303
304// ---- IOCTL (§2.2.31 / §2.2.32) --------------------------------------------
305
306pub const FSCTL_PIPE_TRANSCEIVE: u32 = 0x0011_C017;
307
308/// Send `input` through the pipe and read the response in one round trip.
309pub fn ioctl_transceive(file_id: &[u8; 16], input: &[u8]) -> Vec<u8> {
310    let mut b = Vec::new();
311    b.extend_from_slice(&57u16.to_le_bytes()); // StructureSize
312    b.extend_from_slice(&0u16.to_le_bytes()); // Reserved
313    b.extend_from_slice(&FSCTL_PIPE_TRANSCEIVE.to_le_bytes()); // CtlCode
314    b.extend_from_slice(file_id);
315    let input_off = 64u32 + 56;
316    b.extend_from_slice(&input_off.to_le_bytes()); // InputOffset
317    b.extend_from_slice(&(input.len() as u32).to_le_bytes()); // InputCount
318    b.extend_from_slice(&0u32.to_le_bytes()); // MaxInputResponse
319    b.extend_from_slice(&input_off.to_le_bytes()); // OutputOffset
320    b.extend_from_slice(&0u32.to_le_bytes()); // OutputCount
321    b.extend_from_slice(&0x0001_0000u32.to_le_bytes()); // MaxOutputResponse (64 KiB — SMB2.1 max transact)
322    b.extend_from_slice(&0x0000_0001u32.to_le_bytes()); // Flags = IS_FSCTL
323    b.extend_from_slice(&0u32.to_le_bytes()); // Reserved2
324    b.extend_from_slice(input);
325    b
326}
327
328/// Extract the pipe output (RPC response bytes) from an IOCTL response.
329pub fn ioctl_output(msg: &[u8]) -> Result<Vec<u8>> {
330    // response body: StructureSize(2) Reserved(2) CtlCode(4) FileId(16)
331    // InputOffset(4) InputCount(4) OutputOffset(4) OutputCount(4) ...
332    let body = msg.get(64..).ok_or(SmbError::Truncated)?;
333    let out_off = u32(body, 32) as usize; // from SMB header start
334    let out_len = u32(body, 36) as usize;
335    msg.get(out_off..out_off + out_len)
336        .map(|s| s.to_vec())
337        .ok_or(SmbError::Truncated)
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    #[test]
345    fn negotiate_offers_dialect_210() {
346        let b = negotiate(&[0; 16]);
347        assert_eq!(u16(&b, 0), 36); // StructureSize
348        assert_eq!(u16(&b, 2), 2); // DialectCount (2.0.2 + 2.1.0)
349                                   // dialects at 36 (fixed part) — after 4+2+2+4+16+8 = 36
350        assert_eq!(u16(&b, 36), 0x0202);
351        assert_eq!(u16(&b, 38), 0x0210);
352    }
353
354    #[test]
355    fn create_pipe_name_offset_correct() {
356        let b = create_pipe("samr");
357        assert_eq!(u16(&b, 0), 57);
358        assert_eq!(u16(&b, 44), 64 + 56); // NameOffset field
359        assert_eq!(u16(&b, 46), 8); // "samr" = 4 wchar * 2
360    }
361
362    #[test]
363    fn ioctl_uses_transceive_ctlcode() {
364        let b = ioctl_transceive(&[0; 16], &[1, 2, 3]);
365        assert_eq!(u32(&b, 4), FSCTL_PIPE_TRANSCEIVE);
366        assert_eq!(u32(&b, 28), 3); // InputCount
367    }
368
369    #[test]
370    fn query_directory_req_shape() {
371        let b = query_directory_req(&[0; 16], "*", 0x1_0000);
372        assert_eq!(u16(&b, 0), 33); // StructureSize
373        assert_eq!(b[2], 0x01); // FileInformationClass = FileDirectoryInformation
374        assert_eq!(u16(&b, 24), 64 + 32); // FileNameOffset
375        assert_eq!(u16(&b, 26), 2); // FileNameLength ("*" = 1 wchar × 2)
376        assert_eq!(u32(&b, 28), 0x1_0000); // OutputBufferLength
377    }
378
379    // Hand-build a QUERY_DIRECTORY response with two FileDirectoryInformation
380    // records (a directory "Policies" and a file "GptTmpl.inf") plus the "."/".."
381    // entries that must be filtered. Validates the NDR-free info walk.
382    #[test]
383    fn parse_directory_info_reads_entries_and_filters_dot() {
384        fn rec(out: &mut Vec<u8>, next: u32, attrs: u32, size: u64, name: &str) {
385            let units: Vec<u16> = name.encode_utf16().collect();
386            let name_bytes: Vec<u8> = units.iter().flat_map(|u| u.to_le_bytes()).collect();
387            let start = out.len();
388            out.extend_from_slice(&next.to_le_bytes()); // 0 NextEntryOffset
389            out.extend_from_slice(&0u32.to_le_bytes()); // 4 FileIndex
390            out.extend_from_slice(&[0u8; 32]); // 8..40 four FILETIMEs
391            out.extend_from_slice(&size.to_le_bytes()); // 40 EndOfFile
392            out.extend_from_slice(&0u64.to_le_bytes()); // 48 AllocationSize
393            out.extend_from_slice(&attrs.to_le_bytes()); // 56 FileAttributes
394            out.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes()); // 60 FileNameLength
395            out.extend_from_slice(&name_bytes); // 64.. FileName
396            if next != 0 {
397                // pad this record out to exactly `next` bytes
398                while out.len() - start < next as usize {
399                    out.push(0);
400                }
401            }
402        }
403        let mut buf = Vec::new();
404        rec(&mut buf, 72, 0x10, 0, "."); // filtered
405        rec(&mut buf, 72, 0x10, 0, ".."); // filtered
406        rec(&mut buf, 80, 0x10, 0, "Policies"); // dir
407        rec(&mut buf, 0, 0x20, 1234, "GptTmpl.inf"); // file (last)
408
409        // Wrap in an SMB2 response: 64-byte header + fixed part (StructureSize,
410        // OutputBufferOffset, OutputBufferLength), then the buffer.
411        let out_off = 64u16 + 8;
412        let mut msg = vec![0u8; 64];
413        msg.extend_from_slice(&9u16.to_le_bytes()); // StructureSize
414        msg.extend_from_slice(&out_off.to_le_bytes()); // OutputBufferOffset
415        msg.extend_from_slice(&(buf.len() as u32).to_le_bytes()); // OutputBufferLength
416        msg.extend_from_slice(&buf);
417
418        let entries = parse_directory_info(&msg).unwrap();
419        assert_eq!(entries.len(), 2);
420        assert_eq!(entries[0].name, "Policies");
421        assert!(entries[0].is_dir);
422        assert_eq!(entries[1].name, "GptTmpl.inf");
423        assert!(!entries[1].is_dir);
424        assert_eq!(entries[1].size, 1234);
425    }
426
427    #[test]
428    fn parse_directory_info_survives_hostile_input() {
429        // Truncated / zero buffers must not panic.
430        for cut in 0..80 {
431            let _ = parse_directory_info(&vec![0u8; cut]);
432        }
433        // A record whose NextEntryOffset does not advance (0-cycle guard) and a
434        // name_len that overruns the record must terminate, not loop/OOB.
435        let mut buf = vec![0u8; 64];
436        buf[60..64].copy_from_slice(&0xFFFF_FFFFu32.to_le_bytes()); // FileNameLength = u32::MAX
437        let out_off = 64u16 + 8;
438        let mut msg = vec![0u8; 64];
439        msg.extend_from_slice(&9u16.to_le_bytes());
440        msg.extend_from_slice(&out_off.to_le_bytes());
441        msg.extend_from_slice(&(buf.len() as u32).to_le_bytes());
442        msg.extend_from_slice(&buf);
443        let entries = parse_directory_info(&msg).unwrap();
444        assert!(entries.is_empty()); // name overrun → skipped, next=0 → stop
445    }
446
447    #[test]
448    fn create_file_carries_access_and_options() {
449        let b = create_file("Windows\\Temp\\x.out", 0x0013_0081, 0x7, 1, 0x1060);
450        assert_eq!(u16(&b, 0), 57); // StructureSize
451        assert_eq!(u32(&b, 24), 0x0013_0081); // DesiredAccess
452        assert_eq!(u32(&b, 32), 0x7); // ShareAccess
453        assert_eq!(u32(&b, 36), 1); // CreateDisposition = FILE_OPEN
454        assert_eq!(u32(&b, 40), 0x1060); // CreateOptions (incl DELETE_ON_CLOSE)
455        assert_eq!(u16(&b, 44), 64 + 56); // NameOffset
456        assert_eq!(
457            u16(&b, 46),
458            "Windows\\Temp\\x.out".chars().count() as u16 * 2
459        );
460    }
461
462    #[test]
463    fn read_req_offset_and_length() {
464        let b = read_req(&[0xAB; 16], 0x1_0000, 0x4000);
465        assert_eq!(u16(&b, 0), 49); // StructureSize
466        assert_eq!(u32(&b, 4), 0x4000); // Length
467        assert_eq!(u32(&b, 8), 0x1_0000); // Offset (low dword)
468        assert_eq!(&b[16..32], &[0xAB; 16]); // FileId
469    }
470
471    #[test]
472    fn close_req_shape() {
473        let b = close_req(&[0xCD; 16]);
474        assert_eq!(u16(&b, 0), 24); // StructureSize
475        assert_eq!(&b[8..24], &[0xCD; 16]); // FileId
476    }
477}