1use smb_server_proto::buf::Writer;
5
6pub mod file_class {
8 pub const DIRECTORY: u8 = 0x01;
10 pub const FULL_DIRECTORY: u8 = 0x02;
12 pub const BOTH_DIRECTORY: u8 = 0x03;
14 pub const RENAME: u8 = 0x0A;
16 pub const DISPOSITION: u8 = 0x0D;
18 pub const ALLOCATION: u8 = 0x13;
20 pub const END_OF_FILE: u8 = 0x14;
22 pub const BASIC: u8 = 0x04;
24 pub const STANDARD: u8 = 0x05;
26 pub const INTERNAL: u8 = 0x06;
28 pub const EA: u8 = 0x07;
30 pub const FULL_EA: u8 = 0x0F;
32 pub const ACCESS: u8 = 0x08;
34 pub const NAME: u8 = 0x09;
36 pub const MODE: u8 = 0x10;
38 pub const ALIGNMENT: u8 = 0x11;
40 pub const ALL: u8 = 0x12;
42 pub const STREAM: u8 = 0x16;
44 pub const POSITION: u8 = 0x0E;
46 pub const NETWORK_OPEN: u8 = 0x22;
48 pub const ATTRIBUTE_TAG: u8 = 0x23;
50 pub const NORMALIZED_NAME: u8 = 0x30;
52}
53
54pub mod fs_class {
56 pub const VOLUME: u8 = 0x01;
58 pub const SIZE: u8 = 0x03;
60 pub const DEVICE: u8 = 0x04;
62 pub const ATTRIBUTE: u8 = 0x05;
64 pub const FULL_SIZE: u8 = 0x07;
66}
67
68pub mod find_class {
71 pub const NAMES: u8 = 0x0C;
73 pub const ID_BOTH_DIRECTORY: u8 = 37;
75 pub const ID_FULL_DIRECTORY: u8 = 38;
77}
78
79#[derive(Debug, Clone, Default)]
81pub struct QueryMeta {
82 pub times: [u64; 4],
84 pub attrs: u32,
86 pub eof: u64,
88 pub alloc: u64,
90 pub is_dir: bool,
92}
93
94impl QueryMeta {
95 pub fn from_vfs(m: &smb_server_vfs::FileMeta) -> QueryMeta {
97 QueryMeta {
98 times: [m.times[0].0, m.times[1].0, m.times[2].0, m.times[3].0],
99 attrs: m.attrs.0,
100 eof: if m.is_dir { 0 } else { m.eof },
101 alloc: m.alloc,
102 is_dir: m.is_dir,
103 }
104 }
105}
106
107pub const GENERIC_RW_ACCESS: u32 = 0x0012_0089 | 0x0000_0016;
110
111fn push_utf16_len(d: &mut Writer, s: &str) {
112 let units: Vec<u16> = s.encode_utf16().collect();
113 d.push_u32((units.len() * 2) as u32);
114 for u in units {
115 d.push_u16(u);
116 }
117}
118
119fn push_times(d: &mut Writer, times: &[u64; 4]) {
120 for t in times {
121 d.push_u64(*t);
122 }
123}
124
125pub fn encode_file_info(class: u8, m: &QueryMeta, name: &str) -> Option<Vec<u8>> {
131 let mut d = Writer::new(0);
132 match class {
133 file_class::BASIC => {
134 push_times(&mut d, &m.times);
135 d.raw(&m.attrs.to_le_bytes());
136 d.push_u32(0); }
138 file_class::STANDARD => {
139 d.push_u64(m.alloc);
140 d.push_u64(m.eof);
141 d.push_u32(1); d.push(0); d.push(m.is_dir as u8);
144 d.push_u16(0); }
146 file_class::INTERNAL => {
147 d.push_u64(0); }
149 file_class::EA => {
150 d.push_u32(0); }
152 file_class::ACCESS => {
153 d.raw(&GENERIC_RW_ACCESS.to_le_bytes());
154 }
155 file_class::NAME => push_utf16_len(&mut d, name),
156 file_class::NORMALIZED_NAME => push_utf16_len(&mut d, name),
157 file_class::MODE => {
158 d.raw(&0u32.to_le_bytes());
159 }
160 file_class::ALIGNMENT => {
161 d.raw(&0u32.to_le_bytes());
162 }
163 file_class::POSITION => {
164 d.push_u64(0); }
166 file_class::ATTRIBUTE_TAG => {
167 d.raw(&m.attrs.to_le_bytes());
168 d.push_u32(0); }
170 file_class::NETWORK_OPEN => {
171 push_times(&mut d, &m.times);
172 d.push_u64(m.alloc);
173 d.push_u64(m.eof);
174 d.raw(&m.attrs.to_le_bytes());
175 }
176 file_class::ALL => {
177 push_times(&mut d, &m.times); d.raw(&m.attrs.to_le_bytes()); d.push_u32(0); d.push_u64(m.alloc); d.push_u64(m.eof); d.push_u32(1); d.push(0); d.push(m.is_dir as u8); d.push_u16(0); d.push_u64(0); d.push_u32(0); d.raw(&GENERIC_RW_ACCESS.to_le_bytes()); d.push_u64(0); d.push_u32(0); d.push_u32(0); d.push_u32(0); }
200 _ => return None,
201 }
202 Some(d.into_inner())
203}
204
205pub fn encode_stream_info(streams: &[(String, u64)]) -> Vec<u8> {
210 let mut d = Writer::new(0);
211 for (i, (name, size)) in streams.iter().enumerate() {
212 let units: Vec<u16> = name.encode_utf16().collect();
213 let name_bytes = units.len() * 2;
214 let entry_len = 24 + name_bytes;
216 let padded = entry_len.next_multiple_of(8);
217 let next = if i + 1 == streams.len() { 0 } else { padded };
218 d.push_u32(next as u32); d.push_u32(name_bytes as u32); d.push_u64(*size); d.push_u64(*size); for u in units {
223 d.push_u16(u);
224 }
225 d.raw(&vec![0u8; padded - entry_len]);
226 }
227 d.into_inner()
228}
229
230pub fn encode_fs_info(class: u8) -> Option<Vec<u8>> {
232 let mut d = Writer::new(0);
233 match class {
234 fs_class::VOLUME => {
235 d.push_u64(smb_server_proto::types::FileTime::now().0); d.push_u64(0x2023_0405_0607_0809); push_utf16_len(&mut d, "RUSTSMB"); }
239 fs_class::SIZE | fs_class::FULL_SIZE => {
240 d.push_u64(34_464); if class == fs_class::FULL_SIZE {
242 d.push_u64(17_232); }
244 d.push_u64(17_232); d.push_u32(50); d.push_u32(1000); }
248 fs_class::DEVICE => {
249 d.push_u32(7); d.push_u32(0x0002_0020); }
252 fs_class::ATTRIBUTE => {
253 d.push_u32(0x0007_0007); d.push_u32(255); push_utf16_len(&mut d, "NTFS");
257 }
258 _ => return None,
259 }
260 Some(d.into_inner())
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
269pub struct DecodeSetInfoError;
270
271pub fn decode_set_file_op(class: u8, buf: &[u8]) -> Result<Option<smb_server_vfs::SetOp>, DecodeSetInfoError> {
275 let r64 = |o: usize| -> u64 {
276 buf.get(o..o + 8)
277 .map(|s| u64::from_le_bytes(s.try_into().unwrap()))
278 .unwrap_or(0)
279 };
280 match class {
281 file_class::BASIC => {
282 let ft = |v: u64| (v != 0 && v != u64::MAX).then_some(smb_server_proto::types::FileTime(v));
285 Ok(Some(smb_server_vfs::SetOp::Basic {
286 access: ft(r64(8)),
287 write: ft(r64(16)),
288 }))
289 }
290 file_class::END_OF_FILE => Ok(Some(smb_server_vfs::SetOp::EndOfFile(r64(0)))),
291 file_class::ALLOCATION => Ok(Some(smb_server_vfs::SetOp::Allocation(r64(0)))),
292 file_class::DISPOSITION => Ok(Some(smb_server_vfs::SetOp::Disposition {
293 delete: buf.first().copied().unwrap_or(0) != 0,
294 })),
295 file_class::RENAME => {
296 if buf.len() < 20 {
299 return Err(DecodeSetInfoError);
300 }
301 let replace = buf.first().copied().unwrap_or(0) != 0;
302 let len =
303 u32::from_le_bytes(buf[16..20].try_into().map_err(|_| DecodeSetInfoError)?) as usize;
304 let raw = buf.get(20..(20 + len).min(buf.len())).ok_or(DecodeSetInfoError)?;
305 let units: Vec<u16> = raw
306 .chunks_exact(2)
307 .map(|c| c[0] as u16 | ((c[1] as u16) << 8))
308 .collect();
309 let name = String::from_utf16_lossy(&units).replace('/', "\\");
310 Ok(Some(smb_server_vfs::SetOp::Rename { replace_if_exists: replace, name }))
311 }
312 file_class::POSITION => Ok(None), file_class::FULL_EA => {
314 if buf.len() < 8 {
318 return Err(DecodeSetInfoError);
319 }
320 let name_len = buf[5] as usize;
321 let val_len = u16::from_le_bytes([buf[6], buf[7]]) as usize;
322 let name_start = 8;
323 let val_start = name_start + name_len + 1; let val_end = val_start + val_len;
325 if buf.len() < val_end {
326 return Err(DecodeSetInfoError);
327 }
328 let name = String::from_utf8_lossy(&buf[name_start..name_start + name_len]).into_owned();
329 let value = buf[val_start..val_end].to_vec();
330 Ok(Some(smb_server_vfs::SetOp::Ea { name, value }))
331 }
332 _ => Err(DecodeSetInfoError),
333 }
334}
335
336#[derive(Debug, Clone)]
340pub struct FindEntry {
341 pub name: String,
343 pub meta: QueryMeta,
345}
346
347fn push_find_fixed(buf: &mut Vec<u8>, class: u8, m: &QueryMeta, name: &str, name_len: usize) {
349 if class == find_class::NAMES {
350 buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&(name_len as u32).to_le_bytes());
353 return;
354 }
355 buf.extend_from_slice(&0u32.to_le_bytes()); for t in &m.times {
357 buf.extend_from_slice(&t.to_le_bytes());
358 }
359 buf.extend_from_slice(&m.eof.to_le_bytes());
360 buf.extend_from_slice(&m.alloc.to_le_bytes());
361 buf.extend_from_slice(&m.attrs.to_le_bytes());
362 buf.extend_from_slice(&(name_len as u32).to_le_bytes());
363 match class {
364 file_class::FULL_DIRECTORY => {
365 buf.extend_from_slice(&0u32.to_le_bytes()); }
367 file_class::BOTH_DIRECTORY => {
368 buf.extend_from_slice(&0u32.to_le_bytes()); push_short_name(buf, name);
370 }
371 find_class::ID_FULL_DIRECTORY => {
372 buf.extend_from_slice(&0u32.to_le_bytes()); buf.extend_from_slice(&0u64.to_le_bytes()); }
375 find_class::ID_BOTH_DIRECTORY => {
376 buf.extend_from_slice(&0u32.to_le_bytes()); push_short_name(buf, name);
378 buf.extend_from_slice(&0u16.to_le_bytes()); buf.extend_from_slice(&0u64.to_le_bytes()); }
381 _ => {}
382 }
383}
384
385fn push_short_name(buf: &mut Vec<u8>, name: &str) {
388 let short = short_name_8_3(name);
389 let units: Vec<u16> = short.encode_utf16().collect();
390 buf.push((units.len() * 2) as u8); buf.push(0); let mut field = [0u8; 24];
393 for (i, u) in units.iter().take(12).enumerate() {
394 field[i * 2..i * 2 + 2].copy_from_slice(&u.to_le_bytes());
395 }
396 buf.extend_from_slice(&field);
397}
398
399const SHORT_NAME_SYMBOLS: &str = "!#$%&'()-@^_`{}~";
401
402pub fn short_name_8_3(name: &str) -> String {
407 if is_valid_8_3(name) {
408 return String::new();
409 }
410 let (base, ext) = match name.rsplit_once('.') {
411 Some((b, e)) if !b.is_empty() => (b, e),
412 _ => (name, ""),
413 };
414 let clean = |s: &str| -> String {
415 s.chars()
416 .filter(|c| c.is_ascii_alphanumeric() || SHORT_NAME_SYMBOLS.contains(*c))
417 .map(|c| c.to_ascii_uppercase())
418 .collect()
419 };
420 let b: String = clean(base).chars().take(6).collect();
421 let e: String = clean(ext).chars().take(3).collect();
422 let b = if b.is_empty() { "_".to_string() } else { b };
423 if e.is_empty() {
424 format!("{b}~1")
425 } else {
426 format!("{b}~1.{e}")
427 }
428}
429
430fn is_valid_8_3(name: &str) -> bool {
433 let (base, ext) = match name.rsplit_once('.') {
434 Some((b, e)) => (b, e),
435 None => (name, ""),
436 };
437 let ok = |s: &str, max: usize| -> bool {
438 s.len() <= max
439 && s.chars()
440 .all(|c| c.is_ascii_alphanumeric() || SHORT_NAME_SYMBOLS.contains(c))
441 };
442 !base.is_empty() && ok(base, 8) && ok(ext, 3)
443}
444
445pub fn encode_find_entries(entries: &[FindEntry], class: u8) -> Vec<u8> {
450 let mut buf: Vec<u8> = Vec::new();
451 for (i, e) in entries.iter().enumerate() {
452 let start = buf.len();
453 let mut name_bytes = Vec::with_capacity(e.name.len() * 2);
454 for u in e.name.encode_utf16() {
455 name_bytes.extend_from_slice(&u.to_le_bytes());
456 }
457
458 let next_pos = buf.len();
459 buf.extend_from_slice(&0u32.to_le_bytes()); push_find_fixed(&mut buf, class, &e.meta, &e.name, name_bytes.len());
462 buf.extend_from_slice(&name_bytes);
463
464 let stride = buf.len() - start;
465 if i + 1 < entries.len() {
466 let aligned = (stride + 7) & !7usize;
467 while buf.len() < start + aligned {
468 buf.push(0);
469 }
470 let next = (buf.len() - start) as u32;
471 buf[next_pos..next_pos + 4].copy_from_slice(&next.to_le_bytes());
472 }
473 }
474 buf
475}
476
477#[cfg(test)]
478mod stream_info_tests {
479 use super::*;
480
481 #[test]
482 fn encodes_default_plus_ads_chain() {
483 let streams = vec![
484 ("::$DATA".to_string(), 9u64),
485 (":Zone.Identifier:$DATA".to_string(), 26u64),
486 ];
487 let buf = encode_stream_info(&streams);
488 let next0 = u32::from_le_bytes(buf[0..4].try_into().unwrap());
490 assert_ne!(next0, 0, "first entry links to the second");
491 assert_eq!(u32::from_le_bytes(buf[4..8].try_into().unwrap()), 14, "::$DATA name bytes");
492 assert_eq!(u64::from_le_bytes(buf[8..16].try_into().unwrap()), 9, "default stream size");
493 assert_eq!(next0 % 8, 0, "entries are 8-byte aligned");
494 let last = next0 as usize;
496 assert_eq!(u32::from_le_bytes(buf[last..last + 4].try_into().unwrap()), 0, "chain terminates");
497 }
498
499 #[test]
500 fn empty_list_is_empty_buffer() {
501 assert!(encode_stream_info(&[]).is_empty());
502 }
503}
504
505#[cfg(test)]
506mod short_name_tests {
507 use super::*;
508
509 #[test]
510 fn generates_8_3_for_long_names() {
511 assert_eq!(short_name_8_3("LongFileName.txtx"), "LONGFI~1.TXT");
512 assert_eq!(short_name_8_3("noext_longname"), "NOEXT_~1");
513 assert_eq!(short_name_8_3("READ.ME"), "");
515 assert_eq!(short_name_8_3("A.B"), "");
516 }
517}