1#![forbid(unsafe_code)]
16#![deny(missing_docs)]
17#![warn(missing_debug_implementations)]
18
19use async_trait::async_trait;
20use smb_server_proto::types::{AttrFlags, Disposition, FileTime};
21use smb_server_vfs::{Entry, FileMeta, OpenFile, SetOp, Vfs, VfsError, VfsResult, map_io};
22use tokio_uring::fs::{File, OpenOptions};
23
24#[derive(Debug)]
26pub struct PosixVfs {
27 root: std::path::PathBuf,
28 sd_cache: std::sync::Mutex<std::collections::HashMap<std::path::PathBuf, Vec<u8>>>,
31}
32
33pub struct PosixInner {
38 pub file: Option<File>,
40 pub pos: u64,
42 pub stream: Option<String>,
45}
46
47impl std::fmt::Debug for PosixInner {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 f.debug_struct("PosixInner")
50 .field("open", &self.file.is_some())
51 .field("pos", &self.pos)
52 .field("stream", &self.stream)
53 .finish()
54 }
55}
56
57impl PosixVfs {
58 pub fn new(root: impl Into<std::path::PathBuf>) -> Self {
60 let root = root.into();
61 let root = std::fs::canonicalize(&root).unwrap_or(root);
66 PosixVfs {
67 root,
68 sd_cache: std::sync::Mutex::new(std::collections::HashMap::new()),
69 }
70 }
71
72 fn resolve(&self, path: &str) -> std::path::PathBuf {
73 let abs = std::path::PathBuf::from(path);
76 if abs.is_absolute() && abs.starts_with(&self.root) {
77 return abs;
78 }
79 resolve_under(&self.root, path)
80 }
81
82 #[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))]
86 async fn create_stream(
87 &self,
88 base_rel: &str,
89 sname: &str,
90 access: u32,
91 disposition: u32,
92 options: u32,
93 ) -> VfsResult<(Box<OpenFile>, FileMeta, u32)> {
94 const OPT_DELETE_ON_CLOSE: u32 = 0x1000;
95 let path = self.resolve(base_rel);
96
97 let base_is_file = std::fs::symlink_metadata(&path)
100 .map(|m| !m.is_dir())
101 .unwrap_or(false);
102 if !base_is_file {
103 match Disposition::from_u32(disposition) {
104 Some(Disposition::Open) | Some(Disposition::Overwrite) => {
105 return Err(VfsError::NotFound);
106 }
107 _ => {
108 if let Some(parent) = path.parent() {
109 let _ = tokio_uring::fs::create_dir_all(parent).await;
110 }
111 let f = OpenOptions::new()
112 .write(true)
113 .create(true)
114 .open(&path)
115 .await
116 .map_err(map_io)?;
117 let _ = f.close().await;
118 }
119 }
120 }
121
122 let xname = stream_xattr(sname);
123 let stream_exists = xattr::get(&path, &xname).ok().flatten().is_some();
124 let action = if stream_exists {
125 match Disposition::from_u32(disposition) {
126 Some(Disposition::Supersede) => {
127 xattr::set(&path, &xname, b"").map_err(map_io)?;
128 0
129 }
130 Some(Disposition::Overwrite) | Some(Disposition::OverwriteIf) => {
131 xattr::set(&path, &xname, b"").map_err(map_io)?;
132 3
133 }
134 Some(Disposition::Create) => return Err(VfsError::AlreadyExists),
135 _ => 1,
136 }
137 } else {
138 match Disposition::from_u32(disposition) {
139 Some(Disposition::Open) | Some(Disposition::Overwrite) => {
140 return Err(VfsError::NotFound);
141 }
142 _ => {
143 xattr::set(&path, &xname, b"").map_err(map_io)?;
144 2
145 }
146 }
147 };
148
149 let size = xattr::get(&path, &xname)
150 .ok()
151 .flatten()
152 .map(|v| v.len() as u64)
153 .unwrap_or(0);
154 let (read_access, write_access) = access_flags(access);
155 let md = std::fs::metadata(&path).map_err(map_io)?;
156 let mut m = meta_of(&md);
157 m.eof = size;
158 m.alloc = size;
159 let open = Box::new(OpenFile {
160 path: path.to_string_lossy().into_owned(),
161 rel: base_rel.to_string(),
162 is_dir: false,
163 can_read: read_access,
164 can_write: write_access,
165 delete_on_close: options & OPT_DELETE_ON_CLOSE != 0,
166 delete_pending: false,
167 inner: Box::new(PosixInner {
168 file: None,
169 pos: 0,
170 stream: Some(sname.to_string()),
171 }),
172 });
173 Ok((open, m, action))
174 }
175}
176
177fn split_stream(rel: &str) -> (String, Option<String>) {
182 let (dir, last) = match rel.rfind(['\\', '/']) {
183 Some(i) => (&rel[..=i], &rel[i + 1..]),
184 None => ("", rel),
185 };
186 match last.find(':') {
187 None => (rel.to_string(), None),
188 Some(ci) => {
189 let base = &last[..ci];
190 let sname = last[ci + 1..].split(':').next().unwrap_or("");
191 let base_full = format!("{dir}{base}");
192 if sname.is_empty() || sname.eq_ignore_ascii_case("$DATA") {
195 (base_full, None)
196 } else {
197 (base_full, Some(sname.to_string()))
198 }
199 }
200 }
201}
202
203fn stream_xattr(name: &str) -> String {
205 format!("{STREAM_XATTR_PREFIX}{name}")
206}
207
208#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] fn access_flags(access: u32) -> (bool, bool) {
211 let read = access & (0x8000_0000 | 0x0000_0001 | 0x0000_0008 | 0x0000_0080 | 0x1000_0000) != 0;
212 let write = access
213 & (0x4000_0000 | 0x0000_0002 | 0x0000_0004 | 0x0001_0000 | 0x2000_0000 | 0x1000_0000)
214 != 0;
215 (read, write)
216}
217
218fn symlink_stop(root: &std::path::Path, rel: &str) -> Option<(String, u16, bool)> {
223 let bytes = rel.as_bytes();
224 let mut cur = root.to_path_buf();
225 let mut i = 0;
226 while i < rel.len() {
227 while i < rel.len() && (bytes[i] == b'\\' || bytes[i] == b'/') {
228 i += 1;
229 }
230 let start = i;
231 while i < rel.len() && bytes[i] != b'\\' && bytes[i] != b'/' {
232 i += 1;
233 }
234 let comp = &rel[start..i];
235 if comp.is_empty() || comp == "." || comp == ".." || comp.contains(':') {
236 continue;
237 }
238 cur.push(comp);
239 let Ok(m) = std::fs::symlink_metadata(&cur) else {
240 continue;
241 };
242 if !m.file_type().is_symlink() {
243 continue;
244 }
245 let target = std::fs::read_link(&cur)
246 .map(|p| p.to_string_lossy().into_owned())
247 .unwrap_or_default();
248 let unparsed_len = (rel[i..].encode_utf16().count() * size_of::<u16>()) as u16;
250 let relative = !std::path::Path::new(&target).is_absolute();
251 return Some((target, unparsed_len, relative));
252 }
253 None
254}
255
256fn resolve_under(root: &std::path::Path, rel: &str) -> std::path::PathBuf {
257 let mut cur = root.to_path_buf();
258 for comp in rel.split(['\\', '/']) {
259 if comp.is_empty() || comp == "." || comp == ".." || comp.contains(':') {
260 continue;
261 }
262 cur.push(comp);
263 }
264 let mut fixed = root.to_path_buf();
266 let suffix = cur.strip_prefix(root).unwrap_or(std::path::Path::new(""));
267 for comp in suffix.components() {
268 let candidate = fixed.join(comp.as_os_str());
269 if candidate.symlink_metadata().is_ok() {
270 fixed.push(comp.as_os_str());
271 continue;
272 }
273 match find_case_insensitive(&fixed, &comp.as_os_str().to_string_lossy()) {
274 Some(actual) => fixed.push(actual),
275 None => fixed.push(comp.as_os_str()),
276 }
277 }
278 fixed
279}
280
281fn find_case_insensitive(dir: &std::path::Path, name: &str) -> Option<String> {
282 let rd = std::fs::read_dir(dir).ok()?;
283 let lower = name.to_lowercase();
284 rd.flatten()
285 .find(|e| e.file_name().to_string_lossy().to_lowercase() == lower)
286 .map(|e| e.file_name().to_string_lossy().into_owned())
287}
288
289#[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] fn meta_of(md: &std::fs::Metadata) -> FileMeta {
291 use std::os::unix::fs::MetadataExt;
292 let attrs = AttrFlags::new(if md.is_dir() {
293 AttrFlags::DIRECTORY
294 } else if md.mode() & 0o222 == 0 {
295 AttrFlags::ARCHIVE | AttrFlags::READONLY
296 } else {
297 AttrFlags::ARCHIVE
298 });
299 let eof = if md.is_dir() { 0 } else { md.len() };
300 FileMeta {
301 times: [
302 FileTime::from_unix(md.ctime(), md.ctime_nsec() as u32),
303 FileTime::from_unix(md.atime(), md.atime_nsec() as u32),
304 FileTime::from_unix(md.mtime(), md.mtime_nsec() as u32),
305 FileTime::from_unix(md.ctime(), md.ctime_nsec() as u32),
306 ],
307 alloc: eof.div_ceil(4096) * 4096,
308 eof,
309 attrs,
310 is_dir: md.is_dir(),
311 }
312}
313
314fn set_file_times(path: &std::path::Path, atime: Option<u64>, mtime: Option<u64>) -> VfsResult<()> {
315 use std::fs::FileTimes;
316 let f = std::fs::OpenOptions::new()
317 .write(true)
318 .open(path)
319 .map_err(map_io)?;
320 let to_sys = |ft: u64| {
321 let (secs, nanos) = FileTime(ft).to_unix();
322 std::time::SystemTime::UNIX_EPOCH + std::time::Duration::new(secs.max(0) as u64, nanos)
323 };
324 let mut times = FileTimes::new();
325 if let Some(a) = atime {
326 times = times.set_accessed(to_sys(a));
327 }
328 if let Some(m) = mtime {
329 times = times.set_modified(to_sys(m));
330 }
331 f.set_times(times).map_err(map_io)?;
332 Ok(())
333}
334
335pub fn wildcard_match(name: &str, pattern: &str) -> bool {
337 fn rec(s: &[char], p: &[char]) -> bool {
338 if p.is_empty() {
339 return s.is_empty();
340 }
341 match p[0] {
342 '*' => (0..=s.len()).any(|i| rec(&s[i..], &p[1..])),
343 '?' => !s.is_empty() && rec(&s[1..], &p[1..]),
344 c => !s.is_empty() && s[0] == c && rec(&s[1..], &p[1..]),
345 }
346 }
347 rec(
348 &name.chars().collect::<Vec<_>>(),
349 &pattern.chars().collect::<Vec<_>>(),
350 )
351}
352
353#[async_trait(?Send)]
354impl Vfs for PosixVfs {
355 #[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] async fn create(
357 &self,
358 rel: &str,
359 is_dir: bool,
360 access: u32,
361 disposition: u32,
362 options: u32,
363 _attrs: u32,
364 ) -> VfsResult<(Box<OpenFile>, FileMeta, u32)> {
365 const OPT_DIRECTORY_FILE: u32 = 0x1;
366 const OPT_DELETE_ON_CLOSE: u32 = 0x1000;
367
368 let (base_rel, stream) = split_stream(rel);
371 if let Some(sname) = stream {
372 return self
373 .create_stream(&base_rel, &sname, access, disposition, options)
374 .await;
375 }
376 let rel = base_rel.as_str();
377 let path = self.resolve(rel);
378 const OPT_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
382 if options & OPT_OPEN_REPARSE_POINT == 0
383 && let Some((target, unparsed_len, relative)) = symlink_stop(&self.root, rel) {
384 return Err(VfsError::StoppedOnSymlink {
385 target,
386 unparsed_len,
387 relative,
388 });
389 }
390 let want_dir = is_dir || options & OPT_DIRECTORY_FILE != 0;
391 let existing = std::fs::symlink_metadata(&path).ok();
392 let exists = existing.is_some();
393 let existing_is_dir = existing.as_ref().map(|m| m.is_dir()).unwrap_or(false);
394
395 if want_dir || existing_is_dir {
398 let action = if exists {
399 1
400 } else {
401 match Disposition::from_u32(disposition) {
402 Some(Disposition::Open) | Some(Disposition::Overwrite) => {
403 return Err(VfsError::NotFound);
404 }
405 _ => {}
406 }
407 tokio_uring::fs::create_dir_all(&path)
408 .await
409 .map_err(map_io)?;
410 2
411 };
412 let md = std::fs::metadata(&path).map_err(map_io)?;
413 let (read_access, write_access) = access_flags(access);
414 let open = Box::new(OpenFile {
415 path: path.to_string_lossy().into_owned(),
416 rel: rel.to_string(),
417 is_dir: true,
418 can_read: read_access,
419 can_write: write_access,
420 delete_on_close: options & OPT_DELETE_ON_CLOSE != 0,
421 delete_pending: false,
422 inner: Box::new(PosixInner {
423 file: None,
424 pos: 0,
425 stream: None,
426 }),
427 });
428 return Ok((open, meta_of(&md), action));
429 }
430
431 let (action, truncate, create) = if exists {
433 match Disposition::from_u32(disposition) {
434 Some(Disposition::Supersede) => (0u32, true, false),
435 Some(Disposition::Overwrite) | Some(Disposition::OverwriteIf) => (3, true, false),
436 _ => (1, false, false),
437 }
438 } else {
439 match Disposition::from_u32(disposition) {
440 Some(Disposition::Open) | Some(Disposition::Overwrite) => {
441 return Err(VfsError::NotFound);
442 }
443 _ => {}
444 }
445 if let Some(parent) = path.parent() {
446 let _ = tokio_uring::fs::create_dir_all(parent).await;
447 }
448 (2, false, true)
449 };
450
451 let (read_access, write_access) = access_flags(access);
452
453 let mut oo = OpenOptions::new();
454 oo.read(read_access || !write_access);
455 oo.write(write_access || truncate || create);
456 if create {
457 oo.create(true);
458 }
459 if truncate {
460 oo.truncate(true);
461 }
462 let file = oo.open(&path).await.map_err(map_io)?;
463 let md = std::fs::metadata(&path).map_err(map_io)?;
464
465 let open = Box::new(OpenFile {
466 path: path.to_string_lossy().into_owned(),
467 rel: rel.to_string(),
468 is_dir: false,
469 can_read: read_access,
470 can_write: write_access,
471 delete_on_close: options & OPT_DELETE_ON_CLOSE != 0,
472 delete_pending: false,
473 inner: Box::new(PosixInner {
474 file: Some(file),
475 pos: 0,
476 stream: None,
477 }),
478 });
479 Ok((open, meta_of(&md), action))
480 }
481
482 async fn read(&self, open: &mut OpenFile, offset: u64, len: usize) -> VfsResult<Vec<u8>> {
483 let inner = open
484 .inner_as_mut::<PosixInner>()
485 .ok_or(VfsError::NotSupported)?;
486 if let Some(sname) = inner.stream.clone() {
487 let blob = xattr::get(self.resolve(&open.path), stream_xattr(&sname))
488 .ok()
489 .flatten()
490 .unwrap_or_default();
491 let start = (offset as usize).min(blob.len());
492 let end = (start + len).min(blob.len());
493 return Ok(blob[start..end].to_vec());
494 }
495 let f = inner.file.as_ref().ok_or(VfsError::AccessDenied)?;
496 let (res, mut buf) = f.read_at(vec![0u8; len], offset).await;
498 let n = res.map_err(map_io)?;
499 buf.truncate(n);
500 Ok(buf)
501 }
502
503 async fn write(
504 &self,
505 open: &mut OpenFile,
506 offset: u64,
507 data: &[u8],
508 write_through: bool,
509 ) -> VfsResult<u64> {
510 let inner = open
511 .inner_as_mut::<PosixInner>()
512 .ok_or(VfsError::NotSupported)?;
513 if let Some(sname) = inner.stream.clone() {
514 let p = self.resolve(&open.path);
516 let xname = stream_xattr(&sname);
517 let mut blob = xattr::get(&p, &xname).ok().flatten().unwrap_or_default();
518 let start = offset as usize;
519 if blob.len() < start + data.len() {
520 blob.resize(start + data.len(), 0);
521 }
522 blob[start..start + data.len()].copy_from_slice(data);
523 xattr::set(&p, &xname, &blob).map_err(map_io)?;
524 return Ok(data.len() as u64);
525 }
526 let f = inner.file.as_ref().ok_or(VfsError::AccessDenied)?;
527 let total = data.len();
528 let (res, _buf) = f.write_all_at(data.to_vec(), offset).await;
529 res.map_err(map_io)?;
530 if write_through {
531 f.sync_all().await.map_err(map_io)?;
532 }
533 Ok(total as u64)
534 }
535
536 #[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] async fn seek(&self, open: &mut OpenFile, mode: u16, offset: i64) -> VfsResult<u64> {
538 let size = std::fs::metadata(&open.path).map(|m| m.len()).unwrap_or(0);
540 let inner = open
541 .inner_as_mut::<PosixInner>()
542 .ok_or(VfsError::NotSupported)?;
543 let base = match mode & 3 {
544 0 => 0i64,
545 1 => inner.pos as i64,
546 _ => size as i64,
547 };
548 let np = base.saturating_add(offset).max(0) as u64;
549 inner.pos = np;
550 Ok(np)
551 }
552
553 async fn flush(&self, open: &mut OpenFile) -> VfsResult<()> {
554 let inner = open
555 .inner_as_ref::<PosixInner>()
556 .ok_or(VfsError::NotSupported)?;
557 if let Some(f) = inner.file.as_ref() {
558 f.sync_all().await.map_err(map_io)?;
559 }
560 Ok(())
561 }
562
563 async fn flush_all(&self) -> VfsResult<()> {
564 Ok(())
565 }
566
567 async fn close(&self, mut open: Box<OpenFile>) -> VfsResult<()> {
568 if let Some(sname) = open
571 .inner_as_ref::<PosixInner>()
572 .and_then(|i| i.stream.clone())
573 {
574 if open.delete_on_close || open.delete_pending {
575 let _ = xattr::remove(self.resolve(&open.path), stream_xattr(&sname));
576 }
577 return Ok(());
578 }
579 if let Some(inner) = open.inner_as_mut::<PosixInner>()
582 && let Some(f) = inner.file.take() {
583 let _ = f.close().await;
584 }
585 if open.delete_on_close || open.delete_pending {
586 if open.is_dir {
587 tokio_uring::fs::remove_dir(&open.path)
588 .await
589 .map_err(map_io)?;
590 } else {
591 tokio_uring::fs::remove_file(&open.path)
592 .await
593 .map_err(map_io)?;
594 }
595 }
596 Ok(())
597 }
598
599 async fn mkdir(&self, rel: &str) -> VfsResult<()> {
600 let p = self.resolve(rel);
601 tokio_uring::fs::create_dir(&p)
602 .await
603 .map_err(|e| match e.kind() {
604 std::io::ErrorKind::AlreadyExists => VfsError::AlreadyExists,
605 std::io::ErrorKind::NotFound => VfsError::NotFound,
606 _ => VfsError::AccessDenied,
607 })
608 }
609
610 async fn rmdir(&self, rel: &str) -> VfsResult<()> {
611 let p = self.resolve(rel);
612 tokio_uring::fs::remove_dir(&p)
613 .await
614 .map_err(|e| match e.kind() {
615 std::io::ErrorKind::DirectoryNotEmpty => VfsError::DirectoryNotEmpty,
616 std::io::ErrorKind::NotFound => VfsError::NotFound,
617 _ => VfsError::AccessDenied,
618 })
619 }
620
621 async fn check_dir(&self, rel: &str) -> VfsResult<()> {
622 let p = self.resolve(rel);
623 if p.is_dir() {
624 Ok(())
625 } else {
626 Err(VfsError::NotFound)
627 }
628 }
629
630 async fn unlink(&self, rel: &str) -> VfsResult<()> {
631 let p = self.resolve(rel);
632 let md = p.symlink_metadata().map_err(|_| VfsError::NotFound)?;
633 if md.is_dir() {
634 return Err(VfsError::InvalidArgument);
635 }
636 tokio_uring::fs::remove_file(&p).await.map_err(map_io)
637 }
638
639 async fn delete_pattern(&self, dir_rel: &str, pattern: &str) -> VfsResult<bool> {
640 let dir_abs = self.resolve(dir_rel);
641 let names = std::fs::read_dir(&dir_abs).map_err(|_| VfsError::NotFound)?;
642 let mut deleted_any = false;
643 for entry in names.flatten() {
644 let n = entry.file_name().to_string_lossy().into_owned();
645 if wildcard_match(&n.to_lowercase(), &pattern.to_lowercase()) && entry.path().is_file()
646 {
647 tokio_uring::fs::remove_file(entry.path())
648 .await
649 .map_err(map_io)?;
650 deleted_any = true;
651 }
652 }
653 Ok(deleted_any)
654 }
655
656 async fn rename(&self, old_rel: &str, new_rel: &str) -> VfsResult<()> {
657 let src = self.resolve(old_rel);
658 let dst = self.resolve(new_rel);
659 if !src.exists() {
660 return Err(VfsError::NotFound);
661 }
662 tokio_uring::fs::rename(&src, &dst).await.map_err(map_io)
663 }
664
665 async fn list(&self, dir_rel: &str) -> VfsResult<Vec<Entry>> {
666 let dir_abs = self.resolve(dir_rel);
667 let rd = std::fs::read_dir(&dir_abs).map_err(|_| VfsError::NotFound)?;
668 let mut out = Vec::new();
669 for e in rd.flatten() {
670 let name = e.file_name().to_string_lossy().into_owned();
671 let md = e.metadata().map_err(map_io)?;
672 out.push(Entry {
673 name,
674 meta: meta_of(&md),
675 });
676 }
677 out.sort_by(|a, b| a.name.cmp(&b.name));
678 Ok(out)
679 }
680
681 async fn stat(&self, rel: &str) -> VfsResult<FileMeta> {
682 let p = self.resolve(rel);
683 let md = p.symlink_metadata().map_err(|_| VfsError::NotFound)?;
684 Ok(meta_of(&md))
685 }
686
687 async fn set_info_open(&self, open: &mut OpenFile, op: &SetOp) -> VfsResult<()> {
688 match op {
689 SetOp::Disposition { delete } => open.delete_pending = *delete,
690 SetOp::Rename { name, .. } => {
691 let target = open.path.clone();
692 self.set_info_path(&target, op).await?;
693 open.path = self.resolve(name).to_string_lossy().into_owned();
697 open.rel = name.clone();
698 }
699 other => {
700 let target = open.path.clone();
701 self.set_info_path(&target, other).await?;
702 }
703 }
704 Ok(())
705 }
706
707 async fn set_info_path(&self, rel: &str, op: &SetOp) -> VfsResult<()> {
708 let p = self.resolve(rel);
709 match op {
710 SetOp::Disposition { delete } => {
711 if *delete {
712 if p.is_dir() {
713 tokio_uring::fs::remove_dir(&p).await.map_err(map_io)?;
714 } else {
715 tokio_uring::fs::remove_file(&p).await.map_err(map_io)?;
716 }
717 }
718 }
719 SetOp::Allocation(size) => {
720 if *size == 0 {
721 std::fs::OpenOptions::new()
722 .write(true)
723 .open(&p)
724 .map_err(map_io)?
725 .set_len(0)
726 .map_err(map_io)?;
727 }
728 }
729 SetOp::EndOfFile(len) => {
730 std::fs::OpenOptions::new()
731 .write(true)
732 .open(&p)
733 .map_err(map_io)?
734 .set_len(*len)
735 .map_err(map_io)?;
736 }
737 SetOp::Basic { access, write } => {
738 let valid = |ft: &FileTime| ft.0 != 0 && ft.0 != u64::MAX;
739 let a = access.filter(valid).map(|t| t.0);
740 let w = write.filter(valid).map(|t| t.0);
741 if a.is_some() || w.is_some() {
742 set_file_times(&p, a, w)?;
743 }
744 }
745 SetOp::Rename {
746 replace_if_exists: _,
747 name,
748 } => {
749 let dest = self.resolve(name);
750 tokio_uring::fs::rename(&p, &dest).await.map_err(map_io)?;
751 }
752 SetOp::Ea { name, value } => {
753 xattr::set(&p, format!("user.smbea.{name}"), value).map_err(map_io)?;
756 }
757 }
758 Ok(())
759 }
760
761 #[cfg_attr(dylint_lib = "no_magic_numbers", allow(no_magic_numbers))] async fn query_disk(&self) -> VfsResult<(u32, u32, u16, u16)> {
763 Ok((100_000, 50_000, 512, 64))
765 }
766
767 async fn get_security(&self, rel: &str) -> VfsResult<Option<Vec<u8>>> {
768 let p = self.resolve(rel);
769 if let Ok(Some(bytes)) = xattr::get(&p, NTACL_XATTR) {
770 return Ok(Some(bytes));
771 }
772 Ok(self.sd_cache.lock().unwrap().get(&p).cloned())
773 }
774
775 async fn set_security(&self, rel: &str, descriptor: &[u8]) -> VfsResult<()> {
776 let p = self.resolve(rel);
777 if xattr::set(&p, NTACL_XATTR, descriptor).is_err() {
780 self.sd_cache.lock().unwrap().insert(p, descriptor.to_vec());
781 }
782 Ok(())
783 }
784
785 async fn list_streams(&self, rel: &str) -> VfsResult<Vec<(String, u64)>> {
786 let p = self.resolve(rel);
787 let mut out = Vec::new();
788 if let Ok(names) = xattr::list(&p) {
789 for n in names {
790 let n = n.to_string_lossy();
791 if let Some(sname) = n.strip_prefix(STREAM_XATTR_PREFIX) {
792 let size = xattr::get(&p, n.as_ref())
793 .ok()
794 .flatten()
795 .map(|v| v.len() as u64)
796 .unwrap_or(0);
797 out.push((format!(":{sname}:$DATA"), size));
798 }
799 }
800 }
801 Ok(out)
802 }
803}
804
805const NTACL_XATTR: &str = "user.rustsmb.ntacl";
808
809const STREAM_XATTR_PREFIX: &str = "user.rustsmb.stream.";
812
813#[cfg(test)]
814mod tests {
815 use super::*;
816 use smb_server_vfs::Vfs;
817
818 #[test]
823 fn stat_by_stored_path_survives_relative_root() {
824 tokio_uring::start(async {
825 let dir = format!("target/it_share_{}", std::process::id());
826 std::fs::create_dir_all(&dir).unwrap();
827 std::fs::write(format!("{dir}/hello.txt"), b"hello!").unwrap();
828
829 let vfs = PosixVfs::new(&dir);
830 let (open, _meta, _action) = vfs
832 .create("hello.txt", false, 0x8000_0000, 1, 0, 0)
833 .await
834 .expect("open existing file");
835 let meta = vfs
837 .stat(&open.path)
838 .await
839 .expect("stat by stored handle path");
840 assert_eq!(meta.eof, 6, "reported size matches file contents");
841
842 std::fs::remove_dir_all(&dir).ok();
843 });
844 }
845
846 #[test]
849 fn ads_stream_round_trip() {
850 tokio_uring::start(async {
851 let dir = format!("target/it_ads_{}", std::process::id());
852 std::fs::create_dir_all(&dir).unwrap();
853 std::fs::write(format!("{dir}/host.txt"), b"base-data").unwrap();
854 let vfs = PosixVfs::new(&dir);
855
856 let payload = b"[ZoneTransfer]\r\nZoneId=3\r\n";
857 let (mut s, _m, _a) = vfs
859 .create(
860 "host.txt:Zone.Identifier:$DATA",
861 false,
862 0x4000_0000,
863 5,
864 0,
865 0,
866 )
867 .await
868 .expect("create stream");
869 vfs.write(&mut s, 0, payload, false)
870 .await
871 .expect("write stream");
872 vfs.close(s).await.unwrap();
873
874 let (mut s2, m2, _a) = vfs
876 .create(
877 "host.txt:Zone.Identifier:$DATA",
878 false,
879 0x8000_0000,
880 1,
881 0,
882 0,
883 )
884 .await
885 .expect("open stream");
886 assert_eq!(m2.eof, payload.len() as u64, "stream size reported");
887 let got = vfs.read(&mut s2, 0, 4096).await.expect("read stream");
888 assert_eq!(got, payload, "stream round-trips");
889 vfs.close(s2).await.unwrap();
890
891 let streams = vfs.list_streams("host.txt").await.expect("list");
892 assert!(
893 streams
894 .iter()
895 .any(|(n, sz)| n == ":Zone.Identifier:$DATA" && *sz == payload.len() as u64),
896 "ADS enumerated: {streams:?}"
897 );
898 assert_eq!(
899 std::fs::read(format!("{dir}/host.txt")).unwrap(),
900 b"base-data",
901 "base data stream untouched",
902 );
903 std::fs::remove_dir_all(&dir).ok();
904 });
905 }
906}