Skip to main content

netget/server/smb/
wire.rs

1//! SMB2 wire format: the Direct TCP transport frame, the 64-byte header, and every response
2//! body this server writes.
3//!
4//! Everything here is a pure function of its arguments, so the layout can be checked without a
5//! socket (`tests/server/smb/header_layout_test.rs` walks every builder). The session loop in
6//! `mod.rs` owns the state and the model; this module owns the bytes.
7//!
8//! **Every response header is built by [`ResponseHeader::encode`] and nothing else.** A client
9//! matches a reply to its outstanding request by MessageId (MS-SMB2 3.2.5.1.2), so one builder
10//! that lays the header out differently from the others breaks every exchange that goes through
11//! it while the rest of the session looks healthy. The offsets are written once, below.
12
13/// Length of the SMB2 sync header (MS-SMB2 2.2.1.2).
14pub const HEADER_LEN: usize = 64;
15
16/// `ProtocolId` of an SMB2 message.
17pub const PROTOCOL_ID: [u8; 4] = *b"\xFESMB";
18
19// Header field offsets (MS-SMB2 2.2.1.2, SYNC form). Kept as named constants so the one place
20// that writes them and the one place that reads them cannot drift apart.
21pub const OFF_PROTOCOL_ID: usize = 0;
22pub const OFF_STRUCTURE_SIZE: usize = 4;
23pub const OFF_CREDIT_CHARGE: usize = 6;
24pub const OFF_STATUS: usize = 8;
25pub const OFF_COMMAND: usize = 12;
26pub const OFF_CREDITS: usize = 14;
27pub const OFF_FLAGS: usize = 16;
28pub const OFF_NEXT_COMMAND: usize = 20;
29pub const OFF_MESSAGE_ID: usize = 24;
30pub const OFF_RESERVED: usize = 32;
31pub const OFF_TREE_ID: usize = 36;
32pub const OFF_SESSION_ID: usize = 40;
33pub const OFF_SIGNATURE: usize = 48;
34
35/// Header `Flags` bits (MS-SMB2 2.2.1.2).
36pub const FLAGS_SERVER_TO_REDIR: u32 = 0x0000_0001;
37pub const FLAGS_ASYNC_COMMAND: u32 = 0x0000_0002;
38pub const FLAGS_RELATED_OPERATIONS: u32 = 0x0000_0004;
39pub const FLAGS_SIGNED: u32 = 0x0000_0008;
40
41/// SMB2 command codes (MS-SMB2 2.2.1.2).
42pub mod command {
43    pub const NEGOTIATE: u16 = 0x0000;
44    pub const SESSION_SETUP: u16 = 0x0001;
45    pub const LOGOFF: u16 = 0x0002;
46    pub const TREE_CONNECT: u16 = 0x0003;
47    pub const TREE_DISCONNECT: u16 = 0x0004;
48    pub const CREATE: u16 = 0x0005;
49    pub const CLOSE: u16 = 0x0006;
50    pub const FLUSH: u16 = 0x0007;
51    pub const READ: u16 = 0x0008;
52    pub const WRITE: u16 = 0x0009;
53    pub const LOCK: u16 = 0x000A;
54    pub const IOCTL: u16 = 0x000B;
55    pub const CANCEL: u16 = 0x000C;
56    pub const ECHO: u16 = 0x000D;
57    pub const QUERY_DIRECTORY: u16 = 0x000E;
58    pub const CHANGE_NOTIFY: u16 = 0x000F;
59    pub const QUERY_INFO: u16 = 0x0010;
60    pub const SET_INFO: u16 = 0x0011;
61    pub const OPLOCK_BREAK: u16 = 0x0012;
62}
63
64/// NTSTATUS values this server sends (MS-ERREF 2.3.1).
65pub mod status {
66    pub const SUCCESS: u32 = 0x0000_0000;
67    pub const INVALID_INFO_CLASS: u32 = 0xC000_0003;
68    pub const INVALID_PARAMETER: u32 = 0xC000_000D;
69    pub const INVALID_DEVICE_REQUEST: u32 = 0xC000_0010;
70    pub const END_OF_FILE: u32 = 0xC000_0011;
71    pub const MORE_PROCESSING_REQUIRED: u32 = 0xC000_0016;
72    pub const ACCESS_DENIED: u32 = 0xC000_0022;
73    pub const DATA_ERROR: u32 = 0xC000_003E;
74    pub const LOGON_FAILURE: u32 = 0xC000_006D;
75    pub const INSUFFICIENT_RESOURCES: u32 = 0xC000_009A;
76    pub const NOT_SUPPORTED: u32 = 0xC000_00BB;
77    pub const NETWORK_NAME_DELETED: u32 = 0xC000_00C9;
78    pub const INTERNAL_ERROR: u32 = 0xC000_00E5;
79    pub const TOO_MANY_OPENED_FILES: u32 = 0xC000_011F;
80    pub const FILE_CLOSED: u32 = 0xC000_0128;
81    pub const USER_SESSION_DELETED: u32 = 0xC000_0203;
82    pub const NO_MORE_FILES: u32 = 0x8000_0006;
83}
84
85// ---------------------------------------------------------------------------------------------
86// Direct TCP transport (MS-SMB2 2.1)
87// ---------------------------------------------------------------------------------------------
88
89/// Direct TCP transport header type byte for a session message. Every SMB2 message on port 445
90/// is preceded by `00` and a 24-bit big-endian length; this is the RFC 1002 session-service
91/// header with the length widened, and it is what every real client writes.
92pub const NBSS_SESSION_MESSAGE: u8 = 0x00;
93/// RFC 1002 session request, sent by a client that dialled port 139 before any SMB traffic.
94pub const NBSS_SESSION_REQUEST: u8 = 0x81;
95/// RFC 1002 positive session response.
96pub const NBSS_POSITIVE_RESPONSE: u8 = 0x82;
97/// RFC 1002 session keep-alive; carries no payload and gets no answer.
98pub const NBSS_KEEPALIVE: u8 = 0x85;
99
100/// Largest length a Direct TCP header can carry (24 bits).
101pub const NBSS_MAX_LEN: usize = 0x00FF_FFFF;
102
103/// Prefix one SMB2 message (or compound chain) with its Direct TCP transport header.
104pub fn frame(message: &[u8]) -> Vec<u8> {
105    assert!(
106        message.len() <= NBSS_MAX_LEN,
107        "an SMB2 message longer than 24 bits cannot be framed"
108    );
109    let len = message.len() as u32;
110    let mut out = Vec::with_capacity(4 + message.len());
111    out.push(NBSS_SESSION_MESSAGE);
112    out.extend_from_slice(&len.to_be_bytes()[1..4]);
113    out.extend_from_slice(message);
114    out
115}
116
117/// Split a transport header into its type byte and 24-bit length.
118pub fn parse_frame_header(header: [u8; 4]) -> (u8, usize) {
119    let len = u32::from_be_bytes([0, header[1], header[2], header[3]]) as usize;
120    (header[0], len)
121}
122
123// ---------------------------------------------------------------------------------------------
124// Header
125// ---------------------------------------------------------------------------------------------
126
127/// The fields of a request header this server reads.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub struct RequestHeader {
130    pub credit_charge: u16,
131    pub command: u16,
132    pub credit_request: u16,
133    pub flags: u32,
134    pub next_command: u32,
135    pub message_id: u64,
136    pub tree_id: u32,
137    pub session_id: u64,
138}
139
140impl RequestHeader {
141    /// Parse the 64-byte header at the start of `msg`. `None` if it is short or is not SMB2.
142    pub fn parse(msg: &[u8]) -> Option<Self> {
143        if msg.len() < HEADER_LEN || msg[OFF_PROTOCOL_ID..OFF_PROTOCOL_ID + 4] != PROTOCOL_ID {
144            return None;
145        }
146        Some(Self {
147            credit_charge: le16(msg, OFF_CREDIT_CHARGE),
148            command: le16(msg, OFF_COMMAND),
149            credit_request: le16(msg, OFF_CREDITS),
150            flags: le32(msg, OFF_FLAGS),
151            next_command: le32(msg, OFF_NEXT_COMMAND),
152            message_id: le64(msg, OFF_MESSAGE_ID),
153            tree_id: le32(msg, OFF_TREE_ID),
154            session_id: le64(msg, OFF_SESSION_ID),
155        })
156    }
157
158    pub fn is_related(&self) -> bool {
159        self.flags & FLAGS_RELATED_OPERATIONS != 0
160    }
161}
162
163/// The most credits one response grants. A client asks for what it wants in `CreditRequest`;
164/// granting up to this many keeps a pipelining client supplied without letting one request
165/// inflate its window without limit.
166pub const MAX_CREDIT_GRANT: u16 = 64;
167
168/// Every field of a response header. [`ResponseHeader::encode`] is the only code in this server
169/// that lays a header out.
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub struct ResponseHeader {
172    pub command: u16,
173    pub status: u32,
174    pub message_id: u64,
175    pub credit_charge: u16,
176    pub credits: u16,
177    /// Flags beyond `SERVER_TO_REDIR`, which `encode` always sets.
178    pub flags: u32,
179    pub next_command: u32,
180    pub tree_id: u32,
181    pub session_id: u64,
182}
183
184impl ResponseHeader {
185    /// A response to `req`: its command, MessageId, CreditCharge, TreeId and SessionId echoed,
186    /// `RELATED_OPERATIONS` carried over for a compound, and credits granted.
187    ///
188    /// The TreeId and SessionId are the request's; a caller that has just allocated one
189    /// (SESSION_SETUP, TREE_CONNECT) overwrites the field it allocated.
190    pub fn for_request(req: &RequestHeader, status: u32) -> Self {
191        Self {
192            command: req.command,
193            status,
194            message_id: req.message_id,
195            credit_charge: req.credit_charge,
196            credits: req.credit_request.clamp(1, MAX_CREDIT_GRANT),
197            flags: req.flags & FLAGS_RELATED_OPERATIONS,
198            next_command: 0,
199            tree_id: req.tree_id,
200            session_id: req.session_id,
201        }
202    }
203
204    pub fn with_status(mut self, status: u32) -> Self {
205        self.status = status;
206        self
207    }
208
209    pub fn with_session_id(mut self, session_id: u64) -> Self {
210        self.session_id = session_id;
211        self
212    }
213
214    pub fn with_tree_id(mut self, tree_id: u32) -> Self {
215        self.tree_id = tree_id;
216        self
217    }
218
219    /// The 64-byte header (MS-SMB2 2.2.1.2, SYNC form). Signature is zero: this server signs
220    /// nothing, and says so in NEGOTIATE (signing enabled, not required).
221    pub fn encode(&self) -> [u8; HEADER_LEN] {
222        let mut h = [0u8; HEADER_LEN];
223        h[OFF_PROTOCOL_ID..OFF_PROTOCOL_ID + 4].copy_from_slice(&PROTOCOL_ID);
224        h[OFF_STRUCTURE_SIZE..OFF_STRUCTURE_SIZE + 2].copy_from_slice(&64u16.to_le_bytes());
225        h[OFF_CREDIT_CHARGE..OFF_CREDIT_CHARGE + 2]
226            .copy_from_slice(&self.credit_charge.to_le_bytes());
227        h[OFF_STATUS..OFF_STATUS + 4].copy_from_slice(&self.status.to_le_bytes());
228        h[OFF_COMMAND..OFF_COMMAND + 2].copy_from_slice(&self.command.to_le_bytes());
229        h[OFF_CREDITS..OFF_CREDITS + 2].copy_from_slice(&self.credits.to_le_bytes());
230        let flags = self.flags | FLAGS_SERVER_TO_REDIR;
231        h[OFF_FLAGS..OFF_FLAGS + 4].copy_from_slice(&flags.to_le_bytes());
232        h[OFF_NEXT_COMMAND..OFF_NEXT_COMMAND + 4].copy_from_slice(&self.next_command.to_le_bytes());
233        h[OFF_MESSAGE_ID..OFF_MESSAGE_ID + 8].copy_from_slice(&self.message_id.to_le_bytes());
234        // OFF_RESERVED (ProcessId in the SYNC form) stays zero.
235        h[OFF_TREE_ID..OFF_TREE_ID + 4].copy_from_slice(&self.tree_id.to_le_bytes());
236        h[OFF_SESSION_ID..OFF_SESSION_ID + 8].copy_from_slice(&self.session_id.to_le_bytes());
237        // OFF_SIGNATURE..64 stays zero.
238        h
239    }
240
241    fn message(&self, body: &[u8]) -> Vec<u8> {
242        let mut out = Vec::with_capacity(HEADER_LEN + body.len());
243        out.extend_from_slice(&self.encode());
244        out.extend_from_slice(body);
245        out
246    }
247}
248
249/// Join the responses to a compound request into one chain (MS-SMB2 3.3.4.1.3): every response
250/// but the last is padded to an 8-byte boundary and its `NextCommand` names that padded length.
251pub fn chain(responses: Vec<Vec<u8>>) -> Vec<u8> {
252    let count = responses.len();
253    let mut out = Vec::new();
254    for (i, mut response) in responses.into_iter().enumerate() {
255        if i + 1 < count {
256            while response.len() % 8 != 0 {
257                response.push(0);
258            }
259            let next = response.len() as u32;
260            response[OFF_NEXT_COMMAND..OFF_NEXT_COMMAND + 4].copy_from_slice(&next.to_le_bytes());
261        }
262        out.extend_from_slice(&response);
263    }
264    out
265}
266
267// ---------------------------------------------------------------------------------------------
268// Response bodies. Each builder returns a whole message: header then body.
269// ---------------------------------------------------------------------------------------------
270
271/// SMB2 ERROR response (MS-SMB2 2.2.2): the status is in the header, the body is empty.
272pub fn error_response(hdr: &ResponseHeader) -> Vec<u8> {
273    let mut body = Vec::with_capacity(9);
274    body.extend_from_slice(&9u16.to_le_bytes()); // StructureSize
275    body.push(0); // ErrorContextCount
276    body.push(0); // Reserved
277    body.extend_from_slice(&0u32.to_le_bytes()); // ByteCount
278    body.push(0); // ErrorData: one byte when ByteCount is zero
279    hdr.message(&body)
280}
281
282/// The four-byte body shared by LOGOFF, TREE_DISCONNECT, FLUSH and ECHO responses.
283pub fn empty_response(hdr: &ResponseHeader) -> Vec<u8> {
284    hdr.message(&[4, 0, 0, 0])
285}
286
287/// What NEGOTIATE advertises. Grouped so the builder is a function of values, not of globals.
288#[derive(Debug, Clone)]
289pub struct NegotiateParams {
290    pub dialect: u16,
291    pub server_guid: [u8; 16],
292    pub capabilities: u32,
293    pub max_transact_size: u32,
294    pub max_read_size: u32,
295    pub max_write_size: u32,
296    pub system_time: u64,
297    pub security_blob: Vec<u8>,
298}
299
300/// NEGOTIATE response (MS-SMB2 2.2.4).
301pub fn negotiate_response(hdr: &ResponseHeader, p: &NegotiateParams) -> Vec<u8> {
302    let mut body = Vec::with_capacity(64 + p.security_blob.len());
303    body.extend_from_slice(&65u16.to_le_bytes()); // StructureSize
304    body.extend_from_slice(&0x0001u16.to_le_bytes()); // SecurityMode: SIGNING_ENABLED
305    body.extend_from_slice(&p.dialect.to_le_bytes()); // DialectRevision
306    body.extend_from_slice(&0u16.to_le_bytes()); // NegotiateContextCount / Reserved
307    body.extend_from_slice(&p.server_guid); // ServerGuid
308    body.extend_from_slice(&p.capabilities.to_le_bytes());
309    body.extend_from_slice(&p.max_transact_size.to_le_bytes());
310    body.extend_from_slice(&p.max_read_size.to_le_bytes());
311    body.extend_from_slice(&p.max_write_size.to_le_bytes());
312    body.extend_from_slice(&p.system_time.to_le_bytes()); // SystemTime
313    body.extend_from_slice(&0u64.to_le_bytes()); // ServerStartTime (MUST be 0 for 2.x)
314    let (offset, len) = if p.security_blob.is_empty() {
315        (0u16, 0u16)
316    } else {
317        ((HEADER_LEN + 64) as u16, p.security_blob.len() as u16)
318    };
319    body.extend_from_slice(&offset.to_le_bytes()); // SecurityBufferOffset
320    body.extend_from_slice(&len.to_le_bytes()); // SecurityBufferLength
321    body.extend_from_slice(&0u32.to_le_bytes()); // NegotiateContextOffset / Reserved2
322    debug_assert_eq!(body.len(), 64);
323    if p.security_blob.is_empty() {
324        body.push(0); // StructureSize 65 counts one byte of Buffer
325    } else {
326        body.extend_from_slice(&p.security_blob);
327    }
328    hdr.message(&body)
329}
330
331/// SESSION_SETUP `SessionFlags` (MS-SMB2 2.2.6).
332pub const SESSION_FLAG_IS_GUEST: u16 = 0x0001;
333pub const SESSION_FLAG_IS_NULL: u16 = 0x0002;
334
335/// SESSION_SETUP response (MS-SMB2 2.2.6).
336pub fn session_setup_response(hdr: &ResponseHeader, session_flags: u16, blob: &[u8]) -> Vec<u8> {
337    let mut body = Vec::with_capacity(8 + blob.len().max(1));
338    body.extend_from_slice(&9u16.to_le_bytes()); // StructureSize
339    body.extend_from_slice(&session_flags.to_le_bytes());
340    let offset = if blob.is_empty() {
341        0u16
342    } else {
343        (HEADER_LEN + 8) as u16
344    };
345    body.extend_from_slice(&offset.to_le_bytes()); // SecurityBufferOffset
346    body.extend_from_slice(&(blob.len() as u16).to_le_bytes()); // SecurityBufferLength
347    if blob.is_empty() {
348        body.push(0);
349    } else {
350        body.extend_from_slice(blob);
351    }
352    hdr.message(&body)
353}
354
355/// `ShareType` values in a TREE_CONNECT response.
356pub const SHARE_TYPE_DISK: u8 = 0x01;
357pub const SHARE_TYPE_PIPE: u8 = 0x02;
358
359/// TREE_CONNECT response (MS-SMB2 2.2.10).
360pub fn tree_connect_response(hdr: &ResponseHeader, share_type: u8, maximal_access: u32) -> Vec<u8> {
361    let mut body = Vec::with_capacity(16);
362    body.extend_from_slice(&16u16.to_le_bytes()); // StructureSize
363    body.push(share_type);
364    body.push(0); // Reserved
365    body.extend_from_slice(&0u32.to_le_bytes()); // ShareFlags: manual caching, no DFS
366    body.extend_from_slice(&0u32.to_le_bytes()); // Capabilities
367    body.extend_from_slice(&maximal_access.to_le_bytes()); // MaximalAccess
368    hdr.message(&body)
369}
370
371/// A file's attributes and times as the info classes and CREATE/CLOSE responses carry them.
372#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
373pub struct FileMeta {
374    pub is_directory: bool,
375    pub size: u64,
376    /// FILETIME (100ns since 1601) used for all four timestamps; 0 when unknown.
377    pub time: u64,
378    /// A stable per-file number for `FileInternalInformation` and the `Id` directory classes.
379    pub file_index: u64,
380}
381
382pub const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x0000_0010;
383pub const FILE_ATTRIBUTE_ARCHIVE: u32 = 0x0000_0020;
384pub const FILE_ATTRIBUTE_NORMAL: u32 = 0x0000_0080;
385
386impl FileMeta {
387    pub fn attributes(&self) -> u32 {
388        if self.is_directory {
389            FILE_ATTRIBUTE_DIRECTORY
390        } else {
391            FILE_ATTRIBUTE_NORMAL
392        }
393    }
394
395    /// Size rounded up to the 4 KiB allocation unit this server reports.
396    pub fn allocation_size(&self) -> u64 {
397        self.size.div_ceil(4096) * 4096
398    }
399
400    fn times(&self, out: &mut Vec<u8>) {
401        for _ in 0..4 {
402            out.extend_from_slice(&self.time.to_le_bytes());
403        }
404    }
405}
406
407/// `CreateAction` values (MS-SMB2 2.2.14).
408pub const FILE_OPENED: u32 = 0x0000_0001;
409pub const FILE_CREATED: u32 = 0x0000_0002;
410
411/// CREATE response (MS-SMB2 2.2.14), with no create contexts.
412pub fn create_response(
413    hdr: &ResponseHeader,
414    file_id: &[u8; 16],
415    meta: &FileMeta,
416    create_action: u32,
417) -> Vec<u8> {
418    let mut body = Vec::with_capacity(89);
419    body.extend_from_slice(&89u16.to_le_bytes()); // StructureSize
420    body.push(0); // OplockLevel: none
421    body.push(0); // Flags
422    body.extend_from_slice(&create_action.to_le_bytes());
423    meta.times(&mut body); // Creation, LastAccess, LastWrite, Change
424    body.extend_from_slice(&meta.allocation_size().to_le_bytes());
425    body.extend_from_slice(&meta.size.to_le_bytes()); // EndofFile
426    body.extend_from_slice(&meta.attributes().to_le_bytes());
427    body.extend_from_slice(&0u32.to_le_bytes()); // Reserved2
428    body.extend_from_slice(file_id);
429    body.extend_from_slice(&0u32.to_le_bytes()); // CreateContextsOffset
430    body.extend_from_slice(&0u32.to_le_bytes()); // CreateContextsLength
431    body.push(0); // StructureSize 89 counts one byte of Buffer
432    hdr.message(&body)
433}
434
435/// CLOSE `Flags`: the client asked for the attributes to be returned.
436pub const CLOSE_FLAG_POSTQUERY_ATTRIB: u16 = 0x0001;
437
438/// CLOSE response (MS-SMB2 2.2.16). With `meta`, the attributes are filled in and
439/// `SMB2_CLOSE_FLAG_POSTQUERY_ATTRIB` is set; without it every field is zero, as the
440/// specification requires when the client did not ask.
441pub fn close_response(hdr: &ResponseHeader, meta: Option<&FileMeta>) -> Vec<u8> {
442    let mut body = Vec::with_capacity(60);
443    body.extend_from_slice(&60u16.to_le_bytes()); // StructureSize
444    match meta {
445        Some(meta) => {
446            body.extend_from_slice(&CLOSE_FLAG_POSTQUERY_ATTRIB.to_le_bytes());
447            body.extend_from_slice(&0u32.to_le_bytes()); // Reserved
448            meta.times(&mut body);
449            body.extend_from_slice(&meta.allocation_size().to_le_bytes());
450            body.extend_from_slice(&meta.size.to_le_bytes());
451            body.extend_from_slice(&meta.attributes().to_le_bytes());
452        }
453        None => body.resize(60, 0),
454    }
455    hdr.message(&body)
456}
457
458/// Where READ puts the payload: straight after the 16-byte fixed body.
459pub const READ_DATA_OFFSET: u8 = (HEADER_LEN + 16) as u8;
460
461/// READ response (MS-SMB2 2.2.20).
462pub fn read_response(hdr: &ResponseHeader, data: &[u8]) -> Vec<u8> {
463    let mut body = Vec::with_capacity(16 + data.len());
464    body.extend_from_slice(&17u16.to_le_bytes()); // StructureSize
465    body.push(READ_DATA_OFFSET); // DataOffset, from the start of the header
466    body.push(0); // Reserved
467    body.extend_from_slice(&(data.len() as u32).to_le_bytes()); // DataLength
468    body.extend_from_slice(&0u32.to_le_bytes()); // DataRemaining
469    body.extend_from_slice(&0u32.to_le_bytes()); // Reserved2 / Flags
470    body.extend_from_slice(data);
471    if data.is_empty() {
472        body.push(0);
473    }
474    hdr.message(&body)
475}
476
477/// WRITE response (MS-SMB2 2.2.22).
478pub fn write_response(hdr: &ResponseHeader, count: u32) -> Vec<u8> {
479    let mut body = Vec::with_capacity(17);
480    body.extend_from_slice(&17u16.to_le_bytes()); // StructureSize
481    body.extend_from_slice(&0u16.to_le_bytes()); // Reserved
482    body.extend_from_slice(&count.to_le_bytes()); // Count
483    body.extend_from_slice(&0u32.to_le_bytes()); // Remaining
484    body.extend_from_slice(&0u16.to_le_bytes()); // WriteChannelInfoOffset
485    body.extend_from_slice(&0u16.to_le_bytes()); // WriteChannelInfoLength
486    body.push(0); // StructureSize 17 counts one byte of Buffer
487    hdr.message(&body)
488}
489
490/// The QUERY_INFO and QUERY_DIRECTORY responses share one shape (MS-SMB2 2.2.38 / 2.2.34):
491/// StructureSize 9, an offset and a length, then the buffer.
492fn output_buffer_response(hdr: &ResponseHeader, buffer: &[u8]) -> Vec<u8> {
493    let mut body = Vec::with_capacity(8 + buffer.len().max(1));
494    body.extend_from_slice(&9u16.to_le_bytes()); // StructureSize
495    body.extend_from_slice(&((HEADER_LEN + 8) as u16).to_le_bytes()); // OutputBufferOffset
496    body.extend_from_slice(&(buffer.len() as u32).to_le_bytes()); // OutputBufferLength
497    if buffer.is_empty() {
498        body.push(0);
499    } else {
500        body.extend_from_slice(buffer);
501    }
502    hdr.message(&body)
503}
504
505/// QUERY_INFO response (MS-SMB2 2.2.38).
506pub fn query_info_response(hdr: &ResponseHeader, buffer: &[u8]) -> Vec<u8> {
507    output_buffer_response(hdr, buffer)
508}
509
510/// QUERY_DIRECTORY response (MS-SMB2 2.2.34).
511pub fn query_directory_response(hdr: &ResponseHeader, entries: &[u8]) -> Vec<u8> {
512    output_buffer_response(hdr, entries)
513}
514
515// ---------------------------------------------------------------------------------------------
516// Information classes (MS-FSCC 2.4 and 2.5)
517// ---------------------------------------------------------------------------------------------
518
519/// `InfoType` in a QUERY_INFO request.
520pub const INFO_FILE: u8 = 0x01;
521pub const INFO_FILESYSTEM: u8 = 0x02;
522pub const INFO_SECURITY: u8 = 0x03;
523pub const INFO_QUOTA: u8 = 0x04;
524
525/// File information classes (MS-FSCC 2.4).
526pub mod file_class {
527    pub const DIRECTORY: u8 = 1;
528    pub const FULL_DIRECTORY: u8 = 2;
529    pub const BOTH_DIRECTORY: u8 = 3;
530    pub const BASIC: u8 = 4;
531    pub const STANDARD: u8 = 5;
532    pub const INTERNAL: u8 = 6;
533    pub const EA: u8 = 7;
534    pub const ACCESS: u8 = 8;
535    pub const NAMES: u8 = 12;
536    pub const POSITION: u8 = 14;
537    pub const MODE: u8 = 16;
538    pub const ALIGNMENT: u8 = 17;
539    pub const ALL: u8 = 18;
540    pub const STREAM: u8 = 22;
541    pub const NETWORK_OPEN: u8 = 34;
542    pub const ATTRIBUTE_TAG: u8 = 35;
543    pub const ID_BOTH_DIRECTORY: u8 = 37;
544    pub const ID_FULL_DIRECTORY: u8 = 38;
545}
546
547/// File system information classes (MS-FSCC 2.5).
548pub mod fs_class {
549    pub const VOLUME: u8 = 1;
550    pub const SIZE: u8 = 3;
551    pub const DEVICE: u8 = 4;
552    pub const ATTRIBUTE: u8 = 5;
553    pub const FULL_SIZE: u8 = 7;
554    pub const SECTOR_SIZE: u8 = 11;
555}
556
557/// Whether answering this file information class needs the file's size, which only the model
558/// knows. The others are answered from the handle alone.
559pub fn file_class_needs_size(class: u8) -> bool {
560    matches!(
561        class,
562        file_class::STANDARD | file_class::ALL | file_class::NETWORK_OPEN | file_class::STREAM
563    )
564}
565
566/// Encode a file information class for `meta`. `None` for a class this server does not answer.
567pub fn file_info(class: u8, meta: &FileMeta) -> Option<Vec<u8>> {
568    let mut out = Vec::new();
569    match class {
570        file_class::BASIC => basic_info(meta, &mut out),
571        file_class::STANDARD => standard_info(meta, &mut out),
572        file_class::INTERNAL => out.extend_from_slice(&meta.file_index.to_le_bytes()),
573        file_class::EA => out.extend_from_slice(&0u32.to_le_bytes()),
574        file_class::ACCESS => out.extend_from_slice(&0x001F_01FFu32.to_le_bytes()),
575        file_class::POSITION => out.extend_from_slice(&0u64.to_le_bytes()),
576        file_class::MODE => out.extend_from_slice(&0u32.to_le_bytes()),
577        file_class::ALIGNMENT => out.extend_from_slice(&0u32.to_le_bytes()),
578        file_class::ALL => {
579            basic_info(meta, &mut out); // 40
580            standard_info(meta, &mut out); // 24
581            out.extend_from_slice(&meta.file_index.to_le_bytes()); // Internal
582            out.extend_from_slice(&0u32.to_le_bytes()); // EaSize
583            out.extend_from_slice(&0x001F_01FFu32.to_le_bytes()); // AccessFlags
584            out.extend_from_slice(&0u64.to_le_bytes()); // CurrentByteOffset
585            out.extend_from_slice(&0u32.to_le_bytes()); // Mode
586            out.extend_from_slice(&0u32.to_le_bytes()); // AlignmentRequirement
587            out.extend_from_slice(&0u32.to_le_bytes()); // FileNameLength, no name
588        }
589        file_class::STREAM => {
590            if meta.is_directory {
591                return Some(Vec::new());
592            }
593            let name: Vec<u8> = utf16le("::$DATA");
594            out.extend_from_slice(&0u32.to_le_bytes()); // NextEntryOffset
595            out.extend_from_slice(&(name.len() as u32).to_le_bytes());
596            out.extend_from_slice(&meta.size.to_le_bytes());
597            out.extend_from_slice(&meta.allocation_size().to_le_bytes());
598            out.extend_from_slice(&name);
599        }
600        file_class::NETWORK_OPEN => {
601            meta.times(&mut out);
602            out.extend_from_slice(&meta.allocation_size().to_le_bytes());
603            out.extend_from_slice(&meta.size.to_le_bytes());
604            out.extend_from_slice(&meta.attributes().to_le_bytes());
605            out.extend_from_slice(&0u32.to_le_bytes()); // Reserved
606        }
607        file_class::ATTRIBUTE_TAG => {
608            out.extend_from_slice(&meta.attributes().to_le_bytes());
609            out.extend_from_slice(&0u32.to_le_bytes()); // ReparseTag
610        }
611        _ => return None,
612    }
613    Some(out)
614}
615
616fn basic_info(meta: &FileMeta, out: &mut Vec<u8>) {
617    meta.times(out);
618    out.extend_from_slice(&meta.attributes().to_le_bytes());
619    out.extend_from_slice(&0u32.to_le_bytes()); // Reserved
620}
621
622fn standard_info(meta: &FileMeta, out: &mut Vec<u8>) {
623    out.extend_from_slice(&meta.allocation_size().to_le_bytes());
624    out.extend_from_slice(&meta.size.to_le_bytes()); // EndOfFile
625    out.extend_from_slice(&1u32.to_le_bytes()); // NumberOfLinks
626    out.push(0); // DeletePending
627    out.push(meta.is_directory as u8); // Directory
628    out.extend_from_slice(&0u16.to_le_bytes()); // Reserved
629}
630
631/// The volume this server reports. None of it is measured — there is no disk behind the share
632/// — so the numbers are fixed and say so: a 1 GiB volume, half free, 4 KiB clusters.
633const FS_TOTAL_UNITS: u64 = 262_144;
634const FS_FREE_UNITS: u64 = 131_072;
635const FS_SECTORS_PER_UNIT: u32 = 8;
636const FS_BYTES_PER_SECTOR: u32 = 512;
637const FS_SERIAL: u32 = 0x4E47_5342; // "NGSB"
638
639/// Encode a file system information class. `None` for a class this server does not answer.
640pub fn fs_info(class: u8, volume_label: &str, created: u64) -> Option<Vec<u8>> {
641    let mut out = Vec::new();
642    match class {
643        fs_class::VOLUME => {
644            let label = utf16le(volume_label);
645            out.extend_from_slice(&created.to_le_bytes()); // VolumeCreationTime
646            out.extend_from_slice(&FS_SERIAL.to_le_bytes());
647            out.extend_from_slice(&(label.len() as u32).to_le_bytes());
648            out.push(0); // SupportsObjects
649            out.push(0); // Reserved
650            out.extend_from_slice(&label);
651        }
652        fs_class::SIZE => {
653            out.extend_from_slice(&FS_TOTAL_UNITS.to_le_bytes());
654            out.extend_from_slice(&FS_FREE_UNITS.to_le_bytes());
655            out.extend_from_slice(&FS_SECTORS_PER_UNIT.to_le_bytes());
656            out.extend_from_slice(&FS_BYTES_PER_SECTOR.to_le_bytes());
657        }
658        fs_class::DEVICE => {
659            out.extend_from_slice(&0x0000_0007u32.to_le_bytes()); // FILE_DEVICE_DISK
660            out.extend_from_slice(&0u32.to_le_bytes()); // Characteristics
661        }
662        fs_class::ATTRIBUTE => {
663            let name = utf16le("NTFS");
664            // FILE_CASE_SENSITIVE_SEARCH | FILE_CASE_PRESERVED_NAMES | FILE_UNICODE_ON_DISK
665            out.extend_from_slice(&0x0000_0007u32.to_le_bytes());
666            out.extend_from_slice(&255u32.to_le_bytes()); // MaximumComponentNameLength
667            out.extend_from_slice(&(name.len() as u32).to_le_bytes());
668            out.extend_from_slice(&name);
669        }
670        fs_class::FULL_SIZE => {
671            out.extend_from_slice(&FS_TOTAL_UNITS.to_le_bytes());
672            out.extend_from_slice(&FS_FREE_UNITS.to_le_bytes()); // CallerAvailable
673            out.extend_from_slice(&FS_FREE_UNITS.to_le_bytes()); // ActualAvailable
674            out.extend_from_slice(&FS_SECTORS_PER_UNIT.to_le_bytes());
675            out.extend_from_slice(&FS_BYTES_PER_SECTOR.to_le_bytes());
676        }
677        fs_class::SECTOR_SIZE => {
678            let sector = FS_BYTES_PER_SECTOR;
679            out.extend_from_slice(&sector.to_le_bytes()); // LogicalBytesPerSector
680            out.extend_from_slice(&sector.to_le_bytes()); // PhysicalBytesPerSectorForAtomicity
681            out.extend_from_slice(&sector.to_le_bytes()); // PhysicalBytesPerSectorForPerformance
682            out.extend_from_slice(&sector.to_le_bytes()); // FileSystemEffective...Atomicity
683            out.extend_from_slice(&0u32.to_le_bytes()); // Flags
684            out.extend_from_slice(&0u32.to_le_bytes()); // ByteOffsetForSectorAlignment
685            out.extend_from_slice(&0u32.to_le_bytes()); // ByteOffsetForPartitionAlignment
686        }
687        _ => return None,
688    }
689    Some(out)
690}
691
692/// Whether QUERY_DIRECTORY can answer this information class.
693pub fn directory_class_supported(class: u8) -> bool {
694    matches!(
695        class,
696        file_class::DIRECTORY
697            | file_class::FULL_DIRECTORY
698            | file_class::BOTH_DIRECTORY
699            | file_class::NAMES
700            | file_class::ID_BOTH_DIRECTORY
701            | file_class::ID_FULL_DIRECTORY
702    )
703}
704
705/// One directory entry in `class`, without its `NextEntryOffset` set and without padding.
706pub fn directory_entry(class: u8, name: &str, meta: &FileMeta) -> Vec<u8> {
707    let name = utf16le(name);
708    let mut e = Vec::with_capacity(104 + name.len());
709    e.extend_from_slice(&0u32.to_le_bytes()); // NextEntryOffset, set by `join_directory_entries`
710    e.extend_from_slice(&0u32.to_le_bytes()); // FileIndex
711    if class == file_class::NAMES {
712        e.extend_from_slice(&(name.len() as u32).to_le_bytes());
713        e.extend_from_slice(&name);
714        return e;
715    }
716    meta.times(&mut e);
717    e.extend_from_slice(&meta.size.to_le_bytes()); // EndOfFile
718    e.extend_from_slice(&meta.allocation_size().to_le_bytes());
719    e.extend_from_slice(&meta.attributes().to_le_bytes());
720    e.extend_from_slice(&(name.len() as u32).to_le_bytes()); // FileNameLength
721    match class {
722        file_class::DIRECTORY => {}
723        file_class::FULL_DIRECTORY => e.extend_from_slice(&0u32.to_le_bytes()), // EaSize
724        file_class::BOTH_DIRECTORY => {
725            e.extend_from_slice(&0u32.to_le_bytes()); // EaSize
726            e.push(0); // ShortNameLength
727            e.push(0); // Reserved
728            e.extend_from_slice(&[0u8; 24]); // ShortName
729        }
730        file_class::ID_BOTH_DIRECTORY => {
731            e.extend_from_slice(&0u32.to_le_bytes()); // EaSize
732            e.push(0); // ShortNameLength
733            e.push(0); // Reserved1
734            e.extend_from_slice(&[0u8; 24]); // ShortName
735            e.extend_from_slice(&0u16.to_le_bytes()); // Reserved2
736            e.extend_from_slice(&meta.file_index.to_le_bytes()); // FileId
737        }
738        file_class::ID_FULL_DIRECTORY => {
739            e.extend_from_slice(&0u32.to_le_bytes()); // EaSize
740            e.extend_from_slice(&0u32.to_le_bytes()); // Reserved
741            e.extend_from_slice(&meta.file_index.to_le_bytes()); // FileId
742        }
743        _ => {}
744    }
745    e.extend_from_slice(&name);
746    e
747}
748
749/// Join directory entries into one buffer (MS-FSCC 2.4): each entry but the last is padded to
750/// 8 bytes and its `NextEntryOffset` names the padded length.
751pub fn join_directory_entries(entries: &[Vec<u8>]) -> Vec<u8> {
752    let mut out = Vec::new();
753    for (i, entry) in entries.iter().enumerate() {
754        let start = out.len();
755        out.extend_from_slice(entry);
756        if i + 1 < entries.len() {
757            while (out.len() - start) % 8 != 0 {
758                out.push(0);
759            }
760            let next = (out.len() - start) as u32;
761            out[start..start + 4].copy_from_slice(&next.to_le_bytes());
762        }
763    }
764    out
765}
766
767/// Size of an entry once padded to 8 bytes, which is what it costs in a joined buffer when it
768/// is not the last.
769pub fn padded_len(entry: &[u8]) -> usize {
770    entry.len().div_ceil(8) * 8
771}
772
773// ---------------------------------------------------------------------------------------------
774// Requests
775//
776// Every byte of a request the server reads is located here, as a pure function of the message,
777// so the parsing a peer controls can be fuzzed without a socket, a session or a model
778// (`fuzz/fuzz_targets/smb2_request.rs`). Each parser takes the whole message — header and body
779// — or the body alone, says which, and returns `None` for a request too short to carry its own
780// fields or whose offsets point outside it; the session loop answers that with
781// `STATUS_INVALID_PARAMETER`. Nothing here indexes past what it has checked.
782// ---------------------------------------------------------------------------------------------
783
784/// Why a compound chain could not be walked any further.
785#[derive(Debug, Clone, Copy, PartialEq, Eq)]
786pub enum ChainError {
787    /// The bytes at this position are not an SMB2 header.
788    NotSmb2,
789    /// `NextCommand` does not point at another request inside the frame (MS-SMB2 3.3.5.2.7):
790    /// shorter than a header, past the end, or not 8-byte aligned.
791    BadNextCommand(RequestHeader),
792}
793
794/// One request of a compound chain, located but not yet interpreted.
795#[derive(Debug, Clone, Copy)]
796pub struct ChainedRequest<'a> {
797    pub header: RequestHeader,
798    /// This request's header and body, and nothing of the requests after it.
799    pub message: &'a [u8],
800    /// Where the next request starts, relative to the start of `message`; `None` for the last.
801    pub next: Option<usize>,
802}
803
804/// Locate the request at the start of `rest`, the unread tail of a frame.
805pub fn next_in_chain(rest: &[u8]) -> Result<ChainedRequest<'_>, ChainError> {
806    let header = RequestHeader::parse(rest).ok_or(ChainError::NotSmb2)?;
807    let next = header.next_command as usize;
808    if next == 0 {
809        return Ok(ChainedRequest {
810            header,
811            message: rest,
812            next: None,
813        });
814    }
815    if next < HEADER_LEN || next > rest.len() || !next.is_multiple_of(8) {
816        return Err(ChainError::BadNextCommand(header));
817    }
818    Ok(ChainedRequest {
819        header,
820        message: &rest[..next],
821        next: Some(next),
822    })
823}
824
825/// The body of a message: everything after its 64-byte header.
826pub fn body(message: &[u8]) -> &[u8] {
827    message.get(HEADER_LEN..).unwrap_or_default()
828}
829
830/// `message[offset..offset + len]`, where both come from the peer. `None` if it overruns.
831fn buffer(message: &[u8], offset: usize, len: usize) -> Option<&[u8]> {
832    message.get(offset..offset.checked_add(len)?)
833}
834
835fn file_id(body: &[u8], at: usize) -> Option<[u8; 16]> {
836    body.get(at..at + 16)?.try_into().ok()
837}
838
839/// NEGOTIATE (MS-SMB2 2.2.3): the dialects the client offers. Takes the body.
840pub fn parse_negotiate(body: &[u8]) -> Option<Vec<u16>> {
841    if body.len() < 36 {
842        return None;
843    }
844    let count = le16(body, 2) as usize;
845    let list = body.get(36..36 + 2 * count)?;
846    Some(
847        list.chunks_exact(2)
848            .map(|c| u16::from_le_bytes([c[0], c[1]]))
849            .collect(),
850    )
851}
852
853/// SESSION_SETUP (MS-SMB2 2.2.5): the security buffer, which may be empty. Takes the message,
854/// because `SecurityBufferOffset` is measured from the start of the header.
855pub fn parse_session_setup(message: &[u8]) -> Option<&[u8]> {
856    let body = body(message);
857    if body.len() < 24 {
858        return None;
859    }
860    let offset = le16(body, 12) as usize;
861    let len = le16(body, 14) as usize;
862    if len == 0 {
863        return Some(&[]);
864    }
865    buffer(message, offset, len)
866}
867
868/// TREE_CONNECT (MS-SMB2 2.2.9): the UNC path, `\\server\share`. Takes the message.
869pub fn parse_tree_connect(message: &[u8]) -> Option<String> {
870    let body = body(message);
871    if body.len() < 8 {
872        return None;
873    }
874    let offset = le16(body, 4) as usize;
875    let len = le16(body, 6) as usize;
876    from_utf16le(buffer(message, offset, len)?)
877}
878
879/// The fields of a CREATE request (MS-SMB2 2.2.13) this server acts on.
880#[derive(Debug, Clone, PartialEq, Eq)]
881pub struct CreateRequest {
882    pub disposition: u32,
883    pub options: u32,
884    /// `/`-separated and rooted at the share; the share root is `/`.
885    pub path: String,
886}
887
888/// CREATE (MS-SMB2 2.2.13). Takes the message: `NameOffset` is measured from the header. The
889/// name is relative to the share and uses `\`; it comes back `/`-separated and `/`-rooted.
890pub fn parse_create(message: &[u8]) -> Option<CreateRequest> {
891    let body = body(message);
892    if body.len() < 56 {
893        return None;
894    }
895    let disposition = le32(body, 36);
896    let options = le32(body, 40);
897    let name_offset = le16(body, 44) as usize;
898    let name_len = le16(body, 46) as usize;
899    let name = if name_len == 0 {
900        String::new()
901    } else {
902        from_utf16le(buffer(message, name_offset, name_len)?)?
903    };
904    let name = name.replace('\\', "/");
905    Some(CreateRequest {
906        disposition,
907        options,
908        path: format!("/{}", name.trim_matches('/')),
909    })
910}
911
912/// CLOSE (MS-SMB2 2.2.15): its `Flags` and FileId. Takes the body.
913pub fn parse_close(body: &[u8]) -> Option<(u16, [u8; 16])> {
914    if body.len() < 24 {
915        return None;
916    }
917    Some((le16(body, 2), file_id(body, 8)?))
918}
919
920/// FLUSH (MS-SMB2 2.2.17): its FileId. Takes the body.
921pub fn parse_flush(body: &[u8]) -> Option<[u8; 16]> {
922    if body.len() < 24 {
923        return None;
924    }
925    file_id(body, 8)
926}
927
928/// The fields of a READ request (MS-SMB2 2.2.19).
929#[derive(Debug, Clone, Copy, PartialEq, Eq)]
930pub struct ReadRequest {
931    /// As the client sent it; the session loop caps it at `MaxReadSize`.
932    pub length: u32,
933    pub offset: u64,
934    pub file_id: [u8; 16],
935}
936
937/// READ (MS-SMB2 2.2.19). Takes the body.
938pub fn parse_read(body: &[u8]) -> Option<ReadRequest> {
939    if body.len() < 48 {
940        return None;
941    }
942    Some(ReadRequest {
943        length: le32(body, 4),
944        offset: le64(body, 8),
945        file_id: file_id(body, 16)?,
946    })
947}
948
949/// The fields of a WRITE request (MS-SMB2 2.2.21).
950#[derive(Debug, Clone, Copy, PartialEq, Eq)]
951pub struct WriteRequest<'a> {
952    /// The length the client declared, which the session loop checks against `MaxWriteSize`
953    /// before it looks at the data.
954    pub length: u32,
955    pub offset: u64,
956    pub file_id: [u8; 16],
957    /// `length` bytes at `DataOffset`, or `None` when they run past the end of the message.
958    pub data: Option<&'a [u8]>,
959}
960
961/// WRITE (MS-SMB2 2.2.21). Takes the message: `DataOffset` is measured from the header.
962pub fn parse_write(message: &[u8]) -> Option<WriteRequest<'_>> {
963    let body = body(message);
964    if body.len() < 48 {
965        return None;
966    }
967    let data_offset = le16(body, 2) as usize;
968    let length = le32(body, 4);
969    Some(WriteRequest {
970        length,
971        offset: le64(body, 8),
972        file_id: file_id(body, 16)?,
973        data: buffer(message, data_offset, length as usize),
974    })
975}
976
977/// The fields of a QUERY_INFO request (MS-SMB2 2.2.37) this server acts on.
978#[derive(Debug, Clone, Copy, PartialEq, Eq)]
979pub struct QueryInfoRequest {
980    pub info_type: u8,
981    pub class: u8,
982    pub output_len: u32,
983    pub file_id: [u8; 16],
984}
985
986/// QUERY_INFO (MS-SMB2 2.2.37): StructureSize(2) InfoType(1) FileInfoClass(1)
987/// OutputBufferLength(4) InputBufferOffset(2) Reserved(2) InputBufferLength(4)
988/// AdditionalInformation(4) Flags(4) FileId(16). Takes the body.
989pub fn parse_query_info(body: &[u8]) -> Option<QueryInfoRequest> {
990    if body.len() < 40 {
991        return None;
992    }
993    Some(QueryInfoRequest {
994        info_type: body[2],
995        class: body[3],
996        output_len: le32(body, 4),
997        file_id: file_id(body, 24)?,
998    })
999}
1000
1001/// The fields of a QUERY_DIRECTORY request (MS-SMB2 2.2.33).
1002#[derive(Debug, Clone, PartialEq, Eq)]
1003pub struct QueryDirectoryRequest {
1004    pub class: u8,
1005    pub flags: u8,
1006    pub file_id: [u8; 16],
1007    /// The search pattern; `*` when the client sent none.
1008    pub pattern: String,
1009    pub output_len: u32,
1010}
1011
1012/// QUERY_DIRECTORY (MS-SMB2 2.2.33): StructureSize(2) FileInformationClass(1) Flags(1)
1013/// FileIndex(4) FileId(16) FileNameOffset(2) FileNameLength(2) OutputBufferLength(4). Takes the
1014/// message: `FileNameOffset` is measured from the header.
1015pub fn parse_query_directory(message: &[u8]) -> Option<QueryDirectoryRequest> {
1016    let body = body(message);
1017    if body.len() < 32 {
1018        return None;
1019    }
1020    let name_offset = le16(body, 24) as usize;
1021    let name_len = le16(body, 26) as usize;
1022    let pattern = if name_len == 0 {
1023        "*".to_string()
1024    } else {
1025        from_utf16le(buffer(message, name_offset, name_len)?)?
1026    };
1027    Some(QueryDirectoryRequest {
1028        class: body[2],
1029        flags: body[3],
1030        file_id: file_id(body, 8)?,
1031        pattern,
1032        output_len: le32(body, 28),
1033    })
1034}
1035
1036// ---------------------------------------------------------------------------------------------
1037// Small helpers
1038// ---------------------------------------------------------------------------------------------
1039
1040pub fn utf16le(s: &str) -> Vec<u8> {
1041    s.encode_utf16().flat_map(|c| c.to_le_bytes()).collect()
1042}
1043
1044/// Decode UTF-16LE, dropping a trailing NUL a client may include in the length.
1045pub fn from_utf16le(bytes: &[u8]) -> Option<String> {
1046    if !bytes.len().is_multiple_of(2) {
1047        return None;
1048    }
1049    let units: Vec<u16> = bytes
1050        .chunks_exact(2)
1051        .map(|c| u16::from_le_bytes([c[0], c[1]]))
1052        .take_while(|&c| c != 0)
1053        .collect();
1054    String::from_utf16(&units).ok()
1055}
1056
1057/// Convert Unix seconds to a Windows FILETIME.
1058pub fn filetime_from_unix(secs: i64, nanos: u32) -> u64 {
1059    const EPOCH_DIFF_SECS: i64 = 11_644_473_600;
1060    let secs = secs.saturating_add(EPOCH_DIFF_SECS).max(0) as u64;
1061    secs.saturating_mul(10_000_000) + (nanos / 100) as u64
1062}
1063
1064pub fn le16(b: &[u8], off: usize) -> u16 {
1065    u16::from_le_bytes([b[off], b[off + 1]])
1066}
1067
1068pub fn le32(b: &[u8], off: usize) -> u32 {
1069    u32::from_le_bytes([b[off], b[off + 1], b[off + 2], b[off + 3]])
1070}
1071
1072pub fn le64(b: &[u8], off: usize) -> u64 {
1073    let mut a = [0u8; 8];
1074    a.copy_from_slice(&b[off..off + 8]);
1075    u64::from_le_bytes(a)
1076}