1use std::fs;
11use std::io::{self, Read, Write};
12use std::path::{Path, PathBuf};
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::time::UNIX_EPOCH;
15
16use crate::fsproto::{self, Entry, ErrKind, Kind, Reply, Request, Splitter, MAX_READ, MAX_WRITE};
17
18pub fn run(home: &Path, input: &mut impl Read, output: &mut impl Write) -> io::Result<()> {
21 run_with(Server::new(home.to_path_buf()), input, output)
22}
23
24pub fn run_with(mut server: Server, input: &mut impl Read, output: &mut impl Write) -> io::Result<()> {
27 let mut splitter = Splitter::new();
28 let mut buf = vec![0u8; 64 * 1024];
29 loop {
30 let n = input.read(&mut buf)?;
31 if n == 0 {
32 return Ok(());
33 }
34 let bodies = splitter
35 .push(&buf[..n])
36 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
37 for body in bodies {
38 let reply = match Request::decode(&body) {
39 Ok((id, req)) => (id, server.handle(req)),
40 Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e)),
42 };
43 output.write_all(&reply.1.encode(reply.0))?;
44 output.flush()?;
45 }
46 }
47}
48
49pub type SessionPid = Box<dyn Fn(&str) -> Option<u32> + Send>;
51
52pub struct Server {
55 home: PathBuf,
56 procs: crate::procs::Sampler,
57 session_pid: Option<SessionPid>,
58}
59
60impl Server {
61 pub fn new(home: PathBuf) -> Self {
62 Self { home, procs: crate::procs::Sampler::new(), session_pid: None }
63 }
64
65 pub fn with_sessions(home: PathBuf, session_pid: SessionPid) -> Self {
68 Self { session_pid: Some(session_pid), ..Self::new(home) }
69 }
70
71 pub fn handle(&mut self, req: Request) -> Reply {
72 match req {
73 Request::Procs { top, filter } => Reply::Procs(self.procs.frame(top, &filter)),
74 Request::PaneProcs { block_id, session } => {
75 let pid = (!session.is_empty()).then(|| self.session_pid.as_ref().and_then(|f| f(&session))).flatten();
76 Reply::PaneProcs(self.procs.pane(&block_id, pid))
77 }
78 req => handle(&self.home, req),
79 }
80 }
81}
82
83pub fn home_dir() -> PathBuf {
84 let var = if cfg!(windows) { "USERPROFILE" } else { "HOME" };
85 std::env::var_os(var)
86 .filter(|h| !h.is_empty())
87 .map(PathBuf::from)
88 .unwrap_or_else(|| PathBuf::from("/"))
89}
90
91pub fn resolve(home: &Path, path: &str) -> PathBuf {
94 let p = if path == "~" {
95 home.to_path_buf()
96 } else if let Some(rest) = path.strip_prefix("~/") {
97 home.join(rest)
98 } else if Path::new(path).is_absolute() {
99 PathBuf::from(path)
100 } else {
101 home.join(path)
102 };
103 p.components().collect()
107}
108
109fn err(e: io::Error) -> Reply {
110 Reply::Err {
111 kind: ErrKind::from_io(&e),
112 message: e.to_string(),
113 }
114}
115
116fn invalid(message: &str) -> Reply {
117 Reply::Err {
118 kind: ErrKind::Invalid,
119 message: message.to_string(),
120 }
121}
122
123pub fn handle(home: &Path, req: Request) -> Reply {
127 let result = match req {
128 Request::Procs { top, filter } => Ok(Reply::Procs(crate::procs::Sampler::new().frame(top, &filter))),
129 Request::PaneProcs { block_id, .. } => Ok(Reply::PaneProcs(crate::procs::Sampler::new().pane(&block_id, None))),
130 Request::Hello => Ok(Reply::Hello {
131 protocol: fsproto::PROTOCOL,
132 home: home.to_string_lossy().into_owned(),
133 os: std::env::consts::OS.to_string(),
134 }),
135 Request::Stat { path } => {
136 let p = resolve(home, &path);
137 entry_of(&p).map(Reply::Stat)
138 }
139 Request::List {
140 path,
141 offset,
142 limit,
143 } => list(&resolve(home, &path), offset, limit),
144 Request::Read { path, offset, len } => read(&resolve(home, &path), offset, len),
145 Request::Write { path, data } => {
146 if data.len() > MAX_WRITE {
147 return Reply::Err {
148 kind: ErrKind::TooLarge,
149 message: format!("over the {} MB a write may be", MAX_WRITE >> 20),
150 };
151 }
152 write_atomic(&resolve(home, &path), &data).map(|()| Reply::Done)
153 }
154 Request::Append { path, data } => {
155 if data.len() > MAX_WRITE {
156 return Reply::Err {
157 kind: ErrKind::TooLarge,
158 message: format!("over the {} MB a write may be", MAX_WRITE >> 20),
159 };
160 }
161 let p = resolve(home, &path);
162 match fs::symlink_metadata(&p) {
165 Ok(m) if m.is_file() => fs::OpenOptions::new()
166 .append(true)
167 .open(&p)
168 .and_then(|mut f| f.write_all(&data))
169 .map(|()| Reply::Done),
170 Ok(_) => return invalid("only a regular file can be appended to"),
171 Err(e) => Err(e),
172 }
173 }
174 Request::Mkdir { path, parents } => {
175 let p = resolve(home, &path);
176 if parents {
177 fs::create_dir_all(&p)
178 } else {
179 fs::create_dir(&p)
180 }
181 .map(|()| Reply::Done)
182 }
183 Request::Rename { from, to } => {
184 let (from, to) = (resolve(home, &from), resolve(home, &to));
185 if let Err(e) = fs::symlink_metadata(&from) {
186 return err(e);
187 }
188 if fs::symlink_metadata(&to).is_ok() && !same_file(&from, &to) {
193 return Reply::Err {
194 kind: ErrKind::AlreadyExists,
195 message: format!("{} already exists", to.display()),
196 };
197 }
198 fs::rename(&from, &to).map(|()| Reply::Done)
199 }
200 Request::Replace { from, to } => {
201 let (from, to) = (resolve(home, &from), resolve(home, &to));
202 for p in [&from, &to] {
205 match fs::symlink_metadata(p) {
206 Ok(m) if m.is_dir() => return invalid("only a file is replaced in one step"),
207 Ok(_) => {}
208 Err(e) => return err(e),
209 }
210 }
211 fs::rename(&from, &to).map(|()| Reply::Done)
212 }
213 Request::Sync { path } => {
214 let p = resolve(home, &path);
215 match fs::symlink_metadata(&p) {
216 Ok(m) if m.is_file() => fs::OpenOptions::new()
217 .read(cfg!(unix))
220 .write(!cfg!(unix))
221 .open(&p)
222 .and_then(|f| f.sync_all())
223 .map(|()| Reply::Done),
224 Ok(_) => return invalid("only a file is synced"),
225 Err(e) => Err(e),
226 }
227 }
228 Request::Realpath { path } => fs::canonicalize(resolve(home, &path))
229 .map(|p| Reply::Path(p.to_string_lossy().into_owned())),
230 Request::SetMeta {
231 path,
232 mode,
233 mtime_ms,
234 } => {
235 let p = resolve(home, &path);
236 match fs::symlink_metadata(&p) {
237 Ok(m) if m.is_file() => set_meta(&p, mode, mtime_ms).map(|()| Reply::Done),
238 Ok(_) => return invalid("only a file's metadata is set"),
239 Err(e) => Err(e),
240 }
241 }
242 Request::Delete { path, recursive } => {
243 let p = resolve(home, &path);
244 if p.components().any(|c| c == std::path::Component::ParentDir) {
247 return invalid("refusing to delete a path with '..' in it");
248 }
249 let real = match (p.parent(), p.file_name()) {
252 (Some(parent), Some(name)) => match fs::canonicalize(parent) {
253 Ok(dir) => dir.join(name),
254 Err(e) => return err(e),
255 },
256 _ => return invalid("refusing to delete the root"),
257 };
258 let real_home = fs::canonicalize(home).unwrap_or_else(|_| home.to_path_buf());
259 if real.parent().is_none() || real == real_home || real_home.starts_with(&real) {
260 return invalid(
261 "refusing to delete the root, the home directory or a folder holding it",
262 );
263 }
264 match fs::symlink_metadata(&real) {
268 Ok(m) if m.file_type().is_symlink() => {
271 fs::remove_file(&real).or_else(|_| fs::remove_dir(&real))
272 }
273 Ok(m) if m.is_dir() => {
274 if recursive {
275 fs::remove_dir_all(&real)
276 } else {
277 fs::remove_dir(&real)
278 }
279 }
280 Ok(_) => fs::remove_file(&real),
281 Err(e) => Err(e),
282 }
283 .map(|()| Reply::Done)
284 }
285 };
286 result.unwrap_or_else(err)
287}
288
289fn set_meta(p: &Path, mode: u32, mtime_ms: i64) -> io::Result<()> {
292 if mtime_ms > 0 {
293 let t = UNIX_EPOCH + std::time::Duration::from_millis(mtime_ms as u64);
294 fs::OpenOptions::new()
295 .write(true)
296 .open(p)?
297 .set_modified(t)?;
298 }
299 #[cfg(unix)]
300 if mode != 0 {
301 use std::os::unix::fs::PermissionsExt;
302 fs::set_permissions(p, fs::Permissions::from_mode(mode & 0o7777))?;
303 }
304 #[cfg(not(unix))]
305 let _ = mode;
306 Ok(())
307}
308
309fn entry_of(p: &Path) -> io::Result<Entry> {
310 let link = fs::symlink_metadata(p)?;
311 let symlink = link.file_type().is_symlink();
312 let meta = if symlink {
314 fs::metadata(p).unwrap_or(link)
315 } else {
316 link
317 };
318 let kind = if meta.is_dir() {
319 Kind::Dir
320 } else if meta.is_file() {
321 Kind::File
322 } else {
323 Kind::Other
324 };
325 let mtime_ms = meta
326 .modified()
327 .ok()
328 .and_then(|t| t.duration_since(UNIX_EPOCH).ok())
329 .map(|d| d.as_millis() as i64)
330 .unwrap_or(0);
331 #[cfg(unix)]
332 let mode = {
333 use std::os::unix::fs::PermissionsExt;
334 meta.permissions().mode() & 0o7777
336 };
337 #[cfg(not(unix))]
338 let mode = 0;
339 let link_target = if symlink {
340 fs::read_link(p)
341 .map(|t| t.to_string_lossy().into_owned())
342 .unwrap_or_default()
343 } else {
344 String::new()
345 };
346 Ok(Entry {
347 link_target,
348 name: p
349 .file_name()
350 .map(|n| n.to_string_lossy().into_owned())
351 .unwrap_or_else(|| p.to_string_lossy().into_owned()),
352 kind,
353 size: meta.len(),
354 mtime_ms,
355 mode,
356 symlink,
357 })
358}
359
360fn same_file(a: &Path, b: &Path) -> bool {
363 #[cfg(unix)]
364 {
365 use std::os::unix::fs::MetadataExt;
366 match (fs::symlink_metadata(a), fs::symlink_metadata(b)) {
367 (Ok(x), Ok(y)) => x.dev() == y.dev() && x.ino() == y.ino(),
368 _ => false,
369 }
370 }
371 #[cfg(not(unix))]
372 {
373 match (fs::canonicalize(a), fs::canonicalize(b)) {
374 (Ok(x), Ok(y)) => x == y,
375 _ => false,
376 }
377 }
378}
379
380fn list(dir: &Path, offset: u32, limit: u32) -> io::Result<Reply> {
381 let mut names: Vec<std::ffi::OsString> = fs::read_dir(dir)?
382 .filter_map(|e| e.ok().map(|e| e.file_name()))
383 .collect();
384 names.sort();
385 let total = names.len() as u32;
386 let entries = names
387 .into_iter()
388 .skip(offset as usize)
389 .take(limit.min(5000) as usize)
390 .filter_map(|n| entry_of(&dir.join(n)).ok())
392 .collect();
393 Ok(Reply::List { entries, total })
394}
395
396fn read(p: &Path, offset: u64, len: u32) -> io::Result<Reply> {
397 use std::io::{Seek, SeekFrom};
398 #[cfg(unix)]
402 let mut f = {
403 use std::os::unix::fs::OpenOptionsExt;
404 fs::OpenOptions::new()
405 .read(true)
406 .custom_flags(libc::O_NONBLOCK)
407 .open(p)?
408 };
409 #[cfg(not(unix))]
410 let mut f = fs::File::open(p)?;
411 let opened = f.metadata()?;
412 if !opened.is_dir() && !opened.is_file() {
413 return Ok(Reply::Err {
414 kind: ErrKind::Invalid,
415 message: format!("{} isn't a regular file", p.display()),
416 });
417 }
418 if opened.is_dir() {
419 return Ok(Reply::Err {
420 kind: ErrKind::IsADirectory,
421 message: format!("{} is a directory", p.display()),
422 });
423 }
424 f.seek(SeekFrom::Start(offset))?;
425 let want = len.min(MAX_READ) as usize;
426 let mut data = Vec::with_capacity(want);
427 Read::by_ref(&mut f)
428 .take(want as u64)
429 .read_to_end(&mut data)?;
430 let eof = data.len() < want || f.read(&mut [0u8; 1])? == 0;
432 Ok(Reply::Read { data, eof })
433}
434
435fn write_atomic(p: &Path, data: &[u8]) -> io::Result<()> {
440 static N: AtomicU64 = AtomicU64::new(0);
441 let target = match fs::symlink_metadata(p) {
442 Ok(m) if m.file_type().is_symlink() => fs::canonicalize(p)?,
443 _ => p.to_path_buf(),
444 };
445 let dir = target
446 .parent()
447 .filter(|d| !d.as_os_str().is_empty())
448 .unwrap_or(Path::new("."));
449 let name = target
450 .file_name()
451 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "not a file path"))?;
452 let tmp = dir.join(format!(
453 ".{}.amx-{}-{}.tmp",
454 name.to_string_lossy(),
455 std::process::id(),
456 N.fetch_add(1, Ordering::Relaxed)
457 ));
458 let old = fs::metadata(&target).ok();
459 if old.as_ref().is_some_and(|m| m.is_dir()) {
460 return Err(io::Error::new(
461 io::ErrorKind::InvalidInput,
462 format!("{} is a directory", target.display()),
463 ));
464 }
465 let result = (|| {
466 let mut f = fs::OpenOptions::new()
467 .write(true)
468 .create_new(true)
469 .open(&tmp)?;
470 f.write_all(data)?;
471 f.sync_all()?;
472 if let Some(m) = &old {
473 fs::set_permissions(&tmp, m.permissions())?;
474 }
475 fs::rename(&tmp, &target)
476 })();
477 if result.is_err() {
478 let _ = fs::remove_file(&tmp);
479 }
480 result
481}
482
483#[cfg(test)]
484mod tests {
485 use super::*;
486
487 fn exchange(home: &Path, requests: &[Request]) -> Vec<Reply> {
489 let mut input = Vec::new();
490 for (i, r) in requests.iter().enumerate() {
491 input.extend(r.encode(i as u32));
492 }
493 let mut output = Vec::new();
494 run(home, &mut input.as_slice(), &mut output).unwrap();
495 let mut s = Splitter::new();
496 s.push(&output)
497 .unwrap()
498 .iter()
499 .enumerate()
500 .map(|(i, b)| {
501 let (id, reply) = Reply::decode(b).unwrap();
502 assert_eq!(id, i as u32, "replies in order, each with its id");
503 reply
504 })
505 .collect()
506 }
507
508 #[test]
509 fn files_are_listed_read_written_renamed_and_deleted() {
510 let home = tempfile::tempdir().unwrap();
511 let h = home.path();
512 let replies = exchange(
513 h,
514 &[
515 Request::Hello,
516 Request::Mkdir {
517 path: "~/d/e".into(),
518 parents: true,
519 },
520 Request::Write {
521 path: "d/a.txt".into(),
522 data: b"hello world".to_vec(),
523 },
524 Request::Read {
525 path: "~/d/a.txt".into(),
526 offset: 6,
527 len: 100,
528 },
529 Request::Read {
530 path: "~/d/a.txt".into(),
531 offset: 0,
532 len: 5,
533 },
534 Request::List {
535 path: "~/d".into(),
536 offset: 0,
537 limit: 10,
538 },
539 Request::List {
540 path: "~/d".into(),
541 offset: 1,
542 limit: 10,
543 },
544 Request::Rename {
545 from: "~/d/a.txt".into(),
546 to: "~/d/b.txt".into(),
547 },
548 Request::Stat {
549 path: "~/d/a.txt".into(),
550 },
551 Request::Delete {
552 path: "~/d".into(),
553 recursive: false,
554 },
555 Request::Delete {
556 path: "~/d".into(),
557 recursive: true,
558 },
559 Request::Stat { path: "~/d".into() },
560 ],
561 );
562 match &replies[0] {
563 Reply::Hello {
564 protocol, home: hh, ..
565 } => {
566 assert_eq!(*protocol, fsproto::PROTOCOL);
567 assert_eq!(Path::new(hh), h);
568 }
569 r => panic!("{r:?}"),
570 }
571 assert_eq!(replies[1], Reply::Done);
572 assert_eq!(replies[2], Reply::Done);
573 assert_eq!(
574 replies[3],
575 Reply::Read {
576 data: b"world".to_vec(),
577 eof: true
578 }
579 );
580 assert_eq!(
581 replies[4],
582 Reply::Read {
583 data: b"hello".to_vec(),
584 eof: false
585 }
586 );
587 match &replies[5] {
588 Reply::List { entries, total } => {
589 assert_eq!(*total, 2);
590 let got: Vec<(&str, Kind, u64)> = entries
591 .iter()
592 .map(|e| (e.name.as_str(), e.kind, e.size))
593 .collect();
594 assert_eq!(
595 got,
596 vec![("a.txt", Kind::File, 11), ("e", Kind::Dir, got[1].2)]
597 );
598 assert!(entries[0].mtime_ms > 0);
599 }
600 r => panic!("{r:?}"),
601 }
602 match &replies[6] {
603 Reply::List { entries, total } => {
604 assert_eq!(
605 (*total, entries.len(), entries[0].name.as_str()),
606 (2, 1, "e")
607 )
608 }
609 r => panic!("{r:?}"),
610 }
611 assert_eq!(replies[7], Reply::Done);
612 assert!(matches!(
613 replies[8],
614 Reply::Err {
615 kind: ErrKind::NotFound,
616 ..
617 }
618 ));
619 assert!(matches!(replies[9], Reply::Err { .. }), "{:?}", replies[9]);
621 assert_eq!(replies[10], Reply::Done);
622 assert!(matches!(
623 replies[11],
624 Reply::Err {
625 kind: ErrKind::NotFound,
626 ..
627 }
628 ));
629 }
630
631 #[test]
632 fn a_write_replaces_whole_keeps_the_mode_and_follows_a_link() {
633 let home = tempfile::tempdir().unwrap();
634 let h = home.path();
635 let f = h.join("f.sh");
636 fs::write(&f, b"old").unwrap();
637 #[cfg(unix)]
638 {
639 use std::os::unix::fs::PermissionsExt;
640 fs::set_permissions(&f, fs::Permissions::from_mode(0o750)).unwrap();
641 std::os::unix::fs::symlink(&f, h.join("link")).unwrap();
642 }
643 let replies = exchange(
644 h,
645 &[
646 Request::Write {
647 path: "~/f.sh".into(),
648 data: b"new".to_vec(),
649 },
650 #[cfg(unix)]
651 Request::Write {
652 path: "~/link".into(),
653 data: b"via link".to_vec(),
654 },
655 ],
656 );
657 assert!(replies.iter().all(|r| *r == Reply::Done), "{replies:?}");
658 #[cfg(unix)]
659 {
660 use std::os::unix::fs::PermissionsExt;
661 assert_eq!(fs::read(&f).unwrap(), b"via link");
662 assert_eq!(
663 fs::metadata(&f).unwrap().permissions().mode() & 0o777,
664 0o750
665 );
666 assert!(fs::symlink_metadata(h.join("link"))
667 .unwrap()
668 .file_type()
669 .is_symlink());
670 }
671 #[cfg(not(unix))]
672 assert_eq!(fs::read(&f).unwrap(), b"new");
673 let left: Vec<_> = fs::read_dir(h)
675 .unwrap()
676 .filter_map(|e| e.ok())
677 .filter(|e| e.file_name().to_string_lossy().ends_with(".tmp"))
678 .collect();
679 assert!(left.is_empty());
680 }
681
682 #[test]
683 fn a_case_only_rename_is_not_refused_as_a_clash() {
684 let home = tempfile::tempdir().unwrap();
685 let h = home.path();
686 fs::write(h.join("a.txt"), b"a").unwrap();
687 let case_insensitive = h.join("A.TXT").exists();
688 let r = exchange(
689 h,
690 &[Request::Rename {
691 from: "a.txt".into(),
692 to: "A.txt".into(),
693 }],
694 );
695 assert_eq!(
696 r[0],
697 Reply::Done,
698 "case-insensitive disk: {case_insensitive}"
699 );
700 let names: Vec<String> = fs::read_dir(h)
701 .unwrap()
702 .map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
703 .collect();
704 assert_eq!(names, vec!["A.txt"]);
705 }
706
707 #[test]
708 fn a_path_never_ends_in_a_separator() {
709 let h = Path::new("/home/u");
710 assert_eq!(resolve(h, "~/link/"), Path::new("/home/u/link"));
711 assert_eq!(resolve(h, "/srv/data//"), Path::new("/srv/data"));
712 assert_eq!(resolve(h, "rel/"), Path::new("/home/u/rel"));
713 assert_eq!(resolve(h, "~"), h);
714 }
715
716 #[test]
717 fn append_adds_to_an_existing_file_only() {
718 let home = tempfile::tempdir().unwrap();
719 let h = home.path();
720 let r = exchange(
721 h,
722 &[
723 Request::Write {
724 path: "~/up".into(),
725 data: b"ab".to_vec(),
726 },
727 Request::Append {
728 path: "~/up".into(),
729 data: b"cd".to_vec(),
730 },
731 Request::Append {
732 path: "~/missing".into(),
733 data: b"x".to_vec(),
734 },
735 Request::Append {
736 path: "~".into(),
737 data: b"x".to_vec(),
738 },
739 ],
740 );
741 assert_eq!((&r[0], &r[1]), (&Reply::Done, &Reply::Done));
742 assert!(
743 matches!(
744 r[2],
745 Reply::Err {
746 kind: ErrKind::NotFound,
747 ..
748 }
749 ),
750 "{:?}",
751 r[2]
752 );
753 assert!(
754 matches!(
755 r[3],
756 Reply::Err {
757 kind: ErrKind::Invalid,
758 ..
759 }
760 ),
761 "{:?}",
762 r[3]
763 );
764 assert_eq!(fs::read(h.join("up")).unwrap(), b"abcd");
765 assert!(!h.join("missing").exists());
766 }
767
768 #[test]
769 fn replace_realpath_and_set_meta() {
770 let home = tempfile::tempdir().unwrap();
771 let h = home.path();
772 fs::write(h.join("new"), b"new").unwrap();
773 fs::write(h.join("old"), b"old").unwrap();
774 fs::create_dir(h.join("d")).unwrap();
775 let r = exchange(
776 h,
777 &[
778 Request::Replace {
779 from: "~/new".into(),
780 to: "~/old".into(),
781 },
782 Request::Replace {
783 from: "~/old".into(),
784 to: "~/d".into(),
785 },
786 Request::Realpath {
787 path: "~/d/../old".into(),
788 },
789 Request::SetMeta {
790 path: "~/old".into(),
791 mode: 0o751,
792 mtime_ms: 1_600_000_000_000,
793 },
794 Request::SetMeta {
795 path: "~/d".into(),
796 mode: 0o700,
797 mtime_ms: 0,
798 },
799 ],
800 );
801 assert_eq!(r[0], Reply::Done);
802 assert_eq!(fs::read(h.join("old")).unwrap(), b"new");
803 assert!(!h.join("new").exists());
804 assert!(
805 matches!(
806 r[1],
807 Reply::Err {
808 kind: ErrKind::Invalid,
809 ..
810 }
811 ),
812 "{:?}",
813 r[1]
814 );
815 match &r[2] {
816 Reply::Path(p) => assert_eq!(Path::new(p), fs::canonicalize(h.join("old")).unwrap()),
817 other => panic!("{other:?}"),
818 }
819 assert_eq!(r[3], Reply::Done);
820 let e = entry_of(&h.join("old")).unwrap();
821 assert_eq!(e.mtime_ms, 1_600_000_000_000);
822 #[cfg(unix)]
823 assert_eq!(e.mode, 0o751);
824 assert!(matches!(
825 r[4],
826 Reply::Err {
827 kind: ErrKind::Invalid,
828 ..
829 }
830 ));
831 }
832
833 #[cfg(unix)]
836 #[test]
837 fn a_read_takes_only_a_regular_file() {
838 let home = tempfile::tempdir().unwrap();
839 let h = home.path();
840 assert!(std::process::Command::new("mkfifo")
841 .arg(h.join("pipe"))
842 .status()
843 .unwrap()
844 .success());
845 fs::write(h.join("real"), b"r").unwrap();
846 std::os::unix::fs::symlink(h.join("real"), h.join("link")).unwrap();
847 let r = exchange(
848 h,
849 &[
850 Request::Read {
851 path: "~/pipe".into(),
852 offset: 0,
853 len: 10,
854 },
855 Request::Read {
856 path: "~/link".into(),
857 offset: 0,
858 len: 10,
859 },
860 Request::Sync {
861 path: "~/real".into(),
862 },
863 ],
864 );
865 assert!(
866 matches!(
867 r[0],
868 Reply::Err {
869 kind: ErrKind::Invalid,
870 ..
871 }
872 ),
873 "{:?}",
874 r[0]
875 );
876 assert_eq!(
877 r[1],
878 Reply::Read {
879 data: b"r".to_vec(),
880 eof: true
881 },
882 "a link is read through"
883 );
884 assert_eq!(r[2], Reply::Done);
885 use std::os::unix::fs::PermissionsExt;
887 fs::set_permissions(h.join("real"), fs::Permissions::from_mode(0o444)).unwrap();
888 let r = exchange(
889 h,
890 &[Request::Sync {
891 path: "~/real".into(),
892 }],
893 );
894 assert_eq!(r[0], Reply::Done);
895 }
896
897 #[test]
898 fn modes_are_permission_bits_only() {
899 let home = tempfile::tempdir().unwrap();
900 fs::write(home.path().join("f"), b"").unwrap();
901 let e = entry_of(&home.path().join("f")).unwrap();
902 assert_eq!(e.mode & !0o7777, 0, "{:o}", e.mode);
903 }
904
905 #[test]
906 fn it_refuses_what_would_destroy_more_than_asked() {
907 let home = tempfile::tempdir().unwrap();
908 let h = home.path();
909 fs::write(h.join("a"), b"a").unwrap();
910 fs::write(h.join("b"), b"b").unwrap();
911 fs::create_dir(h.join("x")).unwrap();
912 #[cfg(unix)]
913 {
914 std::os::unix::fs::symlink(h, h.join("to-home")).unwrap();
915 std::os::unix::fs::symlink(h, h.join("to-home2")).unwrap();
916 }
917 let replies = exchange(
918 h,
919 &[
920 Request::Rename {
921 from: "a".into(),
922 to: "b".into(),
923 },
924 Request::Delete {
925 path: "~".into(),
926 recursive: true,
927 },
928 Request::Delete {
929 path: "/".into(),
930 recursive: true,
931 },
932 Request::Read {
933 path: "~".into(),
934 offset: 0,
935 len: 10,
936 },
937 Request::Delete {
939 path: "~/x/..".into(),
940 recursive: true,
941 },
942 Request::Delete {
943 path: "~/..".into(),
944 recursive: true,
945 },
946 Request::Delete {
947 path: h.join("x/..").to_string_lossy().into_owned(),
948 recursive: true,
949 },
950 Request::Rename {
951 from: "missing".into(),
952 to: "b".into(),
953 },
954 ],
955 );
956 assert!(matches!(
957 replies[0],
958 Reply::Err {
959 kind: ErrKind::AlreadyExists,
960 ..
961 }
962 ));
963 for r in [
964 &replies[1],
965 &replies[2],
966 &replies[4],
967 &replies[5],
968 &replies[6],
969 ] {
970 assert!(
971 matches!(
972 r,
973 Reply::Err {
974 kind: ErrKind::Invalid,
975 ..
976 }
977 ),
978 "{r:?}"
979 );
980 }
981 assert!(matches!(replies[3], Reply::Err { .. }));
982 assert!(matches!(
983 replies[7],
984 Reply::Err {
985 kind: ErrKind::NotFound,
986 ..
987 }
988 ));
989 assert_eq!(fs::read(h.join("b")).unwrap(), b"b");
990 assert!(h.join("x").exists());
991
992 #[cfg(unix)]
994 {
995 let r = exchange(
996 h,
997 &[Request::Delete {
998 path: "~/to-home".into(),
999 recursive: true,
1000 }],
1001 );
1002 assert_eq!(r[0], Reply::Done);
1003 assert!(h.join("a").exists() && !h.join("to-home").exists());
1004 let r = exchange(
1007 h,
1008 &[Request::Delete {
1009 path: "~/to-home2/".into(),
1010 recursive: true,
1011 }],
1012 );
1013 assert_eq!(r[0], Reply::Done);
1014 assert!(h.join("a").exists() && h.join("x").exists() && !h.join("to-home2").exists());
1015 }
1016 }
1017}