Skip to main content

smb_server_proto_smb2/
commands.rs

1//! SMB2 command request/response codecs ([MS-SMB2] §2.2.13–§2.2.39).
2//!
3//! Every structure maps directly to its spec section (noted per item).
4//! Buffer fields (`NameOffset`, `DataOffset`, …) are absolute offsets from
5//! the frame start, so parsers take the **complete frame** including the
6//! 64-byte header.
7
8/// Byte offset where each request body begins.
9pub const BODY: usize = 64;
10
11/// SMB2 file identifier (16 bytes, replaces SMB1's 16-bit FID).
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub struct FileId(pub [u8; 16]);
14
15impl FileId {
16    /// All-zero file identifier.
17    pub const ZERO: FileId = FileId([0u8; 16]);
18    /// FileId length in bytes.
19    pub const LEN: usize = 16;
20    /// Wildcard FileId `{0xFF..}` a related compound request uses to inherit the
21    /// prior CREATE's handle ([MS-SMB2] §3.3.5.2.7.2).
22    pub const WILDCARD: [u8; 16] = [0xFF; 16];
23}
24
25fn g16(b: &[u8], o: usize) -> u16 {
26    b.get(o..o + 2)
27        .map(|s| u16::from_le_bytes([s[0], s[1]]))
28        .unwrap_or(0)
29}
30fn g32(b: &[u8], o: usize) -> u32 {
31    b.get(o..o + 4)
32        .map(|s| u32::from_le_bytes(s.try_into().unwrap()))
33        .unwrap_or(0)
34}
35fn g64(b: &[u8], o: usize) -> u64 {
36    b.get(o..o + 8)
37        .map(|s| u64::from_le_bytes(s.try_into().unwrap()))
38        .unwrap_or(0)
39}
40
41// ---------------- CREATE (§2.2.13 / §2.2.14) ----------------
42
43/// CREATE request fixed-part offsets.
44#[allow(dead_code)]
45mod create_off {
46    use super::BODY;
47
48    /// StructureSize (=57).
49    pub const STRUCT: usize = BODY;
50    /// RequestedOplockLevel (1 byte, after the 1-byte SecurityFlags).
51    pub const OPLOCK_LEVEL: usize = BODY + 3;
52    /// SecurityFlags(1) RequestedOplockLevel(1) BulkSecurity(4) — skip to:
53    /// ImpersonationLevel.
54    pub const IMPERSONATION: usize = BODY + 8;
55    /// SmbCreateFlags.
56    pub const CREATE_FLAGS: usize = BODY + 16;
57    /// DesiredAccess.
58    pub const DESIRED_ACCESS: usize = BODY + 24;
59    /// FileAttributes.
60    pub const ATTRIBUTES: usize = BODY + 28;
61    /// ShareAccess.
62    pub const SHARE_ACCESS: usize = BODY + 32;
63    /// CreateDisposition.
64    pub const DISPOSITION: usize = BODY + 36;
65    /// CreateOptions.
66    pub const OPTIONS: usize = BODY + 40;
67    /// NameOffset (absolute).
68    pub const NAME_OFFSET: usize = BODY + 44;
69    /// NameLength.
70    pub const NAME_LENGTH: usize = BODY + 46;
71    /// CreateContextsOffset.
72    pub const CTX_OFFSET: usize = BODY + 48;
73    /// CreateContextsLength.
74    pub const CTX_LENGTH: usize = BODY + 52;
75    /// Fixed part size (56 bytes).
76    pub const FIXED_END: usize = BODY + 56;
77}
78
79/// CREATE request body (§2.2.13).
80#[derive(Debug)]
81pub struct CreateReq {
82    /// Desired access mask.
83    pub desired_access: u32,
84    /// File attributes for new files.
85    pub attrs: u32,
86    /// Share access flags.
87    pub share_access: u32,
88    /// NT create disposition (SUPERSEDE/OPEN/CREATE/OPEN_IF/OVERWRITE/OVERWRITE_IF).
89    pub disposition: u32,
90    /// Create options (DIRECTORY_FILE, DELETE_ON_CLOSE, …).
91    pub options: u32,
92    /// Object name relative to share root (empty = share root itself).
93    pub name: String,
94    /// RequestedOplockLevel ([MS-SMB2] §2.2.13, [`oplock`]).
95    pub oplock_level: u8,
96    /// Parsed `RqLs` lease create-context, if the client requested a lease.
97    pub lease: Option<LeaseReq>,
98    /// Parsed durable-handle request/reconnect intent, if any.
99    pub durable: Option<DurableReq>,
100    /// Bitmask of durable create-context tags present ([`durable::tag`]);
101    /// used to reject conflicting durable-context combinations on reconnect.
102    pub durable_ctx_tags: u8,
103    /// SMB2_CREATE_APP_INSTANCE_ID ([MS-SMB2] §2.2.13.2.13), if present.
104    pub app_instance_id: Option<[u8; 16]>,
105    /// SMB2_CREATE_APP_INSTANCE_VERSION (high, low) ([MS-SMB2] §2.2.13.2.15),
106    /// 3.1.1 only, if present.
107    pub app_instance_version: Option<(u64, u64)>,
108}
109
110/// Durable-handle intent parsed from a CREATE's create-context chain
111/// ([MS-SMB2] §2.2.13.2.3/§2.2.13.2.4/§2.2.13.2.11/§2.2.13.2.12).
112#[derive(Debug, Clone, Copy)]
113pub enum DurableReq {
114    /// `DHnQ` v1 durable-handle request.
115    RequestV1,
116    /// `DH2Q` v2 durable-handle request.
117    RequestV2 {
118        /// Requested handle timeout in milliseconds (0 = server default).
119        timeout: u32,
120        /// Durable-handle flags (e.g. [`durable::FLAG_PERSISTENT`]).
121        flags: u32,
122        /// Client create-guid identifying the handle across reconnects.
123        create_guid: [u8; 16],
124    },
125    /// `DHnC` v1 reconnect carrying the persistent handle id.
126    ReconnectV1 {
127        /// Persistent id (original FileId) to reclaim.
128        file_id: [u8; 16],
129    },
130    /// `DH2C` v2 reconnect carrying the persistent id and create-guid.
131    ReconnectV2 {
132        /// Persistent id (original FileId) to reclaim.
133        file_id: [u8; 16],
134        /// Client create-guid that must match the preserved handle.
135        create_guid: [u8; 16],
136        /// Reconnect flags (`SMB2_DHANDLE_FLAG_PERSISTENT`).
137        flags: u32,
138    },
139}
140
141/// Durable-handle create-context name tags ([MS-SMB2] §2.2.13.2).
142pub mod durable {
143    /// `SMB2_CREATE_DURABLE_HANDLE_REQUEST`.
144    pub const REQ_V1: &[u8] = b"DHnQ";
145    /// `SMB2_CREATE_DURABLE_HANDLE_RECONNECT`.
146    pub const RECONNECT_V1: &[u8] = b"DHnC";
147    /// `SMB2_CREATE_DURABLE_HANDLE_REQUEST_V2`.
148    pub const REQ_V2: &[u8] = b"DH2Q";
149    /// `SMB2_CREATE_DURABLE_HANDLE_RECONNECT_V2`.
150    pub const RECONNECT_V2: &[u8] = b"DH2C";
151    /// Persistent-handle flag (`SMB2_DHANDLE_FLAG_PERSISTENT`).
152    pub const FLAG_PERSISTENT: u32 = 0x0000_0002;
153
154    /// Bitmask flags recording which durable create-context tags a CREATE
155    /// carried, so the server can reject conflicting combinations
156    /// ([MS-SMB2] §3.3.5.9.7/§3.3.5.9.12).
157    pub mod tag {
158        /// `DHnQ` present.
159        pub const REQ_V1: u8 = 0x01;
160        /// `DHnC` present.
161        pub const RECONNECT_V1: u8 = 0x02;
162        /// `DH2Q` present.
163        pub const REQ_V2: u8 = 0x04;
164        /// `DH2C` present.
165        pub const RECONNECT_V2: u8 = 0x08;
166    }
167}
168
169/// Walk a CREATE create-context chain, invoking `f(name, data)` for each entry.
170fn walk_create_contexts(
171    frame: &[u8],
172    ctx_off: usize,
173    ctx_len: usize,
174    mut f: impl FnMut(&[u8], &[u8]),
175) {
176    if ctx_len == 0 || ctx_off < BODY {
177        return;
178    }
179    let mut pos = ctx_off;
180    let end = (ctx_off + ctx_len).min(frame.len());
181    loop {
182        if pos + 16 > end {
183            return;
184        }
185        let next = g32(frame, pos) as usize;
186        let name_off = g16(frame, pos + 4) as usize;
187        let name_len = g16(frame, pos + 6) as usize;
188        let data_off = g16(frame, pos + 10) as usize;
189        let data_len = g32(frame, pos + 12) as usize;
190        if let (Some(name), Some(data)) = (
191            frame.get(pos + name_off..pos + name_off + name_len),
192            frame.get(pos + data_off..(pos + data_off + data_len).min(frame.len())),
193        ) {
194            f(name, data);
195        }
196        if next == 0 {
197            return;
198        }
199        pos += next;
200    }
201}
202
203/// Parse durable-handle intent from a CREATE's create-context chain, keeping the
204/// first durable context so the reconnect type is determined by chain order when
205/// several durable contexts are present ([MS-SMB2] §3.3.5.9.7/§3.3.5.9.12).
206fn parse_durable_context(frame: &[u8], ctx_off: usize, ctx_len: usize) -> Option<DurableReq> {
207    let mut found: Option<DurableReq> = None;
208    walk_create_contexts(frame, ctx_off, ctx_len, |name, data| {
209        if found.is_some() {
210            return;
211        }
212        found = if name == durable::REQ_V2 && data.len() >= 32 {
213            Some(DurableReq::RequestV2 {
214                timeout: u32::from_le_bytes(data[0..4].try_into().unwrap()),
215                flags: u32::from_le_bytes(data[4..8].try_into().unwrap()),
216                create_guid: data[16..32].try_into().unwrap(),
217            })
218        } else if name == durable::RECONNECT_V2 && data.len() >= 32 {
219            Some(DurableReq::ReconnectV2 {
220                file_id: data[0..16].try_into().unwrap(),
221                create_guid: data[16..32].try_into().unwrap(),
222                flags: data
223                    .get(32..36)
224                    .map(|b| u32::from_le_bytes(b.try_into().unwrap()))
225                    .unwrap_or(0),
226            })
227        } else if name == durable::RECONNECT_V1 && data.len() >= 16 {
228            Some(DurableReq::ReconnectV1 {
229                file_id: data[0..16].try_into().unwrap(),
230            })
231        } else if name == durable::REQ_V1 {
232            Some(DurableReq::RequestV1)
233        } else {
234            None
235        };
236    });
237    found
238}
239
240/// Collect a bitmask of which durable create-context tags appear in the chain.
241fn durable_context_tags(frame: &[u8], ctx_off: usize, ctx_len: usize) -> u8 {
242    let mut tags = 0u8;
243    walk_create_contexts(frame, ctx_off, ctx_len, |name, _data| {
244        tags |= match name {
245            n if n == durable::REQ_V1 => durable::tag::REQ_V1,
246            n if n == durable::RECONNECT_V1 => durable::tag::RECONNECT_V1,
247            n if n == durable::REQ_V2 => durable::tag::REQ_V2,
248            n if n == durable::RECONNECT_V2 => durable::tag::RECONNECT_V2,
249            _ => 0,
250        };
251    });
252    tags
253}
254
255/// AppInstance create-context name GUIDs ([MS-SMB2] §2.2.13.2.13/§2.2.13.2.15),
256/// on the wire as the raw 16-byte GUID.
257pub mod app_instance {
258    /// SMB2_CREATE_APP_INSTANCE_ID (0x45BCA66AEFA7F74A9008FA462E144D74).
259    pub const ID_NAME: [u8; 16] = [
260        0x45, 0xBC, 0xA6, 0x6A, 0xEF, 0xA7, 0xF7, 0x4A, 0x90, 0x08, 0xFA, 0x46, 0x2E, 0x14, 0x4D,
261        0x74,
262    ];
263    /// SMB2_CREATE_APP_INSTANCE_VERSION (0xB982D0B73B56074FA07B524A8116A010).
264    pub const VERSION_NAME: [u8; 16] = [
265        0xB9, 0x82, 0xD0, 0xB7, 0x3B, 0x56, 0x07, 0x4F, 0xA0, 0x7B, 0x52, 0x4A, 0x81, 0x16, 0xA0,
266        0x10,
267    ];
268}
269
270/// Parse the 16-byte AppInstanceId from a CREATE's create-context chain
271/// ([MS-SMB2] §2.2.13.2.13): data is StructureSize(2)+Reserved(2)+Id(16).
272fn parse_app_instance_id(frame: &[u8], ctx_off: usize, ctx_len: usize) -> Option<[u8; 16]> {
273    let mut found = None;
274    walk_create_contexts(frame, ctx_off, ctx_len, |name, data| {
275        if found.is_none() && name == app_instance::ID_NAME && data.len() >= 20 {
276            found = Some(data[4..20].try_into().unwrap());
277        }
278    });
279    found
280}
281
282/// Parse AppInstanceVersion (high, low) from the create-context chain
283/// ([MS-SMB2] §2.2.13.2.15): data is StructSize(2)+Reserved(2)+Pad(4)+High(8)+Low(8).
284fn parse_app_instance_version(frame: &[u8], ctx_off: usize, ctx_len: usize) -> Option<(u64, u64)> {
285    let mut found = None;
286    walk_create_contexts(frame, ctx_off, ctx_len, |name, data| {
287        if found.is_none() && name == app_instance::VERSION_NAME && data.len() >= 24 {
288            let high = u64::from_le_bytes(data[8..16].try_into().unwrap());
289            let low = u64::from_le_bytes(data[16..24].try_into().unwrap());
290            found = Some((high, low));
291        }
292    });
293    found
294}
295
296/// Encode the `DH2Q` v2 durable-handle response data ([MS-SMB2] §2.2.14.2.11).
297pub fn durable_v2_resp_data(timeout: u32, flags: u32) -> Vec<u8> {
298    let mut d = Vec::with_capacity(8);
299    d.extend_from_slice(&timeout.to_le_bytes());
300    d.extend_from_slice(&flags.to_le_bytes());
301    d
302}
303
304/// Encode the `DHnQ` v1 durable-handle response data ([MS-SMB2] §2.2.14.2.3):
305/// 8 reserved bytes.
306pub fn durable_v1_resp_data() -> Vec<u8> {
307    vec![0u8; 8]
308}
309
310/// Requested lease from an `SMB2_CREATE_REQUEST_LEASE`(`_V2`) context
311/// ([MS-SMB2] §2.2.13.2.8 / §2.2.13.2.10).
312#[derive(Debug, Clone, Copy)]
313pub struct LeaseReq {
314    /// Client-chosen lease key identifying the shared caching state.
315    pub key: [u8; 16],
316    /// Requested lease state ([`lease`] caching bits).
317    pub state: u32,
318    /// Lease flags.
319    pub flags: u32,
320    /// Parent lease key (v2 only; zero otherwise).
321    pub parent_key: [u8; 16],
322    /// Lease epoch (v2 only; zero otherwise).
323    pub epoch: u16,
324    /// True when the context was the v2 (52-byte) form.
325    pub v2: bool,
326}
327
328impl LeaseReq {
329    /// Parse the lease context data blob (32 bytes v1, 52 bytes v2).
330    fn parse_data(d: &[u8]) -> Option<LeaseReq> {
331        if d.len() < 32 {
332            return None;
333        }
334        let v2 = d.len() >= 52;
335        Some(LeaseReq {
336            key: d.get(0..16)?.try_into().ok()?,
337            state: u32::from_le_bytes(d.get(16..20)?.try_into().ok()?),
338            flags: u32::from_le_bytes(d.get(20..24)?.try_into().ok()?),
339            parent_key: if v2 {
340                d.get(32..48)?.try_into().ok()?
341            } else {
342                [0u8; 16]
343            },
344            epoch: if v2 {
345                u16::from_le_bytes(d.get(48..50)?.try_into().ok()?)
346            } else {
347                0
348            },
349            v2,
350        })
351    }
352}
353
354/// Locate the `RqLs` lease context in a CREATE request's create-context chain
355/// ([MS-SMB2] §2.2.13.2) and parse it.
356fn parse_lease_context(frame: &[u8], ctx_off: usize, ctx_len: usize) -> Option<LeaseReq> {
357    if ctx_len == 0 || ctx_off < BODY {
358        return None;
359    }
360    let mut pos = ctx_off;
361    let end = (ctx_off + ctx_len).min(frame.len());
362    loop {
363        if pos + 16 > end {
364            return None;
365        }
366        let next = g32(frame, pos) as usize;
367        let name_off = g16(frame, pos + 4) as usize;
368        let name_len = g16(frame, pos + 6) as usize;
369        let data_off = g16(frame, pos + 10) as usize;
370        let data_len = g32(frame, pos + 12) as usize;
371        let name = frame.get(pos + name_off..pos + name_off + name_len);
372        if name == Some(lease::CONTEXT_NAME) {
373            let data = frame.get(pos + data_off..(pos + data_off + data_len).min(frame.len()))?;
374            return LeaseReq::parse_data(data);
375        }
376        if next == 0 {
377            return None;
378        }
379        pos += next;
380    }
381}
382
383impl CreateReq {
384    /// Parse from the complete frame; `None` on malformed input.
385    pub fn parse(frame: &[u8]) -> Option<CreateReq> {
386        if frame.len() < create_off::FIXED_END || g16(frame, create_off::STRUCT) != 57 {
387            return None;
388        }
389        let name_off = g16(frame, create_off::NAME_OFFSET) as usize;
390        let name_len = g16(frame, create_off::NAME_LENGTH) as usize;
391        let raw = if name_len > 0 && name_off >= BODY {
392            frame.get(name_off..(name_off + name_len).min(frame.len()))?
393        } else {
394            &[]
395        };
396        // UTF-16LE name, strip null terminator.
397        let units: Vec<u16> = raw
398            .chunks_exact(2)
399            .map(|c| c[0] as u16 | ((c[1] as u16) << 8))
400            .take_while(|&u| u != 0)
401            .collect();
402        let ctx_off = g32(frame, create_off::CTX_OFFSET) as usize;
403        let ctx_len = g32(frame, create_off::CTX_LENGTH) as usize;
404        Some(CreateReq {
405            desired_access: g32(frame, create_off::DESIRED_ACCESS),
406            attrs: g32(frame, create_off::ATTRIBUTES),
407            share_access: g32(frame, create_off::SHARE_ACCESS),
408            disposition: g32(frame, create_off::DISPOSITION),
409            options: g32(frame, create_off::OPTIONS),
410            name: String::from_utf16_lossy(&units),
411            oplock_level: *frame.get(create_off::OPLOCK_LEVEL).unwrap_or(&0),
412            lease: parse_lease_context(frame, ctx_off, ctx_len),
413            durable: parse_durable_context(frame, ctx_off, ctx_len),
414            durable_ctx_tags: durable_context_tags(frame, ctx_off, ctx_len),
415            app_instance_id: parse_app_instance_id(frame, ctx_off, ctx_len),
416            app_instance_version: parse_app_instance_version(frame, ctx_off, ctx_len),
417        })
418    }
419}
420
421/// Oplock levels ([MS-SMB2] §2.2.13 / §2.2.14).
422pub mod oplock {
423    /// No oplock.
424    pub const NONE: u8 = 0x00;
425    /// Level II (shared, read caching).
426    pub const LEVEL_II: u8 = 0x01;
427    /// Exclusive.
428    pub const EXCLUSIVE: u8 = 0x08;
429    /// Batch.
430    pub const BATCH: u8 = 0x09;
431    /// A lease is requested instead of an oplock.
432    pub const LEASE: u8 = 0xFF;
433}
434
435/// Lease caching-state bits and constants ([MS-SMB2] §2.2.13.2.8).
436pub mod lease {
437    /// No caching.
438    pub const NONE: u32 = 0x00;
439    /// Read caching.
440    pub const READ_CACHING: u32 = 0x01;
441    /// Handle caching.
442    pub const HANDLE_CACHING: u32 = 0x02;
443    /// Write caching.
444    pub const WRITE_CACHING: u32 = 0x04;
445    /// Read + handle (the level a write-caching lease breaks down to).
446    pub const RH: u32 = READ_CACHING | HANDLE_CACHING;
447    /// Read + write + handle (full lease).
448    pub const RWH: u32 = READ_CACHING | WRITE_CACHING | HANDLE_CACHING;
449    /// Create-context tag for `SMB2_CREATE_REQUEST_LEASE`.
450    pub const CONTEXT_NAME: &[u8] = b"RqLs";
451    /// Break-notification flag demanding a lease-break acknowledgement.
452    pub const BREAK_FLAG_ACK_REQUIRED: u32 = 0x01;
453}
454
455/// Granted lease returned in a CREATE response `RqLs` context.
456#[derive(Debug, Clone, Copy)]
457pub struct LeaseResp {
458    /// Lease key echoed back to the client.
459    pub key: [u8; 16],
460    /// Granted lease state ([`lease`] bits).
461    pub state: u32,
462    /// Lease flags.
463    pub flags: u32,
464    /// Lease epoch (v2 only).
465    pub epoch: u16,
466    /// Emit the v2 (52-byte) form.
467    pub v2: bool,
468}
469
470/// Encode just the lease structure ([MS-SMB2] §2.2.14.2.10/§2.2.14.2.11 data)
471/// for a granted lease, without the create-context wrapper.
472pub fn lease_context_data(l: &LeaseResp) -> Vec<u8> {
473    let mut c = Vec::new();
474    c.extend_from_slice(&l.key); // LeaseKey
475    c.extend_from_slice(&l.state.to_le_bytes()); // LeaseState
476    c.extend_from_slice(&l.flags.to_le_bytes()); // LeaseFlags
477    c.extend_from_slice(&0u64.to_le_bytes()); // LeaseDuration
478    if l.v2 {
479        c.extend_from_slice(&[0u8; 16]); // ParentLeaseKey
480        c.extend_from_slice(&l.epoch.to_le_bytes()); // Epoch
481        c.extend_from_slice(&0u16.to_le_bytes()); // Reserved
482    }
483    c
484}
485
486/// Encode a chain of CREATE-response create-contexts ([MS-SMB2] §2.2.13.2)
487/// from `(name, data)` pairs, wiring `NextEntryOffset` links and keeping each
488/// entry 8-byte aligned. Returns an empty buffer for no entries.
489pub fn encode_create_contexts(entries: &[(&[u8], Vec<u8>)]) -> Vec<u8> {
490    let mut out = Vec::new();
491    for (i, (name, data)) in entries.iter().enumerate() {
492        let start = out.len();
493        let data_off = (16 + name.len()).next_multiple_of(8);
494        out.extend_from_slice(&0u32.to_le_bytes()); // Next (patched below)
495        out.extend_from_slice(&16u16.to_le_bytes()); // NameOffset
496        out.extend_from_slice(&(name.len() as u16).to_le_bytes()); // NameLength
497        out.extend_from_slice(&0u16.to_le_bytes()); // Reserved
498        let doff = if data.is_empty() { 0 } else { data_off as u16 };
499        out.extend_from_slice(&doff.to_le_bytes()); // DataOffset
500        out.extend_from_slice(&(data.len() as u32).to_le_bytes()); // DataLength
501        out.extend_from_slice(name);
502        while out.len() - start < data_off {
503            out.push(0);
504        }
505        out.extend_from_slice(data);
506        if i + 1 < entries.len() {
507            // Pad to an 8-byte boundary so the next entry (and Next link) align.
508            while (out.len() - start) % 8 != 0 {
509                out.push(0);
510            }
511            let next = (out.len() - start) as u32;
512            out[start..start + 4].copy_from_slice(&next.to_le_bytes());
513        }
514    }
515    out
516}
517
518/// Build CREATE response body (§2.2.14.1).
519///
520/// Fixed part is 88 bytes; the FileId sits after `Reserved2` at offset 64.
521/// `contexts` is a pre-chained create-context blob (see
522/// [`encode_create_contexts`]); when non-empty the context offset/length
523/// fields point at it (offset is header-relative).
524#[allow(clippy::too_many_arguments)]
525pub fn build_create_resp(
526    file_id: FileId,
527    action: u32,
528    times: [u64; 4],
529    attrs: u32,
530    alloc: u64,
531    eof: u64,
532    is_dir: bool,
533    oplock: u8,
534    contexts: &[u8],
535) -> Vec<u8> {
536    let _ = is_dir;
537    // Contexts follow the 88-byte fixed body, at header-relative offset 152.
538    let (ctx_off, ctx_len) = if contexts.is_empty() {
539        (0u32, 0u32)
540    } else {
541        ((BODY + 88) as u32, contexts.len() as u32)
542    };
543    let mut b = Vec::with_capacity(88 + contexts.len());
544    b.extend_from_slice(&89u16.to_le_bytes()); // StructureSize @0
545    b.push(oplock); // OplockLevel @2
546    b.push(0); // Flags @3
547    b.extend_from_slice(&action.to_le_bytes()); // CreateAction @4
548    for t in &times {
549        b.extend_from_slice(&t.to_le_bytes());
550    } // Creation/Access/Write/Change @8..40
551    b.extend_from_slice(&alloc.to_le_bytes()); // AllocationSize @40
552    b.extend_from_slice(&eof.to_le_bytes()); // EndOfFile @48
553    b.extend_from_slice(&attrs.to_le_bytes()); // FileAttributes @56
554    b.extend_from_slice(&0u32.to_le_bytes()); // Reserved2 @60
555    b.extend_from_slice(&file_id.0); // FileId @64..80
556    b.extend_from_slice(&ctx_off.to_le_bytes()); // CreateContextsOffset @80
557    b.extend_from_slice(&ctx_len.to_le_bytes()); // CreateContextsLength @84
558    debug_assert_eq!(b.len(), 88);
559    b.extend_from_slice(contexts);
560    b
561}
562
563/// Build an OPLOCK_BREAK notification body ([MS-SMB2] §2.2.23.1): a 24-byte
564/// structure telling the holder to break its oplock down to `new_level`.
565pub fn build_oplock_break(file_id: FileId, new_level: u8) -> Vec<u8> {
566    let mut b = Vec::with_capacity(24);
567    b.extend_from_slice(&24u16.to_le_bytes()); // StructureSize
568    b.push(new_level); // OplockLevel
569    b.push(0); // Reserved
570    b.extend_from_slice(&0u32.to_le_bytes()); // Reserved2
571    b.extend_from_slice(&file_id.0); // FileId
572    b
573}
574
575/// OPLOCK_BREAK acknowledgement/response body ([MS-SMB2] §2.2.24.2), echoing
576/// the level the holder settled on.
577pub fn build_oplock_break_resp(file_id: FileId, level: u8) -> Vec<u8> {
578    build_oplock_break(file_id, level)
579}
580
581/// Parsed OPLOCK_BREAK acknowledgement ([MS-SMB2] §2.2.24.1).
582#[derive(Debug)]
583pub struct OplockBreakAck {
584    /// Level the client has broken to.
585    pub level: u8,
586    /// Handle being acknowledged.
587    pub file_id: FileId,
588}
589
590impl OplockBreakAck {
591    /// Parse from the complete frame.
592    pub fn parse(frame: &[u8]) -> Option<OplockBreakAck> {
593        if frame.len() < BODY + 24 || g16(frame, BODY) != 24 {
594            return None;
595        }
596        Some(OplockBreakAck {
597            level: *frame.get(BODY + 2)?,
598            file_id: FileId(frame.get(BODY + 8..BODY + 24)?.try_into().ok()?),
599        })
600    }
601}
602
603/// Build a LEASE_BREAK notification body ([MS-SMB2] §2.2.23.2): a 44-byte
604/// structure asking the holder of `key` to drop from `current` to `new` state.
605pub fn build_lease_break(key: [u8; 16], current: u32, new: u32, epoch: u16, flags: u32) -> Vec<u8> {
606    let mut b = Vec::with_capacity(44);
607    b.extend_from_slice(&44u16.to_le_bytes()); // StructureSize
608    b.extend_from_slice(&epoch.to_le_bytes()); // NewEpoch
609    b.extend_from_slice(&flags.to_le_bytes()); // Flags
610    b.extend_from_slice(&key); // LeaseKey
611    b.extend_from_slice(&current.to_le_bytes()); // CurrentLeaseState
612    b.extend_from_slice(&new.to_le_bytes()); // NewLeaseState
613    b.extend_from_slice(&0u32.to_le_bytes()); // BreakReason
614    b.extend_from_slice(&0u32.to_le_bytes()); // AccessMaskHint
615    b.extend_from_slice(&0u32.to_le_bytes()); // ShareMaskHint
616    debug_assert_eq!(b.len(), 44);
617    b
618}
619
620/// LEASE_BREAK response body ([MS-SMB2] §2.2.24.2): a 36-byte structure echoing
621/// the state the holder settled on.
622pub fn build_lease_break_resp(key: [u8; 16], state: u32) -> Vec<u8> {
623    let mut b = Vec::with_capacity(36);
624    b.extend_from_slice(&36u16.to_le_bytes()); // StructureSize
625    b.extend_from_slice(&0u16.to_le_bytes()); // Reserved
626    b.extend_from_slice(&0u32.to_le_bytes()); // Flags
627    b.extend_from_slice(&key); // LeaseKey
628    b.extend_from_slice(&state.to_le_bytes()); // LeaseState
629    b.extend_from_slice(&0u64.to_le_bytes()); // LeaseDuration
630    debug_assert_eq!(b.len(), 36);
631    b
632}
633
634/// Parsed LEASE_BREAK acknowledgement ([MS-SMB2] §2.2.24.1), distinguished from
635/// an oplock-break ack by its StructureSize of 36.
636#[derive(Debug)]
637pub struct LeaseBreakAck {
638    /// Lease key being acknowledged.
639    pub key: [u8; 16],
640    /// State the client has broken to.
641    pub state: u32,
642}
643
644impl LeaseBreakAck {
645    /// Fixed StructureSize of a LEASE_BREAK_ACK request ([MS-SMB2] §2.2.24.2).
646    pub const STRUCTURE_SIZE: u16 = 36;
647
648    /// Parse from the complete frame.
649    pub fn parse(frame: &[u8]) -> Option<LeaseBreakAck> {
650        if frame.len() < BODY + 36 || g16(frame, BODY) != 36 {
651            return None;
652        }
653        Some(LeaseBreakAck {
654            key: frame.get(BODY + 8..BODY + 24)?.try_into().ok()?,
655            state: g32(frame, BODY + 24),
656        })
657    }
658}
659
660// ---------------- READ (§2.2.19 / §2.2.19.1) ----------------
661
662/// READ request fixed-part offsets.
663#[allow(dead_code)]
664mod read_off {
665    use super::BODY;
666
667    /// StructureSize (=49).
668    pub const STRUCT: usize = BODY;
669    /// Padding(1) Reserved(1) then Length.
670    pub const LENGTH: usize = BODY + 4;
671    /// Offset (64-bit file position).
672    pub const OFFSET: usize = BODY + 8;
673    /// FileId.
674    pub const FILE_ID: usize = BODY + 16;
675    /// MinimumCount.
676    pub const MIN_COUNT: usize = BODY + 32;
677    /// Channel.
678    pub const CHANNEL: usize = BODY + 36;
679    /// RemainingBytes.
680    pub const REMAINING: usize = BODY + 40;
681}
682
683/// READ request body (§2.2.19).
684#[derive(Debug)]
685pub struct ReadReq {
686    /// Number of bytes to read.
687    pub length: u32,
688    /// Absolute file offset.
689    pub offset: u64,
690    /// Target file identifier.
691    pub file_id: FileId,
692    /// Bytes the client will accept below Length before a short read.
693    #[allow(dead_code)]
694    pub min_count: u32,
695}
696
697impl ReadReq {
698    /// Parse from the complete frame.
699    pub fn parse(frame: &[u8]) -> Option<ReadReq> {
700        if frame.len() < read_off::STRUCT + 48 || g16(frame, read_off::STRUCT) != 49 {
701            return None;
702        }
703        Some(ReadReq {
704            length: g32(frame, read_off::LENGTH),
705            offset: g64(frame, read_off::OFFSET),
706            file_id: FileId(
707                frame
708                    .get(read_off::FILE_ID..read_off::FILE_ID + 16)?
709                    .try_into()
710                    .ok()?,
711            ),
712            min_count: g32(frame, read_off::MIN_COUNT),
713        })
714    }
715}
716
717/// READ response body (§2.2.19.1); DataOffset is absolute from frame start
718/// (64+16=80), 8-byte aligned.
719pub fn build_read_resp(data: &[u8]) -> Vec<u8> {
720    const DATA_OFF: usize = 80usize;
721    let mut b = Vec::with_capacity(DATA_OFF - BODY + data.len());
722    b.extend_from_slice(&17u16.to_le_bytes()); // StructureSize
723    b.push(DATA_OFF as u8); // DataOffset (absolute from header start)
724    b.push(0); // Reserved
725    b.extend_from_slice(&(data.len() as u32).to_le_bytes()); // DataLength
726    b.extend_from_slice(&0u32.to_le_bytes()); // DataRemaining
727    b.extend_from_slice(&0u32.to_le_bytes()); // Reserved
728    while BODY + b.len() < DATA_OFF {
729        b.push(0);
730    }
731    b.extend_from_slice(data);
732    b
733}
734
735// ---------------- WRITE (§2.2.21 / §2.2.21.1) ----------------
736
737/// WRITE request fixed-part offsets.
738#[allow(dead_code)]
739mod write_off {
740    use super::BODY;
741
742    /// StructureSize (=49).
743    pub const STRUCT: usize = BODY;
744    /// DataOffset (absolute from frame start).
745    pub const DATA_OFFSET: usize = BODY + 2;
746    /// Length of the data payload.
747    pub const LENGTH: usize = BODY + 4;
748    /// Offset (64-bit file position).
749    pub const OFFSET: usize = BODY + 8;
750    /// FileId.
751    pub const FILE_ID: usize = BODY + 16;
752    /// Channel.
753    pub const CHANNEL: usize = BODY + 32;
754    /// RemainingBytes.
755    pub const REMAINING: usize = BODY + 36;
756    /// WriteChannelInfoOffset.
757    pub const CH_INFO_OFFSET: usize = BODY + 40;
758    /// WriteChannelInfoLength.
759    pub const CH_INFO_LENGTH: usize = BODY + 42;
760}
761
762/// WRITE request body (§2.2.21).
763#[derive(Debug)]
764pub struct WriteReq {
765    /// Absolute write offset.
766    pub offset: u64,
767    /// Target file identifier.
768    pub file_id: FileId,
769    /// Data to write.
770    pub payload: Vec<u8>,
771}
772
773impl WriteReq {
774    /// Parse from the complete frame.
775    pub fn parse(frame: &[u8]) -> Option<WriteReq> {
776        if frame.len() < write_off::STRUCT + 48 || g16(frame, write_off::STRUCT) != 49 {
777            return None;
778        }
779        let dlen = g32(frame, write_off::LENGTH) as usize;
780        let doff = g16(frame, write_off::DATA_OFFSET) as usize;
781        let payload = if dlen > 0 && doff >= BODY {
782            frame.get(doff..(doff + dlen).min(frame.len()))?.to_vec()
783        } else {
784            Vec::new()
785        };
786        Some(WriteReq {
787            offset: g64(frame, write_off::OFFSET),
788            file_id: FileId(
789                frame
790                    .get(write_off::FILE_ID..write_off::FILE_ID + 16)?
791                    .try_into()
792                    .ok()?,
793            ),
794            payload,
795        })
796    }
797}
798
799/// WRITE response body (§2.2.21.1).
800pub fn build_write_resp(written: u32) -> Vec<u8> {
801    let mut b = Vec::with_capacity(16);
802    b.extend_from_slice(&17u16.to_le_bytes()); // StructureSize
803    b.extend_from_slice(&0u16.to_le_bytes()); // Reserved
804    b.extend_from_slice(&written.to_le_bytes()); // Count
805    b.extend_from_slice(&0u32.to_le_bytes()); // Remaining
806    b.extend_from_slice(&0u16.to_le_bytes()); // WriteChannelInfoOffset
807    b.extend_from_slice(&0u16.to_le_bytes()); // WriteChannelInfoLength
808    debug_assert_eq!(b.len(), 16);
809    b
810}
811
812// ---------------- CLOSE (§2.2.15 / §2.2.15.1) ----------------
813
814/// CLOSE request fixed-part offsets.
815mod close_off {
816    use super::BODY;
817
818    /// StructureSize (=24).
819    pub const STRUCT: usize = BODY;
820    /// Flags.
821    pub const FLAGS: usize = BODY + 2;
822    /// FileId.
823    pub const FILE_ID: usize = BODY + 8;
824}
825
826/// CLOSE request body (§2.2.15).
827#[derive(Debug)]
828pub struct CloseReq {
829    /// True when the client wants post-close attributes in the reply.
830    pub query_attrs: bool,
831    /// Handle to close.
832    pub file_id: FileId,
833}
834
835impl CloseReq {
836    /// Parse from the complete frame.
837    pub fn parse(frame: &[u8]) -> Option<CloseReq> {
838        if frame.len() < close_off::STRUCT + 24 || g16(frame, close_off::STRUCT) != 24 {
839            return None;
840        }
841        Some(CloseReq {
842            query_attrs: g16(frame, close_off::FLAGS) & 0x0001 != 0,
843            file_id: FileId(
844                frame
845                    .get(close_off::FILE_ID..close_off::FILE_ID + 16)?
846                    .try_into()
847                    .ok()?,
848            ),
849        })
850    }
851}
852
853/// CLOSE response body (§2.2.15.1).
854pub fn build_close_resp(times: [u64; 4], alloc: u64, eof: u64, attrs: u32) -> Vec<u8> {
855    let mut b = Vec::with_capacity(60);
856    b.extend_from_slice(&60u16.to_le_bytes()); // StructureSize
857    b.extend_from_slice(&0u16.to_le_bytes()); // Flags
858    b.extend_from_slice(&0u32.to_le_bytes()); // Reserved
859    for t in &times {
860        b.extend_from_slice(&t.to_le_bytes());
861    }
862    b.extend_from_slice(&alloc.to_le_bytes());
863    b.extend_from_slice(&eof.to_le_bytes());
864    b.extend_from_slice(&attrs.to_le_bytes());
865    debug_assert_eq!(b.len(), 60);
866    b
867}
868
869// ---------------- FLUSH (§2.2.17 / §2.2.17.1) ----------------
870
871/// FLUSH request body (§2.2.17): StructureSize(2)=24 Reserved(2) FileId(16).
872#[derive(Debug)]
873pub struct FlushReq {
874    /// Handle to flush.
875    pub file_id: FileId,
876}
877
878impl FlushReq {
879    /// Parse from the complete frame.
880    pub fn parse(frame: &[u8]) -> Option<FlushReq> {
881        if frame.len() < BODY + 20 || g16(frame, BODY) != 24 {
882            return None;
883        }
884        Some(FlushReq {
885            file_id: FileId(frame.get(BODY + 4..BODY + 20)?.try_into().ok()?),
886        })
887    }
888}
889
890/// FLUSH response body (§2.2.17.1): StructureSize=4 only.
891pub fn build_flush_resp() -> Vec<u8> {
892    let mut b = Vec::with_capacity(4);
893    b.extend_from_slice(&4u16.to_le_bytes()); // StructureSize
894    b.extend_from_slice(&0u16.to_le_bytes()); // Reserved
895    b
896}
897
898// ---------------- QUERY_DIRECTORY (§2.2.33 / §2.2.34) ----------------
899
900/// QUERY_DIRECTORY request fixed-part offsets.
901#[allow(dead_code)]
902mod qdir_off {
903    use super::BODY;
904
905    /// StructureSize (=33).
906    pub const STRUCT: usize = BODY;
907    /// FileInformationClass.
908    pub const CLASS: usize = BODY + 2;
909    /// Flags.
910    pub const FLAGS: usize = BODY + 3;
911    /// FileIndex.
912    pub const FILE_INDEX: usize = BODY + 4;
913    /// FileId of the open directory handle.
914    pub const FILE_ID: usize = BODY + 8;
915    /// FileNameOffset (absolute).
916    pub const NAME_OFFSET: usize = BODY + 24;
917    /// FileNameLength.
918    pub const NAME_LENGTH: usize = BODY + 26;
919    /// Fixed part size (32 bytes).
920    pub const FIXED_END: usize = BODY + 32;
921}
922
923/// FIND flags (§2.2.33.1).
924pub mod find_flags {
925    /// Restart the enumeration from the beginning.
926    pub const RESTART_SCANS: u8 = 0x01;
927    /// Return one entry at most.
928    pub const RETURN_SINGLE_ENTRY: u8 = 0x02;
929    /// Rescan directory contents (may duplicate).
930    pub const SCAN: u8 = 0x04;
931    /// FileIndex field carries meaning.
932    pub const INDEX_SPECIFIED: u8 = 0x08;
933    /// Re-open the search.
934    pub const REOPEN: u8 = 0x10;
935}
936
937/// QUERY_DIRECTORY request body (§2.2.33).
938#[derive(Debug)]
939pub struct QueryDirReq {
940    /// FileInformationClass requested.
941    pub class: u8,
942    /// FIND flags byte.
943    pub flags: u8,
944    /// Resume index (meaningful only when `INDEX_SPECIFIED` is set).
945    pub file_index: u32,
946    /// Directory handle.
947    pub file_id: FileId,
948    /// Search pattern (empty when continuing an enumeration).
949    pub pattern: String,
950}
951
952impl QueryDirReq {
953    /// Parse from the complete frame.
954    pub fn parse(frame: &[u8]) -> Option<QueryDirReq> {
955        if frame.len() < qdir_off::FIXED_END || g16(frame, qdir_off::STRUCT) != 33 {
956            return None;
957        }
958        let off = g16(frame, qdir_off::NAME_OFFSET) as usize;
959        let len = g16(frame, qdir_off::NAME_LENGTH) as usize;
960        let raw = if len > 0 && off >= BODY {
961            frame.get(off..(off + len).min(frame.len()))?
962        } else {
963            &[]
964        };
965        let units: Vec<u16> = raw
966            .chunks_exact(2)
967            .map(|c| c[0] as u16 | ((c[1] as u16) << 8))
968            .collect();
969        Some(QueryDirReq {
970            class: *frame.get(qdir_off::CLASS)?,
971            flags: *frame.get(qdir_off::FLAGS)?,
972            file_index: g32(frame, qdir_off::FILE_INDEX),
973            file_id: FileId(
974                frame
975                    .get(qdir_off::FILE_ID..qdir_off::FILE_ID + 16)?
976                    .try_into()
977                    .ok()?,
978            ),
979            pattern: String::from_utf16_lossy(&units),
980        })
981    }
982}
983
984/// QUERY_DIRECTORY / QUERY_INFO response body (§2.2.34.1 / §2.2.38.1):
985/// StructureSize=9 followed by the output buffer 8-byte aligned.
986pub fn build_info_resp(buffer: &[u8]) -> Vec<u8> {
987    const BUF_OFF: usize = BODY + 8; // StructureSize(2)+Offset(2)+Length(4)
988    let mut b = Vec::with_capacity(BUF_OFF - BODY + buffer.len());
989    b.extend_from_slice(&9u16.to_le_bytes()); // StructureSize
990    b.extend_from_slice(&(BUF_OFF as u16).to_le_bytes()); // OutputBufferOffset
991    b.extend_from_slice(&(buffer.len() as u32).to_le_bytes()); // OutputBufferLength
992    while !(BODY + b.len()).is_multiple_of(8) || BODY + b.len() < BUF_OFF {
993        b.push(0);
994    }
995    b.extend_from_slice(buffer);
996    b
997}
998
999// ---------------- QUERY_INFO (§2.2.37 / §2.2.38) ----------------
1000
1001/// QUERY_INFO request fixed-part offsets.
1002#[allow(dead_code)]
1003mod qinfo_off {
1004    use super::BODY;
1005
1006    /// StructureSize (=41).
1007    pub const STRUCT: usize = BODY;
1008    /// InfoType.
1009    pub const INFO_TYPE: usize = BODY + 2;
1010    /// FileInfoClass.
1011    pub const CLASS: usize = BODY + 3;
1012    /// OutputBufferLength.
1013    pub const OUTPUT_LEN: usize = BODY + 4;
1014    /// InputBufferOffset (absolute).
1015    pub const INPUT_OFFSET: usize = BODY + 8;
1016    /// InputBufferLength.
1017    pub const INPUT_LENGTH: usize = BODY + 12;
1018    /// AdditionalInformation.
1019    pub const ADDITIONAL: usize = BODY + 16;
1020    /// Flags.
1021    pub const FLAGS: usize = BODY + 20;
1022    /// FileId.
1023    pub const FILE_ID: usize = BODY + 24;
1024    /// Fixed part size (40 bytes).
1025    pub const FIXED_END: usize = BODY + 40;
1026}
1027
1028/// QUERY_INFO InfoType values (§2.2.37.1).
1029pub mod info_type {
1030    /// File information (FileInformationClass based).
1031    pub const FILE: u8 = 0x01;
1032    /// File system information.
1033    pub const FS: u8 = 0x02;
1034    /// Security information (SDS).
1035    pub const SECURITY: u8 = 0x03;
1036    /// Quota information.
1037    pub const QUOTA: u8 = 0x04;
1038}
1039
1040/// QUERY_INFO request body (§2.2.37).
1041#[derive(Debug)]
1042pub struct QueryInfoReq {
1043    /// InfoType byte.
1044    pub info_type: u8,
1045    /// FileInformationClass / FsInformationClass.
1046    pub class: u8,
1047    /// Maximum bytes the client accepts in the output buffer.
1048    pub output_len: u32,
1049    /// AdditionalInformation (SECURITY_INFORMATION mask for SECURITY queries).
1050    pub additional: u32,
1051    /// Target handle (FILE info type only).
1052    pub file_id: FileId,
1053    /// Raw input buffer (rename source path etc.), may be empty.
1054    pub input: Vec<u8>,
1055}
1056
1057impl QueryInfoReq {
1058    /// Parse from the complete frame.
1059    pub fn parse(frame: &[u8]) -> Option<QueryInfoReq> {
1060        if frame.len() < qinfo_off::FIXED_END || g16(frame, qinfo_off::STRUCT) != 41 {
1061            return None;
1062        }
1063        let off = g16(frame, qinfo_off::INPUT_OFFSET) as usize;
1064        let len = g32(frame, qinfo_off::INPUT_LENGTH) as usize;
1065        let input = if len > 0 && off >= BODY {
1066            frame.get(off..(off + len).min(frame.len()))?.to_vec()
1067        } else {
1068            Vec::new()
1069        };
1070        Some(QueryInfoReq {
1071            info_type: *frame.get(qinfo_off::INFO_TYPE)?,
1072            class: *frame.get(qinfo_off::CLASS)?,
1073            output_len: g32(frame, qinfo_off::OUTPUT_LEN),
1074            additional: g32(frame, qinfo_off::ADDITIONAL),
1075            file_id: FileId(
1076                frame
1077                    .get(qinfo_off::FILE_ID..qinfo_off::FILE_ID + 16)?
1078                    .try_into()
1079                    .ok()?,
1080            ),
1081            input,
1082        })
1083    }
1084}
1085
1086// ---------------- SET_INFO (§2.2.39 / §2.2.40) ----------------
1087
1088/// SET_INFO request fixed-part offsets.
1089#[allow(dead_code)]
1090mod sinfo_off {
1091    use super::BODY;
1092
1093    /// StructureSize (=33).
1094    pub const STRUCT: usize = BODY;
1095    /// InfoType.
1096    pub const INFO_TYPE: usize = BODY + 2;
1097    /// FileInfoClass.
1098    pub const CLASS: usize = BODY + 3;
1099    /// BufferLength.
1100    pub const BUFFER_LEN: usize = BODY + 4;
1101    /// BufferOffset (absolute).
1102    pub const BUFFER_OFFSET: usize = BODY + 8;
1103    /// AdditionalInformation.
1104    pub const ADDITIONAL: usize = BODY + 12;
1105    /// FileId.
1106    pub const FILE_ID: usize = BODY + 16;
1107    /// Fixed part size (32 bytes).
1108    pub const FIXED_END: usize = BODY + 32;
1109}
1110
1111/// SET_INFO request body (§2.2.39).
1112#[derive(Debug)]
1113pub struct SetInfoReq {
1114    /// InfoType byte.
1115    pub info_type: u8,
1116    /// File/Fs information class.
1117    pub class: u8,
1118    /// AdditionalInformation (SECURITY_INFORMATION mask for SECURITY sets).
1119    pub additional: u32,
1120    /// Target handle.
1121    pub file_id: FileId,
1122    /// Raw info-class payload buffer.
1123    pub buffer: Vec<u8>,
1124}
1125
1126impl SetInfoReq {
1127    /// Parse from the complete frame.
1128    pub fn parse(frame: &[u8]) -> Option<SetInfoReq> {
1129        if frame.len() < sinfo_off::FIXED_END || g16(frame, sinfo_off::STRUCT) != 33 {
1130            return None;
1131        }
1132        let off = g16(frame, sinfo_off::BUFFER_OFFSET) as usize;
1133        let len = g32(frame, sinfo_off::BUFFER_LEN) as usize;
1134        let buffer = if len > 0 && off >= BODY {
1135            frame.get(off..(off + len).min(frame.len()))?.to_vec()
1136        } else {
1137            Vec::new()
1138        };
1139        Some(SetInfoReq {
1140            info_type: *frame.get(sinfo_off::INFO_TYPE)?,
1141            class: *frame.get(sinfo_off::CLASS)?,
1142            additional: g32(frame, sinfo_off::ADDITIONAL),
1143            file_id: FileId(
1144                frame
1145                    .get(sinfo_off::FILE_ID..sinfo_off::FILE_ID + 16)?
1146                    .try_into()
1147                    .ok()?,
1148            ),
1149            buffer,
1150        })
1151    }
1152}
1153
1154/// SET_INFO response body (§2.2.40): StructureSize=2 only.
1155pub fn build_set_info_resp() -> Vec<u8> {
1156    2u16.to_le_bytes().to_vec()
1157}
1158
1159// ---------------- TREE_CONNECT / DISCONNECT / LOGOFF ----------------
1160
1161/// ShareFlags bit requiring SMB3 encryption on the tree ([MS-SMB2] §2.2.10).
1162pub const SHAREFLAG_ENCRYPT_DATA: u32 = 0x0000_8000;
1163
1164/// ShareFlags bit advertising compression support on the tree ([MS-SMB2] §2.2.10).
1165pub const SHAREFLAG_COMPRESS_DATA: u32 = 0x0010_0000;
1166
1167/// ShareFlags bit marking a continuously-available share ([MS-SMB2] §2.2.10).
1168pub const SHAREFLAG_CONTINUOUSLY_AVAILABLE: u32 = 0x0000_0002;
1169
1170/// Share Capabilities bit advertising continuous availability ([MS-SMB2] §2.2.10).
1171pub const SHARE_CAP_CONTINUOUS_AVAILABILITY: u32 = 0x0000_0010;
1172
1173/// TREE_CONNECT response body (§2.2.10.2): 16-byte fixed part, no buffer.
1174pub fn build_tree_connect_resp(share_type: u8, share_flags: u32, capabilities: u32) -> Vec<u8> {
1175    let mut b = Vec::with_capacity(16);
1176    b.extend_from_slice(&16u16.to_le_bytes()); // StructureSize
1177    b.push(share_type); // ShareType: 0x01 disk
1178    b.push(0); // Reserved
1179    b.extend_from_slice(&share_flags.to_le_bytes()); // ShareFlags
1180    b.extend_from_slice(&capabilities.to_le_bytes()); // Capabilities
1181    b.extend_from_slice(&0x001F_01FFu32.to_le_bytes()); // MaximalAccess
1182    debug_assert_eq!(b.len(), 16);
1183    b
1184}
1185
1186/// TREE_DISCONNECT response body (§2.2.11): StructureSize=4 then 2-byte
1187/// Reserved — the whole structure is 4 bytes.
1188pub fn build_tree_disconnect_resp() -> Vec<u8> {
1189    let mut b = Vec::with_capacity(4);
1190    b.extend_from_slice(&4u16.to_le_bytes()); // StructureSize
1191    b.extend_from_slice(&0u16.to_le_bytes()); // Reserved
1192    b
1193}
1194
1195/// LOGOFF response body (§2.2.8): StructureSize=4 then 2-byte Reserved.
1196pub fn build_logoff_resp() -> Vec<u8> {
1197    let mut b = Vec::with_capacity(4);
1198    b.extend_from_slice(&4u16.to_le_bytes()); // StructureSize
1199    b.extend_from_slice(&0u16.to_le_bytes()); // Reserved
1200    b
1201}
1202
1203/// ECHO response body (§2.2.29): StructureSize=4 then 2-byte Reserved.
1204pub fn build_echo_resp() -> Vec<u8> {
1205    let mut b = Vec::with_capacity(4);
1206    b.extend_from_slice(&4u16.to_le_bytes()); // StructureSize
1207    b.extend_from_slice(&0u16.to_le_bytes()); // Reserved
1208    b
1209}
1210
1211/// TREE_CONNECT request fixed-part offsets (§2.2.9).
1212pub mod tcon_off {
1213    /// StructureSize (=9).
1214    pub const STRUCT: usize = super::BODY;
1215    /// PathOffset (absolute).
1216    pub const PATH_OFFSET: usize = super::BODY + 4;
1217    /// PathLength.
1218    pub const PATH_LENGTH: usize = super::BODY + 6;
1219}
1220
1221/// Share types advertised in TREE_CONNECT responses (§2.2.10.1).
1222pub mod share_type {
1223    /// Disk share.
1224    pub const DISK: u8 = 0x01;
1225    /// Named pipe share.
1226    pub const PIPE: u8 = 0x02;
1227    /// Printer share.
1228    pub const PRINT: u8 = 0x03;
1229}
1230
1231/// Capability bits advertised by the server (§2.2.9.2.1 / §2.2.10.2).
1232pub mod caps {
1233    use crate::consts;
1234    /// Distributed file system.
1235    pub const DFS: u32 = consts::CAP_DFS;
1236    /// Large MTU (multi-credit) support.
1237    pub const LARGE_MTU: u32 = consts::CAP_LARGE_MTU;
1238    /// Leasing support.
1239    pub const LEASING: u32 = 0x0000_0008;
1240}
1241
1242/// Lock vector entry (§2.2.26.1).
1243#[derive(Debug)]
1244pub struct LockElem {
1245    /// Byte range start.
1246    pub offset: u64,
1247    /// Byte range length.
1248    pub length: u64,
1249    /// Shared lock.
1250    pub shared: bool,
1251    /// Exclusive lock.
1252    pub exclusive: bool,
1253    /// Unlock operation.
1254    pub unlock: bool,
1255    /// Fail immediately instead of blocking.
1256    pub fail_immediately: bool,
1257}
1258
1259/// LOCK request (§2.2.26). The 24-byte header (StructureSize, LockCount,
1260/// LockSequence, FileId) is followed by `LockCount` 24-byte lock elements;
1261/// StructureSize is fixed at 48 (header + one element) per the SMB2 convention.
1262#[derive(Debug)]
1263pub struct LockReq {
1264    /// Locked handle.
1265    pub file_id: FileId,
1266    /// One entry per locked/unlocked range.
1267    pub locks: Vec<LockElem>,
1268}
1269
1270impl LockReq {
1271    /// Parse from the complete frame.
1272    pub fn parse(frame: &[u8]) -> Option<LockReq> {
1273        if frame.len() < BODY + 48 || g16(frame, BODY) != 48 {
1274            return None;
1275        }
1276        let count = g16(frame, BODY + 2) as usize;
1277        let fid = FileId(frame.get(BODY + 8..BODY + 24)?.try_into().ok()?);
1278        let mut locks = Vec::with_capacity(count.min(1024));
1279        for i in 0..count {
1280            // Lock elements begin right after the 24-byte header.
1281            let base = BODY + 24 + i * 24;
1282            let e = frame.get(base..base + 24)?;
1283            // Flags is a 32-bit field at offset 16 ([MS-SMB2] §2.2.26.1):
1284            // SHARED 0x1, EXCLUSIVE 0x2, UNLOCK 0x4, FAIL_IMMEDIATELY 0x10.
1285            let flags = g32(e, 16);
1286            locks.push(LockElem {
1287                offset: g64(e, 0),
1288                length: g64(e, 8),
1289                shared: flags & 0x01 != 0,
1290                exclusive: flags & 0x02 != 0,
1291                unlock: flags & 0x04 != 0,
1292                fail_immediately: flags & 0x10 != 0,
1293            });
1294        }
1295        Some(LockReq {
1296            file_id: fid,
1297            locks,
1298        })
1299    }
1300}
1301
1302/// LOCK response body (§2.2.27): StructureSize=4.
1303pub fn build_lock_resp() -> Vec<u8> {
1304    let mut b = Vec::with_capacity(4);
1305    b.extend_from_slice(&4u16.to_le_bytes()); // StructureSize
1306    b.extend_from_slice(&0u16.to_le_bytes()); // Reserved
1307    b
1308}
1309
1310/// Wire constants for the SMB2 ERROR response ([MS-SMB2] §2.2.2) and the
1311/// Symbolic Link Error Response it can carry ([MS-SMB2] §2.2.2.2.1).
1312pub mod error {
1313    /// SMB2 ERROR Response StructureSize ([MS-SMB2] §2.2.2).
1314    pub const RESPONSE_STRUCTURE_SIZE: u16 = 9;
1315    /// `SymLinkErrorTag` identifying a symbolic-link error payload (§2.2.2.2.1).
1316    pub const SYMLINK_ERROR_TAG: u32 = 0x4C4D_5953;
1317    /// `IO_REPARSE_TAG_SYMLINK` ([MS-FSCC] §2.1.2.4).
1318    pub const IO_REPARSE_TAG_SYMLINK: u32 = 0xA000_000C;
1319    /// `SYMLINK_FLAG_RELATIVE`: the substitute name is a relative path (§2.2.2.2.1.1).
1320    pub const SYMLINK_FLAG_RELATIVE: u32 = 0x0000_0001;
1321    /// Bytes preceding `PathBuffer` inside the reparse data of §2.2.2.2.1
1322    /// (UnparsedPathLength through Flags: four u16 offsets/lengths + a u32 Flags).
1323    pub const REPARSE_HEADER_LEN: u16 = 12;
1324    /// Bytes from `SymLinkErrorTag` to the start of `PathBuffer` (§2.2.2.2.1):
1325    /// SymLinkErrorTag, ReparseTag, ReparseDataLength, UnparsedPathLength, the
1326    /// four name offset/length fields, and Flags.
1327    pub const SYMLINK_FIXED_LEN: u32 = 24;
1328}
1329
1330/// Build the SMB2 ERROR response body ([MS-SMB2] §2.2.2) carrying a Symbolic
1331/// Link Error Response ([MS-SMB2] §2.2.2.2.1) for STATUS_STOPPED_ON_SYMLINK.
1332///
1333/// `substitute`/`print` are the symlink target (substitute and display names);
1334/// `unparsed_path_len` is the byte length of the request path that follows the
1335/// symlink; `relative` sets `SYMLINK_FLAG_RELATIVE`.
1336pub fn build_symlink_error_response(
1337    substitute: &str,
1338    print: &str,
1339    unparsed_path_len: u16,
1340    relative: bool,
1341) -> Vec<u8> {
1342    let sub: Vec<u8> = substitute
1343        .encode_utf16()
1344        .flat_map(|u| u.to_le_bytes())
1345        .collect();
1346    let prt: Vec<u8> = print.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
1347    let path_len = (sub.len() + prt.len()) as u16;
1348    let flags = if relative {
1349        error::SYMLINK_FLAG_RELATIVE
1350    } else {
1351        0
1352    };
1353
1354    let mut data = Vec::new();
1355    data.extend_from_slice(&(error::SYMLINK_FIXED_LEN + path_len as u32).to_le_bytes()); // SymLinkLength
1356    data.extend_from_slice(&error::SYMLINK_ERROR_TAG.to_le_bytes());
1357    data.extend_from_slice(&error::IO_REPARSE_TAG_SYMLINK.to_le_bytes());
1358    data.extend_from_slice(&(error::REPARSE_HEADER_LEN + path_len).to_le_bytes()); // ReparseDataLength
1359    data.extend_from_slice(&unparsed_path_len.to_le_bytes());
1360    data.extend_from_slice(&0u16.to_le_bytes()); // SubstituteNameOffset
1361    data.extend_from_slice(&(sub.len() as u16).to_le_bytes()); // SubstituteNameLength
1362    data.extend_from_slice(&(sub.len() as u16).to_le_bytes()); // PrintNameOffset
1363    data.extend_from_slice(&(prt.len() as u16).to_le_bytes()); // PrintNameLength
1364    data.extend_from_slice(&flags.to_le_bytes());
1365    data.extend_from_slice(&sub);
1366    data.extend_from_slice(&prt);
1367
1368    let mut body = Vec::with_capacity(8 + data.len());
1369    body.extend_from_slice(&error::RESPONSE_STRUCTURE_SIZE.to_le_bytes()); // StructureSize
1370    body.push(0); // ErrorContextCount
1371    body.push(0); // Reserved
1372    body.extend_from_slice(&(data.len() as u32).to_le_bytes()); // ByteCount
1373    body.extend_from_slice(&data);
1374    body
1375}
1376
1377// ---------------- CHANGE_NOTIFY (§2.2.35 / §2.2.36) ----------------
1378
1379/// CHANGE_NOTIFY completion-filter bits ([MS-SMB2] §2.2.35, [MS-FSCC] §2.7.1).
1380pub mod notify_filter {
1381    /// Renames, additions or deletions of a file name.
1382    pub const FILE_NAME: u32 = 0x0000_0001;
1383    /// Renames, additions or deletions of a directory name.
1384    pub const DIR_NAME: u32 = 0x0000_0002;
1385    /// Attribute changes.
1386    pub const ATTRIBUTES: u32 = 0x0000_0004;
1387    /// Size changes.
1388    pub const SIZE: u32 = 0x0000_0008;
1389    /// Last-write timestamp changes.
1390    pub const LAST_WRITE: u32 = 0x0000_0010;
1391    /// Last-access timestamp changes.
1392    pub const LAST_ACCESS: u32 = 0x0000_0020;
1393    /// Creation timestamp changes.
1394    pub const CREATION: u32 = 0x0000_0040;
1395    /// Extended-attribute changes.
1396    pub const EA: u32 = 0x0000_0080;
1397    /// Named-stream add/rename/delete.
1398    pub const STREAM_NAME: u32 = 0x0000_0200;
1399    /// Named-stream size changes.
1400    pub const STREAM_SIZE: u32 = 0x0000_0400;
1401    /// Named-stream writes.
1402    pub const STREAM_WRITE: u32 = 0x0000_0800;
1403}
1404
1405/// FILE_NOTIFY_INFORMATION action codes ([MS-FSCC] §2.7.1).
1406pub mod notify_action {
1407    /// A file was added to the directory.
1408    pub const ADDED: u32 = 0x0000_0001;
1409    /// A file was removed from the directory.
1410    pub const REMOVED: u32 = 0x0000_0002;
1411    /// A file was modified (size, attributes, timestamps).
1412    pub const MODIFIED: u32 = 0x0000_0003;
1413    /// The old name of a renamed file.
1414    pub const RENAMED_OLD_NAME: u32 = 0x0000_0004;
1415    /// The new name of a renamed file.
1416    pub const RENAMED_NEW_NAME: u32 = 0x0000_0005;
1417}
1418
1419/// SMB2_WATCH_TREE — recurse into subdirectories ([MS-SMB2] §2.2.35).
1420pub const WATCH_TREE: u16 = 0x0001;
1421
1422/// CHANGE_NOTIFY request (§2.2.35).
1423#[derive(Debug)]
1424pub struct ChangeNotifyReq {
1425    /// Watched directory handle.
1426    pub file_id: FileId,
1427    /// Watch the whole subtree rather than just the directory.
1428    pub watch_tree: bool,
1429    /// Maximum bytes of FILE_NOTIFY_INFORMATION the client will accept.
1430    pub output_len: u32,
1431    /// Completion filter selecting which changes fire ([`notify_filter`]).
1432    pub filter: u32,
1433}
1434
1435impl ChangeNotifyReq {
1436    /// Parse from the complete frame.
1437    pub fn parse(frame: &[u8]) -> Option<ChangeNotifyReq> {
1438        if frame.len() < BODY + 32 || g16(frame, BODY) != 32 {
1439            return None;
1440        }
1441        Some(ChangeNotifyReq {
1442            file_id: FileId(frame.get(BODY + 8..BODY + 24)?.try_into().ok()?),
1443            watch_tree: g16(frame, BODY + 2) & WATCH_TREE != 0,
1444            output_len: g32(frame, BODY + 4),
1445            filter: g32(frame, BODY + 24),
1446        })
1447    }
1448}
1449
1450/// Build a FILE_NOTIFY_INFORMATION list ([MS-FSCC] §2.7.1) from `(action,
1451/// name)` pairs. Names are directory-relative, UTF-16LE, no terminator; each
1452/// record is 4-byte aligned and chained through `NextEntryOffset`.
1453pub fn build_file_notify_information(entries: &[(u32, &str)]) -> Vec<u8> {
1454    let mut out = Vec::new();
1455    for (i, (action, name)) in entries.iter().enumerate() {
1456        let rec = out.len();
1457        let name16: Vec<u8> = name.encode_utf16().flat_map(|u| u.to_le_bytes()).collect();
1458        out.extend_from_slice(&0u32.to_le_bytes()); // NextEntryOffset (patched)
1459        out.extend_from_slice(&action.to_le_bytes());
1460        out.extend_from_slice(&(name16.len() as u32).to_le_bytes());
1461        out.extend_from_slice(&name16);
1462        while out.len() % 4 != 0 {
1463            out.push(0);
1464        }
1465        if i + 1 < entries.len() {
1466            let next = (out.len() - rec) as u32;
1467            out[rec..rec + 4].copy_from_slice(&next.to_le_bytes());
1468        }
1469    }
1470    out
1471}
1472
1473/// CHANGE_NOTIFY response body (§2.2.36): StructureSize=9, then the
1474/// FILE_NOTIFY_INFORMATION buffer at offset 72 from the header start.
1475pub fn build_change_notify_resp(buffer: &[u8]) -> Vec<u8> {
1476    let mut b = Vec::with_capacity(8 + buffer.len());
1477    b.extend_from_slice(&9u16.to_le_bytes()); // StructureSize
1478    b.extend_from_slice(&72u16.to_le_bytes()); // OutputBufferOffset (64 + 8)
1479    b.extend_from_slice(&(buffer.len() as u32).to_le_bytes());
1480    b.extend_from_slice(buffer);
1481    b
1482}
1483
1484// ---------------- IOCTL (§2.2.31 / §2.2.32) ----------------
1485
1486/// IOCTL request fixed-part offsets.
1487mod ioctl_off {
1488    use super::BODY;
1489    /// StructureSize (=57).
1490    pub const STRUCT: usize = BODY;
1491    /// CtlCode.
1492    pub const CTL_CODE: usize = BODY + 4;
1493    /// FileId.
1494    pub const FILE_ID: usize = BODY + 8;
1495    /// InputBufferOffset (absolute).
1496    pub const INPUT_OFFSET: usize = BODY + 24;
1497    /// InputBufferLength.
1498    pub const INPUT_COUNT: usize = BODY + 28;
1499    /// MaxOutputResponse.
1500    pub const MAX_OUTPUT: usize = BODY + 44;
1501    /// Flags.
1502    pub const FLAGS: usize = BODY + 48;
1503}
1504
1505/// FSCTL codes used by clients during share access ([MS-SMB2] §2.2.31).
1506pub mod fsctl {
1507    /// FSCTL_DFS_GET_REFERRALS ([MS-SMB2] §2.2.31.1).
1508    pub const DFS_GET_REFERRALS: u32 = 0x0006_0194;
1509    /// FSCTL_PIPE_WAIT ([MS-SMB2] §2.2.31.2):
1510    /// CTL_CODE(FILE_DEVICE_NAMED_PIPE, 6, METHOD_BUFFERED, FILE_ANY_ACCESS).
1511    pub const PIPE_WAIT: u32 = 0x0011_0018;
1512    /// FSCTL_QUERY_NETWORK_INTERFACE_INFO ([MS-SMB2] §2.2.31.4) — multichannel
1513    /// interface discovery.
1514    pub const QUERY_NETWORK_INTERFACE_INFO: u32 = 0x0014_01FC;
1515    /// FSCTL_SRV_REQUEST_RESUME_KEY.
1516    pub const SRV_REQUEST_RESUME_KEY: u32 = 0x0014_0078;
1517    /// FSCTL_SRV_COPYCHUNK — server-side copy ([MS-SMB2] §2.2.31.1).
1518    pub const SRV_COPYCHUNK: u32 = 0x0014_40F2;
1519    /// FSCTL_SRV_COPYCHUNK_WRITE — server-side copy, write handle.
1520    pub const SRV_COPYCHUNK_WRITE: u32 = 0x0014_80F2;
1521    /// FSCTL_OFFLOAD_READ ([MS-FSCC] §2.3.77) — generate a token for a range.
1522    pub const OFFLOAD_READ: u32 = 0x0009_4264;
1523    /// FSCTL_OFFLOAD_WRITE ([MS-FSCC] §2.3.79) — write a token-represented range.
1524    pub const OFFLOAD_WRITE: u32 = 0x0009_8268;
1525    /// FSCTL_SET_SPARSE ([MS-FSCC] §2.3.67).
1526    pub const SET_SPARSE: u32 = 0x0009_00C4;
1527    /// FSCTL_SET_ZERO_DATA ([MS-FSCC] §2.3.79) — punch a zero range.
1528    pub const SET_ZERO_DATA: u32 = 0x0009_80C8;
1529    /// FSCTL_FILE_LEVEL_TRIM ([MS-FSCC] §2.3.73) — deallocate byte ranges.
1530    pub const FILE_LEVEL_TRIM: u32 = 0x0009_8208;
1531    /// FSCTL_GET_INTEGRITY_INFORMATION ([MS-FSCC] §2.3.55).
1532    pub const GET_INTEGRITY_INFORMATION: u32 = 0x0009_027C;
1533    /// FSCTL_SET_INTEGRITY_INFORMATION ([MS-FSCC] §2.3.57).
1534    pub const SET_INTEGRITY_INFORMATION: u32 = 0x0009_C280;
1535    /// FSCTL_LMR_REQ_RESILIENCY.
1536    pub const LMR_REQUEST_RESILIENCY: u32 = 0x0014_01D4;
1537    /// FSCTL_PIPE_TRANSACT ([MS-SMB2] §2.2.31.10 "Transact named pipe"):
1538    /// DCERPC PDUs tunnelled through the IOCTL input/output buffers.
1539    pub const PIPE_TRANSACT: u32 = 0x0011_C017;
1540    /// FSCTL_VALIDATE_NEGOTIATE_INFO.
1541    pub const VALIDATE_NEGOTIATE_INFO: u32 = 0x0014_0204;
1542}
1543
1544/// IOCTL request body (§2.2.31).
1545#[derive(Debug)]
1546pub struct IoctlReq {
1547    /// Control code.
1548    pub ctl_code: u32,
1549    /// Target handle (may be all-FF for special files).
1550    pub file_id: FileId,
1551    /// Raw input buffer.
1552    pub input: Vec<u8>,
1553    /// Bytes the client accepts as output.
1554    pub max_output: u32,
1555    /// True when this is a filesystem device request (Flags bit 1 clear).
1556    #[allow(dead_code)]
1557    pub is_fsctl: bool,
1558}
1559
1560impl IoctlReq {
1561    /// Parse from the complete frame.
1562    pub fn parse(frame: &[u8]) -> Option<IoctlReq> {
1563        if frame.len() < ioctl_off::FLAGS + 8 || g16(frame, ioctl_off::STRUCT) != 57 {
1564            return None;
1565        }
1566        let ioff = g32(frame, ioctl_off::INPUT_OFFSET) as usize;
1567        let icnt = g32(frame, ioctl_off::INPUT_COUNT) as usize;
1568        let input = if icnt > 0 && ioff >= BODY {
1569            frame.get(ioff..(ioff + icnt).min(frame.len()))?.to_vec()
1570        } else {
1571            Vec::new()
1572        };
1573        Some(IoctlReq {
1574            ctl_code: g32(frame, ioctl_off::CTL_CODE),
1575            file_id: FileId(
1576                frame
1577                    .get(ioctl_off::FILE_ID..ioctl_off::FILE_ID + 16)?
1578                    .try_into()
1579                    .ok()?,
1580            ),
1581            input,
1582            max_output: g32(frame, ioctl_off::MAX_OUTPUT),
1583            is_fsctl: g32(frame, ioctl_off::FLAGS) & 0x1 == 0,
1584        })
1585    }
1586}
1587
1588/// Build an IOCTL response body (§2.2.32.1). Fixed part is 48 bytes;
1589/// StructureSize mirrors smbd (49); `output` lands 8-byte aligned after it.
1590pub fn build_ioctl_resp(file_id: FileId, ctl_code: u32, output: &[u8]) -> Vec<u8> {
1591    const FIXED: usize = 48;
1592    let mut b = Vec::with_capacity(FIXED + output.len());
1593    b.extend_from_slice(&49u16.to_le_bytes()); // StructureSize
1594    b.extend_from_slice(&0u16.to_le_bytes()); // Reserved
1595    b.extend_from_slice(&ctl_code.to_le_bytes());
1596    b.extend_from_slice(&file_id.0);
1597    b.extend_from_slice(&0u32.to_le_bytes()); // InputOffset (none)
1598    b.extend_from_slice(&0u32.to_le_bytes()); // InputCount
1599    let out_off = BODY + FIXED;
1600    b.extend_from_slice(&(out_off as u32).to_le_bytes()); // OutputOffset
1601    b.extend_from_slice(&(output.len() as u32).to_le_bytes()); // OutputCount
1602    b.extend_from_slice(&0u32.to_le_bytes()); // Flags
1603    b.extend_from_slice(&0u32.to_le_bytes()); // Reserved2
1604    while !(BODY + b.len()).is_multiple_of(8) || BODY + b.len() < out_off {
1605        b.push(0);
1606    }
1607    b.extend_from_slice(output);
1608    debug_assert_eq!(b.len(), FIXED + output.len());
1609    b
1610}
1611
1612// ---------------- Server-side copy ([MS-SMB2] §2.2.31.1 / §2.2.32.1) ----------
1613
1614/// Server-side copy limits ([MS-SMB2] §3.3.5.15.6).
1615pub mod copychunk_limits {
1616    /// Maximum chunks per request.
1617    pub const MAX_CHUNKS: u32 = 16;
1618    /// Maximum bytes per chunk (1 MiB).
1619    pub const MAX_CHUNK_SIZE: u32 = 1_048_576;
1620    /// Maximum total bytes per request (16 MiB).
1621    pub const MAX_TOTAL_SIZE: u32 = 16_777_216;
1622}
1623
1624/// One SRV_COPYCHUNK entry ([MS-SMB2] §2.2.31.1).
1625#[derive(Debug, Clone, Copy)]
1626pub struct CopyChunk {
1627    /// Byte offset in the source file.
1628    pub source_offset: u64,
1629    /// Byte offset in the target file.
1630    pub target_offset: u64,
1631    /// Number of bytes to copy.
1632    pub length: u32,
1633}
1634
1635/// Parsed SRV_COPYCHUNK_COPY request ([MS-SMB2] §2.2.31.1): a 24-byte source
1636/// resume key followed by a chunk list.
1637#[derive(Debug)]
1638pub struct CopyChunkCopy {
1639    /// Resume key identifying the source open ([`build_resume_key_resp`]).
1640    pub source_key: [u8; 24],
1641    /// Copy operations to perform.
1642    pub chunks: Vec<CopyChunk>,
1643}
1644
1645impl CopyChunkCopy {
1646    /// Parse from the IOCTL input buffer.
1647    pub fn parse(input: &[u8]) -> Option<CopyChunkCopy> {
1648        if input.len() < 32 {
1649            return None;
1650        }
1651        let source_key: [u8; 24] = input[0..24].try_into().ok()?;
1652        let chunk_count = g32(input, 24) as usize;
1653        // Reserved at [28..32].
1654        let mut chunks = Vec::with_capacity(chunk_count.min(64));
1655        for i in 0..chunk_count {
1656            let base = 32 + i * 24;
1657            let e = input.get(base..base + 24)?;
1658            chunks.push(CopyChunk {
1659                source_offset: g64(e, 0),
1660                target_offset: g64(e, 8),
1661                length: g32(e, 16),
1662            });
1663        }
1664        Some(CopyChunkCopy { source_key, chunks })
1665    }
1666}
1667
1668/// Build the SRV_REQUESTED_RESUME_KEY response ([MS-SMB2] §2.2.32.3): the
1669/// 24-byte key then a zero ContextLength.
1670pub fn build_resume_key_resp(key: &[u8; 24]) -> Vec<u8> {
1671    let mut b = Vec::with_capacity(32);
1672    b.extend_from_slice(key);
1673    b.extend_from_slice(&0u32.to_le_bytes()); // ContextLength
1674    b.extend_from_slice(&0u32.to_le_bytes()); // Context (empty, padded)
1675    b
1676}
1677
1678/// Build the SRV_COPYCHUNK_RESPONSE ([MS-SMB2] §2.2.32.1). On success it
1679/// reports chunks/bytes written; on a limits violation the caller sends it
1680/// with STATUS_INVALID_PARAMETER carrying the server's maximums.
1681pub fn build_copychunk_resp(
1682    chunks_written: u32,
1683    chunk_bytes_written: u32,
1684    total_bytes_written: u32,
1685) -> Vec<u8> {
1686    let mut b = Vec::with_capacity(12);
1687    b.extend_from_slice(&chunks_written.to_le_bytes());
1688    b.extend_from_slice(&chunk_bytes_written.to_le_bytes());
1689    b.extend_from_slice(&total_bytes_written.to_le_bytes());
1690    b
1691}
1692
1693/// Parse FILE_ZERO_DATA_INFORMATION ([MS-FSCC] §2.3.79): `(FileOffset,
1694/// BeyondFinalZero)`. The zeroed range is `[FileOffset, BeyondFinalZero)`.
1695pub fn parse_zero_data(input: &[u8]) -> Option<(u64, u64)> {
1696    if input.len() < 16 {
1697        return None;
1698    }
1699    Some((g64(input, 0), g64(input, 8)))
1700}
1701
1702/// A STORAGE_OFFLOAD_TOKEN ([MS-FSCC] §2.3.80) is 512 bytes: an 8-byte header
1703/// (TokenType, Reserved, TokenIdLength — all big-endian) then a 504-byte
1704/// TokenId. This server issues data tokens whose TokenId begins with a 16-byte
1705/// server-local identifier keying the captured source range.
1706pub mod offload {
1707    /// Total STORAGE_OFFLOAD_TOKEN size.
1708    pub const TOKEN_LEN: usize = 512;
1709    /// Byte offset of the TokenId within the token (after the 8-byte header).
1710    pub const TOKEN_ID_OFFSET: usize = 8;
1711    /// Vendor TokenType for a server-generated data token (non-well-known, so
1712    /// distinct from STORAGE_OFFLOAD_TOKEN_TYPE_ZERO_DATA = 0xFFFF0001).
1713    pub const TOKEN_TYPE_DATA: u32 = 0x0000_0001;
1714    /// FSCTL_OFFLOAD_READ_INPUT size.
1715    pub const READ_INPUT_SIZE: u32 = 32;
1716    /// FSCTL_OFFLOAD_READ_OUTPUT size (header + token).
1717    pub const READ_OUTPUT_SIZE: u32 = 16 + TOKEN_LEN as u32;
1718    /// FSCTL_OFFLOAD_WRITE_OUTPUT size.
1719    pub const WRITE_OUTPUT_SIZE: u32 = 16;
1720}
1721
1722/// Parse FSCTL_OFFLOAD_READ_INPUT ([MS-FSCC] §2.3.77) into `(FileOffset,
1723/// CopyLength)`; the Size/Flags/TokenTimeToLive/Reserved header is ignored.
1724pub fn parse_offload_read(input: &[u8]) -> Option<(u64, u64)> {
1725    if input.len() < offload::READ_INPUT_SIZE as usize {
1726        return None;
1727    }
1728    Some((g64(input, 16), g64(input, 24)))
1729}
1730
1731/// Build an FSCTL_OFFLOAD_READ_OUTPUT ([MS-FSCC] §2.3.78) carrying the transfer
1732/// length and a data token whose TokenId embeds `token_id`.
1733pub fn build_offload_read_resp(transfer_length: u64, token_id: &[u8; 16]) -> Vec<u8> {
1734    let mut b = Vec::with_capacity(offload::READ_OUTPUT_SIZE as usize);
1735    b.extend_from_slice(&offload::READ_OUTPUT_SIZE.to_le_bytes()); // Size
1736    b.extend_from_slice(&0u32.to_le_bytes()); // Flags
1737    b.extend_from_slice(&transfer_length.to_le_bytes()); // TransferLength
1738    // STORAGE_OFFLOAD_TOKEN: big-endian TokenType, Reserved, TokenIdLength.
1739    b.extend_from_slice(&offload::TOKEN_TYPE_DATA.to_be_bytes());
1740    b.extend_from_slice(&0u16.to_be_bytes()); // Reserved
1741    b.extend_from_slice(&(token_id.len() as u16).to_be_bytes()); // TokenIdLength
1742    b.extend_from_slice(token_id);
1743    b.resize(offload::READ_OUTPUT_SIZE as usize, 0); // pad TokenId to 504 bytes
1744    b
1745}
1746
1747/// Parse FSCTL_OFFLOAD_WRITE_INPUT ([MS-FSCC] §2.3.79) into `(FileOffset,
1748/// CopyLength, TransferOffset, token_id)`, extracting the 16-byte identifier
1749/// from the token this server issued.
1750pub fn parse_offload_write(input: &[u8]) -> Option<(u64, u64, u64, [u8; 16])> {
1751    let header = 4 + 4 + 8 + 8 + 8; // Size, Flags, FileOffset, CopyLength, TransferOffset
1752    if input.len() < header + offload::TOKEN_LEN {
1753        return None;
1754    }
1755    let file_offset = g64(input, 8);
1756    let copy_length = g64(input, 16);
1757    let transfer_offset = g64(input, 24);
1758    let id_at = header + offload::TOKEN_ID_OFFSET;
1759    let token_id: [u8; 16] = input[id_at..id_at + 16].try_into().ok()?;
1760    Some((file_offset, copy_length, transfer_offset, token_id))
1761}
1762
1763/// Build an FSCTL_OFFLOAD_WRITE_OUTPUT ([MS-FSCC] §2.3.80) reporting the number
1764/// of bytes written.
1765pub fn build_offload_write_resp(length_written: u64) -> Vec<u8> {
1766    let mut b = Vec::with_capacity(offload::WRITE_OUTPUT_SIZE as usize);
1767    b.extend_from_slice(&offload::WRITE_OUTPUT_SIZE.to_le_bytes()); // Size
1768    b.extend_from_slice(&0u32.to_le_bytes()); // Flags
1769    b.extend_from_slice(&length_written.to_le_bytes()); // LengthWritten
1770    b
1771}
1772
1773/// Parse an FSCTL_FILE_LEVEL_TRIM request ([MS-FSCC] §2.3.73) into its `Key`
1774/// and `NumRanges` fields. The per-range array follows but is advisory.
1775pub fn parse_file_level_trim(input: &[u8]) -> Option<(u32, u32)> {
1776    if input.len() < 8 {
1777        return None;
1778    }
1779    Some((g32(input, 0), g32(input, 4)))
1780}
1781
1782/// Build an FSCTL_FILE_LEVEL_TRIM_OUTPUT ([MS-FSCC] §2.3.74) reporting the
1783/// number of ranges processed.
1784pub fn build_file_level_trim_resp(num_ranges_processed: u32) -> Vec<u8> {
1785    num_ranges_processed.to_le_bytes().to_vec()
1786}
1787
1788/// Parse the ChecksumAlgorithm field of an FSCTL_SET_INTEGRITY_INFORMATION
1789/// request ([MS-FSCC] §2.3.57): ChecksumAlgorithm(2) Reserved(2) Flags(4).
1790pub fn parse_set_integrity(input: &[u8]) -> Option<u16> {
1791    (input.len() >= 8).then(|| g16(input, 0))
1792}
1793
1794/// Build an FSCTL_GET_INTEGRITY_INFORMATION reply ([MS-FSCC] §2.3.55):
1795/// ChecksumAlgorithm(2) Reserved(2) Flags(4) ChecksumChunkSizeInBytes(4)
1796/// ClusterSizeInBytes(4).
1797pub fn build_get_integrity_resp(algorithm: u16, flags: u32) -> Vec<u8> {
1798    const CHUNK_SIZE: u32 = 512;
1799    const CLUSTER_SIZE: u32 = 4096;
1800    let mut b = Vec::with_capacity(16);
1801    b.extend_from_slice(&algorithm.to_le_bytes());
1802    b.extend_from_slice(&0u16.to_le_bytes()); // Reserved
1803    b.extend_from_slice(&flags.to_le_bytes());
1804    b.extend_from_slice(&CHUNK_SIZE.to_le_bytes());
1805    b.extend_from_slice(&CLUSTER_SIZE.to_le_bytes());
1806    b
1807}
1808
1809// ---------------- Encryption transform header ([MS-SMB2] §2.2.41) ----------------
1810
1811/// Transform-header field offsets.
1812pub mod tf_off {
1813    /// ProtocolId = `\xFD 'S' 'M' 'B'`.
1814    pub const PROTOCOL_ID: usize = 0;
1815    /// AEAD tag (16 bytes) — zeroed inside the AAD region.
1816    pub const SIGNATURE: usize = 4;
1817    /// Nonce (16 bytes; GCM consumes 12, CCM 11, remainder zeroed).
1818    pub const NONCE: usize = 20;
1819    /// OriginalMessageSize.
1820    pub const MSG_SIZE: usize = 36;
1821    /// Reserved.
1822    pub const RESERVED: usize = 40;
1823    /// Flags (bit 0 = SMB2_TF_FLAGS_ENCRYPTED).
1824    pub const FLAGS: usize = 42;
1825    /// SessionId.
1826    pub const SESSION_ID: usize = 44;
1827    /// Fixed header size.
1828    pub const HDR_SIZE: usize = 52;
1829}
1830
1831/// `\xFD 'S' 'M' 'B'` — transform-frame magic.
1832pub const TF_MAGIC: [u8; 4] = [0xFD, b'S', b'M', b'B'];
1833/// SMB2_TF_FLAGS_ENCRYPTED.
1834pub const TF_FLAGS_ENCRYPTED: u16 = 0x0001;
1835
1836/// Parsed transform header ([MS-SMB2] §2.2.41).
1837#[derive(Debug)]
1838pub struct TransformHdr {
1839    /// OriginalMessageSize — length of the plaintext SMB2 message.
1840    pub original_len: usize,
1841    /// SessionId the message belongs to.
1842    pub session_id: u64,
1843    /// Flags word (ENCRYPTED bit).
1844    pub flags: u16,
1845}
1846
1847impl TransformHdr {
1848    /// Parse the 52-byte header from the start of `frame`; verifies magic.
1849    pub fn parse(frame: &[u8]) -> Option<TransformHdr> {
1850        if frame.len() < tf_off::HDR_SIZE || frame[..4] != TF_MAGIC {
1851            return None;
1852        }
1853        Some(TransformHdr {
1854            original_len: g32(frame, tf_off::MSG_SIZE) as usize,
1855            session_id: g64(frame, tf_off::SESSION_ID),
1856            flags: g16(frame, tf_off::FLAGS),
1857        })
1858    }
1859
1860    /// Additional-authenticated-data view: header bytes from Nonce to the
1861    /// end ([MS-SMB2] §3.1.4.2 — everything after the 16-byte Nonce field,
1862    /// with Signature excluded because it precedes the Nonce).
1863    pub fn aad<'a>(&self, frame: &'a [u8]) -> &'a [u8] {
1864        &frame[tf_off::NONCE..tf_off::HDR_SIZE]
1865    }
1866}
1867
1868/// Build the 52-byte transform header with a zeroed Signature field
1869/// (the AEAD tag is copied in after sealing).
1870#[allow(clippy::too_many_arguments)]
1871pub fn build_transform(session_id: u64, nonce: &[u8; 16], original_len: usize) -> Vec<u8> {
1872    let mut t = Vec::with_capacity(tf_off::HDR_SIZE);
1873    t.extend_from_slice(&TF_MAGIC);
1874    t.extend_from_slice(&[0u8; 16]); // Signature (tag lands here)
1875    t.extend_from_slice(nonce);
1876    t.extend_from_slice(&(original_len as u32).to_le_bytes());
1877    t.extend_from_slice(&0u16.to_le_bytes()); // Reserved
1878    t.extend_from_slice(&TF_FLAGS_ENCRYPTED.to_le_bytes()); // Flags
1879    t.extend_from_slice(&session_id.to_le_bytes());
1880    debug_assert_eq!(t.len(), tf_off::HDR_SIZE);
1881    t
1882}
1883
1884#[cfg(test)]
1885mod change_notify_tests {
1886    use super::*;
1887
1888    fn change_notify_request(file_id: [u8; 16], watch_tree: bool, filter: u32) -> Vec<u8> {
1889        let mut f = vec![0u8; BODY + 32];
1890        f[BODY..BODY + 2].copy_from_slice(&32u16.to_le_bytes()); // StructureSize
1891        let flags: u16 = if watch_tree { WATCH_TREE } else { 0 };
1892        f[BODY + 2..BODY + 4].copy_from_slice(&flags.to_le_bytes());
1893        f[BODY + 4..BODY + 8].copy_from_slice(&65536u32.to_le_bytes()); // OutputBufferLength
1894        f[BODY + 8..BODY + 24].copy_from_slice(&file_id);
1895        f[BODY + 24..BODY + 28].copy_from_slice(&filter.to_le_bytes());
1896        f
1897    }
1898
1899    #[test]
1900    fn parses_change_notify_request() {
1901        let fid = [7u8; 16];
1902        let frame = change_notify_request(
1903            fid,
1904            true,
1905            notify_filter::FILE_NAME | notify_filter::LAST_WRITE,
1906        );
1907        let req = ChangeNotifyReq::parse(&frame).expect("parse");
1908        assert_eq!(req.file_id.0, fid);
1909        assert!(req.watch_tree);
1910        assert_eq!(req.output_len, 65536);
1911        assert_eq!(
1912            req.filter,
1913            notify_filter::FILE_NAME | notify_filter::LAST_WRITE
1914        );
1915    }
1916
1917    #[test]
1918    fn rejects_wrong_structure_size() {
1919        let mut frame = change_notify_request([0u8; 16], false, 0);
1920        frame[BODY..BODY + 2].copy_from_slice(&31u16.to_le_bytes());
1921        assert!(ChangeNotifyReq::parse(&frame).is_none());
1922    }
1923
1924    #[test]
1925    fn single_notify_record_is_self_terminating() {
1926        let buf = build_file_notify_information(&[(notify_action::ADDED, "new.txt")]);
1927        // NextEntryOffset = 0 (last), Action = ADDED, FileNameLength = 14 bytes.
1928        assert_eq!(&buf[0..4], &0u32.to_le_bytes());
1929        assert_eq!(&buf[4..8], &notify_action::ADDED.to_le_bytes());
1930        assert_eq!(&buf[8..12], &14u32.to_le_bytes());
1931        assert_eq!(
1932            &buf[12..26],
1933            &"new.txt"
1934                .encode_utf16()
1935                .flat_map(|u| u.to_le_bytes())
1936                .collect::<Vec<_>>()[..]
1937        );
1938        assert_eq!(buf.len() % 4, 0);
1939    }
1940
1941    #[test]
1942    fn chained_notify_records_link_via_next_offset() {
1943        let buf = build_file_notify_information(&[
1944            (notify_action::ADDED, "a.txt"),
1945            (notify_action::REMOVED, "b.txt"),
1946        ]);
1947        let next = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize;
1948        assert_ne!(next, 0, "first record must point to the second");
1949        assert_eq!(next % 4, 0, "records are 4-byte aligned");
1950        assert_eq!(
1951            &buf[next..next + 4],
1952            &0u32.to_le_bytes(),
1953            "second record terminates"
1954        );
1955        assert_eq!(
1956            &buf[next + 4..next + 8],
1957            &notify_action::REMOVED.to_le_bytes()
1958        );
1959    }
1960
1961    #[test]
1962    fn change_notify_response_header_offsets() {
1963        let notify = build_file_notify_information(&[(notify_action::MODIFIED, "x")]);
1964        let body = build_change_notify_resp(&notify);
1965        assert_eq!(&body[0..2], &9u16.to_le_bytes(), "StructureSize");
1966        assert_eq!(&body[2..4], &72u16.to_le_bytes(), "OutputBufferOffset");
1967        assert_eq!(
1968            &body[4..8],
1969            &(notify.len() as u32).to_le_bytes(),
1970            "OutputBufferLength"
1971        );
1972        assert_eq!(&body[8..], &notify[..]);
1973    }
1974
1975    /// Fixed-size response bodies whose SMB2 StructureSize is 4 must be a full
1976    /// 4 bytes on the wire (StructureSize + 2-byte Reserved); a 2-byte body
1977    /// makes strict clients fail to unpack the Reserved field.
1978    #[test]
1979    fn fixed_four_byte_responses_carry_reserved() {
1980        for (name, body) in [
1981            ("tree_disconnect", build_tree_disconnect_resp()),
1982            ("logoff", build_logoff_resp()),
1983            ("echo", build_echo_resp()),
1984            ("flush", build_flush_resp()),
1985            ("lock", build_lock_resp()),
1986        ] {
1987            assert_eq!(body.len(), 4, "{name} response must be 4 bytes");
1988            assert_eq!(&body[0..2], &4u16.to_le_bytes(), "{name} StructureSize=4");
1989            assert_eq!(&body[2..4], &0u16.to_le_bytes(), "{name} Reserved=0");
1990        }
1991    }
1992}
1993
1994#[cfg(test)]
1995mod fsctl_tests {
1996    use super::*;
1997
1998    #[test]
1999    fn parses_copychunk_copy() {
2000        let key = [0xABu8; 24];
2001        let mut input = Vec::new();
2002        input.extend_from_slice(&key);
2003        input.extend_from_slice(&2u32.to_le_bytes()); // ChunkCount
2004        input.extend_from_slice(&0u32.to_le_bytes()); // Reserved
2005        for (so, to, len) in [(0u64, 100u64, 10u32), (10, 110, 20)] {
2006            input.extend_from_slice(&so.to_le_bytes());
2007            input.extend_from_slice(&to.to_le_bytes());
2008            input.extend_from_slice(&len.to_le_bytes());
2009            input.extend_from_slice(&0u32.to_le_bytes()); // Reserved
2010        }
2011        let cc = CopyChunkCopy::parse(&input).expect("parse");
2012        assert_eq!(cc.source_key, key);
2013        assert_eq!(cc.chunks.len(), 2);
2014        assert_eq!(cc.chunks[1].source_offset, 10);
2015        assert_eq!(cc.chunks[1].target_offset, 110);
2016        assert_eq!(cc.chunks[1].length, 20);
2017    }
2018
2019    #[test]
2020    fn copychunk_response_fields() {
2021        let r = build_copychunk_resp(3, 0, 999);
2022        assert_eq!(&r[0..4], &3u32.to_le_bytes(), "ChunksWritten");
2023        assert_eq!(&r[4..8], &0u32.to_le_bytes(), "ChunkBytesWritten");
2024        assert_eq!(&r[8..12], &999u32.to_le_bytes(), "TotalBytesWritten");
2025    }
2026
2027    #[test]
2028    fn resume_key_response_carries_key() {
2029        let key = [7u8; 24];
2030        let r = build_resume_key_resp(&key);
2031        assert_eq!(&r[0..24], &key);
2032        assert_eq!(&r[24..28], &0u32.to_le_bytes(), "ContextLength=0");
2033    }
2034
2035    #[test]
2036    fn parses_zero_data_range() {
2037        let mut input = Vec::new();
2038        input.extend_from_slice(&4096u64.to_le_bytes());
2039        input.extend_from_slice(&8192u64.to_le_bytes());
2040        assert_eq!(parse_zero_data(&input), Some((4096, 8192)));
2041        assert!(parse_zero_data(&input[..8]).is_none());
2042    }
2043}
2044
2045#[cfg(test)]
2046mod lock_tests {
2047    use super::*;
2048
2049    fn lock_request(file_id: [u8; 16], elems: &[(u64, u64, u32)]) -> Vec<u8> {
2050        let mut f = vec![0u8; BODY];
2051        f.extend_from_slice(&48u16.to_le_bytes()); // StructureSize
2052        f.extend_from_slice(&(elems.len() as u16).to_le_bytes()); // LockCount
2053        f.extend_from_slice(&0u32.to_le_bytes()); // LockSequence
2054        f.extend_from_slice(&file_id); // FileId (BODY+8..BODY+24)
2055        for &(off, len, flags) in elems {
2056            f.extend_from_slice(&off.to_le_bytes());
2057            f.extend_from_slice(&len.to_le_bytes());
2058            f.extend_from_slice(&flags.to_le_bytes());
2059            f.extend_from_slice(&0u32.to_le_bytes()); // Reserved
2060        }
2061        f
2062    }
2063
2064    #[test]
2065    fn parses_lock_elements_at_correct_offset() {
2066        // Elements start at BODY+24 (24-byte header), not BODY+48.
2067        let fid = [9u8; 16];
2068        let frame = lock_request(fid, &[(0, 10, 0x02 | 0x10)]); // EXCLUSIVE|FAIL_IMMEDIATELY
2069        let req = LockReq::parse(&frame).expect("parse");
2070        assert_eq!(req.file_id.0, fid);
2071        assert_eq!(req.locks.len(), 1);
2072        assert_eq!(req.locks[0].offset, 0);
2073        assert_eq!(req.locks[0].length, 10);
2074        assert!(req.locks[0].exclusive);
2075        assert!(req.locks[0].fail_immediately);
2076        assert!(!req.locks[0].unlock);
2077    }
2078
2079    #[test]
2080    fn parses_unlock_and_shared_flags() {
2081        let frame = lock_request([0u8; 16], &[(5, 5, 0x01), (5, 5, 0x04)]); // SHARED, UNLOCK
2082        let req = LockReq::parse(&frame).expect("parse");
2083        assert_eq!(req.locks.len(), 2);
2084        assert!(req.locks[0].shared && !req.locks[0].unlock);
2085        assert!(req.locks[1].unlock && !req.locks[1].shared);
2086    }
2087}
2088
2089#[cfg(test)]
2090mod oplock_codec_tests {
2091    use super::*;
2092
2093    #[test]
2094    fn create_response_carries_oplock_level() {
2095        let body = build_create_resp(
2096            FileId([1; 16]),
2097            1,
2098            [0; 4],
2099            0,
2100            0,
2101            0,
2102            false,
2103            oplock::EXCLUSIVE,
2104            &[],
2105        );
2106        assert_eq!(body[2], oplock::EXCLUSIVE, "OplockLevel @2");
2107        assert_eq!(&body[80..84], &0u32.to_le_bytes(), "no contexts");
2108    }
2109
2110    #[test]
2111    fn oplock_break_notification_shape() {
2112        let brk = build_oplock_break(FileId([9; 16]), oplock::NONE);
2113        assert_eq!(brk.len(), 24);
2114        assert_eq!(&brk[0..2], &24u16.to_le_bytes(), "StructureSize");
2115        assert_eq!(brk[2], oplock::NONE, "OplockLevel");
2116        assert_eq!(&brk[8..24], &[9u8; 16], "FileId");
2117    }
2118
2119    #[test]
2120    fn parses_oplock_break_ack() {
2121        let mut frame = vec![0u8; BODY];
2122        frame.extend_from_slice(&build_oplock_break(FileId([7; 16]), oplock::LEVEL_II));
2123        let ack = OplockBreakAck::parse(&frame).expect("parse");
2124        assert_eq!(ack.level, oplock::LEVEL_II);
2125        assert_eq!(ack.file_id.0, [7; 16]);
2126    }
2127}
2128
2129#[cfg(test)]
2130mod symlink_error_tests {
2131    use super::*;
2132
2133    #[test]
2134    fn symlink_error_response_matches_spec_layout() {
2135        // Relative symlink "t" with an unparsed tail of 8 bytes (4 UTF-16 units).
2136        let body = build_symlink_error_response("t", "t", 8, true);
2137        assert_eq!(
2138            u16::from_le_bytes([body[0], body[1]]),
2139            error::RESPONSE_STRUCTURE_SIZE
2140        );
2141        assert_eq!(body[2], 0, "ErrorContextCount");
2142        let byte_count = u32::from_le_bytes(body[4..8].try_into().unwrap()) as usize;
2143        let data = &body[8..];
2144        assert_eq!(byte_count, data.len());
2145
2146        let name_bytes = 2u16; // "t" as one UTF-16 unit
2147        assert_eq!(
2148            u32::from_le_bytes(data[0..4].try_into().unwrap()),
2149            error::SYMLINK_FIXED_LEN + (2 * name_bytes) as u32,
2150            "SymLinkLength"
2151        );
2152        assert_eq!(
2153            u32::from_le_bytes(data[4..8].try_into().unwrap()),
2154            error::SYMLINK_ERROR_TAG
2155        );
2156        assert_eq!(
2157            u32::from_le_bytes(data[8..12].try_into().unwrap()),
2158            error::IO_REPARSE_TAG_SYMLINK
2159        );
2160        assert_eq!(
2161            u16::from_le_bytes(data[12..14].try_into().unwrap()),
2162            error::REPARSE_HEADER_LEN + 2 * name_bytes,
2163            "ReparseDataLength"
2164        );
2165        assert_eq!(
2166            u16::from_le_bytes(data[14..16].try_into().unwrap()),
2167            8,
2168            "UnparsedPathLength"
2169        );
2170        assert_eq!(
2171            u16::from_le_bytes(data[16..18].try_into().unwrap()),
2172            0,
2173            "SubstituteNameOffset"
2174        );
2175        assert_eq!(
2176            u16::from_le_bytes(data[18..20].try_into().unwrap()),
2177            name_bytes,
2178            "SubstituteNameLength"
2179        );
2180        assert_eq!(
2181            u16::from_le_bytes(data[20..22].try_into().unwrap()),
2182            name_bytes,
2183            "PrintNameOffset"
2184        );
2185        assert_eq!(
2186            u16::from_le_bytes(data[22..24].try_into().unwrap()),
2187            name_bytes,
2188            "PrintNameLength"
2189        );
2190        assert_eq!(
2191            u32::from_le_bytes(data[24..28].try_into().unwrap()),
2192            error::SYMLINK_FLAG_RELATIVE
2193        );
2194    }
2195
2196    #[test]
2197    fn absolute_symlink_clears_relative_flag() {
2198        let body = build_symlink_error_response("/abs", "/abs", 0, false);
2199        assert_eq!(u32::from_le_bytes(body[8..][24..28].try_into().unwrap()), 0);
2200    }
2201}
2202
2203#[cfg(test)]
2204mod lease_codec_tests {
2205    use super::*;
2206
2207    /// Wrap a lease data blob in a `RqLs` create-context inside a full CREATE
2208    /// request frame and confirm the parser recovers it.
2209    fn create_with_lease(key: [u8; 16], state: u32, v2: bool) -> Vec<u8> {
2210        let data_len = if v2 { 52usize } else { 32 };
2211        let mut ctx = Vec::new();
2212        ctx.extend_from_slice(&0u32.to_le_bytes()); // Next
2213        ctx.extend_from_slice(&16u16.to_le_bytes()); // NameOffset
2214        ctx.extend_from_slice(&4u16.to_le_bytes()); // NameLength
2215        ctx.extend_from_slice(&0u16.to_le_bytes()); // Reserved
2216        ctx.extend_from_slice(&24u16.to_le_bytes()); // DataOffset
2217        ctx.extend_from_slice(&(data_len as u32).to_le_bytes()); // DataLength
2218        ctx.extend_from_slice(lease::CONTEXT_NAME);
2219        ctx.extend_from_slice(&[0u8; 4]); // pad to 24
2220        ctx.extend_from_slice(&key);
2221        ctx.extend_from_slice(&state.to_le_bytes());
2222        ctx.extend_from_slice(&0u32.to_le_bytes()); // flags
2223        ctx.extend_from_slice(&0u64.to_le_bytes()); // duration
2224        if v2 {
2225            ctx.extend_from_slice(&[0u8; 16]); // parent key
2226            ctx.extend_from_slice(&7u16.to_le_bytes()); // epoch
2227            ctx.extend_from_slice(&0u16.to_le_bytes()); // reserved
2228        }
2229
2230        let mut f = vec![0u8; BODY];
2231        let mut body = vec![0u8; 56];
2232        body[0..2].copy_from_slice(&57u16.to_le_bytes()); // StructureSize
2233        body[3] = oplock::LEASE; // RequestedOplockLevel
2234        let ctx_off = BODY + 56;
2235        body[48..52].copy_from_slice(&(ctx_off as u32).to_le_bytes()); // CTX offset
2236        body[52..56].copy_from_slice(&(ctx.len() as u32).to_le_bytes()); // CTX length
2237        f.extend_from_slice(&body);
2238        f.extend_from_slice(&ctx);
2239        f
2240    }
2241
2242    #[test]
2243    fn parses_lease_v1_context() {
2244        let frame = create_with_lease([0x11; 16], lease::RWH, false);
2245        let req = CreateReq::parse(&frame).expect("parse");
2246        let l = req.lease.expect("lease context present");
2247        assert_eq!(l.key, [0x11; 16]);
2248        assert_eq!(l.state, lease::RWH);
2249        assert!(!l.v2);
2250    }
2251
2252    #[test]
2253    fn parses_lease_v2_context() {
2254        let frame = create_with_lease([0x22; 16], lease::RH, true);
2255        let l = CreateReq::parse(&frame).unwrap().lease.expect("lease");
2256        assert!(l.v2);
2257        assert_eq!(l.epoch, 7);
2258        assert_eq!(l.state, lease::RH);
2259    }
2260
2261    #[test]
2262    fn create_response_appends_lease_context() {
2263        let grant = LeaseResp {
2264            key: [0x33; 16],
2265            state: lease::RH,
2266            flags: 0,
2267            epoch: 1,
2268            v2: true,
2269        };
2270        let ctx = encode_create_contexts(&[(lease::CONTEXT_NAME, lease_context_data(&grant))]);
2271        let body = build_create_resp(
2272            FileId([1; 16]),
2273            1,
2274            [0; 4],
2275            0,
2276            0,
2277            0,
2278            false,
2279            oplock::LEASE,
2280            &ctx,
2281        );
2282        assert_eq!(body[2], oplock::LEASE, "OplockLevel = lease");
2283        let off = u32::from_le_bytes(body[80..84].try_into().unwrap()) as usize;
2284        let len = u32::from_le_bytes(body[84..88].try_into().unwrap()) as usize;
2285        assert_eq!(off, BODY + 88, "context offset header-relative");
2286        assert_eq!(len, 24 + 52, "v2 context length");
2287        // Context sits right after the 88-byte fixed part; name tag "RqLs".
2288        assert_eq!(&body[88 + 16..88 + 20], lease::CONTEXT_NAME);
2289    }
2290
2291    #[test]
2292    fn parses_durable_v2_request_and_reconnect() {
2293        // Build a DH2Q request context and confirm the guid/timeout parse.
2294        let mut data = Vec::new();
2295        data.extend_from_slice(&5000u32.to_le_bytes()); // Timeout
2296        data.extend_from_slice(&durable::FLAG_PERSISTENT.to_le_bytes()); // Flags
2297        data.extend_from_slice(&[0u8; 8]); // Reserved
2298        data.extend_from_slice(&[0xAB; 16]); // CreateGuid
2299        let ctx = encode_create_contexts(&[(durable::REQ_V2, data)]);
2300        let mut frame = vec![0u8; BODY];
2301        let mut body = vec![0u8; 56];
2302        body[0..2].copy_from_slice(&57u16.to_le_bytes());
2303        let ctx_off = BODY + 56;
2304        body[48..52].copy_from_slice(&(ctx_off as u32).to_le_bytes());
2305        body[52..56].copy_from_slice(&(ctx.len() as u32).to_le_bytes());
2306        frame.extend_from_slice(&body);
2307        frame.extend_from_slice(&ctx);
2308        match CreateReq::parse(&frame).unwrap().durable.expect("durable") {
2309            DurableReq::RequestV2 {
2310                timeout,
2311                flags,
2312                create_guid,
2313            } => {
2314                assert_eq!(timeout, 5000);
2315                assert_eq!(flags, durable::FLAG_PERSISTENT);
2316                assert_eq!(create_guid, [0xAB; 16]);
2317            }
2318            other => panic!("expected RequestV2, got {other:?}"),
2319        }
2320    }
2321
2322    #[test]
2323    fn lease_break_notification_shape() {
2324        let brk = build_lease_break(
2325            [9; 16],
2326            lease::RWH,
2327            lease::RH,
2328            2,
2329            lease::BREAK_FLAG_ACK_REQUIRED,
2330        );
2331        assert_eq!(brk.len(), 44);
2332        assert_eq!(&brk[0..2], &44u16.to_le_bytes(), "StructureSize");
2333        assert_eq!(&brk[8..24], &[9u8; 16], "LeaseKey");
2334        assert_eq!(
2335            u32::from_le_bytes(brk[24..28].try_into().unwrap()),
2336            lease::RWH,
2337            "Current"
2338        );
2339        assert_eq!(
2340            u32::from_le_bytes(brk[28..32].try_into().unwrap()),
2341            lease::RH,
2342            "New"
2343        );
2344    }
2345
2346    #[test]
2347    fn parses_lease_break_ack() {
2348        let mut frame = vec![0u8; BODY];
2349        frame.extend_from_slice(&build_lease_break_resp([7; 16], lease::RH));
2350        let ack = LeaseBreakAck::parse(&frame).expect("parse");
2351        assert_eq!(ack.key, [7; 16]);
2352        assert_eq!(ack.state, lease::RH);
2353    }
2354}
2355
2356#[cfg(test)]
2357mod query_dir_tests {
2358    use super::*;
2359
2360    #[test]
2361    fn parses_resume_file_index() {
2362        let mut f = vec![0u8; 64];
2363        let mut body = vec![0u8; 32];
2364        body[0..2].copy_from_slice(&33u16.to_le_bytes()); // StructureSize
2365        body[3] = find_flags::INDEX_SPECIFIED; // Flags
2366        body[4..8].copy_from_slice(&7u32.to_le_bytes()); // FileIndex
2367        f.extend_from_slice(&body);
2368        let req = QueryDirReq::parse(&f).expect("parse");
2369        assert_eq!(req.file_index, 7, "resume index parsed");
2370        assert_eq!(
2371            req.flags & find_flags::INDEX_SPECIFIED,
2372            find_flags::INDEX_SPECIFIED
2373        );
2374    }
2375}