From 6ad0da98d8371d34dfa6c8018ae4a022ecdc6470 Mon Sep 17 00:00:00 2001 From: Wez Furlong Date: Mon, 13 May 2024 11:43:10 -0700 Subject: [PATCH] deps: remove smol-potat, update futures-lite smol-potat hasn't had updates in 4 years and was bifurcating some big deps. It wasn't necessary; it was trivially easy to replace with smol::block_on with no meaningful increase in boilerplate, and the result is much more understandable in purpose and effect. --- Cargo.lock | 183 +-- wezterm-ssh/Cargo.toml | 1 - wezterm-ssh/tests/e2e/agent_forward.rs | 52 +- wezterm-ssh/tests/e2e/sftp.rs | 1531 ++++++++++++------------ wezterm-ssh/tests/e2e/sftp/file.rs | 278 ++--- window/Cargo.toml | 2 +- 6 files changed, 984 insertions(+), 1063 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1327dc7a2..5c5002980 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -225,8 +225,8 @@ checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a" dependencies = [ "async-task", "concurrent-queue", - "fastrand 2.1.0", - "futures-lite 2.3.0", + "fastrand", + "futures-lite", "slab", ] @@ -236,29 +236,9 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" dependencies = [ - "async-lock 3.3.0", + "async-lock", "blocking", - "futures-lite 2.3.0", -] - -[[package]] -name = "async-io" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" -dependencies = [ - "async-lock 2.8.0", - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-lite 1.13.0", - "log", - "parking", - "polling 2.8.0", - "rustix 0.37.27", - "slab", - "socket2 0.4.10", - "waker-fn", + "futures-lite", ] [[package]] @@ -267,28 +247,19 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" dependencies = [ - "async-lock 3.3.0", + "async-lock", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.3.0", + "futures-lite", "parking", - "polling 3.7.0", + "polling", "rustix 0.38.34", "slab", "tracing", "windows-sys 0.52.0", ] -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - [[package]] name = "async-lock" version = "3.3.0" @@ -306,9 +277,9 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" dependencies = [ - "async-io 2.3.2", + "async-io", "blocking", - "futures-lite 2.3.0", + "futures-lite", ] [[package]] @@ -318,14 +289,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a53fc6301894e04a92cb2584fedde80cb25ba8e02d9dc39d4a87d036e22f397d" dependencies = [ "async-channel", - "async-io 2.3.2", - "async-lock 3.3.0", + "async-io", + "async-lock", "async-signal", "async-task", "blocking", "cfg-if", "event-listener 5.3.0", - "futures-lite 2.3.0", + "futures-lite", "rustix 0.38.34", "tracing", "windows-sys 0.52.0", @@ -348,8 +319,8 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afe66191c335039c7bb78f99dc7520b0cbb166b3a1cb33a03f53d8a1c6f2afda" dependencies = [ - "async-io 2.3.2", - "async-lock 3.3.0", + "async-io", + "async-lock", "atomic-waker", "cfg-if", "futures-core", @@ -381,7 +352,7 @@ dependencies = [ name = "async_ossl" version = "0.1.0" dependencies = [ - "async-io 2.3.2", + "async-io", "openssl", ] @@ -554,10 +525,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "495f7104e962b7356f0aeb34247aca1fe7d2e783b346582db7f2904cb5717e88" dependencies = [ "async-channel", - "async-lock 3.3.0", + "async-lock", "async-task", "futures-io", - "futures-lite 2.3.0", + "futures-lite", "piper", ] @@ -1583,12 +1554,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - [[package]] name = "event-listener" version = "4.0.3" @@ -1669,15 +1634,6 @@ dependencies = [ "regex", ] -[[package]] -name = "fastrand" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] - [[package]] name = "fastrand" version = "2.1.0" @@ -1932,28 +1888,13 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" -[[package]] -name = "futures-lite" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" -dependencies = [ - "fastrand 1.9.0", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - [[package]] name = "futures-lite" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ - "fastrand 2.1.0", + "fastrand", "futures-core", "futures-io", "parking", @@ -2491,7 +2432,7 @@ dependencies = [ "http-body", "hyper", "pin-project-lite", - "socket2 0.5.7", + "socket2", "tokio", "tower", "tower-service", @@ -4006,7 +3947,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "464db0c665917b13ebb5d453ccdec4add5658ee1adc7affc7677615356a8afaf" dependencies = [ "atomic-waker", - "fastrand 2.1.0", + "fastrand", "futures-io", ] @@ -4084,22 +4025,6 @@ dependencies = [ "miniz_oxide 0.7.2", ] -[[package]] -name = "polling" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" -dependencies = [ - "autocfg", - "bitflags 1.3.2", - "cfg-if", - "concurrent-queue", - "libc", - "log", - "pin-project-lite", - "windows-sys 0.48.0", -] - [[package]] name = "polling" version = "3.7.0" @@ -4259,7 +4184,7 @@ version = "0.2.0" dependencies = [ "anyhow", "async-executor", - "async-io 2.3.2", + "async-io", "async-task", "flume", "lazy_static", @@ -5123,43 +5048,12 @@ dependencies = [ "async-channel", "async-executor", "async-fs", - "async-io 2.3.2", - "async-lock 3.3.0", + "async-io", + "async-lock", "async-net", "async-process", "blocking", - "futures-lite 2.3.0", -] - -[[package]] -name = "smol-potat" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "894ffa61af5c0fab697c8c29b1ab10cb6ec4978a1ccac4a81b5b312df1ffd88e" -dependencies = [ - "async-io 1.13.0", - "smol-potat-macro", -] - -[[package]] -name = "smol-potat-macro" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7cd8129a18069385b4eadaa81182b1451fab312ad6f58d1d99253082bf3932" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "socket2" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" -dependencies = [ - "libc", - "winapi", + "futures-lite", ] [[package]] @@ -5427,7 +5321,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", - "fastrand 2.1.0", + "fastrand", "rustix 0.38.34", "windows-sys 0.52.0", ] @@ -5730,7 +5624,7 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", - "socket2 0.5.7", + "socket2", "tokio-macros", "windows-sys 0.48.0", ] @@ -6104,12 +5998,6 @@ dependencies = [ "utf8parse", ] -[[package]] -name = "waker-fn" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" - [[package]] name = "walkdir" version = "2.5.0" @@ -6398,7 +6286,7 @@ name = "wezterm-client" version = "0.1.0" dependencies = [ "anyhow", - "async-io 2.3.2", + "async-io", "async-trait", "async_ossl", "codec", @@ -6533,7 +6421,7 @@ dependencies = [ "env-bootstrap", "env_logger 0.11.3", "euclid", - "fastrand 2.1.0", + "fastrand", "filedescriptor", "finl_unicode", "frecency", @@ -6651,7 +6539,7 @@ name = "wezterm-mux-server-impl" version = "0.1.0" dependencies = [ "anyhow", - "async-io 2.3.2", + "async-io", "async_ossl", "codec", "config", @@ -6707,8 +6595,7 @@ dependencies = [ "rstest", "shell-words", "smol", - "smol-potat", - "socket2 0.5.7", + "socket2", "ssh2", "termwiz", "thiserror", @@ -6749,7 +6636,7 @@ dependencies = [ name = "wezterm-toast-notification" version = "0.1.0" dependencies = [ - "async-io 2.3.2", + "async-io", "cocoa 0.20.2", "core-foundation 0.7.0", "futures-util", @@ -6938,7 +6825,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-channel", - "async-io 2.3.2", + "async-io", "async-task", "async-trait", "bitflags 1.3.2", @@ -6953,7 +6840,7 @@ dependencies = [ "downcast-rs", "euclid", "filedescriptor", - "futures-lite 1.13.0", + "futures-lite", "futures-util", "gl_generator", "glium", @@ -7449,8 +7336,8 @@ dependencies = [ "async-broadcast", "async-executor", "async-fs", - "async-io 2.3.2", - "async-lock 3.3.0", + "async-io", + "async-lock", "async-process", "async-recursion", "async-task", diff --git a/wezterm-ssh/Cargo.toml b/wezterm-ssh/Cargo.toml index 50f7c4ec7..cfc42f076 100644 --- a/wezterm-ssh/Cargo.toml +++ b/wezterm-ssh/Cargo.toml @@ -49,6 +49,5 @@ predicates = "3.0" env_logger = "0.11" rstest = "0.19" shell-words = "1.1" -smol-potat = "1.1.2" termwiz = { version = "0.22", path = "../termwiz" } whoami = "1.5" diff --git a/wezterm-ssh/tests/e2e/agent_forward.rs b/wezterm-ssh/tests/e2e/agent_forward.rs index 2af6cfd02..832a8b9dd 100644 --- a/wezterm-ssh/tests/e2e/agent_forward.rs +++ b/wezterm-ssh/tests/e2e/agent_forward.rs @@ -14,40 +14,42 @@ async fn session_with_agent_forward( } #[rstest] -#[smol_potat::test] #[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] #[cfg_attr(not(feature = "libssh-rs"), ignore)] -async fn ssh_add_should_be_able_to_list_identities_with_agent_forward( +fn ssh_add_should_be_able_to_list_identities_with_agent_forward( #[future] session_with_agent_forward: SessionWithSshd, ) { - let session: SessionWithSshd = session_with_agent_forward.await; + smol::block_on(async { + let session: SessionWithSshd = session_with_agent_forward.await; - let (pty, _child_process) = session - .request_pty("dumb", PtySize::default(), Some("ssh-add -l"), None) - .await - .unwrap(); - let mut reader = pty.try_clone_reader().unwrap(); - let mut output: String = String::new(); - reader.read_to_string(&mut output).unwrap(); - assert_eq!(output, "The agent has no identities.\r\n"); + let (pty, _child_process) = session + .request_pty("dumb", PtySize::default(), Some("ssh-add -l"), None) + .await + .unwrap(); + let mut reader = pty.try_clone_reader().unwrap(); + let mut output: String = String::new(); + reader.read_to_string(&mut output).unwrap(); + assert_eq!(output, "The agent has no identities.\r\n"); + }) } #[rstest] -#[smol_potat::test] #[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] #[cfg_attr(not(feature = "libssh-rs"), ignore)] -async fn no_agent_forward_should_happen_when_disabled(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; +fn no_agent_forward_should_happen_when_disabled(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; - let (pty, _child_process) = session - .request_pty("dumb", PtySize::default(), Some("ssh-add -l"), None) - .await - .unwrap(); - let mut reader = pty.try_clone_reader().unwrap(); - let mut output: String = String::new(); - reader.read_to_string(&mut output).unwrap(); - assert_eq!( - output, - "Could not open a connection to your authentication agent.\r\n" - ); + let (pty, _child_process) = session + .request_pty("dumb", PtySize::default(), Some("ssh-add -l"), None) + .await + .unwrap(); + let mut reader = pty.try_clone_reader().unwrap(); + let mut output: String = String::new(); + reader.read_to_string(&mut output).unwrap(); + assert_eq!( + output, + "Could not open a connection to your authentication agent.\r\n" + ); + }) } diff --git a/wezterm-ssh/tests/e2e/sftp.rs b/wezterm-ssh/tests/e2e/sftp.rs index 957d6982d..2fa8745f6 100644 --- a/wezterm-ssh/tests/e2e/sftp.rs +++ b/wezterm-ssh/tests/e2e/sftp.rs @@ -21,825 +21,852 @@ fn file_type_to_str(file_type: FileType) -> &'static str { } #[rstest] -#[smol_potat::test] #[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn read_dir_should_return_list_of_directories_files_and_symlinks( +fn read_dir_should_return_list_of_directories_files_and_symlinks( #[future] session: SessionWithSshd, ) { - let session: SessionWithSshd = session.await; + smol::block_on(async { + let session: SessionWithSshd = session.await; - // $TEMP/dir1/ - // $TEMP/dir2/ - // $TEMP/file1 - // $TEMP/file2 - // $TEMP/dir-link -> $TEMP/dir1/ - // $TEMP/file-link -> $TEMP/file1 - let temp = TempDir::new().unwrap(); - let dir1 = temp.child("dir1"); - dir1.create_dir_all().unwrap(); - let dir2 = temp.child("dir2"); - dir2.create_dir_all().unwrap(); - let file1 = temp.child("file1"); - file1.touch().unwrap(); - let file2 = temp.child("file2"); - file2.touch().unwrap(); - let link_dir = temp.child("link-dir"); - link_dir.symlink_to_dir(dir1.path()).unwrap(); - let link_file = temp.child("link-file"); - link_file.symlink_to_file(file1.path()).unwrap(); + // $TEMP/dir1/ + // $TEMP/dir2/ + // $TEMP/file1 + // $TEMP/file2 + // $TEMP/dir-link -> $TEMP/dir1/ + // $TEMP/file-link -> $TEMP/file1 + let temp = TempDir::new().unwrap(); + let dir1 = temp.child("dir1"); + dir1.create_dir_all().unwrap(); + let dir2 = temp.child("dir2"); + dir2.create_dir_all().unwrap(); + let file1 = temp.child("file1"); + file1.touch().unwrap(); + let file2 = temp.child("file2"); + file2.touch().unwrap(); + let link_dir = temp.child("link-dir"); + link_dir.symlink_to_dir(dir1.path()).unwrap(); + let link_file = temp.child("link-file"); + link_file.symlink_to_file(file1.path()).unwrap(); - let mut contents = session - .sftp() - .read_dir(temp.path().to_path_buf()) - .await - .expect("Failed to read directory") - .into_iter() - .map(|(p, s)| (p, file_type_to_str(s.ty))) - .collect::>(); - contents.sort_unstable_by_key(|(p, _)| p.to_path_buf()); + let mut contents = session + .sftp() + .read_dir(temp.path().to_path_buf()) + .await + .expect("Failed to read directory") + .into_iter() + .map(|(p, s)| (p, file_type_to_str(s.ty))) + .collect::>(); + contents.sort_unstable_by_key(|(p, _)| p.to_path_buf()); - assert_eq!( - contents, - vec![ - (dir1.path().to_path_buf().try_into().unwrap(), "dir"), - (dir2.path().to_path_buf().try_into().unwrap(), "dir"), - (file1.path().to_path_buf().try_into().unwrap(), "file"), - (file2.path().to_path_buf().try_into().unwrap(), "file"), - (link_dir.path().to_path_buf().try_into().unwrap(), "symlink"), - ( - link_file.path().to_path_buf().try_into().unwrap(), - "symlink" + assert_eq!( + contents, + vec![ + (dir1.path().to_path_buf().try_into().unwrap(), "dir"), + (dir2.path().to_path_buf().try_into().unwrap(), "dir"), + (file1.path().to_path_buf().try_into().unwrap(), "file"), + (file2.path().to_path_buf().try_into().unwrap(), "file"), + (link_dir.path().to_path_buf().try_into().unwrap(), "symlink"), + ( + link_file.path().to_path_buf().try_into().unwrap(), + "symlink" + ), + ] + ); + }) +} + +#[rstest] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] +fn create_dir_should_create_a_directory_on_the_remote_filesystem( + #[future] session: SessionWithSshd, +) { + smol::block_on(async { + let session: SessionWithSshd = session.await; + + let temp = TempDir::new().unwrap(); + + session + .sftp() + .create_dir(temp.child("dir").path().to_path_buf(), 0o644) + .await + .expect("Failed to create directory"); + + // Verify the path exists and is to a directory + temp.child("dir").assert(predicate::path::is_dir()); + }) +} + +#[rstest] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] +fn create_dir_should_return_error_if_unable_to_create_directory( + #[future] session: SessionWithSshd, +) { + smol::block_on(async { + let session: SessionWithSshd = session.await; + + let temp = TempDir::new().unwrap(); + + // Attempt to create a nested directory structure, which is not supported + let result = session + .sftp() + .create_dir(temp.child("dir").child("dir").path().to_path_buf(), 0o644) + .await; + assert!( + result.is_err(), + "Unexpectedly succeeded in creating directory: {:?}", + result + ); + + // Verify the path is not a directory + temp.child("dir") + .child("dir") + .assert(predicate::path::is_dir().not()); + temp.child("dir").assert(predicate::path::is_dir().not()); + }) +} + +#[rstest] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] +fn remove_dir_should_remove_a_remote_directory(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; + + let temp = TempDir::new().unwrap(); + + // Removing an empty directory should succeed + let dir = temp.child("dir"); + dir.create_dir_all().unwrap(); + session + .sftp() + .remove_dir(dir.path().to_path_buf()) + .await + .expect("Failed to remove directory"); + + // Verify the directory no longer exists + dir.assert(predicate::path::is_dir().not()); + }) +} + +#[rstest] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] +fn remove_dir_should_return_an_error_if_failed_to_remove_directory( + #[future] session: SessionWithSshd, +) { + smol::block_on(async { + let session: SessionWithSshd = session.await; + + let temp = TempDir::new().unwrap(); + + // Attempt to remove a missing path + let result = session + .sftp() + .remove_dir(temp.child("missing-dir").path().to_path_buf()) + .await; + assert!( + result.is_err(), + "Unexpectedly succeeded in removing missing directory: {:?}", + result + ); + + // Attempt to remove a non-empty directory + let dir = temp.child("dir"); + dir.create_dir_all().unwrap(); + dir.child("file").touch().unwrap(); + + let result = session.sftp().remove_dir(dir.path().to_path_buf()).await; + assert!( + result.is_err(), + "Unexpectedly succeeded in removing non-empty directory: {:?}", + result + ); + + // Verify the non-empty directory still exists + dir.assert(predicate::path::is_dir()); + + // Attempt to remove a file (not a directory) + let file = temp.child("file"); + file.touch().unwrap(); + let result = session.sftp().remove_dir(file.path().to_path_buf()).await; + assert!( + result.is_err(), + "Unexpectedly succeeded in removing file: {:?}", + result + ); + + // Verify the file still exists + file.assert(predicate::path::is_file()); + }) +} + +#[rstest] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] +fn metadata_should_return_metadata_about_a_file(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; + + let temp = TempDir::new().unwrap(); + let file = temp.child("file"); + file.touch().unwrap(); + + let metadata = session + .sftp() + .metadata(file.path().to_path_buf()) + .await + .expect("Failed to get metadata for file"); + + // Verify that file metadata makes sense + assert!(metadata.is_file(), "Invalid file metadata returned"); + }) +} + +#[rstest] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] +fn metadata_should_return_metadata_about_a_directory(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; + + let temp = TempDir::new().unwrap(); + let dir = temp.child("dir"); + dir.create_dir_all().unwrap(); + + let metadata = session + .sftp() + .metadata(dir.path().to_path_buf()) + .await + .expect("Failed to get metadata for dir"); + + // Verify that file metadata makes sense + assert!(metadata.is_dir(), "Invalid file metadata returned"); + }) +} + +#[rstest] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] +fn metadata_should_return_metadata_about_the_file_pointed_to_by_a_symlink( + #[future] session: SessionWithSshd, +) { + smol::block_on(async { + let session: SessionWithSshd = session.await; + + let temp = TempDir::new().unwrap(); + + let file = temp.child("file"); + file.touch().unwrap(); + let link = temp.child("link"); + link.symlink_to_file(file.path()).unwrap(); + + let metadata = session + .sftp() + .metadata(link.path().to_path_buf()) + .await + .expect("Failed to get metadata for symlink"); + + // Verify that file metadata makes sense + assert!(metadata.is_file(), "Invalid file metadata returned"); + assert!(metadata.ty.is_file(), "Invalid file metadata returned"); + assert!(!metadata.ty.is_symlink(), "Invalid file metadata returned"); + }) +} + +#[rstest] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] +fn metadata_should_return_metadata_about_the_dir_pointed_to_by_a_symlink( + #[future] session: SessionWithSshd, +) { + smol::block_on(async { + let session: SessionWithSshd = session.await; + + let temp = TempDir::new().unwrap(); + + let dir = temp.child("dir"); + dir.create_dir_all().unwrap(); + let link = temp.child("link"); + link.symlink_to_dir(dir.path()).unwrap(); + + let metadata = session + .sftp() + .metadata(link.path().to_path_buf()) + .await + .expect("Failed to get metadata for symlink"); + + // Verify that file metadata makes sense + assert!(metadata.is_dir(), "Invalid file metadata returned"); + assert!(metadata.ty.is_dir(), "Invalid file metadata returned"); + assert!(!metadata.ty.is_symlink(), "Invalid file metadata returned"); + }) +} + +#[rstest] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] +fn metadata_should_fail_if_path_missing(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; + + let temp = TempDir::new().unwrap(); + + let result = session + .sftp() + .metadata(temp.child("missing").path().to_path_buf()) + .await; + assert!( + result.is_err(), + "Metadata unexpectedly succeeded: {:?}", + result + ); + }) +} + +#[rstest] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] +fn symlink_metadata_should_return_metadata_about_a_file(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; + + let temp = TempDir::new().unwrap(); + let file = temp.child("file"); + file.touch().unwrap(); + + let symlink_metadata = session + .sftp() + .symlink_metadata(file.path().to_path_buf()) + .await + .expect("Failed to get metadata for file"); + + // Verify that file metadata makes sense + assert!(symlink_metadata.is_file(), "Invalid file metadata returned"); + }) +} + +#[rstest] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] +fn symlink_metadata_should_return_metadata_about_a_directory(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; + + let temp = TempDir::new().unwrap(); + let dir = temp.child("dir"); + dir.create_dir_all().unwrap(); + + let symlink_metadata = session + .sftp() + .symlink_metadata(dir.path().to_path_buf()) + .await + .expect("Failed to metadata for dir"); + + // Verify that file metadata makes sense + assert!(symlink_metadata.is_dir(), "Invalid file metadata returned"); + }) +} + +#[rstest] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] +fn symlink_metadata_should_return_metadata_about_symlink_pointing_to_a_file( + #[future] session: SessionWithSshd, +) { + smol::block_on(async { + let session: SessionWithSshd = session.await; + + let temp = TempDir::new().unwrap(); + + let file = temp.child("file"); + file.touch().unwrap(); + let link = temp.child("link"); + link.symlink_to_file(file.path()).unwrap(); + + let metadata = session + .sftp() + .symlink_metadata(link.path().to_path_buf()) + .await + .expect("Failed to get metadata for symlink"); + + // Verify that file metadata makes sense + assert!(!metadata.is_file(), "Invalid file metadata returned"); + assert!(!metadata.ty.is_file(), "Invalid file metadata returned"); + assert!(metadata.ty.is_symlink(), "Invalid file metadata returned"); + }) +} + +#[rstest] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] +fn symlink_metadata_should_return_metadata_about_symlink_pointing_to_a_directory( + #[future] session: SessionWithSshd, +) { + smol::block_on(async { + let session: SessionWithSshd = session.await; + + let temp = TempDir::new().unwrap(); + + let dir = temp.child("dir"); + dir.create_dir_all().unwrap(); + let link = temp.child("link"); + link.symlink_to_dir(dir.path()).unwrap(); + + let metadata = session + .sftp() + .symlink_metadata(link.path().to_path_buf()) + .await + .expect("Failed to get metadata for symlink"); + + // Verify that file metadata makes sense + assert!(!metadata.is_dir(), "Invalid file metadata returned"); + assert!(!metadata.ty.is_dir(), "Invalid file metadata returned"); + assert!(metadata.ty.is_symlink(), "Invalid file metadata returned"); + }) +} + +#[rstest] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] +fn symlink_metadata_should_fail_if_path_missing(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; + + let temp = TempDir::new().unwrap(); + + let result = session + .sftp() + .symlink_metadata(temp.child("missing").path().to_path_buf()) + .await; + assert!( + result.is_err(), + "symlink_metadata unexpectedly succeeded: {:?}", + result + ); + }) +} + +#[rstest] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] +fn symlink_should_create_symlink_pointing_to_file(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; + + let temp = TempDir::new().unwrap(); + let file = temp.child("file"); + file.touch().unwrap(); + + let link = temp.child("link"); + + session + .sftp() + .symlink(file.path().to_path_buf(), link.path().to_path_buf()) + .await + .expect("Failed to create symlink"); + + assert!( + std::fs::symlink_metadata(link.path()) + .unwrap() + .file_type() + .is_symlink(), + "Symlink is not a symlink!" + ); + + // TODO: This fails even though the type is a symlink: + // https://github.com/assert-rs/assert_fs/issues/70 + // link.assert(predicate::path::is_symlink()); + }) +} + +#[rstest] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] +fn symlink_should_create_symlink_pointing_to_directory(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; + + let temp = TempDir::new().unwrap(); + let dir = temp.child("dir"); + dir.create_dir_all().unwrap(); + + let link = temp.child("link"); + + session + .sftp() + .symlink(dir.path().to_path_buf(), link.path().to_path_buf()) + .await + .expect("Failed to create symlink"); + + link.assert(predicate::path::is_symlink()); + }) +} + +#[rstest] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] +fn symlink_should_succeed_even_if_path_missing(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; + + let temp = TempDir::new().unwrap(); + let file = temp.child("file"); + + let link = temp.child("link"); + + session + .sftp() + .symlink(file.path().to_path_buf(), link.path().to_path_buf()) + .await + .expect("Failed to create symlink"); + + link.assert(predicate::path::is_symlink()); + }) +} + +#[rstest] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] +fn read_link_should_return_the_target_of_the_symlink(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; + + let temp = TempDir::new().unwrap(); + + // Test a symlink to a directory + let dir = temp.child("dir"); + dir.create_dir_all().unwrap(); + let link = temp.child("link"); + link.symlink_to_dir(dir.path()).unwrap(); + + let path = session + .sftp() + .read_link(link.path().to_path_buf()) + .await + .expect("Failed to read symlink"); + assert_eq!(path, dir.path()); + + // Test a symlink to a file + let file = temp.child("file"); + file.touch().unwrap(); + let link = temp.child("link2"); + link.symlink_to_file(file.path()).unwrap(); + + let path = session + .sftp() + .read_link(link.path().to_path_buf()) + .await + .expect("Failed to read symlink"); + assert_eq!(path, file.path()); + }) +} + +#[rstest] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] +fn read_link_should_fail_if_path_is_not_a_symlink(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; + + let temp = TempDir::new().unwrap(); + + // Test missing path + let result = session + .sftp() + .read_link(temp.child("missing").path().to_path_buf()) + .await; + assert!( + result.is_err(), + "Unexpectedly read link for missing path: {:?}", + result + ); + + // Test a directory + let dir = temp.child("dir"); + dir.create_dir_all().unwrap(); + let result = session.sftp().read_link(dir.path().to_path_buf()).await; + assert!( + result.is_err(), + "Unexpectedly read link for directory: {:?}", + result + ); + + // Test a file + let file = temp.child("file"); + file.touch().unwrap(); + let result = session.sftp().read_link(file.path().to_path_buf()).await; + assert!( + result.is_err(), + "Unexpectedly read link for file: {:?}", + result + ); + }) +} + +#[rstest] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] +fn canonicalize_should_resolve_absolute_path_for_relative_path(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; + + // For resolving parts of a path, all components must exist + let temp = TempDir::new().unwrap(); + temp.child("hello").create_dir_all().unwrap(); + temp.child("world").touch().unwrap(); + + let rel = temp.child(".").child("hello").child("..").child("world"); + + // NOTE: Because sftp realpath can still resolve symlinks within a missing path, there + // is no guarantee that the resulting path matches the missing path. In fact, + // on mac the /tmp dir is a symlink to /private/tmp; so, we cannot successfully + // check the accuracy of the path itself, meaning that we can only validate + // that the operation was okay. + let result = session.sftp().canonicalize(rel.path().to_path_buf()).await; + assert!( + result.is_ok(), + "Canonicalize unexpectedly failed: {:?}", + result + ); + }) +} + +#[rstest] +#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] +fn canonicalize_should_either_return_resolved_path_or_error_if_missing( + #[future] session: SessionWithSshd, +) { + smol::block_on(async { + let session: SessionWithSshd = session.await; + + let temp = TempDir::new().unwrap(); + let missing = temp.child("missing"); + + // NOTE: Because sftp realpath can still resolve symlinks within a missing path, there + // is no guarantee that the resulting path matches the missing path. In fact, + // on mac the /tmp dir is a symlink to /private/tmp; so, we cannot successfully + // check the accuracy of the path itself, meaning that we can only validate + // that the operation was okay. + // + // Additionally, this has divergent behavior. On some platforms, this returns + // the path as is whereas on others this returns a missing path error. We + // have to support both checks. + let result = session + .sftp() + .canonicalize(missing.path().to_path_buf()) + .await; + match result { + Ok(_) => {} + Err(SftpChannelError::Sftp(SftpError::NoSuchFile)) => {} + #[cfg(feature = "libssh-rs")] + Err(SftpChannelError::LibSsh(libssh_rs::Error::Sftp(_))) => {} + x => panic!( + "Unexpected result from canonicalize({}: {:?}", + missing.path().display(), + x ), - ] - ); + } + }) } #[rstest] -#[smol_potat::test] #[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn create_dir_should_create_a_directory_on_the_remote_filesystem( +fn canonicalize_should_fail_if_resolving_missing_path_with_dots( #[future] session: SessionWithSshd, ) { - let session: SessionWithSshd = session.await; + smol::block_on(async { + let session: SessionWithSshd = session.await; - let temp = TempDir::new().unwrap(); + let temp = TempDir::new().unwrap(); + let missing = temp.child(".").child("hello").child("..").child("world"); - session - .sftp() - .create_dir(temp.child("dir").path().to_path_buf(), 0o644) - .await - .expect("Failed to create directory"); - - // Verify the path exists and is to a directory - temp.child("dir").assert(predicate::path::is_dir()); + let result = session + .sftp() + .canonicalize(missing.path().to_path_buf()) + .await; + assert!(result.is_err(), "Canonicalize unexpectedly succeeded"); + }) } #[rstest] -#[smol_potat::test] #[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn create_dir_should_return_error_if_unable_to_create_directory( - #[future] session: SessionWithSshd, -) { - let session: SessionWithSshd = session.await; +fn rename_should_support_singular_file(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; - let temp = TempDir::new().unwrap(); + let temp = TempDir::new().unwrap(); + let file = temp.child("file"); + file.write_str("some text").unwrap(); - // Attempt to create a nested directory structure, which is not supported - let result = session - .sftp() - .create_dir(temp.child("dir").child("dir").path().to_path_buf(), 0o644) - .await; - assert!( - result.is_err(), - "Unexpectedly succeeded in creating directory: {:?}", - result - ); + let dst = temp.child("dst"); - // Verify the path is not a directory - temp.child("dir") - .child("dir") - .assert(predicate::path::is_dir().not()); - temp.child("dir").assert(predicate::path::is_dir().not()); + session + .sftp() + .rename( + file.path().to_path_buf(), + dst.path().to_path_buf(), + Default::default(), + ) + .await + .expect("Failed to rename file"); + + // Verify that file was moved to destination + file.assert(predicate::path::missing()); + dst.assert("some text"); + }) } #[rstest] -#[smol_potat::test] #[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn remove_dir_should_remove_a_remote_directory(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; +fn rename_should_support_dirtectory(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; - let temp = TempDir::new().unwrap(); + let temp = TempDir::new().unwrap(); + let dir = temp.child("dir"); + dir.create_dir_all().unwrap(); + let dir_file = dir.child("file"); + dir_file.write_str("some text").unwrap(); + let dir_dir = dir.child("dir"); + dir_dir.create_dir_all().unwrap(); - // Removing an empty directory should succeed - let dir = temp.child("dir"); - dir.create_dir_all().unwrap(); - session - .sftp() - .remove_dir(dir.path().to_path_buf()) - .await - .expect("Failed to remove directory"); + let dst = temp.child("dst"); - // Verify the directory no longer exists - dir.assert(predicate::path::is_dir().not()); + session + .sftp() + .rename( + dir.path().to_path_buf(), + dst.path().to_path_buf(), + Default::default(), + ) + .await + .expect("Failed to rename directory"); + + // Verify that directory was moved to destination + dir.assert(predicate::path::missing()); + dir_file.assert(predicate::path::missing()); + dir_dir.assert(predicate::path::missing()); + + dst.assert(predicate::path::is_dir()); + dst.child("file").assert("some text"); + dst.child("dir").assert(predicate::path::is_dir()); + }) } #[rstest] -#[smol_potat::test] #[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn remove_dir_should_return_an_error_if_failed_to_remove_directory( - #[future] session: SessionWithSshd, -) { - let session: SessionWithSshd = session.await; +fn rename_should_fail_if_source_path_missing(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; - let temp = TempDir::new().unwrap(); + let temp = TempDir::new().unwrap(); + let missing = temp.child("missing"); + let dst = temp.child("dst"); - // Attempt to remove a missing path - let result = session - .sftp() - .remove_dir(temp.child("missing-dir").path().to_path_buf()) - .await; - assert!( - result.is_err(), - "Unexpectedly succeeded in removing missing directory: {:?}", - result - ); - - // Attempt to remove a non-empty directory - let dir = temp.child("dir"); - dir.create_dir_all().unwrap(); - dir.child("file").touch().unwrap(); - - let result = session.sftp().remove_dir(dir.path().to_path_buf()).await; - assert!( - result.is_err(), - "Unexpectedly succeeded in removing non-empty directory: {:?}", - result - ); - - // Verify the non-empty directory still exists - dir.assert(predicate::path::is_dir()); - - // Attempt to remove a file (not a directory) - let file = temp.child("file"); - file.touch().unwrap(); - let result = session.sftp().remove_dir(file.path().to_path_buf()).await; - assert!( - result.is_err(), - "Unexpectedly succeeded in removing file: {:?}", - result - ); - - // Verify the file still exists - file.assert(predicate::path::is_file()); + let result = session + .sftp() + .rename( + missing.path().to_path_buf(), + dst.path().to_path_buf(), + Default::default(), + ) + .await; + assert!( + result.is_err(), + "Rename unexpectedly succeeded with missing path: {:?}", + result + ); + }) } #[rstest] -#[smol_potat::test] #[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn metadata_should_return_metadata_about_a_file(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; +fn remove_file_should_remove_file(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; - let temp = TempDir::new().unwrap(); - let file = temp.child("file"); - file.touch().unwrap(); + let temp = TempDir::new().unwrap(); + let file = temp.child("file"); + file.touch().unwrap(); - let metadata = session - .sftp() - .metadata(file.path().to_path_buf()) - .await - .expect("Failed to get metadata for file"); + session + .sftp() + .remove_file(file.path().to_path_buf()) + .await + .expect("Failed to remove file"); - // Verify that file metadata makes sense - assert!(metadata.is_file(), "Invalid file metadata returned"); + file.assert(predicate::path::missing()); + }) } #[rstest] -#[smol_potat::test] #[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn metadata_should_return_metadata_about_a_directory(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; +fn remove_file_should_remove_symlink_to_file(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; - let temp = TempDir::new().unwrap(); - let dir = temp.child("dir"); - dir.create_dir_all().unwrap(); + let temp = TempDir::new().unwrap(); + let file = temp.child("file"); + file.touch().unwrap(); + let link = temp.child("link"); + link.symlink_to_file(file.path()).unwrap(); - let metadata = session - .sftp() - .metadata(dir.path().to_path_buf()) - .await - .expect("Failed to get metadata for dir"); + session + .sftp() + .remove_file(link.path().to_path_buf()) + .await + .expect("Failed to remove symlink"); - // Verify that file metadata makes sense - assert!(metadata.is_dir(), "Invalid file metadata returned"); + // Verify link removed but file still exists + link.assert(predicate::path::missing()); + file.assert(predicate::path::is_file()); + }) } #[rstest] -#[smol_potat::test] #[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn metadata_should_return_metadata_about_the_file_pointed_to_by_a_symlink( - #[future] session: SessionWithSshd, -) { - let session: SessionWithSshd = session.await; +fn remove_file_should_remove_symlink_to_directory(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; - let temp = TempDir::new().unwrap(); + let temp = TempDir::new().unwrap(); + let dir = temp.child("dir"); + dir.create_dir_all().unwrap(); + let link = temp.child("link"); + link.symlink_to_dir(dir.path()).unwrap(); - let file = temp.child("file"); - file.touch().unwrap(); - let link = temp.child("link"); - link.symlink_to_file(file.path()).unwrap(); + session + .sftp() + .remove_file(link.path().to_path_buf()) + .await + .expect("Failed to remove symlink"); - let metadata = session - .sftp() - .metadata(link.path().to_path_buf()) - .await - .expect("Failed to get metadata for symlink"); - - // Verify that file metadata makes sense - assert!(metadata.is_file(), "Invalid file metadata returned"); - assert!(metadata.ty.is_file(), "Invalid file metadata returned"); - assert!(!metadata.ty.is_symlink(), "Invalid file metadata returned"); + // Verify link removed but directory still exists + link.assert(predicate::path::missing()); + dir.assert(predicate::path::is_dir()); + }) } #[rstest] -#[smol_potat::test] #[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn metadata_should_return_metadata_about_the_dir_pointed_to_by_a_symlink( - #[future] session: SessionWithSshd, -) { - let session: SessionWithSshd = session.await; +fn remove_file_should_fail_if_path_to_directory(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; - let temp = TempDir::new().unwrap(); + let temp = TempDir::new().unwrap(); + let dir = temp.child("dir"); + dir.create_dir_all().unwrap(); - let dir = temp.child("dir"); - dir.create_dir_all().unwrap(); - let link = temp.child("link"); - link.symlink_to_dir(dir.path()).unwrap(); + let result = session.sftp().remove_file(dir.path().to_path_buf()).await; + assert!( + result.is_err(), + "Unexpectedly removed directory: {:?}", + result + ); - let metadata = session - .sftp() - .metadata(link.path().to_path_buf()) - .await - .expect("Failed to get metadata for symlink"); - - // Verify that file metadata makes sense - assert!(metadata.is_dir(), "Invalid file metadata returned"); - assert!(metadata.ty.is_dir(), "Invalid file metadata returned"); - assert!(!metadata.ty.is_symlink(), "Invalid file metadata returned"); + // Verify directory still here + dir.assert(predicate::path::is_dir()); + }) } #[rstest] -#[smol_potat::test] #[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn metadata_should_fail_if_path_missing(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; +fn remove_file_should_fail_if_path_missing(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; - let temp = TempDir::new().unwrap(); + let temp = TempDir::new().unwrap(); - let result = session - .sftp() - .metadata(temp.child("missing").path().to_path_buf()) - .await; - assert!( - result.is_err(), - "Metadata unexpectedly succeeded: {:?}", - result - ); -} - -#[rstest] -#[smol_potat::test] -#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn symlink_metadata_should_return_metadata_about_a_file(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; - - let temp = TempDir::new().unwrap(); - let file = temp.child("file"); - file.touch().unwrap(); - - let symlink_metadata = session - .sftp() - .symlink_metadata(file.path().to_path_buf()) - .await - .expect("Failed to get metadata for file"); - - // Verify that file metadata makes sense - assert!(symlink_metadata.is_file(), "Invalid file metadata returned"); -} - -#[rstest] -#[smol_potat::test] -#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn symlink_metadata_should_return_metadata_about_a_directory( - #[future] session: SessionWithSshd, -) { - let session: SessionWithSshd = session.await; - - let temp = TempDir::new().unwrap(); - let dir = temp.child("dir"); - dir.create_dir_all().unwrap(); - - let symlink_metadata = session - .sftp() - .symlink_metadata(dir.path().to_path_buf()) - .await - .expect("Failed to metadata for dir"); - - // Verify that file metadata makes sense - assert!(symlink_metadata.is_dir(), "Invalid file metadata returned"); -} - -#[rstest] -#[smol_potat::test] -#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn symlink_metadata_should_return_metadata_about_symlink_pointing_to_a_file( - #[future] session: SessionWithSshd, -) { - let session: SessionWithSshd = session.await; - - let temp = TempDir::new().unwrap(); - - let file = temp.child("file"); - file.touch().unwrap(); - let link = temp.child("link"); - link.symlink_to_file(file.path()).unwrap(); - - let metadata = session - .sftp() - .symlink_metadata(link.path().to_path_buf()) - .await - .expect("Failed to get metadata for symlink"); - - // Verify that file metadata makes sense - assert!(!metadata.is_file(), "Invalid file metadata returned"); - assert!(!metadata.ty.is_file(), "Invalid file metadata returned"); - assert!(metadata.ty.is_symlink(), "Invalid file metadata returned"); -} - -#[rstest] -#[smol_potat::test] -#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn symlink_metadata_should_return_metadata_about_symlink_pointing_to_a_directory( - #[future] session: SessionWithSshd, -) { - let session: SessionWithSshd = session.await; - - let temp = TempDir::new().unwrap(); - - let dir = temp.child("dir"); - dir.create_dir_all().unwrap(); - let link = temp.child("link"); - link.symlink_to_dir(dir.path()).unwrap(); - - let metadata = session - .sftp() - .symlink_metadata(link.path().to_path_buf()) - .await - .expect("Failed to get metadata for symlink"); - - // Verify that file metadata makes sense - assert!(!metadata.is_dir(), "Invalid file metadata returned"); - assert!(!metadata.ty.is_dir(), "Invalid file metadata returned"); - assert!(metadata.ty.is_symlink(), "Invalid file metadata returned"); -} - -#[rstest] -#[smol_potat::test] -#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn symlink_metadata_should_fail_if_path_missing(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; - - let temp = TempDir::new().unwrap(); - - let result = session - .sftp() - .symlink_metadata(temp.child("missing").path().to_path_buf()) - .await; - assert!( - result.is_err(), - "symlink_metadata unexpectedly succeeded: {:?}", - result - ); -} - -#[rstest] -#[smol_potat::test] -#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn symlink_should_create_symlink_pointing_to_file(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; - - let temp = TempDir::new().unwrap(); - let file = temp.child("file"); - file.touch().unwrap(); - - let link = temp.child("link"); - - session - .sftp() - .symlink(file.path().to_path_buf(), link.path().to_path_buf()) - .await - .expect("Failed to create symlink"); - - assert!( - std::fs::symlink_metadata(link.path()) - .unwrap() - .file_type() - .is_symlink(), - "Symlink is not a symlink!" - ); - - // TODO: This fails even though the type is a symlink: - // https://github.com/assert-rs/assert_fs/issues/70 - // link.assert(predicate::path::is_symlink()); -} - -#[rstest] -#[smol_potat::test] -#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn symlink_should_create_symlink_pointing_to_directory(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; - - let temp = TempDir::new().unwrap(); - let dir = temp.child("dir"); - dir.create_dir_all().unwrap(); - - let link = temp.child("link"); - - session - .sftp() - .symlink(dir.path().to_path_buf(), link.path().to_path_buf()) - .await - .expect("Failed to create symlink"); - - link.assert(predicate::path::is_symlink()); -} - -#[rstest] -#[smol_potat::test] -#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn symlink_should_succeed_even_if_path_missing(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; - - let temp = TempDir::new().unwrap(); - let file = temp.child("file"); - - let link = temp.child("link"); - - session - .sftp() - .symlink(file.path().to_path_buf(), link.path().to_path_buf()) - .await - .expect("Failed to create symlink"); - - link.assert(predicate::path::is_symlink()); -} - -#[rstest] -#[smol_potat::test] -#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn read_link_should_return_the_target_of_the_symlink(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; - - let temp = TempDir::new().unwrap(); - - // Test a symlink to a directory - let dir = temp.child("dir"); - dir.create_dir_all().unwrap(); - let link = temp.child("link"); - link.symlink_to_dir(dir.path()).unwrap(); - - let path = session - .sftp() - .read_link(link.path().to_path_buf()) - .await - .expect("Failed to read symlink"); - assert_eq!(path, dir.path()); - - // Test a symlink to a file - let file = temp.child("file"); - file.touch().unwrap(); - let link = temp.child("link2"); - link.symlink_to_file(file.path()).unwrap(); - - let path = session - .sftp() - .read_link(link.path().to_path_buf()) - .await - .expect("Failed to read symlink"); - assert_eq!(path, file.path()); -} - -#[rstest] -#[smol_potat::test] -#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn read_link_should_fail_if_path_is_not_a_symlink(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; - - let temp = TempDir::new().unwrap(); - - // Test missing path - let result = session - .sftp() - .read_link(temp.child("missing").path().to_path_buf()) - .await; - assert!( - result.is_err(), - "Unexpectedly read link for missing path: {:?}", - result - ); - - // Test a directory - let dir = temp.child("dir"); - dir.create_dir_all().unwrap(); - let result = session.sftp().read_link(dir.path().to_path_buf()).await; - assert!( - result.is_err(), - "Unexpectedly read link for directory: {:?}", - result - ); - - // Test a file - let file = temp.child("file"); - file.touch().unwrap(); - let result = session.sftp().read_link(file.path().to_path_buf()).await; - assert!( - result.is_err(), - "Unexpectedly read link for file: {:?}", - result - ); -} - -#[rstest] -#[smol_potat::test] -#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn canonicalize_should_resolve_absolute_path_for_relative_path( - #[future] session: SessionWithSshd, -) { - let session: SessionWithSshd = session.await; - - // For resolving parts of a path, all components must exist - let temp = TempDir::new().unwrap(); - temp.child("hello").create_dir_all().unwrap(); - temp.child("world").touch().unwrap(); - - let rel = temp.child(".").child("hello").child("..").child("world"); - - // NOTE: Because sftp realpath can still resolve symlinks within a missing path, there - // is no guarantee that the resulting path matches the missing path. In fact, - // on mac the /tmp dir is a symlink to /private/tmp; so, we cannot successfully - // check the accuracy of the path itself, meaning that we can only validate - // that the operation was okay. - let result = session.sftp().canonicalize(rel.path().to_path_buf()).await; - assert!( - result.is_ok(), - "Canonicalize unexpectedly failed: {:?}", - result - ); -} - -#[rstest] -#[smol_potat::test] -#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn canonicalize_should_either_return_resolved_path_or_error_if_missing( - #[future] session: SessionWithSshd, -) { - let session: SessionWithSshd = session.await; - - let temp = TempDir::new().unwrap(); - let missing = temp.child("missing"); - - // NOTE: Because sftp realpath can still resolve symlinks within a missing path, there - // is no guarantee that the resulting path matches the missing path. In fact, - // on mac the /tmp dir is a symlink to /private/tmp; so, we cannot successfully - // check the accuracy of the path itself, meaning that we can only validate - // that the operation was okay. - // - // Additionally, this has divergent behavior. On some platforms, this returns - // the path as is whereas on others this returns a missing path error. We - // have to support both checks. - let result = session - .sftp() - .canonicalize(missing.path().to_path_buf()) - .await; - match result { - Ok(_) => {} - Err(SftpChannelError::Sftp(SftpError::NoSuchFile)) => {} - #[cfg(feature = "libssh-rs")] - Err(SftpChannelError::LibSsh(libssh_rs::Error::Sftp(_))) => {} - x => panic!( - "Unexpected result from canonicalize({}: {:?}", - missing.path().display(), - x - ), - } -} - -#[rstest] -#[smol_potat::test] -#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn canonicalize_should_fail_if_resolving_missing_path_with_dots( - #[future] session: SessionWithSshd, -) { - let session: SessionWithSshd = session.await; - - let temp = TempDir::new().unwrap(); - let missing = temp.child(".").child("hello").child("..").child("world"); - - let result = session - .sftp() - .canonicalize(missing.path().to_path_buf()) - .await; - assert!(result.is_err(), "Canonicalize unexpectedly succeeded"); -} - -#[rstest] -#[smol_potat::test] -#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn rename_should_support_singular_file(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; - - let temp = TempDir::new().unwrap(); - let file = temp.child("file"); - file.write_str("some text").unwrap(); - - let dst = temp.child("dst"); - - session - .sftp() - .rename( - file.path().to_path_buf(), - dst.path().to_path_buf(), - Default::default(), - ) - .await - .expect("Failed to rename file"); - - // Verify that file was moved to destination - file.assert(predicate::path::missing()); - dst.assert("some text"); -} - -#[rstest] -#[smol_potat::test] -#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn rename_should_support_dirtectory(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; - - let temp = TempDir::new().unwrap(); - let dir = temp.child("dir"); - dir.create_dir_all().unwrap(); - let dir_file = dir.child("file"); - dir_file.write_str("some text").unwrap(); - let dir_dir = dir.child("dir"); - dir_dir.create_dir_all().unwrap(); - - let dst = temp.child("dst"); - - session - .sftp() - .rename( - dir.path().to_path_buf(), - dst.path().to_path_buf(), - Default::default(), - ) - .await - .expect("Failed to rename directory"); - - // Verify that directory was moved to destination - dir.assert(predicate::path::missing()); - dir_file.assert(predicate::path::missing()); - dir_dir.assert(predicate::path::missing()); - - dst.assert(predicate::path::is_dir()); - dst.child("file").assert("some text"); - dst.child("dir").assert(predicate::path::is_dir()); -} - -#[rstest] -#[smol_potat::test] -#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn rename_should_fail_if_source_path_missing(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; - - let temp = TempDir::new().unwrap(); - let missing = temp.child("missing"); - let dst = temp.child("dst"); - - let result = session - .sftp() - .rename( - missing.path().to_path_buf(), - dst.path().to_path_buf(), - Default::default(), - ) - .await; - assert!( - result.is_err(), - "Rename unexpectedly succeeded with missing path: {:?}", - result - ); -} - -#[rstest] -#[smol_potat::test] -#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn remove_file_should_remove_file(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; - - let temp = TempDir::new().unwrap(); - let file = temp.child("file"); - file.touch().unwrap(); - - session - .sftp() - .remove_file(file.path().to_path_buf()) - .await - .expect("Failed to remove file"); - - file.assert(predicate::path::missing()); -} - -#[rstest] -#[smol_potat::test] -#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn remove_file_should_remove_symlink_to_file(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; - - let temp = TempDir::new().unwrap(); - let file = temp.child("file"); - file.touch().unwrap(); - let link = temp.child("link"); - link.symlink_to_file(file.path()).unwrap(); - - session - .sftp() - .remove_file(link.path().to_path_buf()) - .await - .expect("Failed to remove symlink"); - - // Verify link removed but file still exists - link.assert(predicate::path::missing()); - file.assert(predicate::path::is_file()); -} - -#[rstest] -#[smol_potat::test] -#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn remove_file_should_remove_symlink_to_directory(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; - - let temp = TempDir::new().unwrap(); - let dir = temp.child("dir"); - dir.create_dir_all().unwrap(); - let link = temp.child("link"); - link.symlink_to_dir(dir.path()).unwrap(); - - session - .sftp() - .remove_file(link.path().to_path_buf()) - .await - .expect("Failed to remove symlink"); - - // Verify link removed but directory still exists - link.assert(predicate::path::missing()); - dir.assert(predicate::path::is_dir()); -} - -#[rstest] -#[smol_potat::test] -#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn remove_file_should_fail_if_path_to_directory(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; - - let temp = TempDir::new().unwrap(); - let dir = temp.child("dir"); - dir.create_dir_all().unwrap(); - - let result = session.sftp().remove_file(dir.path().to_path_buf()).await; - assert!( - result.is_err(), - "Unexpectedly removed directory: {:?}", - result - ); - - // Verify directory still here - dir.assert(predicate::path::is_dir()); -} - -#[rstest] -#[smol_potat::test] -#[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn remove_file_should_fail_if_path_missing(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; - - let temp = TempDir::new().unwrap(); - - let result = session - .sftp() - .remove_file(temp.child("missing").path().to_path_buf()) - .await; - assert!( - result.is_err(), - "Unexpectedly removed missing path: {:?}", - result - ); + let result = session + .sftp() + .remove_file(temp.child("missing").path().to_path_buf()) + .await; + assert!( + result.is_err(), + "Unexpectedly removed missing path: {:?}", + result + ); + }) } diff --git a/wezterm-ssh/tests/e2e/sftp/file.rs b/wezterm-ssh/tests/e2e/sftp/file.rs index a7aa164af..8ccfb9c19 100644 --- a/wezterm-ssh/tests/e2e/sftp/file.rs +++ b/wezterm-ssh/tests/e2e/sftp/file.rs @@ -7,187 +7,193 @@ use std::convert::TryInto; use std::path::PathBuf; #[rstest] -#[smol_potat::test] #[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn metadata_should_retrieve_file_stat(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; +fn metadata_should_retrieve_file_stat(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; - let temp = TempDir::new().unwrap(); - let file = temp.child("test-file"); - file.touch().unwrap(); + let temp = TempDir::new().unwrap(); + let file = temp.child("test-file"); + file.touch().unwrap(); - let remote_file = session - .sftp() - .open(file.path().to_path_buf()) - .await - .expect("Failed to open remote file"); + let remote_file = session + .sftp() + .open(file.path().to_path_buf()) + .await + .expect("Failed to open remote file"); - let metadata = remote_file - .metadata() - .await - .expect("Failed to read file metadata"); + let metadata = remote_file + .metadata() + .await + .expect("Failed to read file metadata"); - // Verify that file stat makes sense - assert!(metadata.is_file(), "Invalid file metadata returned"); + // Verify that file stat makes sense + assert!(metadata.is_file(), "Invalid file metadata returned"); + }) } #[rstest] -#[smol_potat::test] #[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn read_dir_should_retrieve_next_dir_entry(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; +fn read_dir_should_retrieve_next_dir_entry(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; - let temp = TempDir::new().unwrap(); - let dir = temp.child("dir"); - dir.create_dir_all().unwrap(); - let file = temp.child("file"); - file.touch().unwrap(); - let link = temp.child("link"); - link.symlink_to_file(file.path()).unwrap(); + let temp = TempDir::new().unwrap(); + let dir = temp.child("dir"); + dir.create_dir_all().unwrap(); + let file = temp.child("file"); + file.touch().unwrap(); + let link = temp.child("link"); + link.symlink_to_file(file.path()).unwrap(); - let remote_dir = session - .sftp() - .open_dir(temp.path().to_path_buf()) - .await - .expect("Failed to open remote directory"); + let remote_dir = session + .sftp() + .open_dir(temp.path().to_path_buf()) + .await + .expect("Failed to open remote directory"); - // Collect all of the directory contents (. and .. are included) - let mut contents = Vec::new(); - while let Ok((path, metadata)) = remote_dir.read_dir().await { - let ft = metadata.ty; - contents.push(( - path, - if ft.is_dir() { - "dir" - } else if ft.is_file() { - "file" - } else { - "symlink" - }, - )); - } - contents.sort_unstable_by_key(|(p, _)| p.to_path_buf()); + // Collect all of the directory contents (. and .. are included) + let mut contents = Vec::new(); + while let Ok((path, metadata)) = remote_dir.read_dir().await { + let ft = metadata.ty; + contents.push(( + path, + if ft.is_dir() { + "dir" + } else if ft.is_file() { + "file" + } else { + "symlink" + }, + )); + } + contents.sort_unstable_by_key(|(p, _)| p.to_path_buf()); - assert_eq!( - contents, - vec![ - (PathBuf::from(".").try_into().unwrap(), "dir"), - (PathBuf::from("..").try_into().unwrap(), "dir"), - (PathBuf::from("dir").try_into().unwrap(), "dir"), - (PathBuf::from("file").try_into().unwrap(), "file"), - (PathBuf::from("link").try_into().unwrap(), "symlink"), - ] - ); + assert_eq!( + contents, + vec![ + (PathBuf::from(".").try_into().unwrap(), "dir"), + (PathBuf::from("..").try_into().unwrap(), "dir"), + (PathBuf::from("dir").try_into().unwrap(), "dir"), + (PathBuf::from("file").try_into().unwrap(), "file"), + (PathBuf::from("link").try_into().unwrap(), "symlink"), + ] + ); + }) } #[rstest] -#[smol_potat::test] #[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn should_support_async_reading(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; +fn should_support_async_reading(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; - let temp = TempDir::new().unwrap(); - let file = temp.child("test-file"); - file.write_str("some file contents").unwrap(); + let temp = TempDir::new().unwrap(); + let file = temp.child("test-file"); + file.write_str("some file contents").unwrap(); - let mut remote_file = session - .sftp() - .open(file.path().to_path_buf()) - .await - .expect("Failed to open remote file"); + let mut remote_file = session + .sftp() + .open(file.path().to_path_buf()) + .await + .expect("Failed to open remote file"); - let mut contents = String::new(); - remote_file - .read_to_string(&mut contents) - .await - .expect("Failed to read file to string"); + let mut contents = String::new(); + remote_file + .read_to_string(&mut contents) + .await + .expect("Failed to read file to string"); - assert_eq!(contents, "some file contents"); + assert_eq!(contents, "some file contents"); - // NOTE: Testing second time to ensure future is properly cleared - let mut contents = String::new(); - remote_file - .read_to_string(&mut contents) - .await - .expect("Failed to read file to string second time"); + // NOTE: Testing second time to ensure future is properly cleared + let mut contents = String::new(); + remote_file + .read_to_string(&mut contents) + .await + .expect("Failed to read file to string second time"); + }) } #[rstest] -#[smol_potat::test] #[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn should_support_async_writing(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; +fn should_support_async_writing(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; - let temp = TempDir::new().unwrap(); - let file = temp.child("test-file"); - file.write_str("some file contents").unwrap(); + let temp = TempDir::new().unwrap(); + let file = temp.child("test-file"); + file.write_str("some file contents").unwrap(); - let mut remote_file = session - .sftp() - .create(file.path().to_path_buf()) - .await - .expect("Failed to open remote file"); + let mut remote_file = session + .sftp() + .create(file.path().to_path_buf()) + .await + .expect("Failed to open remote file"); - remote_file - .write_all(b"new contents for file") - .await - .expect("Failed to write to file"); + remote_file + .write_all(b"new contents for file") + .await + .expect("Failed to write to file"); - file.assert("new contents for file"); + file.assert("new contents for file"); - // NOTE: Testing second time to ensure future is properly cleared - remote_file - .write_all(b"new contents for file") - .await - .expect("Failed to write to file second time"); + // NOTE: Testing second time to ensure future is properly cleared + remote_file + .write_all(b"new contents for file") + .await + .expect("Failed to write to file second time"); + }) } #[rstest] -#[smol_potat::test] #[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn should_support_async_flush(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; +fn should_support_async_flush(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; - let temp = TempDir::new().unwrap(); - let file = temp.child("test-file"); - file.write_str("some file contents").unwrap(); + let temp = TempDir::new().unwrap(); + let file = temp.child("test-file"); + file.write_str("some file contents").unwrap(); - let mut remote_file = session - .sftp() - .create(file.path().to_path_buf()) - .await - .expect("Failed to open remote file"); + let mut remote_file = session + .sftp() + .create(file.path().to_path_buf()) + .await + .expect("Failed to open remote file"); - remote_file.flush().await.expect("Failed to flush file"); + remote_file.flush().await.expect("Failed to flush file"); - // NOTE: Testing second time to ensure future is properly cleared - remote_file - .flush() - .await - .expect("Failed to flush file second time"); + // NOTE: Testing second time to ensure future is properly cleared + remote_file + .flush() + .await + .expect("Failed to flush file second time"); + }) } #[rstest] -#[smol_potat::test] #[cfg_attr(not(any(target_os = "macos", target_os = "linux")), ignore)] -async fn should_support_async_close(#[future] session: SessionWithSshd) { - let session: SessionWithSshd = session.await; +fn should_support_async_close(#[future] session: SessionWithSshd) { + smol::block_on(async { + let session: SessionWithSshd = session.await; - let temp = TempDir::new().unwrap(); - let file = temp.child("test-file"); - file.write_str("some file contents").unwrap(); + let temp = TempDir::new().unwrap(); + let file = temp.child("test-file"); + file.write_str("some file contents").unwrap(); - let mut remote_file = session - .sftp() - .create(file.path().to_path_buf()) - .await - .expect("Failed to open remote file"); + let mut remote_file = session + .sftp() + .create(file.path().to_path_buf()) + .await + .expect("Failed to open remote file"); - remote_file.close().await.expect("Failed to close file"); + remote_file.close().await.expect("Failed to close file"); - // NOTE: Testing second time to ensure future is properly cleared - remote_file - .close() - .await - .expect("Failed to close file second time"); + // NOTE: Testing second time to ensure future is properly cleared + remote_file + .close() + .await + .expect("Failed to close file second time"); + }) } diff --git a/window/Cargo.toml b/window/Cargo.toml index 4cbf86c14..dcfe914f6 100644 --- a/window/Cargo.toml +++ b/window/Cargo.toml @@ -71,7 +71,7 @@ winreg = "0.10" dirs-next = "2.0" filedescriptor = { version="0.8", path = "../filedescriptor" } futures-util = "0.3" -futures-lite = "1.12" +futures-lite = "2.3" x11 = {version ="2.21", features = ["xlib_xcb", "xlib"]} xcb = {version="1.3", features=["render", "randr", "dri2", "xkb", "xlib_xcb", "present", "as-raw-xcb-connection"]} xkbcommon = { version = "0.7.0", features = ["x11", "wayland"] }