From 4a3710b1c45c1b989beba0424979f8c0ded25775 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Sun, 14 Sep 2025 02:50:28 +0800 Subject: [PATCH] feat: new `yazi-sftp` crate (#3166) --- Cargo.lock | 1262 +++++++++++++++++++++++++-- Cargo.toml | 5 +- yazi-fs/Cargo.toml | 2 +- yazi-plugin/Cargo.toml | 2 +- yazi-sftp/Cargo.toml | 19 + yazi-sftp/LICENSE | 201 +++++ yazi-sftp/README.md | 15 + yazi-sftp/src/byte_str.rs | 100 +++ yazi-sftp/src/de.rs | 375 ++++++++ yazi-sftp/src/error.rs | 66 ++ yazi-sftp/src/fs/attrs.rs | 146 ++++ yazi-sftp/src/fs/dir_entry.rs | 46 + yazi-sftp/src/fs/file.rs | 165 ++++ yazi-sftp/src/fs/flags.rs | 16 + yazi-sftp/src/fs/mod.rs | 11 + yazi-sftp/src/fs/read_dir.rs | 56 ++ yazi-sftp/src/id.rs | 18 + yazi-sftp/src/lib.rs | 22 + yazi-sftp/src/macros.rs | 30 + yazi-sftp/src/packet.rs | 265 ++++++ yazi-sftp/src/requests/close.rs | 15 + yazi-sftp/src/requests/extended.rs | 71 ++ yazi-sftp/src/requests/fstat.rs | 15 + yazi-sftp/src/requests/init.rs | 22 + yazi-sftp/src/requests/lstat.rs | 15 + yazi-sftp/src/requests/mkdir.rs | 21 + yazi-sftp/src/requests/mod.rs | 39 + yazi-sftp/src/requests/open.rs | 24 + yazi-sftp/src/requests/open_dir.rs | 15 + yazi-sftp/src/requests/read.rs | 27 + yazi-sftp/src/requests/read_dir.rs | 15 + yazi-sftp/src/requests/readlink.rs | 15 + yazi-sftp/src/requests/realpath.rs | 15 + yazi-sftp/src/requests/remove.rs | 15 + yazi-sftp/src/requests/rename.rs | 22 + yazi-sftp/src/requests/rmdir.rs | 15 + yazi-sftp/src/requests/set_stat.rs | 35 + yazi-sftp/src/requests/stat.rs | 15 + yazi-sftp/src/requests/symlink.rs | 24 + yazi-sftp/src/requests/write.rs | 25 + yazi-sftp/src/responses/attrs.rs | 13 + yazi-sftp/src/responses/data.rs | 11 + yazi-sftp/src/responses/extended.rs | 85 ++ yazi-sftp/src/responses/handle.rs | 11 + yazi-sftp/src/responses/mod.rs | 15 + yazi-sftp/src/responses/name.rs | 26 + yazi-sftp/src/responses/status.rs | 43 + yazi-sftp/src/responses/version.rs | 16 + yazi-sftp/src/ser.rs | 286 ++++++ yazi-sftp/src/session.rs | 265 ++++++ yazi-shared/Cargo.toml | 2 +- yazi-term/Cargo.toml | 2 +- 52 files changed, 3963 insertions(+), 94 deletions(-) create mode 100644 yazi-sftp/Cargo.toml create mode 100644 yazi-sftp/LICENSE create mode 100644 yazi-sftp/README.md create mode 100644 yazi-sftp/src/byte_str.rs create mode 100644 yazi-sftp/src/de.rs create mode 100644 yazi-sftp/src/error.rs create mode 100644 yazi-sftp/src/fs/attrs.rs create mode 100644 yazi-sftp/src/fs/dir_entry.rs create mode 100644 yazi-sftp/src/fs/file.rs create mode 100644 yazi-sftp/src/fs/flags.rs create mode 100644 yazi-sftp/src/fs/mod.rs create mode 100644 yazi-sftp/src/fs/read_dir.rs create mode 100644 yazi-sftp/src/id.rs create mode 100644 yazi-sftp/src/lib.rs create mode 100644 yazi-sftp/src/macros.rs create mode 100644 yazi-sftp/src/packet.rs create mode 100644 yazi-sftp/src/requests/close.rs create mode 100644 yazi-sftp/src/requests/extended.rs create mode 100644 yazi-sftp/src/requests/fstat.rs create mode 100644 yazi-sftp/src/requests/init.rs create mode 100644 yazi-sftp/src/requests/lstat.rs create mode 100644 yazi-sftp/src/requests/mkdir.rs create mode 100644 yazi-sftp/src/requests/mod.rs create mode 100644 yazi-sftp/src/requests/open.rs create mode 100644 yazi-sftp/src/requests/open_dir.rs create mode 100644 yazi-sftp/src/requests/read.rs create mode 100644 yazi-sftp/src/requests/read_dir.rs create mode 100644 yazi-sftp/src/requests/readlink.rs create mode 100644 yazi-sftp/src/requests/realpath.rs create mode 100644 yazi-sftp/src/requests/remove.rs create mode 100644 yazi-sftp/src/requests/rename.rs create mode 100644 yazi-sftp/src/requests/rmdir.rs create mode 100644 yazi-sftp/src/requests/set_stat.rs create mode 100644 yazi-sftp/src/requests/stat.rs create mode 100644 yazi-sftp/src/requests/symlink.rs create mode 100644 yazi-sftp/src/requests/write.rs create mode 100644 yazi-sftp/src/responses/attrs.rs create mode 100644 yazi-sftp/src/responses/data.rs create mode 100644 yazi-sftp/src/responses/extended.rs create mode 100644 yazi-sftp/src/responses/handle.rs create mode 100644 yazi-sftp/src/responses/mod.rs create mode 100644 yazi-sftp/src/responses/name.rs create mode 100644 yazi-sftp/src/responses/status.rs create mode 100644 yazi-sftp/src/responses/version.rs create mode 100644 yazi-sftp/src/ser.rs create mode 100644 yazi-sftp/src/session.rs diff --git a/Cargo.lock b/Cargo.lock index 648bad52..e36560c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,41 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -41,12 +76,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -157,6 +186,18 @@ dependencies = [ "syn", ] +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -216,12 +257,35 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bcrypt-pbkdf" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2" +dependencies = [ + "blowfish", + "pbkdf2", + "sha2", +] + [[package]] name = "better-panic" version = "0.3.0" @@ -280,6 +344,43 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher", +] + [[package]] name = "bstr" version = "1.12.0" @@ -314,6 +415,12 @@ version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "byteorder-lite" version = "0.1.0" @@ -342,10 +449,19 @@ dependencies = [ ] [[package]] -name = "cc" -version = "1.2.35" +name = "cbc" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "590f9024a68a8c40351881787f1934dc11afd69090f5edb6831464694d836ea3" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44" dependencies = [ "find-msvc-tools", "jobserver", @@ -370,15 +486,43 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] -name = "chrono" -version = "0.4.41" +name = "cfg_aliases" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", - "windows-link", + "wasm-bindgen", + "windows-link 0.2.0", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", ] [[package]] @@ -506,6 +650,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + [[package]] name = "convert_case" version = "0.7.1" @@ -521,6 +671,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -595,7 +754,7 @@ dependencies = [ "libc", "mio", "parking_lot", - "rustix 1.0.8", + "rustix 1.1.2", "signal-hook", "signal-hook-mio", "winapi", @@ -616,6 +775,64 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "darling" version = "0.20.11" @@ -651,6 +868,34 @@ dependencies = [ "syn", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "delegate" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6178a82cf56c836a3ba61a7935cdb1c49bfaa6fa4327cd5bf554a503087de26b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.3" @@ -712,6 +957,18 @@ dependencies = [ "syn", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + [[package]] name = "dirs" version = "6.0.0" @@ -730,7 +987,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -742,18 +999,90 @@ dependencies = [ "litrs", ] +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encode_unicode" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_home" version = "0.1.0" @@ -798,12 +1127,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -863,6 +1192,22 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "filedescriptor" version = "0.8.3" @@ -876,9 +1221,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e178e4fba8a2726903f6ba98a6d221e76f9c12c650d5dc0e6afdc50677b49650" +checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" [[package]] name = "flate2" @@ -1012,6 +1357,17 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -1019,8 +1375,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1032,7 +1390,17 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.3+wasi-0.2.4", + "wasi 0.14.5+wasi-0.2.4", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", ] [[package]] @@ -1064,6 +1432,17 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "half" version = "2.6.0" @@ -1104,10 +1483,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "iana-time-zone" -version = "0.1.63" +name = "hex" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1115,7 +1533,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.62.0", ] [[package]] @@ -1173,9 +1591,9 @@ checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" [[package]] name = "indexmap" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921" dependencies = [ "equivalent", "hashbrown 0.15.5", @@ -1208,6 +1626,16 @@ dependencies = [ "libc", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "instability" version = "0.3.9" @@ -1221,6 +1649,34 @@ dependencies = [ "syn", ] +[[package]] +name = "internal-russh-forked-ssh-key" +version = "0.6.11+upstream-0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a77eae781ed6a7709fb15b64862fcca13d886b07c7e2786f5ed34e5e2b9187" +dependencies = [ + "argon2", + "bcrypt-pbkdf", + "ecdsa", + "ed25519-dalek", + "hex", + "hmac", + "num-bigint-dig", + "p256", + "p384", + "p521", + "rand_core 0.6.4", + "rsa", + "sec1", + "sha1", + "sha2", + "signature", + "ssh-cipher", + "ssh-encoding", + "subtle", + "zeroize", +] + [[package]] name = "interpolate_name" version = "0.2.4" @@ -1294,9 +1750,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" dependencies = [ "once_cell", "wasm-bindgen", @@ -1327,12 +1783,15 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "lebe" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" @@ -1350,6 +1809,12 @@ dependencies = [ "cc", ] +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "libredox" version = "0.1.9" @@ -1368,9 +1833,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litrs" @@ -1414,9 +1879,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ea4e65087ff52f3862caff188d489f1fab49a0cb09e01b2e3f1a617b10aaed" +checksum = "bfe949189f46fabb938b3a9a0be30fdd93fd8a09260da863399a8cf3db756ec8" dependencies = [ "hashbrown 0.15.5", ] @@ -1467,6 +1932,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + [[package]] name = "memchr" version = "2.7.5" @@ -1566,6 +2037,18 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.9.4", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -1623,6 +2106,24 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", ] [[package]] @@ -1651,6 +2152,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.2" @@ -1669,6 +2181,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1757,6 +2270,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "option-ext" version = "0.2.0" @@ -1792,6 +2311,66 @@ dependencies = [ "serde", ] +[[package]] +name = "os_str_bytes" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63eceb7b5d757011a87d08eb2123db15d87fb0c281f65d101ce30a1e96c3ad5c" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p521" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2" +dependencies = [ + "base16ct", + "ecdsa", + "elliptic-curve", + "primeorder", + "rand_core 0.6.4", + "sha2", +] + +[[package]] +name = "pageant" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd27df01428302f915ea74737fe88170dd1bab4cbd00ff9548ca85618fcd4e4" +dependencies = [ + "bytes", + "delegate", + "futures", + "log", + "rand 0.8.5", + "thiserror 1.0.69", + "tokio", + "windows 0.58.0", +] + [[package]] name = "palette" version = "0.7.6" @@ -1844,12 +2423,42 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1868,6 +2477,44 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes", + "cbc", + "der", + "pbkdf2", + "scrypt", + "sha2", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "pkcs5", + "rand_core 0.6.4", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -1900,6 +2547,29 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1915,6 +2585,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -1967,9 +2646,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.20" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e790881194f6f6e86945f0a42a6981977323669aeb6c40e9c7ec253133b96f8" +checksum = "f55f4fedc84ed39cb7a489322318976425e42a147e2be79d8f878e2884f94e84" dependencies = [ "num-traits", ] @@ -2222,12 +2901,144 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "rgb" version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "sha2", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "russh" +version = "0.54.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00897b69ab623d39b396af89f1acbb775fb5a730f0db91833da297d5a6cd3f8d" +dependencies = [ + "aes", + "base64ct", + "bitflags 2.9.4", + "block-padding", + "byteorder", + "bytes", + "cbc", + "ctr", + "curve25519-dalek", + "data-encoding", + "delegate", + "der", + "digest", + "ecdsa", + "ed25519-dalek", + "elliptic-curve", + "enum_dispatch", + "futures", + "generic-array", + "getrandom 0.2.16", + "hex-literal", + "hmac", + "home", + "inout", + "internal-russh-forked-ssh-key", + "log", + "md5", + "num-bigint", + "once_cell", + "p256", + "p384", + "p521", + "pageant", + "pbkdf2", + "pkcs1", + "pkcs5", + "pkcs8", + "rand 0.8.5", + "rand_core 0.6.4", + "ring", + "rsa", + "russh-cryptovec", + "russh-util", + "sec1", + "sha1", + "sha2", + "signature", + "spki", + "ssh-encoding", + "subtle", + "thiserror 1.0.69", + "tokio", + "typenum", + "zeroize", +] + +[[package]] +name = "russh-cryptovec" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb0ed583ff0f6b4aa44c7867dd7108df01b30571ee9423e250b4cc939f8c6cf" +dependencies = [ + "libc", + "log", + "nix", + "ssh-encoding", + "winapi", +] + +[[package]] +name = "russh-util" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668424a5dde0bcb45b55ba7de8476b93831b4aa2fa6947e145f3b053e22c60b6" +dependencies = [ + "chrono", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", +] + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -2264,15 +3075,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags 2.9.4", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.0", ] [[package]] @@ -2296,6 +3107,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -2311,6 +3131,31 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "semver" version = "1.0.26" @@ -2377,6 +3222,28 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -2434,6 +3301,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -2477,6 +3354,51 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "ssh-cipher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" +dependencies = [ + "aes", + "aes-gcm", + "cbc", + "chacha20", + "cipher", + "ctr", + "poly1305", + "ssh-encoding", + "subtle", +] + +[[package]] +name = "ssh-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" +dependencies = [ + "base64ct", + "bytes", + "pem-rfc7468", + "sha2", +] + [[package]] name = "static_assertions" version = "1.1.0" @@ -2511,6 +3433,12 @@ dependencies = [ "syn", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.106" @@ -2885,7 +3813,7 @@ dependencies = [ "percent-encoding", "scopeguard", "urlencoding", - "windows", + "windows 0.56.0", ] [[package]] @@ -2904,10 +3832,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] -name = "unicode-ident" -version = "1.0.18" +name = "typenum" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-ident" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-segmentation" @@ -2938,6 +3872,22 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "urlencoding" version = "2.1.3" @@ -3022,6 +3972,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "walkdir" version = "2.5.0" @@ -3040,30 +3996,40 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.3+wasi-0.2.4" +version = "0.14.5+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" +checksum = "a4494f6290a82f5fe584817a676a34b9d6763e8d9d18204009fb31dceca98fd4" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.0+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03fa2761397e5bd52002cd7e73110c71af2109aca4e521a9f40473fe685b0a24" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" dependencies = [ "bumpalo", "log", @@ -3074,10 +4040,23 @@ dependencies = [ ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" +name = "wasm-bindgen-futures" +version = "0.4.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3085,9 +4064,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" dependencies = [ "proc-macro2", "quote", @@ -3098,13 +4077,23 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "weezl" version = "0.1.10" @@ -3119,7 +4108,7 @@ checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" dependencies = [ "either", "env_home", - "rustix 1.0.8", + "rustix 1.1.2", "winsafe", ] @@ -3151,11 +4140,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -3174,6 +4163,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.56.0" @@ -3188,15 +4187,28 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.61.2" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" dependencies = [ "windows-implement 0.60.0", "windows-interface 0.59.1", - "windows-link", - "windows-result 0.3.4", - "windows-strings", + "windows-link 0.2.0", + "windows-result 0.4.0", + "windows-strings 0.5.0", ] [[package]] @@ -3210,6 +4222,17 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-implement" version = "0.60.0" @@ -3232,6 +4255,17 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-interface" version = "0.59.1" @@ -3249,6 +4283,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + [[package]] name = "windows-result" version = "0.1.2" @@ -3260,20 +4300,39 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.3.4" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" dependencies = [ - "windows-link", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +dependencies = [ + "windows-link 0.2.0", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-link", + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +dependencies = [ + "windows-link 0.2.0", ] [[package]] @@ -3303,6 +4362,15 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-sys" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -3325,7 +4393,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -3449,9 +4517,9 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] name = "wit-bindgen" -version = "0.45.0" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" +checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" [[package]] name = "wyz" @@ -3744,7 +4812,7 @@ dependencies = [ "tracing", "trash", "uzers", - "windows-sys 0.60.2", + "windows-sys 0.61.0", "yazi-ffi", "yazi-macro", "yazi-shared", @@ -3800,7 +4868,7 @@ dependencies = [ "twox-hash", "unicode-width 0.2.0", "uzers", - "windows-sys 0.60.2", + "windows-sys 0.61.0", "yazi-adapter", "yazi-binding", "yazi-boot", @@ -3842,7 +4910,7 @@ dependencies = [ "futures", "hashbrown 0.16.0", "libc", - "lru 0.16.0", + "lru 0.16.1", "mlua", "ordered-float 5.0.0", "parking_lot", @@ -3861,6 +4929,18 @@ dependencies = [ "yazi-shared", ] +[[package]] +name = "yazi-sftp" +version = "0.1.0" +dependencies = [ + "bitflags 2.9.4", + "os_str_bytes", + "parking_lot", + "russh", + "serde", + "tokio", +] + [[package]] name = "yazi-shared" version = "25.6.11" @@ -3878,7 +4958,7 @@ dependencies = [ "serde", "tokio", "uzers", - "windows-sys 0.60.2", + "windows-sys 0.61.0", "yazi-macro", ] @@ -3890,7 +4970,7 @@ dependencies = [ "libc", "parking_lot", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.61.0", "yazi-macro", "yazi-shared", ] @@ -3936,24 +5016,30 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + [[package]] name = "zune-core" version = "0.4.12" @@ -3971,9 +5057,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1f7e205ce79eb2da3cd71c5f55f3589785cb7c79f6a03d1c8d1491bda5d089" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" dependencies = [ "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index a1c936f1..ce0ae20c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,9 +32,9 @@ foldhash = "0.2.0" futures = "0.3.31" globset = "0.4.16" hashbrown = { version = "0.16.0", features = [ "serde" ] } -indexmap = { version = "2.11.0", features = [ "serde" ] } +indexmap = { version = "2.11.1", features = [ "serde" ] } libc = "0.2.175" -lru = "0.16.0" +lru = "0.16.1" mlua = { version = "0.11.3", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serde" ] } objc = "0.2.7" ordered-float = { version = "5.0.0", features = [ "serde" ] } @@ -42,6 +42,7 @@ parking_lot = "0.12.4" paste = "1.0.15" ratatui = { version = "0.29.0", features = [ "unstable-rendered-line-info", "unstable-widget-ref" ] } regex = "1.11.2" +russh = { version = "0.54.3", default-features = false, features = [ "ring", "rsa" ] } scopeguard = "1.2.0" serde = { version = "1.0.219", features = [ "derive" ] } serde_json = "1.0.143" diff --git a/yazi-fs/Cargo.toml b/yazi-fs/Cargo.toml index 4c99bbf9..0a9f540d 100644 --- a/yazi-fs/Cargo.toml +++ b/yazi-fs/Cargo.toml @@ -33,7 +33,7 @@ libc = { workspace = true } uzers = { workspace = true } [target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.60.2", features = [ "Win32_Storage_FileSystem" ] } +windows-sys = { version = "0.61.0", features = [ "Win32_Storage_FileSystem" ] } [target.'cfg(target_os = "macos")'.dependencies] core-foundation-sys = { workspace = true } diff --git a/yazi-plugin/Cargo.toml b/yazi-plugin/Cargo.toml index 45210bf9..5da15c92 100644 --- a/yazi-plugin/Cargo.toml +++ b/yazi-plugin/Cargo.toml @@ -49,7 +49,7 @@ libc = { workspace = true } uzers = { workspace = true } [target."cfg(windows)".dependencies] -windows-sys = { version = "0.60.2", features = [ "Win32_System_JobObjects" ] } +windows-sys = { version = "0.61.0", features = [ "Win32_System_JobObjects" ] } [target.'cfg(target_os = "macos")'.dependencies] crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } diff --git a/yazi-sftp/Cargo.toml b/yazi-sftp/Cargo.toml new file mode 100644 index 00000000..32a3895b --- /dev/null +++ b/yazi-sftp/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "yazi-sftp" +version = "0.1.0" +edition = "2024" +license = "MIT" +authors = [ "AspectUnk", "sxyazi " ] +description = "Yazi SFTP client" +homepage = "https://yazi-rs.github.io" +repository = "https://github.com/sxyazi/yazi" + +[dependencies] +bitflags = { workspace = true } +parking_lot = { workspace = true } +russh = { workspace = true } +serde = { workspace = true } +tokio = { workspace = true } + +[target."cfg(windows)".dependencies] +os_str_bytes = { version = "7.1.1", default-features = false, features = [ "conversions" ] } diff --git a/yazi-sftp/LICENSE b/yazi-sftp/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/yazi-sftp/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/yazi-sftp/README.md b/yazi-sftp/README.md new file mode 100644 index 00000000..d40462f4 --- /dev/null +++ b/yazi-sftp/README.md @@ -0,0 +1,15 @@ +## yazi-sftp + +A fork of [`russh-sftp`](https://github.com/AspectUnk/russh-sftp) used by Yazi, with some changes: + +- Supports paths containing invalid UTF-8 +- Supports retrieving file nlink, username, and group +- Uses generic return parameters for a more idiomatic API, e.g.: + ```rust + let attrs: responses::Attrs = session.send(requests::Stat::new(path)).await?; + ``` +- Reduced dependencies +- Performance optimizations: + - Copy-on-write for all packets to avoid unnecessary memory allocation + - Packet lengths are precomputed to avoid secondary allocations + - Avoids cloning buffers in `AsyncRead` and `AsyncWrite` implementations diff --git a/yazi-sftp/src/byte_str.rs b/yazi-sftp/src/byte_str.rs new file mode 100644 index 00000000..ac762919 --- /dev/null +++ b/yazi-sftp/src/byte_str.rs @@ -0,0 +1,100 @@ +use std::{borrow::Cow, ffi::{OsStr, OsString}, ops::Deref, path::{Path, PathBuf}}; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct ByteStr<'a>(Cow<'a, [u8]>); + +impl Deref for ByteStr<'_> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl<'a> From<&'a str> for ByteStr<'a> { + fn from(value: &'a str) -> Self { ByteStr(Cow::Borrowed(value.as_bytes())) } +} + +impl<'a> From<&'a ByteStr<'a>> for ByteStr<'a> { + fn from(value: &'a ByteStr) -> Self { ByteStr(Cow::Borrowed(&value.0)) } +} + +impl<'a> From<&'a OsStr> for ByteStr<'a> { + fn from(value: &'a OsStr) -> Self { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + ByteStr(Cow::Borrowed(value.as_bytes())) + } + #[cfg(windows)] + { + use os_str_bytes::OsStrBytes; + ByteStr(value.to_raw_bytes()) + } + } +} + +impl<'a> From<&'a Path> for ByteStr<'a> { + fn from(value: &'a Path) -> Self { ByteStr::from(value.as_os_str()) } +} + +impl PartialEq<&str> for ByteStr<'_> { + fn eq(&self, other: &&str) -> bool { self.0 == other.as_bytes() } +} + +impl<'a> ByteStr<'a> { + pub fn to_os_str(&self) -> Cow<'_, OsStr> { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + OsStr::from_bytes(&self.0).into() + } + #[cfg(windows)] + { + use os_str_bytes::OsStrBytes; + OsStr::assert_from_raw_bytes(self.0.as_ref()) + } + } + + pub fn into_os_string(self) -> OsString { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStringExt; + OsString::from_vec(self.0.into_owned()) + } + #[cfg(windows)] + { + use os_str_bytes::OsStrBytes; + OsStr::assert_from_raw_bytes(self.0).into_owned() + } + } + + pub fn to_path(&self) -> Cow<'_, Path> { + match self.to_os_str() { + Cow::Borrowed(s) => Path::new(s).into(), + Cow::Owned(s) => PathBuf::from(s).into(), + } + } + + pub fn into_path(self) -> PathBuf { + match self.0 { + Cow::Borrowed(_) => self.to_path().into_owned(), + Cow::Owned(_) => self.into_os_string().into(), + } + } + + pub fn join(&self, other: impl Into>) -> PathBuf { + let other = other.into(); + match self.to_path() { + Cow::Borrowed(p) => p.join(other.to_path()), + Cow::Owned(mut p) => { + p.push(other.to_path()); + p + } + } + } + + pub(super) unsafe fn from_str_bytes_unchecked(bytes: &'a [u8]) -> Self { + Self(Cow::Borrowed(bytes)) + } +} diff --git a/yazi-sftp/src/de.rs b/yazi-sftp/src/de.rs new file mode 100644 index 00000000..4e73ec0f --- /dev/null +++ b/yazi-sftp/src/de.rs @@ -0,0 +1,375 @@ +use serde::{Deserializer as _, de::{EnumAccess, MapAccess, SeqAccess, VariantAccess, value::U32Deserializer}}; + +use crate::Error; + +pub(super) struct Deserializer<'a> { + input: &'a [u8], +} + +impl<'a> Deserializer<'a> { + pub(super) fn once<'de, T>(input: &'de [u8]) -> Result + where + T: serde::Deserialize<'de>, + { + let mut de = Deserializer { input }; + let t = T::deserialize(&mut de)?; + if !de.input.is_empty() { + return Err(Error::serde("trailing bytes")); + } + Ok(t) + } +} + +impl<'de> serde::Deserializer<'de> for &mut Deserializer<'de> { + type Error = Error; + + fn deserialize_any(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + let len = self.input.len(); + visitor.visit_seq(SeqDeserializer { de: self, remaining: len }) + } + + fn deserialize_bool(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(Error::serde("bool not supported")) + } + + fn deserialize_i8(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(Error::serde("i8 not supported")) + } + + fn deserialize_i16(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(Error::serde("i16 not supported")) + } + + fn deserialize_i32(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(Error::serde("i32 not supported")) + } + + fn deserialize_i64(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(Error::serde("i64 not supported")) + } + + fn deserialize_u8(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + let b = *self.input.first().ok_or(Error::serde("u8 not enough"))?; + + self.input = &self.input[1..]; + visitor.visit_u8(b) + } + + fn deserialize_u16(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(Error::serde("u16 not supported")) + } + + fn deserialize_u32(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + let b: [u8; 4] = self.input.get(..4).ok_or(Error::serde("u32 not enough"))?.try_into().unwrap(); + + self.input = &self.input[4..]; + visitor.visit_u32(u32::from_be_bytes(b)) + } + + fn deserialize_u64(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + let b: [u8; 8] = self.input.get(..8).ok_or(Error::serde("u64 not enough"))?.try_into().unwrap(); + + self.input = &self.input[8..]; + visitor.visit_u64(u64::from_be_bytes(b)) + } + + fn deserialize_f32(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(Error::serde("f32 not supported")) + } + + fn deserialize_f64(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(Error::serde("f64 not supported")) + } + + fn deserialize_char(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(Error::serde("char not supported")) + } + + fn deserialize_str(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + let len: [u8; 4] = + self.input.get(..4).ok_or(Error::serde("invalid string length"))?.try_into().unwrap(); + let len = u32::from_be_bytes(len) as usize; + + self.input = &self.input[4..]; + let b = self.input.get(..len).ok_or(Error::serde("string not enough"))?; + + self.input = &self.input[len..]; + visitor.visit_str(str::from_utf8(b).map_err(|e| Error::serde(e.to_string()))?) + } + + fn deserialize_string(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + self.deserialize_str(visitor) + } + + fn deserialize_bytes(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + let len: [u8; 4] = + self.input.get(..4).ok_or(Error::serde("invalid bytes length"))?.try_into().unwrap(); + let len = u32::from_be_bytes(len) as usize; + let b = self.input.get(4..4 + len).ok_or(Error::serde("bytes not enough"))?; + + self.input = &self.input[4 + len..]; + visitor.visit_bytes(b) + } + + fn deserialize_byte_buf(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + self.deserialize_bytes(visitor) + } + + fn deserialize_option(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(Error::serde("option not supported")) + } + + fn deserialize_unit(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_unit_struct( + self, + _name: &'static str, + visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_newtype_struct( + self, + _name: &'static str, + visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_newtype_struct(self) + } + + fn deserialize_seq(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + let len: [u8; 4] = + self.input.get(..4).ok_or(Error::serde("invalid seq length"))?.try_into().unwrap(); + + self.input = &self.input[4..]; + visitor.visit_seq(SeqDeserializer { de: self, remaining: u32::from_be_bytes(len) as _ }) + } + + fn deserialize_tuple(self, len: usize, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_seq(SeqDeserializer { de: self, remaining: len }) + } + + fn deserialize_tuple_struct( + self, + _name: &'static str, + len: usize, + visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + self.deserialize_tuple(len, visitor) + } + + fn deserialize_map(self, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_map(MapDeserializer { de: self }) + } + + fn deserialize_struct( + self, + _name: &'static str, + fields: &'static [&'static str], + visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + self.deserialize_tuple(fields.len(), visitor) + } + + fn deserialize_enum( + self, + _name: &'static str, + _variants: &'static [&'static str], + visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + visitor.visit_enum(self) + } + + fn deserialize_identifier(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(Error::serde("identifier not supported")) + } + + fn deserialize_ignored_any(self, _visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + Err(Error::serde("ignored any not supported")) + } + + fn is_human_readable(&self) -> bool { false } +} + +struct SeqDeserializer<'a, 'de: 'a> { + de: &'a mut Deserializer<'de>, + remaining: usize, +} + +impl<'de> SeqAccess<'de> for SeqDeserializer<'_, 'de> { + type Error = Error; + + fn next_element_seed(&mut self, seed: T) -> Result, Self::Error> + where + T: serde::de::DeserializeSeed<'de>, + { + if self.remaining == 0 { + Ok(None) + } else { + self.remaining -= 1; + seed.deserialize(&mut *self.de).map(Some) + } + } + + fn size_hint(&self) -> Option { Some(self.remaining) } +} + +struct MapDeserializer<'a, 'de: 'a> { + de: &'a mut Deserializer<'de>, +} + +impl<'de> MapAccess<'de> for MapDeserializer<'_, 'de> { + type Error = Error; + + fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> + where + K: serde::de::DeserializeSeed<'de>, + { + if self.de.input.is_empty() { Ok(None) } else { seed.deserialize(&mut *self.de).map(Some) } + } + + fn next_value_seed(&mut self, seed: V) -> Result + where + V: serde::de::DeserializeSeed<'de>, + { + seed.deserialize(&mut *self.de) + } +} + +impl<'de> VariantAccess<'de> for &mut Deserializer<'de> { + type Error = Error; + + fn unit_variant(self) -> Result<(), Self::Error> { Ok(()) } + + fn newtype_variant_seed(self, seed: T) -> Result + where + T: serde::de::DeserializeSeed<'de>, + { + seed.deserialize(self) + } + + fn tuple_variant(self, len: usize, visitor: V) -> Result + where + V: serde::de::Visitor<'de>, + { + use serde::Deserializer; + self.deserialize_tuple(len, visitor) + } + + fn struct_variant( + self, + fields: &'static [&'static str], + visitor: V, + ) -> Result + where + V: serde::de::Visitor<'de>, + { + self.deserialize_tuple(fields.len(), visitor) + } +} + +impl<'de> EnumAccess<'de> for &mut Deserializer<'de> { + type Error = Error; + type Variant = Self; + + fn variant_seed(self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error> + where + V: serde::de::DeserializeSeed<'de>, + { + let b: [u8; 4] = + self.input.get(..4).ok_or(Error::serde("enum not enough"))?.try_into().unwrap(); + self.input = &self.input[4..]; + + let de = U32Deserializer::::new(u32::from_be_bytes(b)); + Ok((seed.deserialize(de)?, self)) + } +} diff --git a/yazi-sftp/src/error.rs b/yazi-sftp/src/error.rs new file mode 100644 index 00000000..84f1d273 --- /dev/null +++ b/yazi-sftp/src/error.rs @@ -0,0 +1,66 @@ +use std::borrow::Cow; + +use crate::responses; + +#[derive(Debug)] +pub enum Error { + IO(std::io::Error), + Serde(Cow<'static, str>), + Status(responses::Status), + Packet(&'static str), + Timeout, + Unsupported, + Custom(Cow<'static, str>), +} + +impl Error { + pub(super) fn serde(s: impl Into>) -> Self { Self::Serde(s.into()) } + + pub(super) fn custom(s: impl Into>) -> Self { Self::Custom(s.into()) } +} + +impl serde::ser::Error for Error { + fn custom(msg: T) -> Self { Self::serde(msg.to_string()) } +} + +impl serde::de::Error for Error { + fn custom(msg: T) -> Self { Self::serde(msg.to_string()) } +} + +impl From for std::io::Error { + fn from(err: Error) -> Self { + match err { + Error::IO(e) => e, + Error::Serde(_) => Self::new(std::io::ErrorKind::InvalidData, err), + Error::Status(_) => Self::other(err), + Error::Packet(_) => Self::new(std::io::ErrorKind::InvalidData, err), + Error::Timeout => Self::new(std::io::ErrorKind::TimedOut, err), + Error::Unsupported => Self::new(std::io::ErrorKind::Unsupported, err), + Error::Custom(_) => Self::other(err), + } + } +} + +impl From> for Error { + fn from(_: tokio::sync::mpsc::error::SendError) -> Self { Self::custom("channel closed") } +} + +impl From for Error { + fn from(_: tokio::sync::oneshot::error::RecvError) -> Self { Self::custom("channel closed") } +} + +impl std::error::Error for Error {} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::IO(e) => write!(f, "IO error: {e}"), + Self::Serde(s) => write!(f, "Serde error: {s}"), + Self::Status(s) => write!(f, "Status error: {s:?}"), + Self::Packet(s) => write!(f, "Unexpected packet: {s}"), + Self::Timeout => write!(f, "Operation timed out"), + Self::Unsupported => write!(f, "Operation not supported"), + Self::Custom(s) => write!(f, "{s}"), + } + } +} diff --git a/yazi-sftp/src/fs/attrs.rs b/yazi-sftp/src/fs/attrs.rs new file mode 100644 index 00000000..e89f5adf --- /dev/null +++ b/yazi-sftp/src/fs/attrs.rs @@ -0,0 +1,146 @@ +use std::{collections::HashMap, fmt}; + +use serde::{Deserialize, Deserializer, Serialize, de::Visitor, ser::SerializeStruct}; + +#[derive(Debug, Default)] +pub struct Attrs { + pub size: Option, + pub uid: Option, + pub gid: Option, + pub perm: Option, + pub atime: Option, + pub mtime: Option, + pub extended: HashMap, +} + +impl Attrs { + pub fn len(&self) -> usize { + let mut len = 4; + if let Some(size) = self.size { + len += size_of_val(&size); + } + if self.uid.is_some() || self.gid.is_some() { + len += 4 + 4; + } + if let Some(perm) = self.perm { + len += size_of_val(&perm); + } + if self.atime.is_some() || self.mtime.is_some() { + len += 4 + 4; + } + if !self.extended.is_empty() { + len += 4 + self.extended.iter().map(|(k, v)| 4 + k.len() + 4 + v.len()).sum::(); + } + len + } +} + +impl Serialize for Attrs { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut flags: u32 = 0; + let mut len = 1; + + if self.size.is_some() { + flags |= 0x1; + len += 1; + } + if self.uid.is_some() || self.gid.is_some() { + flags |= 0x2; + len += 2; + } + if self.perm.is_some() { + flags |= 0x4; + len += 1; + } + if self.atime.is_some() || self.mtime.is_some() { + flags |= 0x8; + len += 2; + } + if !self.extended.is_empty() { + flags |= 0x80000000; + len += 1 + self.extended.len() * 2; + } + + let mut seq = serializer.serialize_struct("Attrs", len)?; + seq.serialize_field("flags", &flags)?; + if let Some(size) = self.size { + seq.serialize_field("size", &size)?; + } + if self.uid.is_some() || self.gid.is_some() { + seq.serialize_field("uid", &self.uid.unwrap_or(0))?; + seq.serialize_field("gid", &self.gid.unwrap_or(0))?; + } + if let Some(perm) = self.perm { + seq.serialize_field("perm", &perm)?; + } + if self.atime.is_some() || self.mtime.is_some() { + seq.serialize_field("atime", &self.atime.unwrap_or(0))?; + seq.serialize_field("mtime", &self.mtime.unwrap_or(0))?; + } + if !self.extended.is_empty() { + let count = self.extended.len() as u32; + seq.serialize_field("extended_count", &count)?; + for (k, v) in self.extended.iter().take(count as usize) { + seq.serialize_field("extended_key", k)?; + seq.serialize_field("extended_value", v)?; + } + } + seq.end() + } +} + +impl<'de> Deserialize<'de> for Attrs { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct AttrsVisitor; + + impl<'de> Visitor<'de> for AttrsVisitor { + type Value = Attrs; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("attributes") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let b = seq.next_element::()?.unwrap_or(0); + let mut attrs = Attrs::default(); + + if b & 0x1 != 0 { + attrs.size = seq.next_element()?; + } + if b & 0x2 != 0 { + attrs.uid = seq.next_element()?; + attrs.gid = seq.next_element()?; + } + if b & 0x4 != 0 { + attrs.perm = seq.next_element()?; + } + if b & 0x8 != 0 { + attrs.atime = seq.next_element()?; + attrs.mtime = seq.next_element()?; + } + if b & 0x80000000 != 0 { + let count: u32 = seq.next_element()?.unwrap_or(0); + for _ in 0..count { + attrs.extended.insert( + seq.next_element()?.unwrap_or_default(), + seq.next_element()?.unwrap_or_default(), + ); + } + } + + Ok(attrs) + } + } + + deserializer.deserialize_any(AttrsVisitor) + } +} diff --git a/yazi-sftp/src/fs/dir_entry.rs b/yazi-sftp/src/fs/dir_entry.rs new file mode 100644 index 00000000..aa273649 --- /dev/null +++ b/yazi-sftp/src/fs/dir_entry.rs @@ -0,0 +1,46 @@ +use std::{borrow::Cow, ffi::OsStr, path::PathBuf}; + +use crate::{ByteStr, fs::Attrs}; + +pub struct DirEntry<'a> { + pub(super) dir: ByteStr<'a>, + pub(super) name: ByteStr<'a>, + pub(super) long_name: ByteStr<'a>, + pub(super) attrs: Attrs, +} + +impl<'a> DirEntry<'a> { + pub fn path(&self) -> PathBuf { self.dir.join(&self.name) } + + pub fn name(&self) -> Cow<'_, OsStr> { self.name.to_os_str() } + + pub fn long_name(&self) -> Cow<'_, OsStr> { self.long_name.to_os_str() } + + pub fn attrs(&self) -> &Attrs { &self.attrs } + + pub fn nlink(&self) -> Option { str::from_utf8(self.long_name_field(1)?).ok()?.parse().ok() } + + pub fn user(&self) -> Option> { + let b = self.long_name_field(2)?; + Some(unsafe { + match ByteStr::from_str_bytes_unchecked(b).to_os_str() { + Cow::Borrowed(_) => OsStr::from_encoded_bytes_unchecked(b).into(), + Cow::Owned(s) => s.into(), + } + }) + } + + pub fn group(&self) -> Option> { + let b = self.long_name_field(3)?; + Some(unsafe { + match ByteStr::from_str_bytes_unchecked(b).to_os_str() { + Cow::Borrowed(_) => OsStr::from_encoded_bytes_unchecked(b).into(), + Cow::Owned(s) => s.into(), + } + }) + } + + fn long_name_field(&self, n: usize) -> Option<&[u8]> { + self.long_name.split(|b| b.is_ascii_whitespace()).filter(|s| !s.is_empty()).nth(n) + } +} diff --git a/yazi-sftp/src/fs/file.rs b/yazi-sftp/src/fs/file.rs new file mode 100644 index 00000000..b0a79272 --- /dev/null +++ b/yazi-sftp/src/fs/file.rs @@ -0,0 +1,165 @@ +use std::{io, pin::Pin, task::{Context, Poll, ready}, time::Duration}; + +use tokio::{io::{AsyncRead, AsyncWrite, ReadBuf}, sync::oneshot, time::{Timeout, timeout}}; + +use crate::{Error, Packet, Session, fs::Attrs}; + +pub struct File<'a> { + session: &'a Session, + handle: String, + + closed: bool, + cursor: u64, + close_rx: Option>>>, + read_rx: Option>>, + write_rx: Option<(oneshot::Receiver>, usize)>, + flush_rx: Option>>>, +} + +impl Unpin for File<'_> {} + +impl Drop for File<'_> { + fn drop(&mut self) { + if !self.closed { + self.session.close(&self.handle).ok(); + } + } +} + +impl<'a> File<'a> { + pub(crate) fn new(session: &'a Session, handle: impl Into) -> Self { + Self { + session, + handle: handle.into(), + + closed: false, + cursor: 0, + close_rx: None, + read_rx: None, + write_rx: None, + flush_rx: None, + } + } + + pub async fn fstat(&self) -> Result { self.session.fstat(&self.handle).await } + + pub async fn fsetstat(&self, attrs: Attrs) -> Result<(), Error> { + self.session.fsetstat(&self.handle, attrs).await + } +} + +impl AsyncRead for File<'_> { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let me = unsafe { self.get_unchecked_mut() }; + + if me.read_rx.is_none() { + let max = buf.remaining().min(261120) as u32; + me.read_rx = Some(me.session.read(&me.handle, me.cursor, max)?); + } + + let result = ready!(Pin::new(me.read_rx.as_mut().unwrap()).poll(cx)); + me.read_rx = None; + + Poll::Ready(match result { + Ok(Packet::Data(data)) => { + let len = buf.remaining().min(data.data.len()); + me.cursor += len as u64; + buf.put_slice(&data.data[..len]); + Ok(()) + } + Ok(Packet::Status(status)) if status.is_eof() => Ok(()), + Ok(Packet::Status(status)) => Err(Error::Status(status).into()), + Ok(_) => Err(Error::Packet("not a Data or Status").into()), + Err(e) => Err(Error::from(e).into()), + }) + } +} + +impl AsyncWrite for File<'_> { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let me = unsafe { self.get_unchecked_mut() }; + + let (rx, len) = match &mut me.write_rx { + Some((rx, len)) => (rx, *len), + None => { + let max = buf.len().min(261120); + let rx = me.session.write(&me.handle, me.cursor, &buf[..max])?; + (&mut me.write_rx.get_or_insert((rx, max)).0, max) + } + }; + + let result = ready!(Pin::new(rx).poll(cx)); + me.write_rx = None; + + Poll::Ready(match result { + Ok(Packet::Status(status)) if status.is_ok() => { + me.cursor += len as u64; + Ok(len) + } + Ok(Packet::Status(status)) => Err(Error::Status(status).into()), + Ok(_) => Err(Error::Packet("not a Status").into()), + Err(e) => Err(Error::from(e).into()), + }) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let me = unsafe { self.get_unchecked_mut() }; + + if me.flush_rx.is_none() { + match me.session.fsync(&me.handle) { + Ok(rx) => me.flush_rx = Some(timeout(Duration::from_secs(10), rx)), + Err(Error::Unsupported) => return Poll::Ready(Ok(())), + Err(e) => Err(e)?, + } + } + + let rx = unsafe { Pin::new_unchecked(me.flush_rx.as_mut().unwrap()) }; + let result = ready!(rx.poll(cx)); + me.flush_rx = None; + + let Ok(result) = result else { + return Poll::Ready(Err(Error::Timeout.into())); + }; + + Poll::Ready(match result { + Ok(Packet::Status(status)) if status.is_ok() => Ok(()), + Ok(Packet::Status(status)) => Err(Error::Status(status).into()), + Ok(_) => Err(Error::Packet("not a Status").into()), + Err(e) => Err(Error::from(e).into()), + }) + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let me = unsafe { self.get_unchecked_mut() }; + + if me.close_rx.is_none() { + me.close_rx = Some(timeout(Duration::from_secs(10), me.session.close(&me.handle)?)); + } + + let rx = unsafe { Pin::new_unchecked(me.close_rx.as_mut().unwrap()) }; + let result = ready!(rx.poll(cx)); + me.close_rx = None; + + let Ok(result) = result else { + return Poll::Ready(Err(Error::Timeout.into())); + }; + + Poll::Ready(match result { + Ok(Packet::Status(status)) if status.is_ok() => { + me.closed = true; + Ok(()) + } + Ok(Packet::Status(status)) => Err(Error::Status(status).into()), + Ok(_) => Err(Error::Packet("not a Status").into()), + Err(e) => Err(Error::from(e).into()), + }) + } +} diff --git a/yazi-sftp/src/fs/flags.rs b/yazi-sftp/src/fs/flags.rs new file mode 100644 index 00000000..a4b27da0 --- /dev/null +++ b/yazi-sftp/src/fs/flags.rs @@ -0,0 +1,16 @@ +use bitflags::bitflags; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)] +pub struct Flags(u32); + +bitflags! { + impl Flags: u32 { + const READ = 0b000001; + const WRITE = 0b000010; + const APPEND = 0b000100; + const CREATE = 0b001000; + const TRUNCATE = 0b010000; + const EXCLUDE = 0b100000; + } +} diff --git a/yazi-sftp/src/fs/mod.rs b/yazi-sftp/src/fs/mod.rs new file mode 100644 index 00000000..2b7e65e8 --- /dev/null +++ b/yazi-sftp/src/fs/mod.rs @@ -0,0 +1,11 @@ +mod attrs; +mod dir_entry; +mod file; +mod flags; +mod read_dir; + +pub use attrs::*; +pub use dir_entry::*; +pub use file::*; +pub use flags::*; +pub use read_dir::*; diff --git a/yazi-sftp/src/fs/read_dir.rs b/yazi-sftp/src/fs/read_dir.rs new file mode 100644 index 00000000..732b0d5c --- /dev/null +++ b/yazi-sftp/src/fs/read_dir.rs @@ -0,0 +1,56 @@ +use std::mem; + +use crate::{ByteStr, Error, Session, fs::DirEntry, requests, responses}; + +pub struct ReadDir<'a> { + dir: ByteStr<'a>, + handle: String, + session: &'a Session, + + name: responses::Name<'a>, + cursor: usize, + done: bool, +} + +impl<'a> ReadDir<'a> { + pub(crate) fn new(session: &'a Session, dir: ByteStr<'a>, handle: String) -> Self { + Self { dir, handle, session, name: Default::default(), cursor: 0, done: false } + } + + pub async fn next(&mut self) -> Result>, Error> { + loop { + self.fetch().await?; + let Some(item) = self.name.items.get_mut(self.cursor).map(mem::take) else { + return Ok(None); + }; + + self.cursor += 1; + if item.name != "." && item.name != ".." { + return Ok(Some(DirEntry { + dir: ByteStr::from(&self.dir), + name: item.name, + long_name: item.long_name, + attrs: item.attrs, + })); + } + } + } + + async fn fetch(&mut self) -> Result<(), Error> { + if self.cursor < self.name.items.len() || self.done { + return Ok(()); + } + + self.name = match self.session.send(requests::ReadDir::new(&self.handle)).await { + Ok(resp) => resp, + Err(Error::Status(status)) if status.is_eof() => { + self.done = true; + return Ok(()); + } + Err(e) => return Err(e), + }; + + self.cursor = 0; + Ok(()) + } +} diff --git a/yazi-sftp/src/id.rs b/yazi-sftp/src/id.rs new file mode 100644 index 00000000..a3eb8cd8 --- /dev/null +++ b/yazi-sftp/src/id.rs @@ -0,0 +1,18 @@ +use std::sync::atomic::{AtomicU32, Ordering}; + +pub(super) struct Id(AtomicU32); + +impl Default for Id { + fn default() -> Self { Self(AtomicU32::new(1)) } +} + +impl Id { + pub(super) fn next(&self) -> u32 { + loop { + let old = self.0.fetch_add(1, Ordering::Relaxed); + if old != 0 { + return old; + } + } + } +} diff --git a/yazi-sftp/src/lib.rs b/yazi-sftp/src/lib.rs new file mode 100644 index 00000000..069c35df --- /dev/null +++ b/yazi-sftp/src/lib.rs @@ -0,0 +1,22 @@ +#![allow(clippy::len_without_is_empty)] + +pub mod fs; +pub mod requests; +pub mod responses; + +mod byte_str; +mod de; +mod error; +mod id; +mod macros; +mod packet; +mod ser; +mod session; + +pub use byte_str::*; +pub(crate) use de::*; +pub use error::*; +pub(crate) use id::*; +pub use packet::*; +pub(crate) use ser::*; +pub use session::*; diff --git a/yazi-sftp/src/macros.rs b/yazi-sftp/src/macros.rs new file mode 100644 index 00000000..224a4788 --- /dev/null +++ b/yazi-sftp/src/macros.rs @@ -0,0 +1,30 @@ +#[macro_export] +macro_rules! impl_from_packet { + ($($variant:ident($type:ty)),* $(,)?) => { + $( + impl<'a> From<$type> for $crate::Packet<'a> { + fn from(value: $type) -> Self { + Self::$variant(value) + } + } + )* + }; +} + +#[macro_export] +macro_rules! impl_try_from_packet { + ($($variant:ident($type:ty)),* $(,)?) => { + $( + impl<'a> TryFrom<$crate::Packet<'a>> for $type { + type Error = $crate::Error; + + fn try_from(value: $crate::Packet<'a>) -> Result { + match value { + $crate::Packet::$variant(v) => Ok(v), + _ => Err($crate::Error::Packet(concat!("not a ", stringify!($variant)))), + } + } + } + )* + }; +} diff --git a/yazi-sftp/src/packet.rs b/yazi-sftp/src/packet.rs new file mode 100644 index 00000000..9e5e4b49 --- /dev/null +++ b/yazi-sftp/src/packet.rs @@ -0,0 +1,265 @@ +use serde::{Deserialize, Serialize}; + +use super::de::Deserializer; +use crate::{Error, impl_from_packet, impl_try_from_packet, requests, responses}; + +#[derive(Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum Packet<'a> { + Init(requests::Init), + Open(requests::Open<'a>), + Close(requests::Close<'a>), + Read(requests::Read<'a>), + Write(requests::Write<'a>), + Lstat(requests::Lstat<'a>), + Fstat(requests::Fstat<'a>), + SetStat(requests::SetStat<'a>), + FSetStat(requests::FSetStat<'a>), + OpenDir(requests::OpenDir<'a>), + ReadDir(requests::ReadDir<'a>), + Remove(requests::Remove<'a>), + Mkdir(requests::Mkdir<'a>), + Rmdir(requests::Rmdir<'a>), + Realpath(requests::Realpath<'a>), + Stat(requests::Stat<'a>), + Rename(requests::Rename<'a>), + Readlink(requests::Readlink<'a>), + Symlink(requests::Symlink<'a>), + ExtendedFsync(requests::Extended<'a, requests::ExtendedFsync<'a>>), + ExtendedHardlink(requests::Extended<'a, requests::ExtendedHardlink<'a>>), + ExtendedLimits(requests::Extended<'a, requests::ExtendedLimits>), + + // Responses + Version(responses::Version), + Status(responses::Status), + Handle(responses::Handle), + Data(responses::Data), + Name(responses::Name<'a>), + Attrs(responses::Attrs), + ExtendedReply(responses::Extended<'a>), +} + +impl_from_packet! { + Init(requests::Init), + Open(requests::Open<'a>), + Close(requests::Close<'a>), + Read(requests::Read<'a>), + Write(requests::Write<'a>), + Lstat(requests::Lstat<'a>), + Fstat(requests::Fstat<'a>), + SetStat(requests::SetStat<'a>), + FSetStat(requests::FSetStat<'a>), + OpenDir(requests::OpenDir<'a>), + ReadDir(requests::ReadDir<'a>), + Remove(requests::Remove<'a>), + Mkdir(requests::Mkdir<'a>), + Rmdir(requests::Rmdir<'a>), + Realpath(requests::Realpath<'a>), + Stat(requests::Stat<'a>), + Rename(requests::Rename<'a>), + Readlink(requests::Readlink<'a>), + Symlink(requests::Symlink<'a>), + ExtendedFsync(requests::Extended<'a, requests::ExtendedFsync<'a>>), + ExtendedHardlink(requests::Extended<'a, requests::ExtendedHardlink<'a>>), + ExtendedLimits(requests::Extended<'a, requests::ExtendedLimits>), + + // Responses + Version(responses::Version), + Status(responses::Status), + Handle(responses::Handle), + Data(responses::Data), + Name(responses::Name<'a>), + Attrs(responses::Attrs), + ExtendedReply(responses::Extended<'a>), +} + +impl_try_from_packet! { + Version(responses::Version), + Status(responses::Status), + Handle(responses::Handle), + Data(responses::Data), + Name(responses::Name<'a>), + Attrs(responses::Attrs), + ExtendedReply(responses::Extended<'a>), +} + +impl Packet<'_> { + fn kind(&self) -> u8 { + match self { + Self::Init(_) => 1, + Self::Open(_) => 3, + Self::Close(_) => 4, + Self::Read(_) => 5, + Self::Write(_) => 6, + Self::Lstat(_) => 7, + Self::Fstat(_) => 8, + Self::SetStat(_) => 9, + Self::FSetStat(_) => 10, + Self::OpenDir(_) => 11, + Self::ReadDir(_) => 12, + Self::Remove(_) => 13, + Self::Mkdir(_) => 14, + Self::Rmdir(_) => 15, + Self::Realpath(_) => 16, + Self::Stat(_) => 17, + Self::Rename(_) => 18, + Self::Readlink(_) => 19, + Self::Symlink(_) => 20, + Self::ExtendedFsync(_) => 200, + Self::ExtendedHardlink(_) => 200, + Self::ExtendedLimits(_) => 200, + + // Responses + Self::Version(_) => 2, + Self::Status(_) => 101, + Self::Handle(_) => 102, + Self::Data(_) => 103, + Self::Name(_) => 104, + Self::Attrs(_) => 105, + Self::ExtendedReply(_) => 201, + } + } + + pub fn id(&self) -> u32 { + match self { + Self::Init(_) => 0, + Self::Open(v) => v.id, + Self::Close(v) => v.id, + Self::Read(v) => v.id, + Self::Write(v) => v.id, + Self::Lstat(v) => v.id, + Self::Fstat(v) => v.id, + Self::SetStat(v) => v.id, + Self::FSetStat(v) => v.id, + Self::OpenDir(v) => v.id, + Self::ReadDir(v) => v.id, + Self::Remove(v) => v.id, + Self::Mkdir(v) => v.id, + Self::Rmdir(v) => v.id, + Self::Realpath(v) => v.id, + Self::Stat(v) => v.id, + Self::Rename(v) => v.id, + Self::Readlink(v) => v.id, + Self::Symlink(v) => v.id, + Self::ExtendedFsync(v) => v.id, + Self::ExtendedHardlink(v) => v.id, + Self::ExtendedLimits(v) => v.id, + + // Responses + Self::Version(_) => 0, + Self::Status(v) => v.id, + Self::Handle(v) => v.id, + Self::Data(v) => v.id, + Self::Name(v) => v.id, + Self::Attrs(v) => v.id, + Self::ExtendedReply(v) => v.id, + } + } + + pub fn with_id(mut self, id: u32) -> Self { + match &mut self { + Self::Init(_) => {} + Self::Open(v) => v.id = id, + Self::Close(v) => v.id = id, + Self::Read(v) => v.id = id, + Self::Write(v) => v.id = id, + Self::Lstat(v) => v.id = id, + Self::Fstat(v) => v.id = id, + Self::SetStat(v) => v.id = id, + Self::FSetStat(v) => v.id = id, + Self::OpenDir(v) => v.id = id, + Self::ReadDir(v) => v.id = id, + Self::Remove(v) => v.id = id, + Self::Mkdir(v) => v.id = id, + Self::Rmdir(v) => v.id = id, + Self::Realpath(v) => v.id = id, + Self::Stat(v) => v.id = id, + Self::Rename(v) => v.id = id, + Self::Readlink(v) => v.id = id, + Self::Symlink(v) => v.id = id, + Self::ExtendedFsync(v) => v.id = id, + Self::ExtendedHardlink(v) => v.id = id, + Self::ExtendedLimits(v) => v.id = id, + + // Responses + Self::Version(_) => {} + Self::Status(v) => v.id = id, + Self::Handle(v) => v.id = id, + Self::Data(v) => v.id = id, + Self::Name(v) => v.id = id, + Self::Attrs(v) => v.id = id, + Self::ExtendedReply(v) => v.id = id, + } + self + } + + fn len(&self) -> usize { + let type_len = 1; + match self { + Self::Init(v) => type_len + v.len(), + Self::Open(v) => type_len + v.len(), + Self::Close(v) => type_len + v.len(), + Self::Read(v) => type_len + v.len(), + Self::Write(v) => type_len + v.len(), + Self::Lstat(v) => type_len + v.len(), + Self::Fstat(v) => type_len + v.len(), + Self::SetStat(v) => type_len + v.len(), + Self::FSetStat(v) => type_len + v.len(), + Self::OpenDir(v) => type_len + v.len(), + Self::ReadDir(v) => type_len + v.len(), + Self::Remove(v) => type_len + v.len(), + Self::Mkdir(v) => type_len + v.len(), + Self::Rmdir(v) => type_len + v.len(), + Self::Realpath(v) => type_len + v.len(), + Self::Stat(v) => type_len + v.len(), + Self::Rename(v) => type_len + v.len(), + Self::Readlink(v) => type_len + v.len(), + Self::Symlink(v) => type_len + v.len(), + Self::ExtendedFsync(v) => type_len + v.len(), + Self::ExtendedHardlink(v) => type_len + v.len(), + Self::ExtendedLimits(v) => type_len + v.len(), + + // Responses + Self::Version(v) => type_len + v.len(), + Self::Status(v) => type_len + v.len(), + Self::Handle(v) => type_len + v.len(), + Self::Data(v) => type_len + v.len(), + Self::Name(v) => type_len + v.len(), + Self::Attrs(v) => type_len + v.len(), + Self::ExtendedReply(v) => type_len + v.len(), + } + } +} + +pub fn to_bytes<'a, T>(value: T) -> Result, Error> +where + T: Into> + Serialize, +{ + let packet: Packet = value.into(); + + let len = u32::try_from(packet.len()).map_err(|_| Error::serde("packet too large"))?; + + let mut output = Vec::with_capacity(4 + len as usize); + output.extend_from_slice(&len.to_be_bytes()); + output.push(packet.kind()); + + let mut serializer = crate::Serializer { output }; + packet.serialize(&mut serializer)?; + Ok(serializer.output) +} + +// TODO: use Vec +pub fn from_bytes(mut bytes: &[u8]) -> Result, Error> { + let kind = *bytes.first().ok_or(Error::serde("empty packet"))?; + bytes = &bytes[1..]; + + Ok(match kind { + 2 => Packet::Version(Deserializer::once(bytes)?), + 101 => Packet::Status(Deserializer::once(bytes)?), + 102 => Packet::Handle(Deserializer::once(bytes)?), + 103 => Packet::Data(Deserializer::once(bytes)?), + 104 => Packet::Name(Deserializer::once(bytes)?), + 201 => Packet::ExtendedReply(Deserializer::once(bytes)?), + _ => return Err(Error::Packet("unknown packet kind")), + }) +} diff --git a/yazi-sftp/src/requests/close.rs b/yazi-sftp/src/requests/close.rs new file mode 100644 index 00000000..094ca5c0 --- /dev/null +++ b/yazi-sftp/src/requests/close.rs @@ -0,0 +1,15 @@ +use std::borrow::Cow; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Close<'a> { + pub id: u32, + pub handle: Cow<'a, str>, +} + +impl<'a> Close<'a> { + pub fn new(handle: impl Into>) -> Self { Self { id: 0, handle: handle.into() } } + + pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.handle.len() } +} diff --git a/yazi-sftp/src/requests/extended.rs b/yazi-sftp/src/requests/extended.rs new file mode 100644 index 00000000..69e25d1f --- /dev/null +++ b/yazi-sftp/src/requests/extended.rs @@ -0,0 +1,71 @@ +use std::{borrow::Cow, fmt::Debug}; + +use serde::{Deserialize, Serialize}; + +use crate::ByteStr; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Extended<'a, D> { + pub id: u32, + pub request: Cow<'a, str>, + pub data: D, +} + +impl Extended<'_, D> { + pub fn new<'a, R>(request: R, data: D) -> Extended<'a, D> + where + R: Into>, + { + Extended { id: 0, request: request.into(), data } + } + + pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.request.len() + self.data.len() } +} + +// --- Data +pub trait ExtendedData: Debug + Serialize + for<'de> Deserialize<'de> { + fn len(&self) -> usize; +} + +// --- Fsync +#[derive(Debug, Deserialize, Serialize)] +pub struct ExtendedFsync<'a> { + pub handle: Cow<'a, str>, +} + +impl<'a> ExtendedFsync<'a> { + pub fn new(handle: impl Into>) -> Self { Self { handle: handle.into() } } +} + +impl ExtendedData for ExtendedFsync<'_> { + fn len(&self) -> usize { 4 + self.handle.len() } +} + +// --- Hardlink +#[derive(Debug, Serialize, Deserialize)] +pub struct ExtendedHardlink<'a> { + pub original: ByteStr<'a>, + pub link: ByteStr<'a>, +} + +impl<'a> ExtendedHardlink<'a> { + pub fn new(original: O, link: L) -> Self + where + O: Into>, + L: Into>, + { + Self { original: original.into(), link: link.into() } + } +} + +impl ExtendedData for ExtendedHardlink<'_> { + fn len(&self) -> usize { 4 + self.original.len() + 4 + self.link.len() } +} + +// --- Limits +#[derive(Debug, Deserialize, Serialize)] +pub struct ExtendedLimits; + +impl ExtendedData for ExtendedLimits { + fn len(&self) -> usize { 0 } +} diff --git a/yazi-sftp/src/requests/fstat.rs b/yazi-sftp/src/requests/fstat.rs new file mode 100644 index 00000000..d077562d --- /dev/null +++ b/yazi-sftp/src/requests/fstat.rs @@ -0,0 +1,15 @@ +use std::borrow::Cow; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Fstat<'a> { + pub id: u32, + pub handle: Cow<'a, str>, +} + +impl<'a> Fstat<'a> { + pub fn new(handle: impl Into>) -> Self { Self { id: 0, handle: handle.into() } } + + pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.handle.len() } +} diff --git a/yazi-sftp/src/requests/init.rs b/yazi-sftp/src/requests/init.rs new file mode 100644 index 00000000..757e2967 --- /dev/null +++ b/yazi-sftp/src/requests/init.rs @@ -0,0 +1,22 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Init { + pub version: u32, + pub extensions: HashMap, +} + +impl Init { + pub fn new(extensions: HashMap) -> Self { Self { version: 3, extensions } } + + pub fn len(&self) -> usize { + size_of_val(&self.version) + + self.extensions.iter().map(|(k, v)| 4 + k.len() + 4 + v.len()).sum::() + } +} + +impl Default for Init { + fn default() -> Self { Self::new(HashMap::new()) } +} diff --git a/yazi-sftp/src/requests/lstat.rs b/yazi-sftp/src/requests/lstat.rs new file mode 100644 index 00000000..4e1cb7be --- /dev/null +++ b/yazi-sftp/src/requests/lstat.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +use crate::ByteStr; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Lstat<'a> { + pub id: u32, + pub path: ByteStr<'a>, +} + +impl Lstat<'_> { + pub fn new<'a>(path: impl Into>) -> Lstat<'a> { Lstat { id: 0, path: path.into() } } + + pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() } +} diff --git a/yazi-sftp/src/requests/mkdir.rs b/yazi-sftp/src/requests/mkdir.rs new file mode 100644 index 00000000..05310eb8 --- /dev/null +++ b/yazi-sftp/src/requests/mkdir.rs @@ -0,0 +1,21 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ByteStr, fs::Attrs}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Mkdir<'a> { + pub id: u32, + pub path: ByteStr<'a>, + pub attrs: Attrs, +} + +impl<'a> Mkdir<'a> { + pub fn new

(path: P, attrs: Attrs) -> Self + where + P: Into>, + { + Self { id: 0, path: path.into(), attrs } + } + + pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() + self.attrs.len() } +} diff --git a/yazi-sftp/src/requests/mod.rs b/yazi-sftp/src/requests/mod.rs new file mode 100644 index 00000000..326188cd --- /dev/null +++ b/yazi-sftp/src/requests/mod.rs @@ -0,0 +1,39 @@ +mod close; +mod extended; +mod fstat; +mod init; +mod lstat; +mod mkdir; +mod open; +mod open_dir; +mod read; +mod read_dir; +mod readlink; +mod realpath; +mod remove; +mod rename; +mod rmdir; +mod set_stat; +mod stat; +mod symlink; +mod write; + +pub use close::*; +pub use extended::*; +pub use fstat::*; +pub use init::*; +pub use lstat::*; +pub use mkdir::*; +pub use open::*; +pub use open_dir::*; +pub use read::*; +pub use read_dir::*; +pub use readlink::*; +pub use realpath::*; +pub use remove::*; +pub use rename::*; +pub use rmdir::*; +pub use set_stat::*; +pub use stat::*; +pub use symlink::*; +pub use write::*; diff --git a/yazi-sftp/src/requests/open.rs b/yazi-sftp/src/requests/open.rs new file mode 100644 index 00000000..25dd4d00 --- /dev/null +++ b/yazi-sftp/src/requests/open.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ByteStr, fs::{Attrs, Flags}}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Open<'a> { + pub id: u32, + pub path: ByteStr<'a>, + pub flags: Flags, + pub attrs: Attrs, +} + +impl<'a> Open<'a> { + pub fn new

(path: P, flags: Flags, attrs: Attrs) -> Self + where + P: Into>, + { + Self { id: 0, path: path.into(), flags, attrs } + } + + pub fn len(&self) -> usize { + size_of_val(&self.id) + 4 + self.path.len() + size_of_val(&self.flags) + self.attrs.len() + } +} diff --git a/yazi-sftp/src/requests/open_dir.rs b/yazi-sftp/src/requests/open_dir.rs new file mode 100644 index 00000000..5b0e3bbc --- /dev/null +++ b/yazi-sftp/src/requests/open_dir.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +use crate::ByteStr; + +#[derive(Debug, Deserialize, Serialize)] +pub struct OpenDir<'a> { + pub id: u32, + pub path: ByteStr<'a>, +} + +impl<'a> OpenDir<'a> { + pub fn new(path: impl Into>) -> Self { Self { id: 0, path: path.into() } } + + pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() } +} diff --git a/yazi-sftp/src/requests/read.rs b/yazi-sftp/src/requests/read.rs new file mode 100644 index 00000000..08c8407f --- /dev/null +++ b/yazi-sftp/src/requests/read.rs @@ -0,0 +1,27 @@ +use std::borrow::Cow; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Read<'a> { + pub id: u32, + pub handle: Cow<'a, str>, + pub offset: u64, + pub len: u32, +} + +impl<'a> Read<'a> { + pub fn new(handle: H, offset: u64, len: u32) -> Self + where + H: Into>, + { + Self { id: 0, handle: handle.into(), offset, len } + } + + pub fn len(&self) -> usize { + size_of_val(&self.id) + + 4 + self.handle.len() + + size_of_val(&self.offset) + + size_of_val(&self.len) + } +} diff --git a/yazi-sftp/src/requests/read_dir.rs b/yazi-sftp/src/requests/read_dir.rs new file mode 100644 index 00000000..8b8b653a --- /dev/null +++ b/yazi-sftp/src/requests/read_dir.rs @@ -0,0 +1,15 @@ +use std::borrow::Cow; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct ReadDir<'a> { + pub id: u32, + pub handle: Cow<'a, str>, +} + +impl<'a> ReadDir<'a> { + pub fn new(handle: &'a str) -> Self { Self { id: 0, handle: handle.into() } } + + pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.handle.len() } +} diff --git a/yazi-sftp/src/requests/readlink.rs b/yazi-sftp/src/requests/readlink.rs new file mode 100644 index 00000000..aea62145 --- /dev/null +++ b/yazi-sftp/src/requests/readlink.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +use crate::ByteStr; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Readlink<'a> { + pub id: u32, + pub path: ByteStr<'a>, +} + +impl<'a> Readlink<'a> { + pub fn new(path: impl Into>) -> Self { Self { id: 0, path: path.into() } } + + pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() } +} diff --git a/yazi-sftp/src/requests/realpath.rs b/yazi-sftp/src/requests/realpath.rs new file mode 100644 index 00000000..00268921 --- /dev/null +++ b/yazi-sftp/src/requests/realpath.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +use crate::ByteStr; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Realpath<'a> { + pub id: u32, + pub path: ByteStr<'a>, +} + +impl<'a> Realpath<'a> { + pub fn new(path: impl Into>) -> Self { Self { id: 0, path: path.into() } } + + pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() } +} diff --git a/yazi-sftp/src/requests/remove.rs b/yazi-sftp/src/requests/remove.rs new file mode 100644 index 00000000..71a11381 --- /dev/null +++ b/yazi-sftp/src/requests/remove.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +use crate::ByteStr; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Remove<'a> { + pub id: u32, + pub path: ByteStr<'a>, +} + +impl<'a> Remove<'a> { + pub fn new(path: impl Into>) -> Self { Self { id: 0, path: path.into() } } + + pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() } +} diff --git a/yazi-sftp/src/requests/rename.rs b/yazi-sftp/src/requests/rename.rs new file mode 100644 index 00000000..698ad86c --- /dev/null +++ b/yazi-sftp/src/requests/rename.rs @@ -0,0 +1,22 @@ +use serde::{Deserialize, Serialize}; + +use crate::ByteStr; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Rename<'a> { + pub id: u32, + pub from: ByteStr<'a>, + pub to: ByteStr<'a>, +} + +impl<'a> Rename<'a> { + pub fn new(from: F, to: T) -> Self + where + F: Into>, + T: Into>, + { + Self { id: 0, from: from.into(), to: to.into() } + } + + pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.from.len() + 4 + self.to.len() } +} diff --git a/yazi-sftp/src/requests/rmdir.rs b/yazi-sftp/src/requests/rmdir.rs new file mode 100644 index 00000000..686afe69 --- /dev/null +++ b/yazi-sftp/src/requests/rmdir.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +use crate::ByteStr; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Rmdir<'a> { + pub id: u32, + pub path: ByteStr<'a>, +} + +impl<'a> Rmdir<'a> { + pub fn new(path: impl Into>) -> Self { Self { id: 0, path: path.into() } } + + pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() } +} diff --git a/yazi-sftp/src/requests/set_stat.rs b/yazi-sftp/src/requests/set_stat.rs new file mode 100644 index 00000000..6096cb83 --- /dev/null +++ b/yazi-sftp/src/requests/set_stat.rs @@ -0,0 +1,35 @@ +use std::borrow::Cow; + +use serde::{Deserialize, Serialize}; + +use crate::{ByteStr, fs::Attrs}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct SetStat<'a> { + pub id: u32, + pub path: ByteStr<'a>, + pub attrs: Attrs, +} + +impl<'a> SetStat<'a> { + pub fn new(path: impl Into>, attrs: Attrs) -> Self { + Self { id: 0, path: path.into(), attrs } + } + + pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() + self.attrs.len() } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct FSetStat<'a> { + pub id: u32, + pub handle: Cow<'a, str>, + pub attrs: Attrs, +} + +impl<'a> FSetStat<'a> { + pub fn new(handle: impl Into>, attrs: Attrs) -> Self { + Self { id: 0, handle: handle.into(), attrs } + } + + pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.handle.len() + self.attrs.len() } +} diff --git a/yazi-sftp/src/requests/stat.rs b/yazi-sftp/src/requests/stat.rs new file mode 100644 index 00000000..51209c46 --- /dev/null +++ b/yazi-sftp/src/requests/stat.rs @@ -0,0 +1,15 @@ +use serde::{Deserialize, Serialize}; + +use crate::ByteStr; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Stat<'a> { + pub id: u32, + pub path: ByteStr<'a>, +} + +impl<'a> Stat<'a> { + pub fn new(path: impl Into>) -> Self { Self { id: 0, path: path.into() } } + + pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() } +} diff --git a/yazi-sftp/src/requests/symlink.rs b/yazi-sftp/src/requests/symlink.rs new file mode 100644 index 00000000..11b010ad --- /dev/null +++ b/yazi-sftp/src/requests/symlink.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; + +use crate::ByteStr; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Symlink<'a> { + pub id: u32, + pub link: ByteStr<'a>, + pub original: ByteStr<'a>, +} + +impl<'a> Symlink<'a> { + pub fn new(link: L, original: O) -> Self + where + L: Into>, + O: Into>, + { + Self { id: 0, link: link.into(), original: original.into() } + } + + pub fn len(&self) -> usize { + size_of_val(&self.id) + 4 + self.link.len() + 4 + self.original.len() + } +} diff --git a/yazi-sftp/src/requests/write.rs b/yazi-sftp/src/requests/write.rs new file mode 100644 index 00000000..1e8213b2 --- /dev/null +++ b/yazi-sftp/src/requests/write.rs @@ -0,0 +1,25 @@ +use std::borrow::Cow; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Write<'a> { + pub id: u32, + pub handle: Cow<'a, str>, + pub offset: u64, + pub data: Cow<'a, [u8]>, +} + +impl Write<'_> { + pub fn new<'a, H, D>(handle: H, offset: u64, data: D) -> Write<'a> + where + H: Into>, + D: Into>, + { + Write { id: 0, handle: handle.into(), offset, data: data.into() } + } + + pub fn len(&self) -> usize { + size_of_val(&self.id) + 4 + self.handle.len() + size_of_val(&self.offset) + 4 + self.data.len() + } +} diff --git a/yazi-sftp/src/responses/attrs.rs b/yazi-sftp/src/responses/attrs.rs new file mode 100644 index 00000000..6a783fa5 --- /dev/null +++ b/yazi-sftp/src/responses/attrs.rs @@ -0,0 +1,13 @@ +use serde::{Deserialize, Serialize}; + +use crate::fs; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Attrs { + pub id: u32, + pub attrs: fs::Attrs, +} + +impl Attrs { + pub fn len(&self) -> usize { size_of_val(&self.id) + self.attrs.len() } +} diff --git a/yazi-sftp/src/responses/data.rs b/yazi-sftp/src/responses/data.rs new file mode 100644 index 00000000..f2e257f2 --- /dev/null +++ b/yazi-sftp/src/responses/data.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Data { + pub id: u32, + pub data: Vec, +} + +impl Data { + pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.data.len() } +} diff --git a/yazi-sftp/src/responses/extended.rs b/yazi-sftp/src/responses/extended.rs new file mode 100644 index 00000000..8be4fafc --- /dev/null +++ b/yazi-sftp/src/responses/extended.rs @@ -0,0 +1,85 @@ +use std::{borrow::Cow, fmt, ops::Deref}; + +use serde::{Deserialize, Serialize, de::Visitor, ser::SerializeSeq}; + +use crate::Error; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Extended<'a> { + pub id: u32, + pub data: ExtendedData<'a>, +} + +impl<'a> Extended<'a> { + pub fn len(&self) -> usize { size_of_val(&self.id) + self.data.len() } +} + +// --- Data +#[derive(Debug)] +pub struct ExtendedData<'a>(Cow<'a, [u8]>); + +impl Deref for ExtendedData<'_> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { &self.0 } +} + +impl Serialize for ExtendedData<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut seq = serializer.serialize_seq(None)?; + for b in self.0.as_ref() { + seq.serialize_element(b)?; + } + seq.end() + } +} + +impl<'de> Deserialize<'de> for ExtendedData<'_> { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct ExtendedDataVisitor; + + impl<'de> Visitor<'de> for ExtendedDataVisitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("extended data") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let mut bytes = Vec::with_capacity(seq.size_hint().unwrap_or(0)); + while let Some(b) = seq.next_element()? { + bytes.push(b); + } + Ok(bytes) + } + } + + deserializer.deserialize_any(ExtendedDataVisitor).map(Cow::Owned).map(ExtendedData) + } +} + +// --- Limits +#[derive(Debug, Deserialize, Serialize)] +pub struct ExtendedLimits { + pub packet_len: u64, + pub read_len: u64, + pub write_len: u64, + pub open_handles: u64, +} + +impl TryFrom> for ExtendedLimits { + type Error = Error; + + fn try_from(value: Extended<'_>) -> Result { + crate::Deserializer::once(&value.data) + } +} diff --git a/yazi-sftp/src/responses/handle.rs b/yazi-sftp/src/responses/handle.rs new file mode 100644 index 00000000..b7680bd6 --- /dev/null +++ b/yazi-sftp/src/responses/handle.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Handle { + pub id: u32, + pub handle: String, +} + +impl Handle { + pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.handle.len() } +} diff --git a/yazi-sftp/src/responses/mod.rs b/yazi-sftp/src/responses/mod.rs new file mode 100644 index 00000000..91a107e0 --- /dev/null +++ b/yazi-sftp/src/responses/mod.rs @@ -0,0 +1,15 @@ +mod attrs; +mod data; +mod extended; +mod handle; +mod name; +mod status; +mod version; + +pub use attrs::*; +pub use data::*; +pub use extended::*; +pub use handle::*; +pub use name::*; +pub use status::*; +pub use version::*; diff --git a/yazi-sftp/src/responses/name.rs b/yazi-sftp/src/responses/name.rs new file mode 100644 index 00000000..6126cd22 --- /dev/null +++ b/yazi-sftp/src/responses/name.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; + +use crate::{ByteStr, fs::Attrs}; + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct Name<'a> { + pub id: u32, + pub items: Vec>, +} + +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct NameItem<'a> { + pub name: ByteStr<'a>, + pub long_name: ByteStr<'a>, + pub attrs: Attrs, +} + +impl Name<'_> { + pub fn len(&self) -> usize { + size_of_val(&self.id) + 4 + self.items.iter().map(|v| v.len()).sum::() + } +} + +impl NameItem<'_> { + pub fn len(&self) -> usize { 4 + self.name.len() + 4 + self.long_name.len() + self.attrs.len() } +} diff --git a/yazi-sftp/src/responses/status.rs b/yazi-sftp/src/responses/status.rs new file mode 100644 index 00000000..fc864c89 --- /dev/null +++ b/yazi-sftp/src/responses/status.rs @@ -0,0 +1,43 @@ +use serde::{Deserialize, Serialize}; + +use crate::Error; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Status { + pub id: u32, + pub code: StatusCode, + pub message: String, + pub language: String, +} + +impl From for Result<(), Error> { + fn from(status: Status) -> Self { + if status.is_ok() { Ok(()) } else { Err(Error::Status(status)) } + } +} + +impl Status { + pub fn len(&self) -> usize { + size_of_val(&self.id) + + size_of_val(&(self.code as u32)) + + 4 + self.message.len() + + 4 + self.language.len() + } + + pub fn is_ok(&self) -> bool { self.code == StatusCode::Ok } + + pub fn is_eof(&self) -> bool { self.code == StatusCode::Eof } +} + +#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)] +pub enum StatusCode { + Ok = 0, + Eof = 1, + NoSuchFile = 2, + PermissionDenied = 3, + Failure = 4, + BadMessage = 5, + NoConnection = 6, + ConnectionLost = 7, + OpUnsupported = 8, +} diff --git a/yazi-sftp/src/responses/version.rs b/yazi-sftp/src/responses/version.rs new file mode 100644 index 00000000..967ff60a --- /dev/null +++ b/yazi-sftp/src/responses/version.rs @@ -0,0 +1,16 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Version { + pub version: u32, + pub extensions: HashMap, +} + +impl Version { + pub fn len(&self) -> usize { + size_of_val(&self.version) + + self.extensions.iter().map(|(k, v)| 4 + k.len() + 4 + v.len()).sum::() + } +} diff --git a/yazi-sftp/src/ser.rs b/yazi-sftp/src/ser.rs new file mode 100644 index 00000000..c7549cce --- /dev/null +++ b/yazi-sftp/src/ser.rs @@ -0,0 +1,286 @@ +use serde::ser::{SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple, SerializeTupleStruct, SerializeTupleVariant}; + +use crate::Error; + +pub(super) struct Serializer { + pub(super) output: Vec, +} + +impl<'a> serde::Serializer for &'a mut Serializer { + type Error = crate::Error; + type Ok = (); + type SerializeMap = &'a mut Serializer; + type SerializeSeq = &'a mut Serializer; + type SerializeStruct = &'a mut Serializer; + type SerializeStructVariant = &'a mut Serializer; + type SerializeTuple = &'a mut Serializer; + type SerializeTupleStruct = &'a mut Serializer; + type SerializeTupleVariant = &'a mut Serializer; + + fn serialize_bool(self, _v: bool) -> Result { + Err(Error::serde("bool not supported")) + } + + fn serialize_i8(self, _v: i8) -> Result { + Err(Error::serde("i8 not supported")) + } + + fn serialize_i16(self, _v: i16) -> Result { + Err(Error::serde("i16 not supported")) + } + + fn serialize_i32(self, _v: i32) -> Result { + Err(Error::serde("i32 not supported")) + } + + fn serialize_i64(self, _v: i64) -> Result { + Err(Error::serde("i64 not supported")) + } + + fn serialize_u8(self, v: u8) -> Result { + self.output.push(v); + Ok(()) + } + + fn serialize_u16(self, _v: u16) -> Result { + Err(Error::serde("u16 not supported")) + } + + fn serialize_u32(self, v: u32) -> Result { + self.output.extend_from_slice(&v.to_be_bytes()); + Ok(()) + } + + fn serialize_u64(self, v: u64) -> Result { + self.output.extend_from_slice(&v.to_be_bytes()); + Ok(()) + } + + fn serialize_f32(self, _v: f32) -> Result { + Err(Error::serde("f32 not supported")) + } + + fn serialize_f64(self, _v: f64) -> Result { + Err(Error::serde("f64 not supported")) + } + + fn serialize_char(self, _v: char) -> Result { + Err(Error::serde("char not supported")) + } + + fn serialize_str(self, v: &str) -> Result { + self.serialize_bytes(v.as_bytes()) + } + + fn serialize_bytes(self, v: &[u8]) -> Result { + let len = u32::try_from(v.len()).map_err(|_| Error::serde("bytes too long"))?; + self.output.extend_from_slice(&len.to_be_bytes()); + self.output.extend_from_slice(v); + Ok(()) + } + + fn serialize_none(self) -> Result { Ok(()) } + + fn serialize_some(self, value: &T) -> Result + where + T: serde::Serialize + ?Sized, + { + value.serialize(self) + } + + fn serialize_unit(self) -> Result { + Err(Error::serde("unit not supported")) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { Ok(()) } + + fn serialize_unit_variant( + self, + _name: &'static str, + variant_index: u32, + _variant: &'static str, + ) -> Result { + self.serialize_u32(variant_index) + } + + fn serialize_newtype_struct( + self, + _name: &'static str, + value: &T, + ) -> Result + where + T: serde::Serialize + ?Sized, + { + value.serialize(self) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + value: &T, + ) -> Result + where + T: serde::Serialize + ?Sized, + { + value.serialize(self) + } + + fn serialize_seq(self, len: Option) -> Result { + if let Some(len) = len { + self.serialize_u32(len.try_into().map_err(|_| Error::serde("sequence too long"))?)?; + } + + Ok(self) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Ok(self) + } + + fn serialize_tuple(self, _len: usize) -> Result { Ok(self) } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Ok(self) + } + + fn serialize_map(self, _len: Option) -> Result { + Ok(self) + } + + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Ok(self) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(Error::serde("struct variant not supported")) + } + + fn is_human_readable(&self) -> bool { false } +} + +impl SerializeMap for &mut Serializer { + type Error = Error; + type Ok = (); + + fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> + where + T: serde::Serialize + ?Sized, + { + key.serialize(&mut **self) + } + + fn serialize_value(&mut self, value: &T) -> Result<(), Self::Error> + where + T: serde::Serialize + ?Sized, + { + value.serialize(&mut **self) + } + + fn end(self) -> Result { Ok(()) } +} + +impl SerializeSeq for &mut Serializer { + type Error = Error; + type Ok = (); + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: serde::Serialize + ?Sized, + { + value.serialize(&mut **self) + } + + fn end(self) -> Result { Ok(()) } +} + +impl SerializeStruct for &mut Serializer { + type Error = Error; + type Ok = (); + + fn serialize_field(&mut self, _key: &'static str, value: &T) -> Result<(), Self::Error> + where + T: serde::Serialize + ?Sized, + { + value.serialize(&mut **self) + } + + fn end(self) -> Result { Ok(()) } +} + +impl SerializeStructVariant for &mut Serializer { + type Error = Error; + type Ok = (); + + fn serialize_field(&mut self, _key: &'static str, value: &T) -> Result<(), Self::Error> + where + T: serde::Serialize + ?Sized, + { + value.serialize(&mut **self) + } + + fn end(self) -> Result { Ok(()) } +} + +impl SerializeTuple for &mut Serializer { + type Error = Error; + type Ok = (); + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: serde::Serialize + ?Sized, + { + value.serialize(&mut **self) + } + + fn end(self) -> Result { Ok(()) } +} + +impl SerializeTupleStruct for &mut Serializer { + type Error = Error; + type Ok = (); + + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> + where + T: serde::Serialize + ?Sized, + { + value.serialize(&mut **self) + } + + fn end(self) -> Result { Ok(()) } +} + +impl SerializeTupleVariant for &mut Serializer { + type Error = Error; + type Ok = (); + + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> + where + T: serde::Serialize + ?Sized, + { + value.serialize(&mut **self) + } + + fn end(self) -> Result { Ok(()) } +} diff --git a/yazi-sftp/src/session.rs b/yazi-sftp/src/session.rs new file mode 100644 index 00000000..7ad34583 --- /dev/null +++ b/yazi-sftp/src/session.rs @@ -0,0 +1,265 @@ +use std::{any::TypeId, collections::HashMap, io::ErrorKind, path::PathBuf, sync::Arc}; + +use parking_lot::Mutex; +use russh::{ChannelStream, client::Msg}; +use serde::Serialize; +use tokio::{io::{AsyncReadExt, AsyncWriteExt}, select, sync::{mpsc, oneshot}}; + +use crate::{ByteStr, Error, Id, Packet, fs::{Attrs, File, Flags, ReadDir}, requests, responses}; + +pub struct Session { + tx: mpsc::UnboundedSender>, + pub(crate) id: Id, + callback: Arc>>>>, + extensions: HashMap, +} + +impl Session { + pub fn make(stream: ChannelStream) -> Self { + let (tx, mut rx) = mpsc::unbounded_channel::>(); + let (mut reader, mut writer) = tokio::io::split(stream); + let callback = Arc::new(Mutex::new(HashMap::<_, oneshot::Sender<_>>::new())); + + tokio::spawn(async move { + while let Some(data) = rx.recv().await { + if data.is_empty() { + rx.close(); + writer.shutdown().await.ok(); + break; + } else { + writer.write_all(&data).await.ok(); + } + } + }); + + let (tx_, callback_) = (tx.clone(), callback.clone()); + tokio::spawn(async move { + loop { + select! { + Ok(len) = reader.read_u32() => { + let mut buf = vec![0; len as usize]; + if let Err(e) = reader.read_exact(&mut buf).await && e.kind() == ErrorKind::UnexpectedEof { + tx_.send(vec![]).ok(); + break; + } + if let Ok(packet) = crate::from_bytes(&buf) + && let Some(cb) = callback_.lock().remove(&packet.id()) + { + cb.send(packet).ok(); + } + } + _ = tx_.closed() => break, + } + } + }); + + Self { tx, id: Id::default(), callback, extensions: HashMap::new() } + } + + pub async fn init(&mut self) -> Result<(), Error> { + let version: responses::Version = self.send(requests::Init::default()).await?; + self.extensions = version.extensions; + Ok(()) + } + + pub async fn open<'a, P>(&self, path: P, flags: Flags, attrs: Attrs) -> Result, Error> + where + P: Into>, + { + let handle: responses::Handle = self.send(requests::Open::new(path, flags, attrs)).await?; + + Ok(File::new(self, handle.handle)) + } + + pub fn close(&self, handle: &str) -> Result>, Error> { + self.send_sync(requests::Close::new(handle)) + } + + pub fn read( + &self, + handle: &str, + offset: u64, + len: u32, + ) -> Result>, Error> { + self.send_sync(requests::Read::new(handle, offset, len)) + } + + pub fn write( + &self, + handle: &str, + offset: u64, + data: &[u8], + ) -> Result>, Error> { + self.send_sync(requests::Write::new(handle, offset, data)) + } + + pub async fn lstat<'a, P>(&self, path: P) -> Result + where + P: Into>, + { + let attrs: responses::Attrs = self.send(requests::Lstat::new(path)).await?; + Ok(attrs.attrs) + } + + pub async fn fstat(&self, handle: &str) -> Result { + let attrs: responses::Attrs = self.send(requests::Fstat::new(handle)).await?; + Ok(attrs.attrs) + } + + pub async fn setstat<'a, P>(&self, path: P, attrs: Attrs) -> Result<(), Error> + where + P: Into>, + { + let status: responses::Status = self.send(requests::SetStat::new(path, attrs)).await?; + status.into() + } + + pub async fn fsetstat(&self, handle: &str, attrs: Attrs) -> Result<(), Error> { + let status: responses::Status = self.send(requests::FSetStat::new(handle, attrs)).await?; + status.into() + } + + pub async fn read_dir<'a>(&'a self, dir: impl Into>) -> Result, Error> { + let dir: ByteStr = dir.into(); + let handle: responses::Handle = self.send(requests::OpenDir::new(&dir)).await?; + + Ok(ReadDir::new(self, dir, handle.handle)) + } + + pub async fn remove<'a, P>(&self, path: P) -> Result<(), Error> + where + P: Into>, + { + let status: responses::Status = self.send(requests::Remove::new(path)).await?; + status.into() + } + + pub async fn mkdir<'a, P>(&self, path: P, attrs: Attrs) -> Result<(), Error> + where + P: Into>, + { + let status: responses::Status = self.send(requests::Mkdir::new(path, attrs)).await?; + status.into() + } + + pub async fn rmdir<'a, P>(&self, path: P) -> Result<(), Error> + where + P: Into>, + { + let status: responses::Status = self.send(requests::Rmdir::new(path)).await?; + status.into() + } + + pub async fn realpath<'a, P>(&self, path: P) -> Result + where + P: Into>, + { + let mut name: responses::Name = self.send(requests::Realpath::new(path)).await?; + if name.items.is_empty() { + Err(Error::custom("realpath returned no names")) + } else { + Ok(name.items.swap_remove(0).name.into_path()) + } + } + + pub async fn stat<'a, P>(&self, path: P) -> Result + where + P: Into>, + { + let attrs: responses::Attrs = self.send(requests::Stat::new(path)).await?; + Ok(attrs.attrs) + } + + pub async fn rename<'a, F, T>(&self, from: F, to: T) -> Result<(), Error> + where + F: Into>, + T: Into>, + { + let status: responses::Status = self.send(requests::Rename::new(from, to)).await?; + status.into() + } + + pub async fn readlink<'a, P>(&self, path: P) -> Result + where + P: Into>, + { + let mut name: responses::Name = self.send(requests::Readlink::new(path)).await?; + if name.items.is_empty() { + Err(Error::custom("readlink returned no names")) + } else { + Ok(name.items.swap_remove(0).name.into_path()) + } + } + + pub async fn symlink<'a, L, O>(&self, link: L, original: O) -> Result<(), Error> + where + L: Into>, + O: Into>, + { + let status: responses::Status = self.send(requests::Symlink::new(link, original)).await?; + status.into() + } + + pub fn fsync(&self, handle: &str) -> Result>, Error> { + if self.extensions.get("fsync@openssh.com").is_none_or(|s| s != "1") { + return Err(Error::Unsupported); + } + + let data = requests::ExtendedFsync::new(handle); + self.send_sync(requests::Extended::new("fsync@openssh.com", data)) + } + + pub async fn hardlink<'a, O, L>(&self, original: O, link: L) -> Result<(), Error> + where + O: Into>, + L: Into>, + { + if self.extensions.get("hardlink@openssh.com").is_none_or(|s| s != "1") { + return Err(Error::Unsupported); + } + + let data = requests::ExtendedHardlink::new(original, link); + let status: responses::Status = + self.send(requests::Extended::new("hardlink@openssh.com", data)).await?; + status.into() + } + + pub async fn limits(&self) -> Result { + if self.extensions.get("limits@openssh.com").is_none_or(|s| s != "1") { + return Err(Error::Unsupported); + } + + let extended: responses::Extended = + self.send(requests::Extended::new("limits@openssh.com", requests::ExtendedLimits)).await?; + extended.try_into() + } + + pub async fn send<'a, I, O>(&self, input: I) -> Result + where + I: Into> + Serialize, + O: TryFrom, Error = Error> + 'static, + { + match self.send_sync(input)?.await? { + Packet::Status(status) if TypeId::of::() != TypeId::of::() => { + Err(Error::Status(status)) + } + response => response.try_into(), + } + } + + pub fn send_sync<'a, I>(&self, input: I) -> Result>, Error> + where + I: Into> + Serialize, + { + let mut request: Packet = input.into(); + if request.id() == 0 { + request = request.with_id(self.id.next()); + } + + let (tx, rx) = oneshot::channel(); + self.callback.lock().insert(request.id(), tx); + self.tx.send(crate::to_bytes(request)?)?; + + Ok(rx) + } +} diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index ed24f436..562fe753 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -30,7 +30,7 @@ libc = { workspace = true } uzers = { workspace = true } [target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.60.2", features = [ "Win32_UI_Shell" ] } +windows-sys = { version = "0.61.0", features = [ "Win32_UI_Shell" ] } [target.'cfg(target_os = "macos")'.dependencies] crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } diff --git a/yazi-term/Cargo.toml b/yazi-term/Cargo.toml index 1a07672d..159af5de 100644 --- a/yazi-term/Cargo.toml +++ b/yazi-term/Cargo.toml @@ -26,4 +26,4 @@ libc = { workspace = true } crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } [target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.60.2", features = [ "Win32_Globalization", "Win32_Storage_FileSystem", "Win32_System_IO", "Win32_System_Console", "Win32_System_Threading", "Win32_Security" ] } +windows-sys = { version = "0.61.0", features = [ "Win32_Globalization", "Win32_Storage_FileSystem", "Win32_System_IO", "Win32_System_Console", "Win32_System_Threading", "Win32_Security" ] }