distant_local/
api.rs

1use std::path::{Path, PathBuf};
2use std::time::SystemTime;
3use std::{env, io};
4
5use async_trait::async_trait;
6use distant_core::protocol::semver;
7use distant_core::protocol::{
8    ChangeKind, ChangeKindSet, DirEntry, Environment, FileType, Metadata, Permissions, ProcessId,
9    PtySize, SearchId, SearchQuery, SetPermissionsOptions, SystemInfo, Version, PROTOCOL_VERSION,
10};
11use distant_core::{DistantApi, DistantCtx};
12use ignore::{DirEntry as WalkDirEntry, WalkBuilder};
13use log::*;
14use tokio::io::AsyncWriteExt;
15use walkdir::WalkDir;
16
17use crate::config::Config;
18
19mod process;
20mod state;
21use state::*;
22
23/// Represents an implementation of [`DistantApi`] that works with the local machine
24/// where the server using this api is running. In other words, this is a direct
25/// impementation of the API instead of a proxy to another machine as seen with
26/// implementations on top of SSH and other protocol.
27pub struct Api {
28    state: GlobalState,
29}
30
31impl Api {
32    /// Initialize the api instance
33    pub fn initialize(config: Config) -> io::Result<Self> {
34        Ok(Self {
35            state: GlobalState::initialize(config)?,
36        })
37    }
38}
39
40#[async_trait]
41impl DistantApi for Api {
42    async fn read_file(&self, ctx: DistantCtx, path: PathBuf) -> io::Result<Vec<u8>> {
43        debug!(
44            "[Conn {}] Reading bytes from file {:?}",
45            ctx.connection_id, path
46        );
47
48        tokio::fs::read(path).await
49    }
50
51    async fn read_file_text(&self, ctx: DistantCtx, path: PathBuf) -> io::Result<String> {
52        debug!(
53            "[Conn {}] Reading text from file {:?}",
54            ctx.connection_id, path
55        );
56
57        tokio::fs::read_to_string(path).await
58    }
59
60    async fn write_file(&self, ctx: DistantCtx, path: PathBuf, data: Vec<u8>) -> io::Result<()> {
61        debug!(
62            "[Conn {}] Writing bytes to file {:?}",
63            ctx.connection_id, path
64        );
65
66        tokio::fs::write(path, data).await
67    }
68
69    async fn write_file_text(
70        &self,
71        ctx: DistantCtx,
72        path: PathBuf,
73        data: String,
74    ) -> io::Result<()> {
75        debug!(
76            "[Conn {}] Writing text to file {:?}",
77            ctx.connection_id, path
78        );
79
80        tokio::fs::write(path, data).await
81    }
82
83    async fn append_file(&self, ctx: DistantCtx, path: PathBuf, data: Vec<u8>) -> io::Result<()> {
84        debug!(
85            "[Conn {}] Appending bytes to file {:?}",
86            ctx.connection_id, path
87        );
88
89        let mut file = tokio::fs::OpenOptions::new()
90            .create(true)
91            .append(true)
92            .open(path)
93            .await?;
94        file.write_all(data.as_ref()).await
95    }
96
97    async fn append_file_text(
98        &self,
99        ctx: DistantCtx,
100        path: PathBuf,
101        data: String,
102    ) -> io::Result<()> {
103        debug!(
104            "[Conn {}] Appending text to file {:?}",
105            ctx.connection_id, path
106        );
107
108        let mut file = tokio::fs::OpenOptions::new()
109            .create(true)
110            .append(true)
111            .open(path)
112            .await?;
113        file.write_all(data.as_ref()).await
114    }
115
116    async fn read_dir(
117        &self,
118        ctx: DistantCtx,
119        path: PathBuf,
120        depth: usize,
121        absolute: bool,
122        canonicalize: bool,
123        include_root: bool,
124    ) -> io::Result<(Vec<DirEntry>, Vec<io::Error>)> {
125        debug!(
126            "[Conn {}] Reading directory {:?} {{depth: {}, absolute: {}, canonicalize: {}, include_root: {}}}",
127            ctx.connection_id, path, depth, absolute, canonicalize, include_root
128        );
129
130        // Canonicalize our provided path to ensure that it is exists, not a loop, and absolute
131        let root_path = tokio::fs::canonicalize(path).await?;
132
133        // Traverse, but don't include root directory in entries (hence min depth 1), unless indicated
134        // to do so (min depth 0)
135        let dir = WalkDir::new(root_path.as_path())
136            .min_depth(usize::from(!include_root))
137            .sort_by_file_name();
138
139        // If depth > 0, will recursively traverse to specified max depth, otherwise
140        // performs infinite traversal
141        let dir = if depth > 0 { dir.max_depth(depth) } else { dir };
142
143        // Determine our entries and errors
144        let mut entries = Vec::new();
145        let mut errors = Vec::new();
146
147        #[inline]
148        fn map_file_type(ft: std::fs::FileType) -> FileType {
149            if ft.is_dir() {
150                FileType::Dir
151            } else if ft.is_file() {
152                FileType::File
153            } else {
154                FileType::Symlink
155            }
156        }
157
158        for entry in dir {
159            match entry.map_err(io::Error::from) {
160                // For entries within the root, we want to transform the path based on flags
161                Ok(e) if e.depth() > 0 => {
162                    // Canonicalize the path if specified, otherwise just return
163                    // the path as is
164                    let mut path = if canonicalize {
165                        match tokio::fs::canonicalize(e.path()).await {
166                            Ok(path) => path,
167                            Err(x) => {
168                                errors.push(x);
169                                continue;
170                            }
171                        }
172                    } else {
173                        e.path().to_path_buf()
174                    };
175
176                    // Strip the path of its prefix based if not flagged as absolute
177                    if !absolute {
178                        // NOTE: In the situation where we canonicalized the path earlier,
179                        //       there is no guarantee that our root path is still the
180                        //       parent of the symlink's destination; so, in that case we MUST just
181                        //       return the path if the strip_prefix fails
182                        path = path
183                            .strip_prefix(root_path.as_path())
184                            .map(Path::to_path_buf)
185                            .unwrap_or(path);
186                    };
187
188                    entries.push(DirEntry {
189                        path,
190                        file_type: map_file_type(e.file_type()),
191                        depth: e.depth(),
192                    });
193                }
194
195                // For the root, we just want to echo back the entry as is
196                Ok(e) => {
197                    entries.push(DirEntry {
198                        path: e.path().to_path_buf(),
199                        file_type: map_file_type(e.file_type()),
200                        depth: e.depth(),
201                    });
202                }
203
204                Err(x) => errors.push(x),
205            }
206        }
207
208        Ok((entries, errors))
209    }
210
211    async fn create_dir(&self, ctx: DistantCtx, path: PathBuf, all: bool) -> io::Result<()> {
212        debug!(
213            "[Conn {}] Creating directory {:?} {{all: {}}}",
214            ctx.connection_id, path, all
215        );
216        if all {
217            tokio::fs::create_dir_all(path).await
218        } else {
219            tokio::fs::create_dir(path).await
220        }
221    }
222
223    async fn remove(&self, ctx: DistantCtx, path: PathBuf, force: bool) -> io::Result<()> {
224        debug!(
225            "[Conn {}] Removing {:?} {{force: {}}}",
226            ctx.connection_id, path, force
227        );
228        let path_metadata = tokio::fs::metadata(path.as_path()).await?;
229        if path_metadata.is_dir() {
230            if force {
231                tokio::fs::remove_dir_all(path).await
232            } else {
233                tokio::fs::remove_dir(path).await
234            }
235        } else {
236            tokio::fs::remove_file(path).await
237        }
238    }
239
240    async fn copy(&self, ctx: DistantCtx, src: PathBuf, dst: PathBuf) -> io::Result<()> {
241        debug!(
242            "[Conn {}] Copying {:?} to {:?}",
243            ctx.connection_id, src, dst
244        );
245        let src_metadata = tokio::fs::metadata(src.as_path()).await?;
246        if src_metadata.is_dir() {
247            // Create the destination directory first, regardless of if anything
248            // is in the source directory
249            tokio::fs::create_dir_all(dst.as_path()).await?;
250
251            for entry in WalkDir::new(src.as_path())
252                .min_depth(1)
253                .follow_links(false)
254                .into_iter()
255                .filter_entry(|e| {
256                    e.file_type().is_file() || e.file_type().is_dir() || e.path_is_symlink()
257                })
258            {
259                let entry = entry?;
260
261                // Get unique portion of path relative to src
262                // NOTE: Because we are traversing files that are all within src, this
263                //       should always succeed
264                let local_src = entry.path().strip_prefix(src.as_path()).unwrap();
265
266                // Get the file without any directories
267                let local_src_file_name = local_src.file_name().unwrap();
268
269                // Get the directory housing the file
270                // NOTE: Because we enforce files/symlinks, there will always be a parent
271                let local_src_dir = local_src.parent().unwrap();
272
273                // Map out the path to the destination
274                let dst_parent_dir = dst.join(local_src_dir);
275
276                // Create the destination directory for the file when copying
277                tokio::fs::create_dir_all(dst_parent_dir.as_path()).await?;
278
279                let dst_path = dst_parent_dir.join(local_src_file_name);
280
281                // Perform copying from entry to destination (if a file/symlink)
282                if !entry.file_type().is_dir() {
283                    tokio::fs::copy(entry.path(), dst_path).await?;
284
285                // Otherwise, if a directory, create it
286                } else {
287                    tokio::fs::create_dir(dst_path).await?;
288                }
289            }
290        } else {
291            tokio::fs::copy(src, dst).await?;
292        }
293
294        Ok(())
295    }
296
297    async fn rename(&self, ctx: DistantCtx, src: PathBuf, dst: PathBuf) -> io::Result<()> {
298        debug!(
299            "[Conn {}] Renaming {:?} to {:?}",
300            ctx.connection_id, src, dst
301        );
302        tokio::fs::rename(src, dst).await
303    }
304
305    async fn watch(
306        &self,
307        ctx: DistantCtx,
308        path: PathBuf,
309        recursive: bool,
310        only: Vec<ChangeKind>,
311        except: Vec<ChangeKind>,
312    ) -> io::Result<()> {
313        let only = only.into_iter().collect::<ChangeKindSet>();
314        let except = except.into_iter().collect::<ChangeKindSet>();
315        debug!(
316            "[Conn {}] Watching {:?} {{recursive: {}, only: {}, except: {}}}",
317            ctx.connection_id, path, recursive, only, except
318        );
319
320        let path = RegisteredPath::register(
321            ctx.connection_id,
322            path.as_path(),
323            recursive,
324            only,
325            except,
326            ctx.reply,
327        )
328        .await?;
329
330        self.state.watcher.watch(path).await?;
331
332        Ok(())
333    }
334
335    async fn unwatch(&self, ctx: DistantCtx, path: PathBuf) -> io::Result<()> {
336        debug!("[Conn {}] Unwatching {:?}", ctx.connection_id, path);
337
338        self.state
339            .watcher
340            .unwatch(ctx.connection_id, path.as_path())
341            .await?;
342        Ok(())
343    }
344
345    async fn exists(&self, ctx: DistantCtx, path: PathBuf) -> io::Result<bool> {
346        debug!("[Conn {}] Checking if {:?} exists", ctx.connection_id, path);
347
348        // Following experimental `std::fs::try_exists`, which checks the error kind of the
349        // metadata lookup to see if it is not found and filters accordingly
350        match tokio::fs::metadata(path.as_path()).await {
351            Ok(_) => Ok(true),
352            Err(x) if x.kind() == io::ErrorKind::NotFound => Ok(false),
353            Err(x) => return Err(x),
354        }
355    }
356
357    async fn metadata(
358        &self,
359        ctx: DistantCtx,
360        path: PathBuf,
361        canonicalize: bool,
362        resolve_file_type: bool,
363    ) -> io::Result<Metadata> {
364        debug!(
365            "[Conn {}] Reading metadata for {:?} {{canonicalize: {}, resolve_file_type: {}}}",
366            ctx.connection_id, path, canonicalize, resolve_file_type
367        );
368        let metadata = tokio::fs::symlink_metadata(path.as_path()).await?;
369        let canonicalized_path = if canonicalize {
370            Some(tokio::fs::canonicalize(path.as_path()).await?)
371        } else {
372            None
373        };
374
375        // If asking for resolved file type and current type is symlink, then we want to refresh
376        // our metadata to get the filetype for the resolved link
377        let file_type = if resolve_file_type && metadata.file_type().is_symlink() {
378            tokio::fs::metadata(path).await?.file_type()
379        } else {
380            metadata.file_type()
381        };
382
383        Ok(Metadata {
384            canonicalized_path,
385            accessed: metadata
386                .accessed()
387                .ok()
388                .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
389                .map(|d| d.as_secs()),
390            created: metadata
391                .created()
392                .ok()
393                .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
394                .map(|d| d.as_secs()),
395            modified: metadata
396                .modified()
397                .ok()
398                .and_then(|t| t.duration_since(SystemTime::UNIX_EPOCH).ok())
399                .map(|d| d.as_secs()),
400            len: metadata.len(),
401            readonly: metadata.permissions().readonly(),
402            file_type: if file_type.is_dir() {
403                FileType::Dir
404            } else if file_type.is_file() {
405                FileType::File
406            } else {
407                FileType::Symlink
408            },
409
410            #[cfg(unix)]
411            unix: Some({
412                use std::os::unix::prelude::*;
413                let mode = metadata.mode();
414                distant_core::protocol::UnixMetadata::from(mode)
415            }),
416            #[cfg(not(unix))]
417            unix: None,
418
419            #[cfg(windows)]
420            windows: Some({
421                use std::os::windows::prelude::*;
422                let attributes = metadata.file_attributes();
423                distant_core::protocol::WindowsMetadata::from(attributes)
424            }),
425            #[cfg(not(windows))]
426            windows: None,
427        })
428    }
429
430    async fn set_permissions(
431        &self,
432        _ctx: DistantCtx,
433        path: PathBuf,
434        permissions: Permissions,
435        options: SetPermissionsOptions,
436    ) -> io::Result<()> {
437        /// Builds permissions from the metadata of `entry`, failing if metadata was unavailable.
438        fn build_permissions(
439            entry: &WalkDirEntry,
440            permissions: &Permissions,
441        ) -> io::Result<std::fs::Permissions> {
442            // Load up our std permissions so we can modify them
443            let mut std_permissions = entry
444                .metadata()
445                .map_err(|x| match x.io_error() {
446                    Some(x) => io::Error::new(x.kind(), format!("(Read permissions failed) {x}")),
447                    None => io::Error::new(
448                        io::ErrorKind::Other,
449                        format!("(Read permissions failed) {x}"),
450                    ),
451                })?
452                .permissions();
453
454            // Apply the readonly flag for all platforms
455            if let Some(readonly) = permissions.is_readonly() {
456                std_permissions.set_readonly(readonly);
457            }
458
459            // On Unix platforms, we can apply a bitset change
460            #[cfg(unix)]
461            {
462                use std::os::unix::prelude::*;
463                let mut current = Permissions::from(std_permissions.clone());
464                current.apply_from(permissions);
465                std_permissions.set_mode(current.to_unix_mode());
466            }
467
468            Ok(std_permissions)
469        }
470
471        async fn set_permissions_impl(
472            entry: &WalkDirEntry,
473            permissions: &Permissions,
474        ) -> io::Result<()> {
475            let permissions = match permissions.is_complete() {
476                // If we are on a Unix platform and we have a full permission set, we do not need
477                // to retrieve the permissions to modify them and can instead produce a new
478                // permission set purely from the permissions
479                #[cfg(unix)]
480                true => std::fs::Permissions::from(*permissions),
481
482                // Otherwise, we have to load in the permissions from metadata and merge with our
483                // changes
484                _ => build_permissions(entry, permissions)?,
485            };
486
487            if log_enabled!(Level::Trace) {
488                let mut output = String::new();
489                output.push_str("readonly = ");
490                output.push_str(if permissions.readonly() {
491                    "true"
492                } else {
493                    "false"
494                });
495
496                #[cfg(unix)]
497                {
498                    use std::os::unix::prelude::*;
499                    output.push_str(&format!(", mode = {:#o}", permissions.mode()));
500                }
501
502                trace!("Setting {:?} permissions to ({})", entry.path(), output);
503            }
504
505            tokio::fs::set_permissions(entry.path(), permissions)
506                .await
507                .map_err(|x| io::Error::new(x.kind(), format!("(Set permissions failed) {x}")))
508        }
509
510        // NOTE: On Unix platforms, setting permissions would automatically resolve the symlink,
511        // but on Windows this is not the case. So, on Windows, we need to resolve our path by
512        // following the symlink prior to feeding it to the walk builder because it does not appear
513        // to resolve the symlink itself.
514        //
515        // We do this by canonicalizing the path if following symlinks is enabled.
516        let path = if options.follow_symlinks {
517            tokio::fs::canonicalize(path).await?
518        } else {
519            path
520        };
521
522        let walk = WalkBuilder::new(path)
523            .follow_links(options.follow_symlinks)
524            .max_depth(if options.recursive { None } else { Some(0) })
525            .standard_filters(false)
526            .skip_stdout(true)
527            .build();
528
529        // Process as much as possible and then fail with an error
530        let mut errors = Vec::new();
531        for entry in walk {
532            match entry {
533                Ok(entry) if entry.path_is_symlink() && options.exclude_symlinks => {}
534                Ok(entry) => {
535                    if let Err(x) = set_permissions_impl(&entry, &permissions).await {
536                        errors.push(format!("{:?}: {x}", entry.path()));
537                    }
538                }
539                Err(x) => {
540                    errors.push(x.to_string());
541                }
542            }
543        }
544
545        if errors.is_empty() {
546            Ok(())
547        } else {
548            Err(io::Error::new(
549                io::ErrorKind::PermissionDenied,
550                errors
551                    .into_iter()
552                    .map(|x| format!("* {x}"))
553                    .collect::<Vec<_>>()
554                    .join("\n"),
555            ))
556        }
557    }
558
559    async fn search(&self, ctx: DistantCtx, query: SearchQuery) -> io::Result<SearchId> {
560        debug!(
561            "[Conn {}] Performing search via {query:?}",
562            ctx.connection_id,
563        );
564
565        self.state.search.start(query, ctx.reply).await
566    }
567
568    async fn cancel_search(&self, ctx: DistantCtx, id: SearchId) -> io::Result<()> {
569        debug!("[Conn {}] Cancelling search {id}", ctx.connection_id,);
570
571        self.state.search.cancel(id).await
572    }
573
574    async fn proc_spawn(
575        &self,
576        ctx: DistantCtx,
577        cmd: String,
578        environment: Environment,
579        current_dir: Option<PathBuf>,
580        pty: Option<PtySize>,
581    ) -> io::Result<ProcessId> {
582        debug!(
583            "[Conn {}] Spawning {} {{environment: {:?}, current_dir: {:?}, pty: {:?}}}",
584            ctx.connection_id, cmd, environment, current_dir, pty
585        );
586        self.state
587            .process
588            .spawn(cmd, environment, current_dir, pty, ctx.reply)
589            .await
590    }
591
592    async fn proc_kill(&self, ctx: DistantCtx, id: ProcessId) -> io::Result<()> {
593        debug!("[Conn {}] Killing process {}", ctx.connection_id, id);
594        self.state.process.kill(id).await
595    }
596
597    async fn proc_stdin(&self, ctx: DistantCtx, id: ProcessId, data: Vec<u8>) -> io::Result<()> {
598        debug!(
599            "[Conn {}] Sending stdin to process {}",
600            ctx.connection_id, id
601        );
602        self.state.process.send_stdin(id, data).await
603    }
604
605    async fn proc_resize_pty(
606        &self,
607        ctx: DistantCtx,
608        id: ProcessId,
609        size: PtySize,
610    ) -> io::Result<()> {
611        debug!(
612            "[Conn {}] Resizing pty of process {} to {}",
613            ctx.connection_id, id, size
614        );
615        self.state.process.resize_pty(id, size).await
616    }
617
618    async fn system_info(&self, ctx: DistantCtx) -> io::Result<SystemInfo> {
619        debug!("[Conn {}] Reading system information", ctx.connection_id);
620        Ok(SystemInfo {
621            family: env::consts::FAMILY.to_string(),
622            os: env::consts::OS.to_string(),
623            arch: env::consts::ARCH.to_string(),
624            current_dir: env::current_dir().unwrap_or_default(),
625            main_separator: std::path::MAIN_SEPARATOR,
626            username: whoami::username(),
627            shell: if cfg!(windows) {
628                env::var("ComSpec").unwrap_or_else(|_| String::from("cmd.exe"))
629            } else {
630                env::var("SHELL").unwrap_or_else(|_| String::from("/bin/sh"))
631            },
632        })
633    }
634
635    async fn version(&self, ctx: DistantCtx) -> io::Result<Version> {
636        debug!("[Conn {}] Querying version", ctx.connection_id);
637
638        // Parse our server's version
639        let mut server_version: semver::Version = env!("CARGO_PKG_VERSION")
640            .parse()
641            .map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
642
643        // Add the package name to the version information
644        if server_version.build.is_empty() {
645            server_version.build = semver::BuildMetadata::new(env!("CARGO_PKG_NAME"))
646                .map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
647        } else {
648            let raw_build_str = format!(
649                "{}.{}",
650                server_version.build.as_str(),
651                env!("CARGO_PKG_NAME")
652            );
653            server_version.build = semver::BuildMetadata::new(&raw_build_str)
654                .map_err(|x| io::Error::new(io::ErrorKind::Other, x))?;
655        }
656
657        Ok(Version {
658            server_version,
659            protocol_version: PROTOCOL_VERSION,
660            capabilities: Version::capabilities()
661                .iter()
662                .map(ToString::to_string)
663                .collect(),
664        })
665    }
666}
667
668#[cfg(test)]
669mod tests {
670    use std::time::Duration;
671
672    use assert_fs::prelude::*;
673    use distant_core::net::server::Reply;
674    use distant_core::protocol::Response;
675    use once_cell::sync::Lazy;
676    use predicates::prelude::*;
677    use test_log::test;
678    use tokio::sync::mpsc;
679
680    use super::*;
681    use crate::config::WatchConfig;
682
683    static TEMP_SCRIPT_DIR: Lazy<assert_fs::TempDir> =
684        Lazy::new(|| assert_fs::TempDir::new().unwrap());
685    static SCRIPT_RUNNER: Lazy<String> = Lazy::new(|| String::from("bash"));
686
687    static ECHO_ARGS_TO_STDOUT_SH: Lazy<assert_fs::fixture::ChildPath> = Lazy::new(|| {
688        let script = TEMP_SCRIPT_DIR.child("echo_args_to_stdout.sh");
689        script
690            .write_str(indoc::indoc!(
691                r#"
692                #/usr/bin/env bash
693                printf "%s" "$*"
694            "#
695            ))
696            .unwrap();
697        script
698    });
699
700    static ECHO_ARGS_TO_STDERR_SH: Lazy<assert_fs::fixture::ChildPath> = Lazy::new(|| {
701        let script = TEMP_SCRIPT_DIR.child("echo_args_to_stderr.sh");
702        script
703            .write_str(indoc::indoc!(
704                r#"
705                #/usr/bin/env bash
706                printf "%s" "$*" 1>&2
707            "#
708            ))
709            .unwrap();
710        script
711    });
712
713    static ECHO_STDIN_TO_STDOUT_SH: Lazy<assert_fs::fixture::ChildPath> = Lazy::new(|| {
714        let script = TEMP_SCRIPT_DIR.child("echo_stdin_to_stdout.sh");
715        script
716            .write_str(indoc::indoc!(
717                r#"
718                #/usr/bin/env bash
719                while IFS= read; do echo "$REPLY"; done
720            "#
721            ))
722            .unwrap();
723        script
724    });
725
726    static SLEEP_SH: Lazy<assert_fs::fixture::ChildPath> = Lazy::new(|| {
727        let script = TEMP_SCRIPT_DIR.child("sleep.sh");
728        script
729            .write_str(indoc::indoc!(
730                r#"
731                #!/usr/bin/env bash
732                sleep "$1"
733            "#
734            ))
735            .unwrap();
736        script
737    });
738
739    static DOES_NOT_EXIST_BIN: Lazy<assert_fs::fixture::ChildPath> =
740        Lazy::new(|| TEMP_SCRIPT_DIR.child("does_not_exist_bin"));
741
742    const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100);
743
744    async fn setup() -> (Api, DistantCtx, mpsc::UnboundedReceiver<Response>) {
745        let api = Api::initialize(Config {
746            watch: WatchConfig {
747                debounce_timeout: DEBOUNCE_TIMEOUT,
748                ..Default::default()
749            },
750        })
751        .unwrap();
752        let (reply, rx) = make_reply();
753        let connection_id = rand::random();
754
755        DistantApi::on_connect(&api, connection_id).await.unwrap();
756        let ctx = DistantCtx {
757            connection_id,
758            reply,
759        };
760        (api, ctx, rx)
761    }
762
763    fn make_reply() -> (
764        Box<dyn Reply<Data = Response>>,
765        mpsc::UnboundedReceiver<Response>,
766    ) {
767        let (tx, rx) = mpsc::unbounded_channel();
768        (Box::new(tx), rx)
769    }
770
771    #[test(tokio::test)]
772    async fn read_file_should_fail_if_file_missing() {
773        let (api, ctx, _rx) = setup().await;
774        let temp = assert_fs::TempDir::new().unwrap();
775        let path = temp.child("missing-file").path().to_path_buf();
776
777        let _ = api.read_file(ctx, path).await.unwrap_err();
778    }
779
780    #[test(tokio::test)]
781    async fn read_file_should_send_blob_with_file_contents() {
782        let (api, ctx, _rx) = setup().await;
783
784        let temp = assert_fs::TempDir::new().unwrap();
785        let file = temp.child("test-file");
786        file.write_str("some file contents").unwrap();
787
788        let bytes = api.read_file(ctx, file.path().to_path_buf()).await.unwrap();
789        assert_eq!(bytes, b"some file contents");
790    }
791
792    #[test(tokio::test)]
793    async fn read_file_text_should_send_error_if_fails_to_read_file() {
794        let (api, ctx, _rx) = setup().await;
795
796        let temp = assert_fs::TempDir::new().unwrap();
797        let path = temp.child("missing-file").path().to_path_buf();
798
799        let _ = api.read_file_text(ctx, path).await.unwrap_err();
800    }
801
802    #[test(tokio::test)]
803    async fn read_file_text_should_send_text_with_file_contents() {
804        let (api, ctx, _rx) = setup().await;
805
806        let temp = assert_fs::TempDir::new().unwrap();
807        let file = temp.child("test-file");
808        file.write_str("some file contents").unwrap();
809
810        let text = api
811            .read_file_text(ctx, file.path().to_path_buf())
812            .await
813            .unwrap();
814        assert_eq!(text, "some file contents");
815    }
816
817    #[test(tokio::test)]
818    async fn write_file_should_send_error_if_fails_to_write_file() {
819        let (api, ctx, _rx) = setup().await;
820
821        // Create a temporary path and add to it to ensure that there are
822        // extra components that don't exist to cause writing to fail
823        let temp = assert_fs::TempDir::new().unwrap();
824        let file = temp.child("dir").child("test-file");
825
826        let _ = api
827            .write_file(ctx, file.path().to_path_buf(), b"some text".to_vec())
828            .await
829            .unwrap_err();
830
831        // Also verify that we didn't actually create the file
832        file.assert(predicate::path::missing());
833    }
834
835    #[test(tokio::test)]
836    async fn write_file_should_send_ok_when_successful() {
837        let (api, ctx, _rx) = setup().await;
838
839        // Path should point to a file that does not exist, but all
840        // other components leading up to it do
841        let temp = assert_fs::TempDir::new().unwrap();
842        let file = temp.child("test-file");
843
844        api.write_file(ctx, file.path().to_path_buf(), b"some text".to_vec())
845            .await
846            .unwrap();
847
848        // Also verify that we actually did create the file
849        // with the associated contents
850        file.assert("some text");
851    }
852
853    #[test(tokio::test)]
854    async fn write_file_text_should_send_error_if_fails_to_write_file() {
855        let (api, ctx, _rx) = setup().await;
856
857        // Create a temporary path and add to it to ensure that there are
858        // extra components that don't exist to cause writing to fail
859        let temp = assert_fs::TempDir::new().unwrap();
860        let file = temp.child("dir").child("test-file");
861
862        api.write_file_text(ctx, file.path().to_path_buf(), "some text".to_string())
863            .await
864            .unwrap_err();
865
866        // Also verify that we didn't actually create the file
867        file.assert(predicate::path::missing());
868    }
869
870    #[test(tokio::test)]
871    async fn write_file_text_should_send_ok_when_successful() {
872        let (api, ctx, _rx) = setup().await;
873
874        // Path should point to a file that does not exist, but all
875        // other components leading up to it do
876        let temp = assert_fs::TempDir::new().unwrap();
877        let file = temp.child("test-file");
878
879        api.write_file_text(ctx, file.path().to_path_buf(), "some text".to_string())
880            .await
881            .unwrap();
882
883        // Also verify that we actually did create the file
884        // with the associated contents
885        file.assert("some text");
886    }
887
888    #[test(tokio::test)]
889    async fn append_file_should_send_error_if_fails_to_create_file() {
890        let (api, ctx, _rx) = setup().await;
891
892        // Create a temporary path and add to it to ensure that there are
893        // extra components that don't exist to cause writing to fail
894        let temp = assert_fs::TempDir::new().unwrap();
895        let file = temp.child("dir").child("test-file");
896
897        api.append_file(
898            ctx,
899            file.path().to_path_buf(),
900            b"some extra contents".to_vec(),
901        )
902        .await
903        .unwrap_err();
904
905        // Also verify that we didn't actually create the file
906        file.assert(predicate::path::missing());
907    }
908
909    #[test(tokio::test)]
910    async fn append_file_should_create_file_if_missing() {
911        let (api, ctx, _rx) = setup().await;
912
913        // Don't create the file directly, but define path
914        // where the file should be
915        let temp = assert_fs::TempDir::new().unwrap();
916        let file = temp.child("test-file");
917
918        api.append_file(
919            ctx,
920            file.path().to_path_buf(),
921            b"some extra contents".to_vec(),
922        )
923        .await
924        .unwrap();
925
926        // Yield to allow chance to finish appending to file
927        tokio::time::sleep(Duration::from_millis(50)).await;
928
929        // Also verify that we actually did create to the file
930        file.assert("some extra contents");
931    }
932
933    #[test(tokio::test)]
934    async fn append_file_should_send_ok_when_successful() {
935        let (api, ctx, _rx) = setup().await;
936
937        // Create a temporary file and fill it with some contents
938        let temp = assert_fs::TempDir::new().unwrap();
939        let file = temp.child("test-file");
940        file.write_str("some file contents").unwrap();
941
942        api.append_file(
943            ctx,
944            file.path().to_path_buf(),
945            b"some extra contents".to_vec(),
946        )
947        .await
948        .unwrap();
949
950        // Yield to allow chance to finish appending to file
951        tokio::time::sleep(Duration::from_millis(50)).await;
952
953        // Also verify that we actually did append to the file
954        file.assert("some file contentssome extra contents");
955    }
956
957    #[test(tokio::test)]
958    async fn append_file_text_should_send_error_if_fails_to_create_file() {
959        let (api, ctx, _rx) = setup().await;
960
961        // Create a temporary path and add to it to ensure that there are
962        // extra components that don't exist to cause writing to fail
963        let temp = assert_fs::TempDir::new().unwrap();
964        let file = temp.child("dir").child("test-file");
965
966        let _ = api
967            .append_file_text(
968                ctx,
969                file.path().to_path_buf(),
970                "some extra contents".to_string(),
971            )
972            .await
973            .unwrap_err();
974
975        // Also verify that we didn't actually create the file
976        file.assert(predicate::path::missing());
977    }
978
979    #[test(tokio::test)]
980    async fn append_file_text_should_create_file_if_missing() {
981        let (api, ctx, _rx) = setup().await;
982
983        // Don't create the file directly, but define path
984        // where the file should be
985        let temp = assert_fs::TempDir::new().unwrap();
986        let file = temp.child("test-file");
987
988        api.append_file_text(
989            ctx,
990            file.path().to_path_buf(),
991            "some extra contents".to_string(),
992        )
993        .await
994        .unwrap();
995
996        // Yield to allow chance to finish appending to file
997        tokio::time::sleep(Duration::from_millis(50)).await;
998
999        // Also verify that we actually did create to the file
1000        file.assert("some extra contents");
1001    }
1002
1003    #[test(tokio::test)]
1004    async fn append_file_text_should_send_ok_when_successful() {
1005        let (api, ctx, _rx) = setup().await;
1006
1007        // Create a temporary file and fill it with some contents
1008        let temp = assert_fs::TempDir::new().unwrap();
1009        let file = temp.child("test-file");
1010        file.write_str("some file contents").unwrap();
1011
1012        api.append_file_text(
1013            ctx,
1014            file.path().to_path_buf(),
1015            "some extra contents".to_string(),
1016        )
1017        .await
1018        .unwrap();
1019
1020        // Yield to allow chance to finish appending to file
1021        tokio::time::sleep(Duration::from_millis(50)).await;
1022
1023        // Also verify that we actually did append to the file
1024        file.assert("some file contentssome extra contents");
1025    }
1026
1027    #[test(tokio::test)]
1028    async fn dir_read_should_send_error_if_directory_does_not_exist() {
1029        let (api, ctx, _rx) = setup().await;
1030
1031        let temp = assert_fs::TempDir::new().unwrap();
1032        let dir = temp.child("test-dir");
1033
1034        let _ = api
1035            .read_dir(
1036                ctx,
1037                dir.path().to_path_buf(),
1038                /* depth */ 0,
1039                /* absolute */ false,
1040                /* canonicalize */ false,
1041                /* include_root */ false,
1042            )
1043            .await
1044            .unwrap_err();
1045    }
1046
1047    // /root/
1048    // /root/file1
1049    // /root/link1 -> /root/sub1/file2
1050    // /root/sub1/
1051    // /root/sub1/file2
1052    async fn setup_dir() -> assert_fs::TempDir {
1053        let root_dir = assert_fs::TempDir::new().unwrap();
1054        root_dir.child("file1").touch().unwrap();
1055
1056        let sub1 = root_dir.child("sub1");
1057        sub1.create_dir_all().unwrap();
1058
1059        let file2 = sub1.child("file2");
1060        file2.touch().unwrap();
1061
1062        let link1 = root_dir.child("link1");
1063        link1.symlink_to_file(file2.path()).unwrap();
1064
1065        root_dir
1066    }
1067
1068    #[test(tokio::test)]
1069    async fn dir_read_should_support_depth_limits() {
1070        let (api, ctx, _rx) = setup().await;
1071
1072        // Create directory with some nested items
1073        let root_dir = setup_dir().await;
1074
1075        let (entries, _) = api
1076            .read_dir(
1077                ctx,
1078                root_dir.path().to_path_buf(),
1079                /* depth */ 1,
1080                /* absolute */ false,
1081                /* canonicalize */ false,
1082                /* include_root */ false,
1083            )
1084            .await
1085            .unwrap();
1086
1087        assert_eq!(entries.len(), 3, "Wrong number of entries found");
1088
1089        assert_eq!(entries[0].file_type, FileType::File);
1090        assert_eq!(entries[0].path, Path::new("file1"));
1091        assert_eq!(entries[0].depth, 1);
1092
1093        assert_eq!(entries[1].file_type, FileType::Symlink);
1094        assert_eq!(entries[1].path, Path::new("link1"));
1095        assert_eq!(entries[1].depth, 1);
1096
1097        assert_eq!(entries[2].file_type, FileType::Dir);
1098        assert_eq!(entries[2].path, Path::new("sub1"));
1099        assert_eq!(entries[2].depth, 1);
1100    }
1101
1102    #[test(tokio::test)]
1103    async fn dir_read_should_support_unlimited_depth_using_zero() {
1104        let (api, ctx, _rx) = setup().await;
1105
1106        // Create directory with some nested items
1107        let root_dir = setup_dir().await;
1108
1109        let (entries, _) = api
1110            .read_dir(
1111                ctx,
1112                root_dir.path().to_path_buf(),
1113                /* depth */ 0,
1114                /* absolute */ false,
1115                /* canonicalize */ false,
1116                /* include_root */ false,
1117            )
1118            .await
1119            .unwrap();
1120
1121        assert_eq!(entries.len(), 4, "Wrong number of entries found");
1122
1123        assert_eq!(entries[0].file_type, FileType::File);
1124        assert_eq!(entries[0].path, Path::new("file1"));
1125        assert_eq!(entries[0].depth, 1);
1126
1127        assert_eq!(entries[1].file_type, FileType::Symlink);
1128        assert_eq!(entries[1].path, Path::new("link1"));
1129        assert_eq!(entries[1].depth, 1);
1130
1131        assert_eq!(entries[2].file_type, FileType::Dir);
1132        assert_eq!(entries[2].path, Path::new("sub1"));
1133        assert_eq!(entries[2].depth, 1);
1134
1135        assert_eq!(entries[3].file_type, FileType::File);
1136        assert_eq!(entries[3].path, Path::new("sub1").join("file2"));
1137        assert_eq!(entries[3].depth, 2);
1138    }
1139
1140    #[test(tokio::test)]
1141    async fn dir_read_should_support_including_directory_in_returned_entries() {
1142        let (api, ctx, _rx) = setup().await;
1143
1144        // Create directory with some nested items
1145        let root_dir = setup_dir().await;
1146
1147        let (entries, _) = api
1148            .read_dir(
1149                ctx,
1150                root_dir.path().to_path_buf(),
1151                /* depth */ 1,
1152                /* absolute */ false,
1153                /* canonicalize */ false,
1154                /* include_root */ true,
1155            )
1156            .await
1157            .unwrap();
1158
1159        assert_eq!(entries.len(), 4, "Wrong number of entries found");
1160
1161        // NOTE: Root entry is always absolute, resolved path
1162        assert_eq!(entries[0].file_type, FileType::Dir);
1163        assert_eq!(entries[0].path, root_dir.path().canonicalize().unwrap());
1164        assert_eq!(entries[0].depth, 0);
1165
1166        assert_eq!(entries[1].file_type, FileType::File);
1167        assert_eq!(entries[1].path, Path::new("file1"));
1168        assert_eq!(entries[1].depth, 1);
1169
1170        assert_eq!(entries[2].file_type, FileType::Symlink);
1171        assert_eq!(entries[2].path, Path::new("link1"));
1172        assert_eq!(entries[2].depth, 1);
1173
1174        assert_eq!(entries[3].file_type, FileType::Dir);
1175        assert_eq!(entries[3].path, Path::new("sub1"));
1176        assert_eq!(entries[3].depth, 1);
1177    }
1178
1179    #[test(tokio::test)]
1180    async fn dir_read_should_support_returning_absolute_paths() {
1181        let (api, ctx, _rx) = setup().await;
1182
1183        // Create directory with some nested items
1184        let root_dir = setup_dir().await;
1185
1186        let (entries, _) = api
1187            .read_dir(
1188                ctx,
1189                root_dir.path().to_path_buf(),
1190                /* depth */ 1,
1191                /* absolute */ true,
1192                /* canonicalize */ false,
1193                /* include_root */ false,
1194            )
1195            .await
1196            .unwrap();
1197
1198        assert_eq!(entries.len(), 3, "Wrong number of entries found");
1199        let root_path = root_dir.path().canonicalize().unwrap();
1200
1201        assert_eq!(entries[0].file_type, FileType::File);
1202        assert_eq!(entries[0].path, root_path.join("file1"));
1203        assert_eq!(entries[0].depth, 1);
1204
1205        assert_eq!(entries[1].file_type, FileType::Symlink);
1206        assert_eq!(entries[1].path, root_path.join("link1"));
1207        assert_eq!(entries[1].depth, 1);
1208
1209        assert_eq!(entries[2].file_type, FileType::Dir);
1210        assert_eq!(entries[2].path, root_path.join("sub1"));
1211        assert_eq!(entries[2].depth, 1);
1212    }
1213
1214    #[test(tokio::test)]
1215    async fn dir_read_should_support_returning_canonicalized_paths() {
1216        let (api, ctx, _rx) = setup().await;
1217
1218        // Create directory with some nested items
1219        let root_dir = setup_dir().await;
1220
1221        let (entries, _) = api
1222            .read_dir(
1223                ctx,
1224                root_dir.path().to_path_buf(),
1225                /* depth */ 1,
1226                /* absolute */ false,
1227                /* canonicalize */ true,
1228                /* include_root */ false,
1229            )
1230            .await
1231            .unwrap();
1232
1233        assert_eq!(entries.len(), 3, "Wrong number of entries found");
1234
1235        assert_eq!(entries[0].file_type, FileType::File);
1236        assert_eq!(entries[0].path, Path::new("file1"));
1237        assert_eq!(entries[0].depth, 1);
1238
1239        // Symlink should be resolved from $ROOT/link1 -> $ROOT/sub1/file2
1240        assert_eq!(entries[1].file_type, FileType::Symlink);
1241        assert_eq!(entries[1].path, Path::new("sub1").join("file2"));
1242        assert_eq!(entries[1].depth, 1);
1243
1244        assert_eq!(entries[2].file_type, FileType::Dir);
1245        assert_eq!(entries[2].path, Path::new("sub1"));
1246        assert_eq!(entries[2].depth, 1);
1247    }
1248
1249    #[test(tokio::test)]
1250    async fn create_dir_should_send_error_if_fails() {
1251        let (api, ctx, _rx) = setup().await;
1252
1253        // Make a path that has multiple non-existent components
1254        // so the creation will fail
1255        let root_dir = setup_dir().await;
1256        let path = root_dir.path().join("nested").join("new-dir");
1257
1258        let _ = api
1259            .create_dir(ctx, path.to_path_buf(), /* all */ false)
1260            .await
1261            .unwrap_err();
1262
1263        // Also verify that the directory was not actually created
1264        assert!(!path.exists(), "Path unexpectedly exists");
1265    }
1266
1267    #[test(tokio::test)]
1268    async fn create_dir_should_send_ok_when_successful() {
1269        let (api, ctx, _rx) = setup().await;
1270        let root_dir = setup_dir().await;
1271        let path = root_dir.path().join("new-dir");
1272
1273        api.create_dir(ctx, path.to_path_buf(), /* all */ false)
1274            .await
1275            .unwrap();
1276
1277        // Also verify that the directory was actually created
1278        assert!(path.exists(), "Directory not created");
1279    }
1280
1281    #[test(tokio::test)]
1282    async fn create_dir_should_support_creating_multiple_dir_components() {
1283        let (api, ctx, _rx) = setup().await;
1284        let root_dir = setup_dir().await;
1285        let path = root_dir.path().join("nested").join("new-dir");
1286
1287        api.create_dir(ctx, path.to_path_buf(), /* all */ true)
1288            .await
1289            .unwrap();
1290
1291        // Also verify that the directory was actually created
1292        assert!(path.exists(), "Directory not created");
1293    }
1294
1295    #[test(tokio::test)]
1296    async fn remove_should_send_error_on_failure() {
1297        let (api, ctx, _rx) = setup().await;
1298        let temp = assert_fs::TempDir::new().unwrap();
1299        let file = temp.child("missing-file");
1300
1301        let _ = api
1302            .remove(ctx, file.path().to_path_buf(), /* false */ false)
1303            .await
1304            .unwrap_err();
1305
1306        // Also, verify that path does not exist
1307        file.assert(predicate::path::missing());
1308    }
1309
1310    #[test(tokio::test)]
1311    async fn remove_should_support_deleting_a_directory() {
1312        let (api, ctx, _rx) = setup().await;
1313        let temp = assert_fs::TempDir::new().unwrap();
1314        let dir = temp.child("dir");
1315        dir.create_dir_all().unwrap();
1316
1317        api.remove(ctx, dir.path().to_path_buf(), /* false */ false)
1318            .await
1319            .unwrap();
1320
1321        // Also, verify that path does not exist
1322        dir.assert(predicate::path::missing());
1323    }
1324
1325    #[test(tokio::test)]
1326    async fn remove_should_delete_nonempty_directory_if_force_is_true() {
1327        let (api, ctx, _rx) = setup().await;
1328        let temp = assert_fs::TempDir::new().unwrap();
1329        let dir = temp.child("dir");
1330        dir.create_dir_all().unwrap();
1331        dir.child("file").touch().unwrap();
1332
1333        api.remove(ctx, dir.path().to_path_buf(), /* false */ true)
1334            .await
1335            .unwrap();
1336
1337        // Also, verify that path does not exist
1338        dir.assert(predicate::path::missing());
1339    }
1340
1341    #[test(tokio::test)]
1342    async fn remove_should_support_deleting_a_single_file() {
1343        let (api, ctx, _rx) = setup().await;
1344        let temp = assert_fs::TempDir::new().unwrap();
1345        let file = temp.child("some-file");
1346        file.touch().unwrap();
1347
1348        api.remove(ctx, file.path().to_path_buf(), /* false */ false)
1349            .await
1350            .unwrap();
1351
1352        // Also, verify that path does not exist
1353        file.assert(predicate::path::missing());
1354    }
1355
1356    #[test(tokio::test)]
1357    async fn copy_should_send_error_on_failure() {
1358        let (api, ctx, _rx) = setup().await;
1359        let temp = assert_fs::TempDir::new().unwrap();
1360        let src = temp.child("src");
1361        let dst = temp.child("dst");
1362
1363        let _ = api
1364            .copy(ctx, src.path().to_path_buf(), dst.path().to_path_buf())
1365            .await
1366            .unwrap_err();
1367
1368        // Also, verify that destination does not exist
1369        dst.assert(predicate::path::missing());
1370    }
1371
1372    #[test(tokio::test)]
1373    async fn copy_should_support_copying_an_entire_directory() {
1374        let (api, ctx, _rx) = setup().await;
1375        let temp = assert_fs::TempDir::new().unwrap();
1376
1377        let src = temp.child("src");
1378        src.create_dir_all().unwrap();
1379        let src_file = src.child("file");
1380        src_file.write_str("some contents").unwrap();
1381
1382        let dst = temp.child("dst");
1383        let dst_file = dst.child("file");
1384
1385        api.copy(ctx, src.path().to_path_buf(), dst.path().to_path_buf())
1386            .await
1387            .unwrap();
1388
1389        // Verify that we have source and destination directories and associated contents
1390        src.assert(predicate::path::is_dir());
1391        src_file.assert(predicate::path::is_file());
1392        dst.assert(predicate::path::is_dir());
1393        dst_file.assert(predicate::path::eq_file(src_file.path()));
1394    }
1395
1396    #[test(tokio::test)]
1397    async fn copy_should_support_copying_an_empty_directory() {
1398        let (api, ctx, _rx) = setup().await;
1399        let temp = assert_fs::TempDir::new().unwrap();
1400        let src = temp.child("src");
1401        src.create_dir_all().unwrap();
1402        let dst = temp.child("dst");
1403
1404        api.copy(ctx, src.path().to_path_buf(), dst.path().to_path_buf())
1405            .await
1406            .unwrap();
1407
1408        // Verify that we still have source and destination directories
1409        src.assert(predicate::path::is_dir());
1410        dst.assert(predicate::path::is_dir());
1411    }
1412
1413    #[test(tokio::test)]
1414    async fn copy_should_support_copying_a_directory_that_only_contains_directories() {
1415        let (api, ctx, _rx) = setup().await;
1416        let temp = assert_fs::TempDir::new().unwrap();
1417
1418        let src = temp.child("src");
1419        src.create_dir_all().unwrap();
1420        let src_dir = src.child("dir");
1421        src_dir.create_dir_all().unwrap();
1422
1423        let dst = temp.child("dst");
1424        let dst_dir = dst.child("dir");
1425
1426        api.copy(ctx, src.path().to_path_buf(), dst.path().to_path_buf())
1427            .await
1428            .unwrap();
1429
1430        // Verify that we have source and destination directories and associated contents
1431        src.assert(predicate::path::is_dir().name("src"));
1432        src_dir.assert(predicate::path::is_dir().name("src/dir"));
1433        dst.assert(predicate::path::is_dir().name("dst"));
1434        dst_dir.assert(predicate::path::is_dir().name("dst/dir"));
1435    }
1436
1437    #[test(tokio::test)]
1438    async fn copy_should_support_copying_a_single_file() {
1439        let (api, ctx, _rx) = setup().await;
1440        let temp = assert_fs::TempDir::new().unwrap();
1441        let src = temp.child("src");
1442        src.write_str("some text").unwrap();
1443        let dst = temp.child("dst");
1444
1445        api.copy(ctx, src.path().to_path_buf(), dst.path().to_path_buf())
1446            .await
1447            .unwrap();
1448
1449        // Verify that we still have source and that destination has source's contents
1450        src.assert(predicate::path::is_file());
1451        dst.assert(predicate::path::eq_file(src.path()));
1452    }
1453
1454    #[test(tokio::test)]
1455    async fn rename_should_fail_if_path_missing() {
1456        let (api, ctx, _rx) = setup().await;
1457        let temp = assert_fs::TempDir::new().unwrap();
1458        let src = temp.child("src");
1459        let dst = temp.child("dst");
1460
1461        let _ = api
1462            .rename(ctx, src.path().to_path_buf(), dst.path().to_path_buf())
1463            .await
1464            .unwrap_err();
1465
1466        // Also, verify that destination does not exist
1467        dst.assert(predicate::path::missing());
1468    }
1469
1470    #[test(tokio::test)]
1471    async fn rename_should_support_renaming_an_entire_directory() {
1472        let (api, ctx, _rx) = setup().await;
1473        let temp = assert_fs::TempDir::new().unwrap();
1474
1475        let src = temp.child("src");
1476        src.create_dir_all().unwrap();
1477        let src_file = src.child("file");
1478        src_file.write_str("some contents").unwrap();
1479
1480        let dst = temp.child("dst");
1481        let dst_file = dst.child("file");
1482
1483        api.rename(ctx, src.path().to_path_buf(), dst.path().to_path_buf())
1484            .await
1485            .unwrap();
1486
1487        // Verify that we moved the contents
1488        src.assert(predicate::path::missing());
1489        src_file.assert(predicate::path::missing());
1490        dst.assert(predicate::path::is_dir());
1491        dst_file.assert("some contents");
1492    }
1493
1494    #[test(tokio::test)]
1495    async fn rename_should_support_renaming_a_single_file() {
1496        let (api, ctx, _rx) = setup().await;
1497        let temp = assert_fs::TempDir::new().unwrap();
1498        let src = temp.child("src");
1499        src.write_str("some text").unwrap();
1500        let dst = temp.child("dst");
1501
1502        api.rename(ctx, src.path().to_path_buf(), dst.path().to_path_buf())
1503            .await
1504            .unwrap();
1505
1506        // Verify that we moved the file
1507        src.assert(predicate::path::missing());
1508        dst.assert("some text");
1509    }
1510
1511    /// Validates a response as being a series of changes that include the provided paths
1512    fn validate_changed_path(data: &Response, expected_path: &Path, should_panic: bool) -> bool {
1513        match data {
1514            Response::Changed(change) if should_panic => {
1515                let path = change.path.canonicalize().unwrap();
1516                assert_eq!(path, expected_path, "Wrong path reported: {:?}", change);
1517
1518                true
1519            }
1520            Response::Changed(change) => {
1521                let path = change.path.canonicalize().unwrap();
1522                path == expected_path
1523            }
1524            x if should_panic => panic!("Unexpected response: {:?}", x),
1525            _ => false,
1526        }
1527    }
1528
1529    #[test(tokio::test)]
1530    async fn watch_should_support_watching_a_single_file() {
1531        // NOTE: Supporting multiple replies being sent back as part of creating, modifying, etc.
1532        let (api, ctx, mut rx) = setup().await;
1533        let temp = assert_fs::TempDir::new().unwrap();
1534
1535        let file = temp.child("file");
1536        file.touch().unwrap();
1537
1538        api.watch(
1539            ctx,
1540            file.path().to_path_buf(),
1541            /* recursive */ false,
1542            /* only */ Default::default(),
1543            /* except */ Default::default(),
1544        )
1545        .await
1546        .unwrap();
1547
1548        // Update the file and verify we get a notification
1549        file.write_str("some text").unwrap();
1550
1551        let data = rx
1552            .recv()
1553            .await
1554            .expect("Channel closed before we got change");
1555        validate_changed_path(
1556            &data,
1557            &file.path().to_path_buf().canonicalize().unwrap(),
1558            /* should_panic */ true,
1559        );
1560    }
1561
1562    #[test(tokio::test)]
1563    async fn watch_should_support_watching_a_directory_recursively() {
1564        // NOTE: Supporting multiple replies being sent back as part of creating, modifying, etc.
1565        let (api, ctx, mut rx) = setup().await;
1566        let temp = assert_fs::TempDir::new().unwrap();
1567
1568        let file = temp.child("file");
1569        file.touch().unwrap();
1570
1571        let dir = temp.child("dir");
1572        dir.create_dir_all().unwrap();
1573
1574        api.watch(
1575            ctx,
1576            temp.path().to_path_buf(),
1577            /* recursive */ true,
1578            /* only */ Default::default(),
1579            /* except */ Default::default(),
1580        )
1581        .await
1582        .unwrap();
1583
1584        // Update the file and verify we get a notification
1585        file.write_str("some text").unwrap();
1586
1587        // Create a nested file and verify we get a notification
1588        let nested_file = dir.child("nested-file");
1589        nested_file.write_str("some text").unwrap();
1590
1591        // Sleep a bit to give time to get all changes happening
1592        // TODO: Can we slim down this sleep? Or redesign test in some other way?
1593        tokio::time::sleep(DEBOUNCE_TIMEOUT + Duration::from_millis(100)).await;
1594
1595        // Collect all responses, as we may get multiple for interactions within a directory
1596        let mut responses = Vec::new();
1597        while let Ok(res) = rx.try_recv() {
1598            responses.push(res);
1599        }
1600
1601        // Validate that we have at least one change reported for each of our paths
1602        assert!(
1603            responses.len() >= 2,
1604            "Less than expected total responses: {:?}",
1605            responses
1606        );
1607
1608        let path = file.path().to_path_buf();
1609        assert!(
1610            responses.iter().any(|res| validate_changed_path(
1611                res,
1612                &file.path().to_path_buf().canonicalize().unwrap(),
1613                /* should_panic */ false,
1614            )),
1615            "Missing {:?} in {:?}",
1616            path,
1617            responses
1618                .iter()
1619                .map(|x| format!("{:?}", x))
1620                .collect::<Vec<String>>(),
1621        );
1622
1623        let path = nested_file.path().to_path_buf();
1624        assert!(
1625            responses.iter().any(|res| validate_changed_path(
1626                res,
1627                &file.path().to_path_buf().canonicalize().unwrap(),
1628                /* should_panic */ false,
1629            )),
1630            "Missing {:?} in {:?}",
1631            path,
1632            responses
1633                .iter()
1634                .map(|x| format!("{:?}", x))
1635                .collect::<Vec<String>>(),
1636        );
1637    }
1638
1639    #[test(tokio::test)]
1640    async fn watch_should_report_changes_using_the_ctx_replies() {
1641        // NOTE: Supporting multiple replies being sent back as part of creating, modifying, etc.
1642        let (api, ctx_1, mut rx_1) = setup().await;
1643        let (ctx_2, mut rx_2) = {
1644            let (reply, rx) = make_reply();
1645            let ctx = DistantCtx {
1646                connection_id: ctx_1.connection_id,
1647                reply,
1648            };
1649            (ctx, rx)
1650        };
1651
1652        let temp = assert_fs::TempDir::new().unwrap();
1653
1654        let file_1 = temp.child("file_1");
1655        file_1.touch().unwrap();
1656
1657        let file_2 = temp.child("file_2");
1658        file_2.touch().unwrap();
1659
1660        // Sleep a bit to give time to get all changes happening
1661        // TODO: Can we slim down this sleep? Or redesign test in some other way?
1662        tokio::time::sleep(Duration::from_millis(100)).await;
1663
1664        // Initialize watch on file 1
1665        api.watch(
1666            ctx_1,
1667            file_1.path().to_path_buf(),
1668            /* recursive */ false,
1669            /* only */ Default::default(),
1670            /* except */ Default::default(),
1671        )
1672        .await
1673        .unwrap();
1674
1675        // Initialize watch on file 2
1676        api.watch(
1677            ctx_2,
1678            file_2.path().to_path_buf(),
1679            /* recursive */ false,
1680            /* only */ Default::default(),
1681            /* except */ Default::default(),
1682        )
1683        .await
1684        .unwrap();
1685
1686        // Update the files and verify we get notifications from different origins
1687        file_1.write_str("some text").unwrap();
1688        let data = rx_1
1689            .recv()
1690            .await
1691            .expect("Channel closed before we got change");
1692        validate_changed_path(
1693            &data,
1694            &file_1.path().to_path_buf().canonicalize().unwrap(),
1695            /* should_panic */ true,
1696        );
1697
1698        // Update the files and verify we get notifications from different origins
1699        file_2.write_str("some text").unwrap();
1700        let data = rx_2
1701            .recv()
1702            .await
1703            .expect("Channel closed before we got change");
1704        validate_changed_path(
1705            &data,
1706            &file_2.path().to_path_buf().canonicalize().unwrap(),
1707            /* should_panic */ true,
1708        );
1709    }
1710
1711    #[test(tokio::test)]
1712    async fn exists_should_send_true_if_path_exists() {
1713        let (api, ctx, _rx) = setup().await;
1714        let temp = assert_fs::TempDir::new().unwrap();
1715        let file = temp.child("file");
1716        file.touch().unwrap();
1717
1718        let exists = api.exists(ctx, file.path().to_path_buf()).await.unwrap();
1719        assert!(exists, "Expected exists to be true, but was false");
1720    }
1721
1722    #[test(tokio::test)]
1723    async fn exists_should_send_false_if_path_does_not_exist() {
1724        let (api, ctx, _rx) = setup().await;
1725        let temp = assert_fs::TempDir::new().unwrap();
1726        let file = temp.child("file");
1727
1728        let exists = api.exists(ctx, file.path().to_path_buf()).await.unwrap();
1729        assert!(!exists, "Expected exists to be false, but was true");
1730    }
1731
1732    #[test(tokio::test)]
1733    async fn metadata_should_send_error_on_failure() {
1734        let (api, ctx, _rx) = setup().await;
1735        let temp = assert_fs::TempDir::new().unwrap();
1736        let file = temp.child("file");
1737
1738        let _ = api
1739            .metadata(
1740                ctx,
1741                file.path().to_path_buf(),
1742                /* canonicalize */ false,
1743                /* resolve_file_type */ false,
1744            )
1745            .await
1746            .unwrap_err();
1747    }
1748
1749    #[test(tokio::test)]
1750    async fn metadata_should_send_back_metadata_on_file_if_exists() {
1751        let (api, ctx, _rx) = setup().await;
1752        let temp = assert_fs::TempDir::new().unwrap();
1753        let file = temp.child("file");
1754        file.write_str("some text").unwrap();
1755
1756        let metadata = api
1757            .metadata(
1758                ctx,
1759                file.path().to_path_buf(),
1760                /* canonicalize */ false,
1761                /* resolve_file_type */ false,
1762            )
1763            .await
1764            .unwrap();
1765
1766        assert!(
1767            matches!(
1768                metadata,
1769                Metadata {
1770                    canonicalized_path: None,
1771                    file_type: FileType::File,
1772                    len: 9,
1773                    readonly: false,
1774                    ..
1775                }
1776            ),
1777            "{:?}",
1778            metadata
1779        );
1780    }
1781
1782    #[cfg(unix)]
1783    #[test(tokio::test)]
1784    async fn metadata_should_include_unix_specific_metadata_on_unix_platform() {
1785        let (api, ctx, _rx) = setup().await;
1786        let temp = assert_fs::TempDir::new().unwrap();
1787        let file = temp.child("file");
1788        file.write_str("some text").unwrap();
1789
1790        let metadata = api
1791            .metadata(
1792                ctx,
1793                file.path().to_path_buf(),
1794                /* canonicalize */ false,
1795                /* resolve_file_type */ false,
1796            )
1797            .await
1798            .unwrap();
1799
1800        #[allow(clippy::match_single_binding)]
1801        match metadata {
1802            Metadata { unix, windows, .. } => {
1803                assert!(unix.is_some(), "Unexpectedly missing unix metadata on unix");
1804                assert!(
1805                    windows.is_none(),
1806                    "Unexpectedly got windows metadata on unix"
1807                );
1808            }
1809        }
1810    }
1811
1812    #[cfg(windows)]
1813    #[test(tokio::test)]
1814    async fn metadata_should_include_windows_specific_metadata_on_windows_platform() {
1815        let (api, ctx, _rx) = setup().await;
1816        let temp = assert_fs::TempDir::new().unwrap();
1817        let file = temp.child("file");
1818        file.write_str("some text").unwrap();
1819
1820        let metadata = api
1821            .metadata(
1822                ctx,
1823                file.path().to_path_buf(),
1824                /* canonicalize */ false,
1825                /* resolve_file_type */ false,
1826            )
1827            .await
1828            .unwrap();
1829
1830        #[allow(clippy::match_single_binding)]
1831        match metadata {
1832            Metadata { unix, windows, .. } => {
1833                assert!(
1834                    windows.is_some(),
1835                    "Unexpectedly missing windows metadata on windows"
1836                );
1837                assert!(unix.is_none(), "Unexpectedly got unix metadata on windows");
1838            }
1839        }
1840    }
1841
1842    #[test(tokio::test)]
1843    async fn metadata_should_send_back_metadata_on_dir_if_exists() {
1844        let (api, ctx, _rx) = setup().await;
1845        let temp = assert_fs::TempDir::new().unwrap();
1846        let dir = temp.child("dir");
1847        dir.create_dir_all().unwrap();
1848
1849        let metadata = api
1850            .metadata(
1851                ctx,
1852                dir.path().to_path_buf(),
1853                /* canonicalize */ false,
1854                /* resolve_file_type */ false,
1855            )
1856            .await
1857            .unwrap();
1858
1859        assert!(
1860            matches!(
1861                metadata,
1862                Metadata {
1863                    canonicalized_path: None,
1864                    file_type: FileType::Dir,
1865                    readonly: false,
1866                    ..
1867                }
1868            ),
1869            "{:?}",
1870            metadata
1871        );
1872    }
1873
1874    #[test(tokio::test)]
1875    async fn metadata_should_send_back_metadata_on_symlink_if_exists() {
1876        let (api, ctx, _rx) = setup().await;
1877        let temp = assert_fs::TempDir::new().unwrap();
1878        let file = temp.child("file");
1879        file.write_str("some text").unwrap();
1880
1881        let symlink = temp.child("link");
1882        symlink.symlink_to_file(file.path()).unwrap();
1883
1884        let metadata = api
1885            .metadata(
1886                ctx,
1887                symlink.path().to_path_buf(),
1888                /* canonicalize */ false,
1889                /* resolve_file_type */ false,
1890            )
1891            .await
1892            .unwrap();
1893
1894        assert!(
1895            matches!(
1896                metadata,
1897                Metadata {
1898                    canonicalized_path: None,
1899                    file_type: FileType::Symlink,
1900                    readonly: false,
1901                    ..
1902                }
1903            ),
1904            "{:?}",
1905            metadata
1906        );
1907    }
1908
1909    #[test(tokio::test)]
1910    async fn metadata_should_include_canonicalized_path_if_flag_specified() {
1911        let (api, ctx, _rx) = setup().await;
1912        let temp = assert_fs::TempDir::new().unwrap();
1913        let file = temp.child("file");
1914        file.write_str("some text").unwrap();
1915
1916        let symlink = temp.child("link");
1917        symlink.symlink_to_file(file.path()).unwrap();
1918
1919        let metadata = api
1920            .metadata(
1921                ctx,
1922                symlink.path().to_path_buf(),
1923                /* canonicalize */ true,
1924                /* resolve_file_type */ false,
1925            )
1926            .await
1927            .unwrap();
1928
1929        match metadata {
1930            Metadata {
1931                canonicalized_path: Some(path),
1932                file_type: FileType::Symlink,
1933                readonly: false,
1934                ..
1935            } => assert_eq!(
1936                path,
1937                file.path().canonicalize().unwrap(),
1938                "Symlink canonicalized path does not match referenced file"
1939            ),
1940            x => panic!("Unexpected response: {:?}", x),
1941        }
1942    }
1943
1944    #[test(tokio::test)]
1945    async fn metadata_should_resolve_file_type_of_symlink_if_flag_specified() {
1946        let (api, ctx, _rx) = setup().await;
1947        let temp = assert_fs::TempDir::new().unwrap();
1948        let file = temp.child("file");
1949        file.write_str("some text").unwrap();
1950
1951        let symlink = temp.child("link");
1952        symlink.symlink_to_file(file.path()).unwrap();
1953
1954        let metadata = api
1955            .metadata(
1956                ctx,
1957                symlink.path().to_path_buf(),
1958                /* canonicalize */ false,
1959                /* resolve_file_type */ true,
1960            )
1961            .await
1962            .unwrap();
1963
1964        assert!(
1965            matches!(
1966                metadata,
1967                Metadata {
1968                    file_type: FileType::File,
1969                    ..
1970                }
1971            ),
1972            "{:?}",
1973            metadata
1974        );
1975    }
1976
1977    #[test(tokio::test)]
1978    async fn set_permissions_should_set_readonly_flag_if_specified() {
1979        let (api, ctx, _rx) = setup().await;
1980        let temp = assert_fs::TempDir::new().unwrap();
1981        let file = temp.child("file");
1982        file.write_str("some text").unwrap();
1983
1984        // Verify that not readonly by default
1985        let permissions = tokio::fs::symlink_metadata(file.path())
1986            .await
1987            .unwrap()
1988            .permissions();
1989        assert!(!permissions.readonly(), "File is already set to readonly");
1990
1991        // Change the file permissions
1992        api.set_permissions(
1993            ctx,
1994            file.path().to_path_buf(),
1995            Permissions::readonly(),
1996            Default::default(),
1997        )
1998        .await
1999        .unwrap();
2000
2001        // Retrieve permissions to verify set
2002        let permissions = tokio::fs::symlink_metadata(file.path())
2003            .await
2004            .unwrap()
2005            .permissions();
2006        assert!(permissions.readonly(), "File not set to readonly");
2007    }
2008
2009    #[test(tokio::test)]
2010    #[cfg_attr(not(unix), ignore)]
2011    async fn set_permissions_should_set_unix_permissions_if_on_unix_platform() {
2012        #[cfg(unix)]
2013        {
2014            use std::os::unix::prelude::*;
2015
2016            let (api, ctx, _rx) = setup().await;
2017            let temp = assert_fs::TempDir::new().unwrap();
2018            let file = temp.child("file");
2019            file.write_str("some text").unwrap();
2020
2021            // Verify that permissions do not match our readonly state
2022            let permissions = tokio::fs::symlink_metadata(file.path())
2023                .await
2024                .unwrap()
2025                .permissions();
2026            let mode = permissions.mode() & 0o777;
2027            assert_ne!(mode, 0o400, "File is already set to 0o400");
2028
2029            // Change the file permissions
2030            api.set_permissions(
2031                ctx,
2032                file.path().to_path_buf(),
2033                Permissions::from_unix_mode(0o400),
2034                Default::default(),
2035            )
2036            .await
2037            .unwrap();
2038
2039            // Retrieve file permissions to verify set
2040            let permissions = tokio::fs::symlink_metadata(file.path())
2041                .await
2042                .unwrap()
2043                .permissions();
2044
2045            // Drop the upper bits that mode can have (only care about read/write/exec)
2046            let mode = permissions.mode() & 0o777;
2047
2048            assert_eq!(mode, 0o400, "Wrong permissions on file: {:o}", mode);
2049        }
2050        #[cfg(not(unix))]
2051        {
2052            unreachable!();
2053        }
2054    }
2055
2056    #[test(tokio::test)]
2057    #[cfg_attr(unix, ignore)]
2058    async fn set_permissions_should_set_readonly_flag_if_not_on_unix_platform() {
2059        let (api, ctx, _rx) = setup().await;
2060        let temp = assert_fs::TempDir::new().unwrap();
2061        let file = temp.child("file");
2062        file.write_str("some text").unwrap();
2063
2064        // Verify that not readonly by default
2065        let permissions = tokio::fs::symlink_metadata(file.path())
2066            .await
2067            .unwrap()
2068            .permissions();
2069        assert!(!permissions.readonly(), "File is already set to readonly");
2070
2071        // Change the file permissions to be readonly (in general)
2072        api.set_permissions(
2073            ctx,
2074            file.path().to_path_buf(),
2075            Permissions::from_unix_mode(0o400),
2076            Default::default(),
2077        )
2078        .await
2079        .unwrap();
2080
2081        #[cfg(not(unix))]
2082        {
2083            // Retrieve file permissions to verify set
2084            let permissions = tokio::fs::symlink_metadata(file.path())
2085                .await
2086                .unwrap()
2087                .permissions();
2088
2089            assert!(permissions.readonly(), "File not marked as readonly");
2090        }
2091        #[cfg(unix)]
2092        {
2093            unreachable!();
2094        }
2095    }
2096
2097    #[test(tokio::test)]
2098    async fn set_permissions_should_not_recurse_if_option_false() {
2099        let (api, ctx, _rx) = setup().await;
2100        let temp = assert_fs::TempDir::new().unwrap();
2101        let file = temp.child("file");
2102        file.write_str("some text").unwrap();
2103
2104        let symlink = temp.child("link");
2105        symlink.symlink_to_file(file.path()).unwrap();
2106
2107        // Verify that dir is not readonly by default
2108        let permissions = tokio::fs::symlink_metadata(temp.path())
2109            .await
2110            .unwrap()
2111            .permissions();
2112        assert!(
2113            !permissions.readonly(),
2114            "Temp dir is already set to readonly"
2115        );
2116
2117        // Verify that file is not readonly by default
2118        let permissions = tokio::fs::symlink_metadata(file.path())
2119            .await
2120            .unwrap()
2121            .permissions();
2122        assert!(!permissions.readonly(), "File is already set to readonly");
2123
2124        // Verify that symlink is not readonly by default
2125        let permissions = tokio::fs::symlink_metadata(symlink.path())
2126            .await
2127            .unwrap()
2128            .permissions();
2129        assert!(
2130            !permissions.readonly(),
2131            "Symlink is already set to readonly"
2132        );
2133
2134        // Change the permissions of the directory and not the contents underneath
2135        api.set_permissions(
2136            ctx,
2137            temp.path().to_path_buf(),
2138            Permissions::readonly(),
2139            SetPermissionsOptions {
2140                recursive: false,
2141                ..Default::default()
2142            },
2143        )
2144        .await
2145        .unwrap();
2146
2147        // Retrieve permissions of the file, symlink, and directory to verify set
2148        let permissions = tokio::fs::symlink_metadata(temp.path())
2149            .await
2150            .unwrap()
2151            .permissions();
2152        assert!(permissions.readonly(), "Temp directory not set to readonly");
2153
2154        let permissions = tokio::fs::symlink_metadata(file.path())
2155            .await
2156            .unwrap()
2157            .permissions();
2158        assert!(!permissions.readonly(), "File unexpectedly set to readonly");
2159
2160        let permissions = tokio::fs::symlink_metadata(symlink.path())
2161            .await
2162            .unwrap()
2163            .permissions();
2164        assert!(
2165            !permissions.readonly(),
2166            "Symlink unexpectedly set to readonly"
2167        );
2168    }
2169
2170    #[test(tokio::test)]
2171    async fn set_permissions_should_traverse_symlinks_while_recursing_if_following_symlinks_enabled(
2172    ) {
2173        let (api, ctx, _rx) = setup().await;
2174        let temp = assert_fs::TempDir::new().unwrap();
2175        let file = temp.child("file");
2176        file.write_str("some text").unwrap();
2177
2178        let temp2 = assert_fs::TempDir::new().unwrap();
2179        let file2 = temp2.child("file");
2180        file2.write_str("some text").unwrap();
2181
2182        let symlink = temp.child("link");
2183        symlink.symlink_to_dir(temp2.path()).unwrap();
2184
2185        // Verify that symlink is not readonly by default
2186        let permissions = tokio::fs::symlink_metadata(file2.path())
2187            .await
2188            .unwrap()
2189            .permissions();
2190        assert!(!permissions.readonly(), "File2 is already set to readonly");
2191
2192        // Change the main directory permissions
2193        api.set_permissions(
2194            ctx,
2195            temp.path().to_path_buf(),
2196            Permissions::readonly(),
2197            SetPermissionsOptions {
2198                follow_symlinks: true,
2199                recursive: true,
2200                ..Default::default()
2201            },
2202        )
2203        .await
2204        .unwrap();
2205
2206        // Retrieve permissions referenced by another directory
2207        let permissions = tokio::fs::symlink_metadata(file2.path())
2208            .await
2209            .unwrap()
2210            .permissions();
2211        assert!(permissions.readonly(), "File2 not set to readonly");
2212    }
2213
2214    #[test(tokio::test)]
2215    async fn set_permissions_should_not_traverse_symlinks_while_recursing_if_following_symlinks_disabled(
2216    ) {
2217        let (api, ctx, _rx) = setup().await;
2218        let temp = assert_fs::TempDir::new().unwrap();
2219        let file = temp.child("file");
2220        file.write_str("some text").unwrap();
2221
2222        let temp2 = assert_fs::TempDir::new().unwrap();
2223        let file2 = temp2.child("file");
2224        file2.write_str("some text").unwrap();
2225
2226        let symlink = temp.child("link");
2227        symlink.symlink_to_dir(temp2.path()).unwrap();
2228
2229        // Verify that symlink is not readonly by default
2230        let permissions = tokio::fs::symlink_metadata(file2.path())
2231            .await
2232            .unwrap()
2233            .permissions();
2234        assert!(!permissions.readonly(), "File2 is already set to readonly");
2235
2236        // Change the main directory permissions
2237        api.set_permissions(
2238            ctx,
2239            temp.path().to_path_buf(),
2240            Permissions::readonly(),
2241            SetPermissionsOptions {
2242                follow_symlinks: false,
2243                recursive: true,
2244                ..Default::default()
2245            },
2246        )
2247        .await
2248        .unwrap();
2249
2250        // Retrieve permissions referenced by another directory
2251        let permissions = tokio::fs::symlink_metadata(file2.path())
2252            .await
2253            .unwrap()
2254            .permissions();
2255        assert!(
2256            !permissions.readonly(),
2257            "File2 unexpectedly set to readonly"
2258        );
2259    }
2260
2261    #[test(tokio::test)]
2262    async fn set_permissions_should_skip_symlinks_if_exclude_symlinks_enabled() {
2263        let (api, ctx, _rx) = setup().await;
2264        let temp = assert_fs::TempDir::new().unwrap();
2265        let file = temp.child("file");
2266        file.write_str("some text").unwrap();
2267
2268        let symlink = temp.child("link");
2269        symlink.symlink_to_file(file.path()).unwrap();
2270
2271        // Verify that symlink is not readonly by default
2272        let permissions = tokio::fs::symlink_metadata(symlink.path())
2273            .await
2274            .unwrap()
2275            .permissions();
2276        assert!(
2277            !permissions.readonly(),
2278            "Symlink is already set to readonly"
2279        );
2280
2281        // Change the symlink permissions
2282        api.set_permissions(
2283            ctx,
2284            symlink.path().to_path_buf(),
2285            Permissions::readonly(),
2286            SetPermissionsOptions {
2287                exclude_symlinks: true,
2288                ..Default::default()
2289            },
2290        )
2291        .await
2292        .unwrap();
2293
2294        // Retrieve permissions to verify not set
2295        let permissions = tokio::fs::symlink_metadata(symlink.path())
2296            .await
2297            .unwrap()
2298            .permissions();
2299        assert!(
2300            !permissions.readonly(),
2301            "Symlink (or file underneath) set to readonly"
2302        );
2303    }
2304
2305    #[test(tokio::test)]
2306    async fn set_permissions_should_support_recursive_if_option_specified() {
2307        let (api, ctx, _rx) = setup().await;
2308        let temp = assert_fs::TempDir::new().unwrap();
2309        let file = temp.child("file");
2310        file.write_str("some text").unwrap();
2311
2312        // Verify that dir is not readonly by default
2313        let permissions = tokio::fs::symlink_metadata(temp.path())
2314            .await
2315            .unwrap()
2316            .permissions();
2317        assert!(
2318            !permissions.readonly(),
2319            "Temp dir is already set to readonly"
2320        );
2321
2322        // Verify that file is not readonly by default
2323        let permissions = tokio::fs::symlink_metadata(file.path())
2324            .await
2325            .unwrap()
2326            .permissions();
2327        assert!(!permissions.readonly(), "File is already set to readonly");
2328
2329        // Change the permissions of the file pointed to by the symlink
2330        api.set_permissions(
2331            ctx,
2332            temp.path().to_path_buf(),
2333            Permissions::readonly(),
2334            SetPermissionsOptions {
2335                recursive: true,
2336                ..Default::default()
2337            },
2338        )
2339        .await
2340        .unwrap();
2341
2342        // Retrieve permissions of the file, symlink, and directory to verify set
2343        let permissions = tokio::fs::symlink_metadata(temp.path())
2344            .await
2345            .unwrap()
2346            .permissions();
2347        assert!(permissions.readonly(), "Temp directory not set to readonly");
2348
2349        let permissions = tokio::fs::symlink_metadata(file.path())
2350            .await
2351            .unwrap()
2352            .permissions();
2353        assert!(permissions.readonly(), "File not set to readonly");
2354    }
2355
2356    #[test(tokio::test)]
2357    async fn set_permissions_should_support_following_explicit_symlink_if_option_specified() {
2358        let (api, ctx, _rx) = setup().await;
2359        let temp = assert_fs::TempDir::new().unwrap();
2360        let file = temp.child("file");
2361        file.write_str("some text").unwrap();
2362
2363        let symlink = temp.child("link");
2364        symlink.symlink_to_file(file.path()).unwrap();
2365
2366        // Verify that file is not readonly by default
2367        let permissions = tokio::fs::symlink_metadata(file.path())
2368            .await
2369            .unwrap()
2370            .permissions();
2371        assert!(!permissions.readonly(), "File is already set to readonly");
2372
2373        // Verify that symlink is not readonly by default
2374        let permissions = tokio::fs::symlink_metadata(symlink.path())
2375            .await
2376            .unwrap()
2377            .permissions();
2378        assert!(
2379            !permissions.readonly(),
2380            "Symlink is already set to readonly"
2381        );
2382
2383        // Change the permissions of the file pointed to by the symlink
2384        api.set_permissions(
2385            ctx,
2386            symlink.path().to_path_buf(),
2387            Permissions::readonly(),
2388            SetPermissionsOptions {
2389                follow_symlinks: true,
2390                ..Default::default()
2391            },
2392        )
2393        .await
2394        .unwrap();
2395
2396        // Retrieve permissions of the file and symlink to verify set
2397        let permissions = tokio::fs::symlink_metadata(file.path())
2398            .await
2399            .unwrap()
2400            .permissions();
2401        assert!(permissions.readonly(), "File not set to readonly");
2402
2403        let permissions = tokio::fs::symlink_metadata(symlink.path())
2404            .await
2405            .unwrap()
2406            .permissions();
2407        assert!(
2408            !permissions.readonly(),
2409            "Symlink unexpectedly set to readonly"
2410        );
2411    }
2412
2413    // NOTE: Ignoring on windows because it's using WSL which wants a Linux path
2414    //       with / but thinks it's on windows and is providing \
2415    #[test(tokio::test)]
2416    #[cfg_attr(windows, ignore)]
2417    async fn proc_spawn_should_send_error_on_failure() {
2418        let (api, ctx, _rx) = setup().await;
2419
2420        let _ = api
2421            .proc_spawn(
2422                ctx,
2423                /* cmd */ DOES_NOT_EXIST_BIN.to_str().unwrap().to_string(),
2424                /* environment */ Environment::new(),
2425                /* current_dir */ None,
2426                /* pty */ None,
2427            )
2428            .await
2429            .unwrap_err();
2430    }
2431
2432    // NOTE: Ignoring on windows because it's using WSL which wants a Linux path
2433    //       with / but thinks it's on windows and is providing \
2434    #[test(tokio::test)]
2435    #[cfg_attr(windows, ignore)]
2436    async fn proc_spawn_should_return_id_of_spawned_process() {
2437        let (api, ctx, _rx) = setup().await;
2438
2439        let id = api
2440            .proc_spawn(
2441                ctx,
2442                /* cmd */
2443                format!(
2444                    "{} {}",
2445                    *SCRIPT_RUNNER,
2446                    ECHO_ARGS_TO_STDOUT_SH.to_str().unwrap()
2447                ),
2448                /* environment */ Environment::new(),
2449                /* current_dir */ None,
2450                /* pty */ None,
2451            )
2452            .await
2453            .unwrap();
2454        assert!(id > 0);
2455    }
2456
2457    // NOTE: Ignoring on windows because it's using WSL which wants a Linux path
2458    //       with / but thinks it's on windows and is providing \
2459    #[test(tokio::test)]
2460    #[cfg_attr(windows, ignore)]
2461    async fn proc_spawn_should_send_back_stdout_periodically_when_available() {
2462        let (api, ctx, mut rx) = setup().await;
2463
2464        let proc_id = api
2465            .proc_spawn(
2466                ctx,
2467                /* cmd */
2468                format!(
2469                    "{} {} some stdout",
2470                    *SCRIPT_RUNNER,
2471                    ECHO_ARGS_TO_STDOUT_SH.to_str().unwrap()
2472                ),
2473                /* environment */ Environment::new(),
2474                /* current_dir */ None,
2475                /* pty */ None,
2476            )
2477            .await
2478            .unwrap();
2479
2480        // Gather two additional responses:
2481        //
2482        // 1. An indirect response for stdout
2483        // 2. An indirect response that is proc completing
2484        //
2485        // Note that order is not a guarantee, so we have to check that
2486        // we get one of each type of response
2487        let data_1 = rx.recv().await.expect("Missing first response");
2488        let data_2 = rx.recv().await.expect("Missing second response");
2489
2490        let mut got_stdout = false;
2491        let mut got_done = false;
2492
2493        let mut check_data = |data: &Response| match data {
2494            Response::ProcStdout { id, data } => {
2495                assert_eq!(
2496                    *id, proc_id,
2497                    "Got {}, but expected {} as process id",
2498                    id, proc_id
2499                );
2500                assert_eq!(data, b"some stdout", "Got wrong stdout");
2501                got_stdout = true;
2502            }
2503            Response::ProcDone { id, success, .. } => {
2504                assert_eq!(
2505                    *id, proc_id,
2506                    "Got {}, but expected {} as process id",
2507                    id, proc_id
2508                );
2509                assert!(success, "Process should have completed successfully");
2510                got_done = true;
2511            }
2512            x => panic!("Unexpected response: {:?}", x),
2513        };
2514
2515        check_data(&data_1);
2516        check_data(&data_2);
2517        assert!(got_stdout, "Missing stdout response");
2518        assert!(got_done, "Missing done response");
2519    }
2520
2521    // NOTE: Ignoring on windows because it's using WSL which wants a Linux path
2522    //       with / but thinks it's on windows and is providing \
2523    #[test(tokio::test)]
2524    #[cfg_attr(windows, ignore)]
2525    async fn proc_spawn_should_send_back_stderr_periodically_when_available() {
2526        let (api, ctx, mut rx) = setup().await;
2527
2528        let proc_id = api
2529            .proc_spawn(
2530                ctx,
2531                /* cmd */
2532                format!(
2533                    "{} {} some stderr",
2534                    *SCRIPT_RUNNER,
2535                    ECHO_ARGS_TO_STDERR_SH.to_str().unwrap()
2536                ),
2537                /* environment */ Environment::new(),
2538                /* current_dir */ None,
2539                /* pty */ None,
2540            )
2541            .await
2542            .unwrap();
2543
2544        // Gather two additional responses:
2545        //
2546        // 1. An indirect response for stderr
2547        // 2. An indirect response that is proc completing
2548        //
2549        // Note that order is not a guarantee, so we have to check that
2550        // we get one of each type of response
2551        let data_1 = rx.recv().await.expect("Missing first response");
2552        let data_2 = rx.recv().await.expect("Missing second response");
2553
2554        let mut got_stderr = false;
2555        let mut got_done = false;
2556
2557        let mut check_data = |data: &Response| match data {
2558            Response::ProcStderr { id, data } => {
2559                assert_eq!(
2560                    *id, proc_id,
2561                    "Got {}, but expected {} as process id",
2562                    id, proc_id
2563                );
2564                assert_eq!(data, b"some stderr", "Got wrong stderr");
2565                got_stderr = true;
2566            }
2567            Response::ProcDone { id, success, .. } => {
2568                assert_eq!(
2569                    *id, proc_id,
2570                    "Got {}, but expected {} as process id",
2571                    id, proc_id
2572                );
2573                assert!(success, "Process should have completed successfully");
2574                got_done = true;
2575            }
2576            x => panic!("Unexpected response: {:?}", x),
2577        };
2578
2579        check_data(&data_1);
2580        check_data(&data_2);
2581        assert!(got_stderr, "Missing stderr response");
2582        assert!(got_done, "Missing done response");
2583    }
2584
2585    // NOTE: Ignoring on windows because it's using WSL which wants a Linux path
2586    //       with / but thinks it's on windows and is providing \
2587    #[test(tokio::test)]
2588    #[cfg_attr(windows, ignore)]
2589    async fn proc_spawn_should_send_done_signal_when_completed() {
2590        let (api, ctx, mut rx) = setup().await;
2591
2592        let proc_id = api
2593            .proc_spawn(
2594                ctx,
2595                /* cmd */
2596                format!("{} {} 0.1", *SCRIPT_RUNNER, SLEEP_SH.to_str().unwrap()),
2597                /* environment */ Environment::new(),
2598                /* current_dir */ None,
2599                /* pty */ None,
2600            )
2601            .await
2602            .unwrap();
2603
2604        // Wait for process to finish
2605        match rx.recv().await.unwrap() {
2606            Response::ProcDone { id, .. } => assert_eq!(
2607                id, proc_id,
2608                "Got {}, but expected {} as process id",
2609                id, proc_id
2610            ),
2611            x => panic!("Unexpected response: {:?}", x),
2612        }
2613    }
2614
2615    // NOTE: Ignoring on windows because it's using WSL which wants a Linux path
2616    //       with / but thinks it's on windows and is providing \
2617    #[test(tokio::test)]
2618    #[cfg_attr(windows, ignore)]
2619    async fn proc_spawn_should_clear_process_from_state_when_killed() {
2620        let (api, ctx_1, mut rx) = setup().await;
2621        let (ctx_2, _rx) = {
2622            let (reply, rx) = make_reply();
2623            let ctx = DistantCtx {
2624                connection_id: ctx_1.connection_id,
2625                reply,
2626            };
2627            (ctx, rx)
2628        };
2629
2630        let proc_id = api
2631            .proc_spawn(
2632                ctx_1,
2633                /* cmd */
2634                format!("{} {} 1", *SCRIPT_RUNNER, SLEEP_SH.to_str().unwrap()),
2635                /* environment */ Environment::new(),
2636                /* current_dir */ None,
2637                /* pty */ None,
2638            )
2639            .await
2640            .unwrap();
2641
2642        // Send kill signal
2643        api.proc_kill(ctx_2, proc_id).await.unwrap();
2644
2645        // Wait for the completion response to come in
2646        match rx.recv().await.unwrap() {
2647            Response::ProcDone { id, .. } => assert_eq!(
2648                id, proc_id,
2649                "Got {}, but expected {} as process id",
2650                id, proc_id
2651            ),
2652            x => panic!("Unexpected response: {:?}", x),
2653        }
2654    }
2655
2656    #[test(tokio::test)]
2657    async fn proc_kill_should_fail_if_given_non_existent_process() {
2658        let (api, ctx, _rx) = setup().await;
2659
2660        // Send kill to a non-existent process
2661        let _ = api.proc_kill(ctx, 0xDEADBEEF).await.unwrap_err();
2662    }
2663
2664    #[test(tokio::test)]
2665    async fn proc_stdin_should_fail_if_given_non_existent_process() {
2666        let (api, ctx, _rx) = setup().await;
2667
2668        // Send stdin to a non-existent process
2669        let _ = api
2670            .proc_stdin(ctx, 0xDEADBEEF, b"some input".to_vec())
2671            .await
2672            .unwrap_err();
2673    }
2674
2675    // NOTE: Ignoring on windows because it's using WSL which wants a Linux path
2676    //       with / but thinks it's on windows and is providing \
2677    #[test(tokio::test)]
2678    #[cfg_attr(windows, ignore)]
2679    async fn proc_stdin_should_send_stdin_to_process() {
2680        let (api, ctx_1, mut rx) = setup().await;
2681        let (ctx_2, _rx) = {
2682            let (reply, rx) = make_reply();
2683            let ctx = DistantCtx {
2684                connection_id: ctx_1.connection_id,
2685                reply,
2686            };
2687            (ctx, rx)
2688        };
2689
2690        // First, run a program that listens for stdin
2691        let id = api
2692            .proc_spawn(
2693                ctx_1,
2694                /* cmd */
2695                format!(
2696                    "{} {}",
2697                    *SCRIPT_RUNNER,
2698                    ECHO_STDIN_TO_STDOUT_SH.to_str().unwrap()
2699                ),
2700                Environment::new(),
2701                /* current_dir */ None,
2702                /* pty */ None,
2703            )
2704            .await
2705            .unwrap();
2706
2707        // Second, send stdin to the remote process
2708        api.proc_stdin(ctx_2, id, b"hello world\n".to_vec())
2709            .await
2710            .unwrap();
2711
2712        // Third, check the async response of stdout to verify we got stdin
2713        match rx.recv().await.unwrap() {
2714            Response::ProcStdout { data, .. } => {
2715                assert_eq!(data, b"hello world\n", "Mirrored data didn't match");
2716            }
2717            x => panic!("Unexpected response: {:?}", x),
2718        }
2719    }
2720
2721    #[test(tokio::test)]
2722    async fn system_info_should_return_system_info_based_on_binary() {
2723        let (api, ctx, _rx) = setup().await;
2724
2725        let system_info = api.system_info(ctx).await.unwrap();
2726        assert_eq!(
2727            system_info,
2728            SystemInfo {
2729                family: std::env::consts::FAMILY.to_string(),
2730                os: std::env::consts::OS.to_string(),
2731                arch: std::env::consts::ARCH.to_string(),
2732                current_dir: std::env::current_dir().unwrap_or_default(),
2733                main_separator: std::path::MAIN_SEPARATOR,
2734                username: whoami::username(),
2735                shell: if cfg!(windows) {
2736                    std::env::var("ComSpec").unwrap_or_else(|_| String::from("cmd.exe"))
2737                } else {
2738                    std::env::var("SHELL").unwrap_or_else(|_| String::from("/bin/sh"))
2739                }
2740            }
2741        );
2742    }
2743}