sftp-0.3.0/.cargo_vcs_info.json0000644000000001361046102023000120320ustar { "git": { "sha1": "099f2d4c399f16a4b9a5bd7aba636283a48d411f" }, "path_in_vcs": "" }sftp-0.3.0/.codespellrc000064400000000000000000000000461046102023000131000ustar 00000000000000[codespell] ignore-words-list = crate sftp-0.3.0/.github/CODEOWNERS000064400000000000000000000000121046102023000135240ustar 00000000000000* @jelmer sftp-0.3.0/.github/FUNDING.yml000064400000000000000000000000171046102023000137530ustar 00000000000000github: jelmer sftp-0.3.0/.github/dependabot.yml000064400000000000000000000006251046102023000147730ustar 00000000000000# Please see the documentation for all configuration options: # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "cargo" directory: "/" schedule: interval: "weekly" rebase-strategy: "disabled" - package-ecosystem: "github-actions" directory: "/" schedule: interval: weekly sftp-0.3.0/.github/workflows/rust.yml000064400000000000000000000005341046102023000157170ustar 00000000000000name: Rust on: push: pull_request: env: CARGO_TERM_COLOR: always jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Build run: cargo build --verbose env: RUSTFLAGS: -Dwarnings - name: Run tests run: cargo test --verbose env: RUSTFLAGS: -Dwarnings sftp-0.3.0/.gitignore000064400000000000000000000000121046102023000125610ustar 00000000000000target *~ sftp-0.3.0/CODE_OF_CONDUCT.md000064400000000000000000000125451046102023000134060ustar 00000000000000 # Contributor Covenant Code of Conduct ## Our Pledge We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socioeconomic status, nationality, personal appearance, race, religion, or sexual identity and orientation. We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards Examples of behavior that contributes to a positive environment for our community include: * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD]. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations]. [homepage]: https://www.contributor-covenant.org [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html [Mozilla CoC]: https://github.com/mozilla/diversity [FAQ]: https://www.contributor-covenant.org/faq [translations]: https://www.contributor-covenant.org/translations sftp-0.3.0/Cargo.lock0000644000001763341046102023000100230ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "adler2" 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 0.1.7", "generic-array 0.14.7", ] [[package]] name = "aead" version = "0.6.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b657e772794c6b04730ea897b66a058ccd866c16d1967da05eeeecec39043fe" dependencies = [ "crypto-common 0.2.1", "inout 0.2.2", ] [[package]] name = "aes" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher 0.4.4", "cpufeatures 0.2.17", ] [[package]] name = "aes" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8" dependencies = [ "cipher 0.5.1", "cpubits", "cpufeatures 0.3.0", ] [[package]] name = "aes-gcm" version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" dependencies = [ "aead 0.5.2", "aes 0.8.4", "cipher 0.4.4", "ctr 0.9.2", "ghash 0.5.1", "subtle", ] [[package]] name = "aes-gcm" version = "0.11.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e22c0c90bbe8d4f77c3ca9ddabe41a1f8382d6fc1f7cea89459d0f320371f972" dependencies = [ "aead 0.6.0-rc.10", "aes 0.9.0", "cipher 0.5.1", "ctr 0.10.0", "ghash 0.6.0", "subtle", ] [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anyhow" version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "argon2" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2", "cpufeatures 0.2.17", "password-hash", ] [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" dependencies = [ "aws-lc-sys", "untrusted", "zeroize", ] [[package]] name = "aws-lc-sys" version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" dependencies = [ "cc", "cmake", "dunce", "fs_extra", ] [[package]] name = "base16ct" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" [[package]] name = "base64ct" version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bcrypt-pbkdf" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2" dependencies = [ "blowfish", "pbkdf2 0.12.2", "sha2 0.10.9", ] [[package]] name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "blake2" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ "digest 0.10.7", ] [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array 0.14.7", ] [[package]] name = "block-buffer" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" dependencies = [ "hybrid-array", ] [[package]] name = "block-padding" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ "generic-array 0.14.7", ] [[package]] name = "block-padding" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" dependencies = [ "hybrid-array", ] [[package]] name = "blowfish" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" dependencies = [ "byteorder", "cipher 0.4.4", ] [[package]] name = "bumpalo" version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cbc" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ "cipher 0.4.4", ] [[package]] name = "cbc" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98db6aeaef0eeef2c1e3ce9a27b739218825dae116076352ac3777076aa22225" dependencies = [ "cipher 0.5.1", ] [[package]] name = "cc" version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "jobserver", "libc", "shlex", ] [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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 0.4.4", "cpufeatures 0.2.17", ] [[package]] name = "chacha20" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", "cpufeatures 0.3.0", "rand_core 0.10.1", ] [[package]] name = "chrono" version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-link", ] [[package]] name = "cipher" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common 0.1.7", "inout 0.1.4", ] [[package]] name = "cipher" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea" dependencies = [ "block-buffer 0.12.0", "crypto-common 0.2.1", "inout 0.2.2", ] [[package]] name = "clipboard-win" version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" dependencies = [ "error-code", ] [[package]] name = "cmake" version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] [[package]] name = "cmov" version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "const-oid" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "core-foundation-sys" version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpubits" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ef0c543070d296ea414df2dd7625d1b24866ce206709d8a4a424f28377f5861" [[package]] name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "cpufeatures" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" dependencies = [ "libc", ] [[package]] name = "crc32fast" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] [[package]] name = "crypto-bigint" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42a0d26b245348befa0c121944541476763dcc46ede886c88f9d12e1697d27c3" dependencies = [ "cpubits", "ctutils", "getrandom 0.4.2", "hybrid-array", "num-traits", "rand_core 0.10.1", "serdect", "subtle", "zeroize", ] [[package]] name = "crypto-common" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array 0.14.7", "typenum", ] [[package]] name = "crypto-common" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" dependencies = [ "getrandom 0.4.2", "hybrid-array", "rand_core 0.10.1", ] [[package]] name = "crypto-primes" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21f41f23de7d24cdbda7f0c4d9c0351f99a4ceb258ef30e5c1927af8987ffe5a" dependencies = [ "crypto-bigint", "libm", "rand_core 0.10.1", ] [[package]] name = "ctr" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ "cipher 0.4.4", ] [[package]] name = "ctr" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17469f8eb9bdbfad10f71f4cfddfd38b01143520c0e717d8796ccb4d44d44e42" dependencies = [ "cipher 0.5.1", ] [[package]] name = "ctutils" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" dependencies = [ "cmov", "subtle", ] [[package]] name = "curve25519-dalek" version = "5.0.0-pre.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335f1947f241137a14106b6f5acc5918a5ede29c9d71d3f2cb1678d5075d9fc3" dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest 0.11.2", "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 = "data-encoding" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "delegate" version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "780eb241654bf097afb00fc5f054a09b687dad862e485fdcf8399bb056565370" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "der" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" dependencies = [ "const-oid 0.10.2", "pem-rfc7468 1.0.0", "zeroize", ] [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", "const-oid 0.9.6", "crypto-common 0.1.7", "subtle", ] [[package]] name = "digest" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" dependencies = [ "block-buffer 0.12.0", "const-oid 0.10.2", "crypto-common 0.2.1", "ctutils", ] [[package]] name = "dunce" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "ecdsa" version = "0.17.0-rc.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc4bf51f0534ed6e59a0f2f26272b64ba55c470133f8424c2adfd1c4d59d9988" dependencies = [ "der", "digest 0.11.2", "elliptic-curve", "rfc6979", "signature", "spki", "zeroize", ] [[package]] name = "ed25519" version = "3.0.0-rc.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6e914c7c52decb085cea910552e24c63ac019e3ab8bf001ff736da9a9d9d890" dependencies = [ "pkcs8", "signature", ] [[package]] name = "ed25519-dalek" version = "3.0.0-pre.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053618a4c3d3bc24f188aa660ae75a46eeab74ef07fb415c61431e5e7cd4749b" dependencies = [ "curve25519-dalek", "ed25519", "rand_core 0.10.1", "serde", "sha2 0.11.0", "signature", "subtle", "zeroize", ] [[package]] name = "elliptic-curve" version = "0.14.0-rc.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b148a81cede8f4023248f980cffdf7611c46f2add469c6980e815b7c5b764ba5" dependencies = [ "base16ct", "crypto-bigint", "crypto-common 0.2.1", "digest 0.11.2", "hkdf", "hybrid-array", "once_cell", "pem-rfc7468 1.0.0", "pkcs8", "rand_core 0.10.1", "rustcrypto-ff", "rustcrypto-group", "sec1", "subtle", "zeroize", ] [[package]] name = "endian-type" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "869b0adbda23651a9c5c0c3d270aac9fcb52e8622a8f2b17e57802d7791962f2" [[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 = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "error-code" version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" [[package]] name = "fiat-crypto" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" [[package]] name = "find-msvc-tools" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", ] [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "fs_extra" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", "futures-executor", "futures-io", "futures-sink", "futures-task", "futures-util", ] [[package]] name = "futures-channel" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", ] [[package]] name = "futures-core" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", "futures-util", ] [[package]] name = "futures-io" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-sink" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-macro", "futures-sink", "futures-task", "memchr", "pin-project-lite", "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", ] [[package]] name = "generic-array" version = "1.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaf57c49a95fd1fe24b90b3033bee6dc7e8f1288d51494cb44e627c295e38542" dependencies = [ "generic-array 0.14.7", "rustversion", "typenum", ] [[package]] name = "getrandom" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", "libc", "wasi", "wasm-bindgen", ] [[package]] name = "getrandom" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi 5.3.0", "wasip2", ] [[package]] name = "getrandom" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", "rand_core 0.10.1", "wasip2", "wasip3", ] [[package]] name = "ghash" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" dependencies = [ "opaque-debug", "polyval 0.6.2", ] [[package]] name = "ghash" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" dependencies = [ "polyval 0.7.1", ] [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash", ] [[package]] name = "hashbrown" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "hex-literal" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e712f64ec3850b98572bffac52e2c6f282b29fe6c5fa6d42334b30be438d95c1" [[package]] name = "hkdf" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" dependencies = [ "hmac 0.13.0", ] [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ "digest 0.10.7", ] [[package]] name = "hmac" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ "digest 0.11.2", ] [[package]] name = "home" version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ "windows-sys", ] [[package]] name = "hybrid-array" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" dependencies = [ "ctutils", "subtle", "typenum", "zeroize", ] [[package]] name = "iana-time-zone" version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "log", "wasm-bindgen", "windows-core", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "id-arena" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "indexmap" version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.0", "serde", "serde_core", ] [[package]] name = "inout" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "block-padding 0.3.3", "generic-array 0.14.7", ] [[package]] name = "inout" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" dependencies = [ "block-padding 0.4.2", "hybrid-array", ] [[package]] name = "internal-russh-forked-ssh-key" version = "0.6.18+upstream-0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25f8a978272e3cbdf4768f7363eb1c8e1e6ba63c52a3ed05e29e222da4aec7cb" dependencies = [ "argon2", "bcrypt-pbkdf", "crypto-bigint", "ecdsa", "ed25519-dalek", "hex", "hmac 0.13.0", "num-bigint-dig", "p256", "p384", "p521", "rand_core 0.10.1", "rsa", "sec1", "sha1 0.11.0", "sha2 0.11.0", "signature", "ssh-cipher", "ssh-encoding", "subtle", "zeroize", ] [[package]] name = "internal-russh-num-bigint" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae8e22120c32fb4d19ec55fba35015f57095cd95a2e3b732e44457f5915b2ee8" dependencies = [ "num-integer", "num-traits", "rand 0.10.1", "rand_core 0.10.1", ] [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" dependencies = [ "getrandom 0.3.4", "libc", ] [[package]] name = "js-sys" version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "cfg-if", "futures-util", "once_cell", "wasm-bindgen", ] [[package]] name = "keccak" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" dependencies = [ "cfg-if", "cpufeatures 0.3.0", ] [[package]] name = "kem" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01737161ba802849cfd486b5bd209d38ba4943494c249a8126005170c7621edd" dependencies = [ "crypto-common 0.2.1", "rand_core 0.10.1", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ "spin", ] [[package]] name = "leb128fmt" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libm" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libssh2-sys" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "220e4f05ad4a218192533b300327f5150e809b54c4ec83b5a1d91833601811b9" dependencies = [ "cc", "libc", "libz-sys", "openssl-sys", "pkg-config", "vcpkg", ] [[package]] name = "libz-sys" version = "1.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "lock_api" version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ "scopeguard", ] [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "md5" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "miniz_oxide" version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", ] [[package]] name = "mio" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", "windows-sys", ] [[package]] name = "ml-kem" version = "0.3.0-rc.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04437cb1a66c0b78740927b76cc61f218344b9f6ef3dd430e283274a718ef0e9" dependencies = [ "hybrid-array", "kem", "module-lattice", "rand_core 0.10.1", "sha3", ] [[package]] name = "module-lattice" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "164eb3faeaecbd14b0b2a917c1b4d0c035097a9c559b0bed85c2cdd032bc8faa" dependencies = [ "ctutils", "hybrid-array", "num-traits", ] [[package]] name = "nibble_vec" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" dependencies = [ "smallvec", ] [[package]] name = "nix" version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" dependencies = [ "bitflags", "cfg-if", "cfg_aliases", "libc", ] [[package]] name = "num-bigint-dig" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ "lazy_static", "libm", "num-integer", "num-iter", "num-traits", "rand 0.8.6", "serde", "smallvec", ] [[package]] name = "num-integer" version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 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-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "opaque-debug" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl-sys" version = "0.9.114" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" dependencies = [ "cc", "libc", "pkg-config", "vcpkg", ] [[package]] name = "p256" version = "0.14.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b97e3bf0465157ae90975ff52dbeb1362ba618924878c9f74c25baa27a65f9a" dependencies = [ "ecdsa", "elliptic-curve", "primefield", "primeorder", "sha2 0.11.0", ] [[package]] name = "p384" version = "0.14.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "437f30ebcb1e16ff48acead5f08bd69fbcdbc82421687bb48af5c315a0bfab03" dependencies = [ "ecdsa", "elliptic-curve", "fiat-crypto", "primefield", "primeorder", "sha2 0.11.0", ] [[package]] name = "p521" version = "0.14.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e9fd792bab86ecf6249561752fb5a413511f999887107dd054bbda5143743d7" dependencies = [ "base16ct", "ecdsa", "elliptic-curve", "primefield", "primeorder", "sha2 0.11.0", ] [[package]] name = "pageant" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b537f975f6d8dcf48db368d7ec209d583b015713b5df0f5d92d2631e4ff5595" dependencies = [ "byteorder", "bytes", "delegate", "futures", "log", "rand 0.8.6", "sha2 0.10.9", "thiserror 1.0.69", "tokio", "windows", "windows-strings", ] [[package]] name = "parking_lot" version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-link", ] [[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 = "pbkdf2" version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest 0.10.7", "hmac 0.12.1", ] [[package]] name = "pbkdf2" version = "0.13.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f24f3eb2f4471b1730d59e4b730b747939960a8c7eb0c33c5a9076f2d3dddea" dependencies = [ "digest 0.11.2", "hmac 0.13.0", ] [[package]] name = "pem-rfc7468" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" dependencies = [ "base64ct", ] [[package]] name = "pem-rfc7468" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" dependencies = [ "base64ct", ] [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkcs1" version = "0.8.0-rc.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "986d2e952779af96ea048f160fd9194e1751b4faea78bcf3ceb456efe008088e" dependencies = [ "der", "spki", ] [[package]] name = "pkcs5" version = "0.8.0-rc.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5a777c6e26664bc9504b3ce3f6133f8f20d9071f130a4f9fcbd3186959d8dd6" dependencies = [ "aes 0.9.0", "aes-gcm 0.11.0-rc.3", "cbc 0.2.0", "der", "pbkdf2 0.13.0-rc.10", "rand_core 0.10.1", "scrypt", "sha2 0.11.0", "spki", ] [[package]] name = "pkcs8" version = "0.11.0-rc.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12922b6296c06eb741b02d7b5161e3aaa22864af38dfa025a1a3ba3f68c84577" dependencies = [ "der", "pkcs5", "rand_core 0.10.1", "spki", ] [[package]] name = "pkg-config" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "poly1305" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" dependencies = [ "cpufeatures 0.2.17", "opaque-debug", "universal-hash 0.5.1", ] [[package]] name = "polyval" version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" dependencies = [ "cfg-if", "cpufeatures 0.2.17", "opaque-debug", "universal-hash 0.5.1", ] [[package]] name = "polyval" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfc63250416fea14f5749b90725916a6c903f599d51cb635aa7a52bfd03eede" dependencies = [ "cpubits", "cpufeatures 0.3.0", "universal-hash 0.6.1", ] [[package]] name = "ppv-lite86" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] [[package]] name = "prettyplease" version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn", ] [[package]] name = "primefield" version = "0.14.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b52e6ee42db392378a95622b463c9740631171d1efce43fa445a569c1600cb6" dependencies = [ "crypto-bigint", "crypto-common 0.2.1", "rand_core 0.10.1", "rustcrypto-ff", "subtle", "zeroize", ] [[package]] name = "primeorder" version = "0.14.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0556580e42c19833f5d232aca11a7687a503ee41f937b54f5ae1d50fc2a6a36a" dependencies = [ "elliptic-curve", ] [[package]] name = "proc-macro2" version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "r-efi" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "radix_trie" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b4431027dcd37fc2a73ef740b5f233aa805897935b8bce0195e41bbf9a3289a" dependencies = [ "endian-type", "nibble_vec", ] [[package]] name = "rand" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha", "rand_core 0.6.4", ] [[package]] name = "rand" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20 0.10.0", "getrandom 0.4.2", "rand_core 0.10.1", ] [[package]] name = "rand_chacha" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core 0.6.4", ] [[package]] name = "rand_core" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.17", ] [[package]] name = "rand_core" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "rfc6979" version = "0.5.0-rc.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23a3127ee32baec36af75b4107082d9bd823501ec14a4e016be4b6b37faa74ae" dependencies = [ "hmac 0.13.0", "subtle", ] [[package]] name = "rsa" version = "0.10.0-rc.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87ed3e93fc7e473e464b9726f4759659e72bc8665e4b8ea227547024f416d905" dependencies = [ "const-oid 0.10.2", "crypto-bigint", "crypto-primes", "digest 0.11.2", "pkcs1", "pkcs8", "rand_core 0.10.1", "sha2 0.11.0", "signature", "spki", "zeroize", ] [[package]] name = "russh" version = "0.60.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d937f3f4a79bffd67fc12fd437785effdfc8b94edc89ab90392f9ac9e11cc9fc" dependencies = [ "aes 0.8.4", "aws-lc-rs", "bitflags", "block-padding 0.3.3", "byteorder", "bytes", "cbc 0.1.2", "cipher 0.5.1", "crypto-bigint", "ctr 0.9.2", "curve25519-dalek", "data-encoding", "delegate", "der", "digest 0.10.7", "ecdsa", "ed25519-dalek", "elliptic-curve", "enum_dispatch", "flate2", "futures", "generic-array 1.3.5", "getrandom 0.2.17", "hex-literal", "hmac 0.12.1", "inout 0.1.4", "internal-russh-forked-ssh-key", "internal-russh-num-bigint", "log", "md5", "ml-kem", "module-lattice", "p256", "p384", "p521", "pageant", "pbkdf2 0.12.2", "pkcs1", "pkcs5", "pkcs8", "polyval 0.7.1", "rand 0.10.1", "rand_core 0.10.1", "rsa", "russh-cryptovec", "russh-util", "sec1", "sha1 0.10.6", "sha2 0.10.9", "signature", "spki", "ssh-encoding", "subtle", "thiserror 2.0.18", "tokio", "typenum", "universal-hash 0.6.1", "zeroize", ] [[package]] name = "russh-cryptovec" version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36140e8a20297bc2e8338807c3d9ca911f7fa49d7539cbcd6d48d3befd70efd8" dependencies = [ "log", "nix", "ssh-encoding", "windows-sys", ] [[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_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustcrypto-ff" version = "0.14.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd2a8adb347447693cd2ba0d218c4b66c62da9b0a5672b17b981e4291ec65ff6" dependencies = [ "rand_core 0.10.1", "subtle", ] [[package]] name = "rustcrypto-group" version = "0.14.0-rc.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "369f9b61aa45933c062c9f6b5c3c50ab710687eca83dd3802653b140b43f85ed" dependencies = [ "rand_core 0.10.1", "rustcrypto-ff", "subtle", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "rustyline" version = "18.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a990b25f351b25139ddc7f21ee3f6f56f86d6846b74ac8fad3a719a287cd4a0" dependencies = [ "bitflags", "cfg-if", "clipboard-win", "home", "libc", "log", "memchr", "nix", "radix_trie", "unicode-segmentation", "unicode-width", "utf8parse", "windows-sys", ] [[package]] name = "salsa20" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c" dependencies = [ "cfg-if", "cipher 0.5.1", ] [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scrypt" version = "0.12.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e03ed5b54ed5fcc8e016cd94301416bc2c01c05c87a6742b97468337c8804598" dependencies = [ "cfg-if", "pbkdf2 0.13.0-rc.10", "salsa20", "sha2 0.11.0", ] [[package]] name = "sec1" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" dependencies = [ "base16ct", "ctutils", "der", "hybrid-array", "subtle", "zeroize", ] [[package]] name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", "serde", "serde_core", "zmij", ] [[package]] name = "serdect" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9af4a3e75ebd5599b30d4de5768e00b5095d518a79fefc3ecbaf77e665d1ec06" dependencies = [ "base16ct", "serde", ] [[package]] name = "sftp" version = "0.3.0" dependencies = [ "byteorder", "russh", "rustyline", "shell-words", "ssh2", "tokio", ] [[package]] name = "sha1" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures 0.2.17", "digest 0.10.7", ] [[package]] name = "sha1" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", "cpufeatures 0.3.0", "digest 0.11.2", ] [[package]] name = "sha2" version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures 0.2.17", "digest 0.10.7", ] [[package]] name = "sha2" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", "cpufeatures 0.3.0", "digest 0.11.2", ] [[package]] name = "sha3" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be176f1a57ce4e3d31c1a166222d9768de5954f811601fb7ca06fc8203905ce1" dependencies = [ "digest 0.11.2", "keccak", ] [[package]] name = "shell-words" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signature" version = "3.0.0-rc.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f1880df446116126965eeec169136b2e0251dba37c6223bcc819569550edea3" dependencies = [ "digest 0.11.2", "rand_core 0.10.1", ] [[package]] name = "simd-adler32" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", "windows-sys", ] [[package]] name = "spin" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "spki" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" 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 0.8.4", "aes-gcm 0.10.3", "cbc 0.1.2", "chacha20 0.9.1", "cipher 0.4.4", "ctr 0.9.2", "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 0.7.0", "sha2 0.10.9", ] [[package]] name = "ssh2" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f84d13b3b8a0d4e91a2629911e951db1bb8671512f5c09d7d4ba34500ba68c8" dependencies = [ "bitflags", "libc", "libssh2-sys", "parking_lot", ] [[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.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl 2.0.18", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thiserror-impl" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio" version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", "mio", "pin-project-lite", "socket2", "tokio-macros", "windows-sys", ] [[package]] name = "tokio-macros" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "typenum" version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-width" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "universal-hash" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ "crypto-common 0.1.7", "subtle", ] [[package]] name = "universal-hash" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" dependencies = [ "crypto-common 0.2.1", "ctutils", ] [[package]] name = "untrusted" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "utf8parse" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "vcpkg" version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ "wit-bindgen 0.57.1", ] [[package]] name = "wasip3" version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ "js-sys", "wasm-bindgen", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-encoder" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ "leb128fmt", "wasmparser", ] [[package]] name = "wasm-metadata" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", "indexmap", "wasm-encoder", "wasmparser", ] [[package]] name = "wasmparser" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", "indexmap", "semver", ] [[package]] name = "windows" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ "windows-collections", "windows-core", "windows-future", "windows-numerics", ] [[package]] name = "windows-collections" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ "windows-core", ] [[package]] name = "windows-core" version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", "windows-link", "windows-result", "windows-strings", ] [[package]] name = "windows-future" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ "windows-core", "windows-link", "windows-threading", ] [[package]] name = "windows-implement" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-interface" version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ "windows-core", "windows-link", ] [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] [[package]] name = "windows-threading" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" dependencies = [ "windows-link", ] [[package]] name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ "wit-bindgen-rust-macro", ] [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "wit-bindgen-core" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", "heck", "wit-parser", ] [[package]] name = "wit-bindgen-rust" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", "indexmap", "prettyplease", "syn", "wasm-metadata", "wit-bindgen-core", "wit-component", ] [[package]] name = "wit-bindgen-rust-macro" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" dependencies = [ "anyhow", "prettyplease", "proc-macro2", "quote", "syn", "wit-bindgen-core", "wit-bindgen-rust", ] [[package]] name = "wit-component" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", "indexmap", "log", "serde", "serde_derive", "serde_json", "wasm-encoder", "wasm-metadata", "wasmparser", "wit-parser", ] [[package]] name = "wit-parser" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", "indexmap", "log", "semver", "serde", "serde_derive", "serde_json", "unicode-xid", "wasmparser", ] [[package]] name = "zerocopy" version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" sftp-0.3.0/Cargo.toml0000644000000030211046102023000100240ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" name = "sftp" version = "0.3.0" authors = ["Jelmer Vernooij "] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "SFTP Client Implementation" homepage = "https://github.com/jelmer/sftp-rs" readme = "README.md" license = "Apache-2.0" repository = "https://github.com/jelmer/sftp-rs" [features] async = ["dep:tokio"] bin = [ "dep:rustyline", "dep:shell-words", ] default = ["bin"] russh = [ "async", "dep:russh", ] ssh2 = ["dep:ssh2"] [lib] name = "sftp" path = "src/lib.rs" [[bin]] name = "sftp" path = "src/bin/sftp.rs" required-features = ["bin"] [dependencies.byteorder] version = "1" [dependencies.russh] version = "0.60" optional = true [dependencies.rustyline] version = "18" optional = true [dependencies.shell-words] version = "1" optional = true [dependencies.ssh2] version = "0.9" optional = true [dependencies.tokio] version = "1" features = [ "io-util", "rt", "sync", "macros", "time", "net", ] optional = true sftp-0.3.0/Cargo.toml.orig000064400000000000000000000014401046102023000134660ustar 00000000000000[package] name = "sftp" version = "0.3.0" authors = [ "Jelmer Vernooij "] edition = "2021" license = "Apache-2.0" description = "SFTP Client Implementation" repository = "https://github.com/jelmer/sftp-rs" homepage = "https://github.com/jelmer/sftp-rs" [lib] [[bin]] name = "sftp" required-features = ["bin"] [dependencies] byteorder = "1" russh = { version = "0.60", optional = true } rustyline = { version = "18", optional = true } shell-words = { version = "1", optional = true } ssh2 = { version = "0.9", optional = true } tokio = { version = "1", optional = true, features = ["io-util", "rt", "sync", "macros", "time", "net"] } [features] default = ["bin"] bin = ["dep:rustyline", "dep:shell-words"] ssh2 = ["dep:ssh2"] async = ["dep:tokio"] russh = ["async", "dep:russh"] sftp-0.3.0/README.md000064400000000000000000000012471046102023000120630ustar 00000000000000SFTP in Rust ============ This rust crate contains a basic implementation of SFTP in Rust. It currently just contains a client implementation, but a server implementation is planned. It's meant to be used on top of a SSH Channel or a socket to the sftp server. It doesn't contain a SSH implementation, but will integrate with e.g. a command-line client running "ssh -s $localhost sftp". The basics of it work. However, it currently doesn't have any tests or much documentation. It mostly follows the published RFC for version 3, but deviates where other servers and clients ignore the RFC. RFC: https://datatracker.ietf.org/doc/html/draft-ietf-secsh-filexfer-02#section-7.8 sftp-0.3.0/src/async.rs000064400000000000000000000611721046102023000130610ustar 00000000000000//! Asynchronous SFTP client over a tokio `AsyncRead + AsyncWrite` channel. //! //! A single background task owns the read half, demultiplexing responses by //! request id, so multiple requests can be in flight concurrently on a single //! connection. use crate::protocol::*; use std::collections::HashMap; use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::{Arc, Mutex as StdMutex}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; use tokio::sync::{oneshot, Mutex as TokioMutex}; type Pending = Arc)>>>>; pub struct AsyncSftpClient { writer: TokioMutex, pending: Pending, last_request_id: AtomicU32, version: u32, extensions: Vec<(String, String)>, reader_task: TokioMutex>>, } impl Drop for AsyncSftpClient { fn drop(&mut self) { if let Ok(mut guard) = self.reader_task.try_lock() { if let Some(handle) = guard.take() { handle.abort(); } } } } async fn read_packet_async(r: &mut R) -> std::io::Result<(u8, Vec)> { let mut hdr = [0u8; 4]; r.read_exact(&mut hdr).await?; let len = i32::from_be_bytes(hdr) as usize; if len == 0 { return Err(std::io::Error::other("zero-length packet")); } let mut buf = vec![0u8; len]; r.read_exact(&mut buf).await?; let kind = buf[0]; Ok((kind, buf[1..].to_vec())) } async fn write_packet_async( w: &mut W, kind: u8, body: &[u8], ) -> std::io::Result<()> { let mut hdr = Vec::with_capacity(5); hdr.extend_from_slice(&(body.len() as u32 + 1).to_be_bytes()); hdr.push(kind); w.write_all(&hdr).await?; w.write_all(body).await?; w.flush().await?; Ok(()) } impl AsyncSftpClient { /// Construct a new async SFTP client by negotiating the protocol over the /// given split read/write halves. Spawns a background reader task on the /// current tokio runtime. pub async fn new(mut reader: R, mut writer: W) -> std::io::Result where R: AsyncRead + Unpin + Send + 'static, { write_packet_async(&mut writer, SSH_FXP_INIT, &build_init()).await?; let (kind, body) = read_packet_async(&mut reader).await?; if kind != SSH_FXP_VERSION { return Err(std::io::Error::other(format!( "Unexpected response to init: {}", kind ))); } let (version, extensions) = parse_version(&body)?; let pending: Pending = Arc::new(StdMutex::new(HashMap::new())); let pending_for_task = pending.clone(); let reader_task = tokio::spawn(async move { run_reader(reader, pending_for_task).await; }); Ok(Self { writer: TokioMutex::new(writer), pending, last_request_id: AtomicU32::new(0), version, extensions, reader_task: TokioMutex::new(Some(reader_task)), }) } pub fn extensions(&self) -> &[(String, String)] { &self.extensions } pub fn version(&self) -> u32 { self.version } async fn process(&self, cmd: u8, body: &[u8]) -> std::io::Result<(u8, Vec)> { let request_id = self.last_request_id.fetch_add(1, Ordering::SeqCst); let body = with_request_id(request_id, body); let (tx, rx) = oneshot::channel(); self.pending.lock().unwrap().insert(request_id, tx); if let Err(e) = { let mut guard = self.writer.lock().await; write_packet_async(&mut *guard, cmd, &body).await } { self.pending.lock().unwrap().remove(&request_id); return Err(e); } rx.await .map_err(|_| std::io::Error::other("reader task closed before response arrived")) } pub async fn mkdir(&self, path: &str, attr: &Attributes) -> Result<()> { let (cmd, data) = self .process(SSH_FXP_MKDIR, &build_path_and_attrs(path, attr)?) .await?; expect_status(cmd, &data) } pub async fn rmdir(&self, path: &str) -> Result<()> { let (cmd, data) = self.process(SSH_FXP_RMDIR, &build_path_only(path)).await?; expect_status(cmd, &data) } pub async fn readlink(&self, path: &str) -> Result { let (cmd, data) = self .process(SSH_FXP_READLINK, &build_path_only(path)) .await?; let names = expect_name(cmd, &data)?; Ok(names[0].0.clone()) } pub async fn symlink(&self, path: &str, target: &str) -> Result<()> { let (cmd, data) = self .process(SSH_FXP_SYMLINK, &build_two_paths(path, target)) .await?; expect_status(cmd, &data) } pub async fn hardlink(&self, path: &str, target: &str) -> Result<()> { self.link(path, target, false).await } pub async fn link(&self, path: &str, target: &str, symlink: bool) -> Result<()> { let (cmd, data) = self .process(SSH_FXP_LINK, &build_link(path, target, symlink)) .await?; expect_status(cmd, &data) } pub async fn open(&self, path: &str, options: OpenOptions, attr: &Attributes) -> Result { let (cmd, data) = self .process(SSH_FXP_OPEN, &build_open(path, options.get(), attr)?) .await?; Ok(File(expect_handle(cmd, &data)?)) } pub async fn realpath( &self, path: &str, control_byte: Option, compose_path: Option<&str>, ) -> Result { let (cmd, data) = self .process( SSH_FXP_REALPATH, &build_realpath(path, control_byte, compose_path), ) .await?; let names = expect_name(cmd, &data)?; Ok(names[0].0.clone()) } pub async fn setstat(&self, path: &str, attr: &Attributes) -> Result<()> { let (cmd, data) = self .process(SSH_FXP_SETSTAT, &build_path_and_attrs(path, attr)?) .await?; expect_status(cmd, &data) } pub async fn stat(&self, path: &str, flags: Option) -> Result { let (cmd, data) = self .process( SSH_FXP_STAT, &build_path_and_flags(path, flags.unwrap_or(0)), ) .await?; expect_attrs(cmd, &data) } pub async fn remove(&self, path: &str) -> Result<()> { let (cmd, data) = self.process(SSH_FXP_REMOVE, &build_path_only(path)).await?; expect_status(cmd, &data) } pub async fn rename(&self, oldpath: &str, newpath: &str, flags: Option) -> Result<()> { let (cmd, data) = self .process(SSH_FXP_RENAME, &build_rename(oldpath, newpath, flags)) .await?; expect_status(cmd, &data) } pub async fn lstat(&self, path: &str, flags: Option) -> Result { let (cmd, data) = self .process( SSH_FXP_LSTAT, &build_path_and_flags(path, flags.unwrap_or(0)), ) .await?; expect_attrs(cmd, &data) } pub async fn opendir(&self, path: &str) -> Result { let (cmd, data) = self .process(SSH_FXP_OPENDIR, &build_path_only(path)) .await?; Ok(Directory(expect_handle(cmd, &data)?)) } pub async fn extended(&self, request: &str, data: &[u8]) -> Result>> { let (cmd, payload) = self .process(SSH_FXP_EXTENDED, &build_extended(request, data)) .await?; expect_extended(cmd, payload) } pub async fn block(&self, file: &File, offset: u64, length: u64, lockmask: u32) -> Result<()> { let (cmd, data) = self .process( SSH_FXP_BLOCK, &build_block(&file.0, offset, length, lockmask), ) .await?; expect_status(cmd, &data) } pub async fn unblock(&self, file: &File, offset: u64, length: u64) -> Result<()> { let (cmd, data) = self .process(SSH_FXP_UNBLOCK, &build_unblock(&file.0, offset, length)) .await?; expect_status(cmd, &data) } pub async fn fsetstat(&self, file: &File, attr: &Attributes) -> Result<()> { let (cmd, data) = self .process(SSH_FXP_FSETSTAT, &build_handle_and_attrs(&file.0, attr)?) .await?; expect_status(cmd, &data) } pub async fn fstat(&self, file: &File, flags: Option) -> Result { let (cmd, data) = self .process( SSH_FXP_FSTAT, &build_handle_and_flags(&file.0, flags.unwrap_or(0)), ) .await?; expect_attrs(cmd, &data) } pub async fn pwrite(&self, file: &File, offset: u64, data: &[u8]) -> Result<()> { let (cmd, payload) = self .process(SSH_FXP_WRITE, &build_pwrite(&file.0, offset, data)) .await?; expect_status(cmd, &payload) } pub async fn pread(&self, file: &File, offset: u64, length: u32) -> Result> { let (cmd, data) = self .process(SSH_FXP_READ, &build_pread(&file.0, offset, length)) .await?; expect_data(cmd, &data) } pub async fn fclose(&self, file: &File) -> Result<()> { let (cmd, data) = self .process(SSH_FXP_CLOSE, &build_handle_only(&file.0)) .await?; expect_status(cmd, &data) } pub async fn flineseek(&self, file: &File, lineno: u64) -> Result<()> { let mut buf = build_handle_only(&file.0); buf.extend_from_slice(&lineno.to_be_bytes()); self.extended("text-seek", buf.as_slice()).await?; Ok(()) } pub async fn closedir(&self, dir: &Directory) -> Result<()> { let (cmd, data) = self .process(SSH_FXP_CLOSE, &build_handle_only(&dir.0)) .await?; expect_status(cmd, &data) } pub async fn readdir(&self, dir: &Directory) -> Result> { let (cmd, data) = self .process(SSH_FXP_READDIR, &build_handle_only(&dir.0)) .await?; expect_readdir(cmd, &data) } } async fn run_reader(mut reader: R, pending: Pending) { loop { match read_packet_async(&mut reader).await { Ok((cmd, buf)) => { let (req_id, payload) = match split_request_id(&buf) { Ok(v) => (v.0, v.1.to_vec()), Err(_) => continue, }; if let Some(tx) = pending.lock().unwrap().remove(&req_id) { let _ = tx.send((cmd, payload)); } } Err(_) => { // Connection closed or fatal read error; drop all pending senders so // their awaits return errors. pending.lock().unwrap().clear(); return; } } } } #[cfg(test)] mod tests { use super::*; use tokio::io::duplex; /// Serve INIT/VERSION then respond to every numbered request with an OK status, /// preserving the request id (which lives in the first 4 bytes of the body). async fn run_stub_server(mut srv: tokio::io::DuplexStream) { // INIT: read its packet let (kind, _body) = read_packet_async(&mut srv).await.unwrap(); assert_eq!(kind, SSH_FXP_INIT); // VERSION reply: version=3, no extensions let body = 3u32.to_be_bytes().to_vec(); write_packet_async(&mut srv, SSH_FXP_VERSION, &body) .await .unwrap(); // From here on, echo OK status for every request. loop { match read_packet_async(&mut srv).await { Ok((_cmd, body)) => { let req_id = u32::from_be_bytes([body[0], body[1], body[2], body[3]]); let mut resp = Vec::new(); resp.extend_from_slice(&req_id.to_be_bytes()); resp.extend_from_slice(&SSH_FX_OK.to_be_bytes()); resp.extend_from_slice(&0u32.to_be_bytes()); resp.extend_from_slice(&0u32.to_be_bytes()); if write_packet_async(&mut srv, SSH_FXP_STATUS, &resp) .await .is_err() { return; } } Err(_) => return, } } } #[tokio::test] async fn handshake_and_concurrent_requests() { let (client_io, server_io) = duplex(64 * 1024); tokio::spawn(run_stub_server(server_io)); let (cr, cw) = tokio::io::split(client_io); let client = AsyncSftpClient::new(cr, cw).await.unwrap(); assert_eq!(client.version(), 3); // Three concurrent requests with distinct ids exercise the demux path. let attrs = Attributes::new(); let (r1, r2, r3) = tokio::join!( client.mkdir("/a", &attrs), client.mkdir("/b", &attrs), client.rmdir("/c"), ); r1.unwrap(); r2.unwrap(); r3.unwrap(); } #[tokio::test] async fn request_fails_after_reader_exits() { let (client_io, mut server_io) = duplex(64 * 1024); tokio::spawn(async move { let (kind, _body) = read_packet_async(&mut server_io).await.unwrap(); assert_eq!(kind, SSH_FXP_INIT); let body = 3u32.to_be_bytes().to_vec(); write_packet_async(&mut server_io, SSH_FXP_VERSION, &body) .await .unwrap(); // Drop the server side so the reader task sees EOF. }); let (cr, cw) = tokio::io::split(client_io); let client = AsyncSftpClient::new(cr, cw).await.unwrap(); let attrs = Attributes::new(); // The request must not hang after the reader exits. let result = tokio::time::timeout( std::time::Duration::from_secs(2), client.mkdir("/x", &attrs), ) .await; assert!(result.is_ok(), "request hung after reader closed"); } /// Programmable stub: for each received request, the handler returns /// `(response_cmd, response_body_without_request_id)`. The dispatcher re-adds /// the request id at the front. async fn run_router(mut srv: tokio::io::DuplexStream, mut handler: F) where F: FnMut(u8, &[u8]) -> (u8, Vec) + Send, { let (kind, _body) = read_packet_async(&mut srv).await.unwrap(); assert_eq!(kind, SSH_FXP_INIT); write_packet_async(&mut srv, SSH_FXP_VERSION, &3u32.to_be_bytes()) .await .unwrap(); loop { let (cmd, body) = match read_packet_async(&mut srv).await { Ok(p) => p, Err(_) => return, }; let (req_id, payload) = split_request_id(&body).unwrap(); let (resp_cmd, resp_body) = handler(cmd, payload); let mut wire = req_id.to_be_bytes().to_vec(); wire.extend_from_slice(&resp_body); if write_packet_async(&mut srv, resp_cmd, &wire).await.is_err() { return; } } } fn ok_status() -> (u8, Vec) { let mut body = Vec::new(); body.extend_from_slice(&SSH_FX_OK.to_be_bytes()); body.extend_from_slice(&0u32.to_be_bytes()); // empty error message body.extend_from_slice(&0u32.to_be_bytes()); // empty lang tag (SSH_FXP_STATUS, body) } fn err_status(code: u32) -> (u8, Vec) { let mut body = Vec::new(); body.extend_from_slice(&code.to_be_bytes()); body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&0u32.to_be_bytes()); (SSH_FXP_STATUS, body) } fn handle_body(handle: &[u8]) -> (u8, Vec) { let mut body = Vec::with_capacity(4 + handle.len()); body.extend_from_slice(&(handle.len() as u32).to_be_bytes()); body.extend_from_slice(handle); (SSH_FXP_HANDLE, body) } fn attrs_body(a: &Attributes) -> (u8, Vec) { (SSH_FXP_ATTRS, a.serialize().unwrap()) } fn data_body(payload: &[u8]) -> (u8, Vec) { let mut body = Vec::with_capacity(4 + payload.len()); body.extend_from_slice(&(payload.len() as u32).to_be_bytes()); body.extend_from_slice(payload); (SSH_FXP_DATA, body) } fn name_body(entries: &[(&str, Attributes)]) -> (u8, Vec) { let mut body = Vec::new(); body.extend_from_slice(&(entries.len() as u32).to_be_bytes()); for (name, attrs) in entries { body.extend_from_slice(&(name.len() as u32).to_be_bytes()); body.extend_from_slice(name.as_bytes()); body.extend_from_slice(&attrs.serialize().unwrap()); } (SSH_FXP_NAME, body) } fn readdir_body(entries: &[(&str, &str, Attributes)]) -> (u8, Vec) { let mut body = Vec::new(); body.extend_from_slice(&(entries.len() as u32).to_be_bytes()); for (name, longname, attrs) in entries { body.extend_from_slice(&(name.len() as u32).to_be_bytes()); body.extend_from_slice(name.as_bytes()); body.extend_from_slice(&(longname.len() as u32).to_be_bytes()); body.extend_from_slice(longname.as_bytes()); body.extend_from_slice(&attrs.serialize().unwrap()); } (SSH_FXP_NAME, body) } /// Spin up a client against a stub server that uses `handler`. async fn with_stub( handler: F, ) -> AsyncSftpClient> where F: FnMut(u8, &[u8]) -> (u8, Vec) + Send + 'static, { let (client_io, server_io) = duplex(64 * 1024); tokio::spawn(run_router(server_io, handler)); let (cr, cw) = tokio::io::split(client_io); AsyncSftpClient::new(cr, cw).await.unwrap() } #[tokio::test] async fn async_open_returns_handle() { let client = with_stub(|cmd, _body| { assert_eq!(cmd, SSH_FXP_OPEN); handle_body(b"HANDLE42") }) .await; let f = client .open("/x", OpenOptions::new().read(true), &Attributes::new()) .await .unwrap(); assert_eq!(f.0, b"HANDLE42".to_vec()); } #[tokio::test] async fn async_open_propagates_no_such_file() { let client = with_stub(|cmd, _| { assert_eq!(cmd, SSH_FXP_OPEN); err_status(SSH_FX_NO_SUCH_FILE) }) .await; match client .open( "/missing", OpenOptions::new().read(true), &Attributes::new(), ) .await { Err(Error::NoSuchFile(_, _)) => {} other => panic!("expected NoSuchFile, got {:?}", other), } } #[tokio::test] async fn async_stat_returns_attributes() { let mut a = Attributes::new(); a.size = Some(1234); a.permissions = Some(0o100644); let a_clone = a.clone(); let client = with_stub(move |cmd, _| { assert_eq!(cmd, SSH_FXP_STAT); attrs_body(&a_clone) }) .await; let got = client.stat("/file", None).await.unwrap(); assert_eq!(got, a); } #[tokio::test] async fn async_lstat_returns_attributes() { let client = with_stub(|cmd, _| { assert_eq!(cmd, SSH_FXP_LSTAT); attrs_body(&Attributes::new()) }) .await; client.lstat("/x", None).await.unwrap(); } #[tokio::test] async fn async_fstat_returns_attributes() { let client = with_stub(|cmd, _| { assert_eq!(cmd, SSH_FXP_FSTAT); attrs_body(&Attributes::new()) }) .await; client.fstat(&File(b"h".to_vec()), None).await.unwrap(); } #[tokio::test] async fn async_pread_returns_data() { let client = with_stub(|cmd, _| { assert_eq!(cmd, SSH_FXP_READ); data_body(b"hello") }) .await; let data = client.pread(&File(b"h".to_vec()), 0, 5).await.unwrap(); assert_eq!(data, b"hello".to_vec()); } #[tokio::test] async fn async_pread_propagates_eof() { let client = with_stub(|cmd, _| { assert_eq!(cmd, SSH_FXP_READ); err_status(SSH_FX_EOF) }) .await; match client.pread(&File(b"h".to_vec()), 0, 5).await { Err(Error::Eof(_, _)) => {} other => panic!("expected Eof, got {:?}", other), } } #[tokio::test] async fn async_pwrite_returns_ok() { let client = with_stub(|cmd, _| { assert_eq!(cmd, SSH_FXP_WRITE); ok_status() }) .await; client .pwrite(&File(b"h".to_vec()), 0, b"data") .await .unwrap(); } #[tokio::test] async fn async_opendir_and_readdir() { let client = with_stub(|cmd, _| match cmd { SSH_FXP_OPENDIR => handle_body(b"D"), SSH_FXP_READDIR => readdir_body(&[ ("a", "-rw-r--r-- a", Attributes::new()), ("b", "-rw-r--r-- b", Attributes::new()), ]), other => panic!("unexpected cmd {}", other), }) .await; let dir = client.opendir("/d").await.unwrap(); let entries = client.readdir(&dir).await.unwrap(); assert_eq!(entries.len(), 2); assert_eq!(entries[0].0, "a"); assert_eq!(entries[1].0, "b"); } #[tokio::test] async fn async_readdir_eof_surfaces() { let client = with_stub(|cmd, _| match cmd { SSH_FXP_OPENDIR => handle_body(b"D"), SSH_FXP_READDIR => err_status(SSH_FX_EOF), other => panic!("unexpected cmd {}", other), }) .await; let dir = client.opendir("/d").await.unwrap(); match client.readdir(&dir).await { Err(Error::Eof(_, _)) => {} other => panic!("expected Eof, got {:?}", other), } } #[tokio::test] async fn async_realpath_returns_first_name() { let client = with_stub(|cmd, _| { assert_eq!(cmd, SSH_FXP_REALPATH); name_body(&[("/home/alice", Attributes::new())]) }) .await; assert_eq!( client.realpath(".", None, None).await.unwrap(), "/home/alice" ); } #[tokio::test] async fn async_readlink_returns_target() { let client = with_stub(|cmd, _| { assert_eq!(cmd, SSH_FXP_READLINK); name_body(&[("/actual", Attributes::new())]) }) .await; assert_eq!(client.readlink("/lnk").await.unwrap(), "/actual"); } #[tokio::test] async fn async_status_mutators() { let client = with_stub(|cmd, _| { assert!(matches!( cmd, SSH_FXP_MKDIR | SSH_FXP_RMDIR | SSH_FXP_REMOVE | SSH_FXP_RENAME | SSH_FXP_SYMLINK | SSH_FXP_LINK | SSH_FXP_SETSTAT | SSH_FXP_FSETSTAT | SSH_FXP_CLOSE | SSH_FXP_BLOCK | SSH_FXP_UNBLOCK )); ok_status() }) .await; let attrs = Attributes::new(); let handle = File(b"h".to_vec()); let dir = Directory(b"d".to_vec()); client.mkdir("/a", &attrs).await.unwrap(); client.rmdir("/a").await.unwrap(); client.remove("/a").await.unwrap(); client.rename("/a", "/b", None).await.unwrap(); client.symlink("/b", "/a").await.unwrap(); client.hardlink("/b", "/a").await.unwrap(); client.setstat("/a", &attrs).await.unwrap(); client.fsetstat(&handle, &attrs).await.unwrap(); client.fclose(&handle).await.unwrap(); client.closedir(&dir).await.unwrap(); client.block(&handle, 0, 0, 0).await.unwrap(); client.unblock(&handle, 0, 0).await.unwrap(); } #[tokio::test] async fn async_extended_returns_payload_on_reply() { let client = with_stub(|cmd, _| { assert_eq!(cmd, SSH_FXP_EXTENDED); (SSH_FXP_EXTENDED_REPLY, b"payload".to_vec()) }) .await; let out = client.extended("x", b"").await.unwrap(); assert_eq!(out, Some(b"payload".to_vec())); } #[tokio::test] async fn async_extended_returns_none_on_ok_status() { let client = with_stub(|cmd, _| { assert_eq!(cmd, SSH_FXP_EXTENDED); ok_status() }) .await; assert_eq!(client.extended("x", b"").await.unwrap(), None); } } sftp-0.3.0/src/bin/sftp.rs000064400000000000000000000430131046102023000134620ustar 00000000000000use rustyline::error::ReadlineError; use rustyline::DefaultEditor; use sftp::{Attributes, Error as SftpError, OpenOptions, SftpClient}; use std::io::{Read, Write}; use std::path::{Path, PathBuf}; use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio}; /// A bidirectional channel backed by the stdin/stdout of an `ssh` subprocess. struct SshChannel { child: Child, stdin: Option, stdout: Option, } impl SshChannel { fn spawn(destination: &str) -> std::io::Result { let mut child = Command::new("ssh") .arg("-s") .arg(destination) .arg("sftp") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::inherit()) .spawn()?; let stdin = child .stdin .take() .ok_or_else(|| std::io::Error::other("failed to capture ssh stdin"))?; let stdout = child .stdout .take() .ok_or_else(|| std::io::Error::other("failed to capture ssh stdout"))?; Ok(Self { child, stdin: Some(stdin), stdout: Some(stdout), }) } } impl Read for SshChannel { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { match self.stdout.as_mut() { Some(s) => s.read(buf), None => Ok(0), } } } impl Write for SshChannel { fn write(&mut self, buf: &[u8]) -> std::io::Result { match self.stdin.as_mut() { Some(s) => s.write(buf), None => Err(std::io::Error::from(std::io::ErrorKind::BrokenPipe)), } } fn flush(&mut self) -> std::io::Result<()> { match self.stdin.as_mut() { Some(s) => s.flush(), None => Ok(()), } } } impl Drop for SshChannel { fn drop(&mut self) { // Close stdin first so the ssh subsystem sees EOF and exits; // otherwise wait() blocks forever. drop(self.stdin.take()); drop(self.stdout.take()); let _ = self.child.wait(); } } struct Shell { client: SftpClient, remote_cwd: String, } impl Shell { fn new(client: SftpClient) -> Result { let remote_cwd = client.realpath(".", None, None)?; Ok(Self { client, remote_cwd }) } fn resolve_remote(&self, path: &str) -> String { if path.starts_with('/') { path.to_string() } else if self.remote_cwd.ends_with('/') { format!("{}{}", self.remote_cwd, path) } else { format!("{}/{}", self.remote_cwd, path) } } fn cmd_pwd(&self) { println!("Remote working directory: {}", self.remote_cwd); } fn cmd_cd(&mut self, path: Option<&str>) -> Result<(), SftpError> { let target = match path { Some(p) => self.resolve_remote(p), None => "/".to_string(), }; let canonical = self.client.realpath(&target, None, None)?; let attrs = self.client.stat(&canonical, None)?; if !is_dir(&attrs) { return Err(SftpError::NotADirectory(String::new(), canonical)); } self.remote_cwd = canonical; Ok(()) } fn cmd_ls(&self, args: &[String]) -> Result<(), SftpError> { let (long, target) = parse_ls_args(args); let path = match target { Some(p) => self.resolve_remote(&p), None => self.remote_cwd.clone(), }; let attrs = self.client.stat(&path, None)?; let entries: Vec<(String, String, Attributes)> = if is_dir(&attrs) { let dir = self.client.opendir(&path)?; let mut all = Vec::new(); loop { match self.client.readdir(&dir) { Ok(batch) => all.extend(batch), Err(SftpError::Eof(_, _)) => break, Err(e) => { let _ = self.client.closedir(&dir); return Err(e); } } } self.client.closedir(&dir)?; all.sort_by(|a, b| a.0.cmp(&b.0)); all } else { let name = Path::new(&path) .file_name() .map(|s| s.to_string_lossy().into_owned()) .unwrap_or_else(|| path.clone()); vec![(name, format_long(&path, &attrs), attrs)] }; if long { for (_, longname, _) in &entries { if longname.is_empty() { continue; } println!("{}", longname); } } else { for (name, _, _) in &entries { println!("{}", name); } } Ok(()) } fn cmd_mkdir(&self, path: &str) -> Result<(), SftpError> { let target = self.resolve_remote(path); self.client.mkdir(&target, &Attributes::new()) } fn cmd_rmdir(&self, path: &str) -> Result<(), SftpError> { let target = self.resolve_remote(path); self.client.rmdir(&target) } fn cmd_rm(&self, path: &str) -> Result<(), SftpError> { let target = self.resolve_remote(path); self.client.remove(&target) } fn cmd_rename(&self, old: &str, new: &str) -> Result<(), SftpError> { let from = self.resolve_remote(old); let to = self.resolve_remote(new); self.client.rename(&from, &to, None) } fn cmd_symlink(&self, target: &str, link: &str) -> Result<(), SftpError> { let link_path = self.resolve_remote(link); self.client.symlink(&link_path, target) } fn cmd_ln(&self, target: &str, link: &str, symbolic: bool) -> Result<(), SftpError> { let link_path = self.resolve_remote(link); if symbolic { self.client.symlink(&link_path, target) } else { let target_path = self.resolve_remote(target); self.client.hardlink(&link_path, &target_path) } } fn cmd_chmod(&self, mode_str: &str, path: &str) -> Result<(), SftpError> { let mode = u32::from_str_radix(mode_str, 8).map_err(|e| { SftpError::Io(std::io::Error::new( std::io::ErrorKind::InvalidInput, format!("invalid mode '{}': {}", mode_str, e), )) })?; let target = self.resolve_remote(path); let mut attrs = Attributes::new(); attrs.permissions = Some(mode); self.client.setstat(&target, &attrs) } fn cmd_stat(&self, path: &str) -> Result<(), SftpError> { let target = self.resolve_remote(path); let attrs = self.client.stat(&target, None)?; print_attrs(&target, &attrs); Ok(()) } fn cmd_get(&self, remote: &str, local: Option<&str>) -> Result<(), SftpError> { let remote_path = self.resolve_remote(remote); let local_path = match local { Some(p) => PathBuf::from(p), None => PathBuf::from( Path::new(&remote_path) .file_name() .map(|s| s.to_string_lossy().into_owned()) .unwrap_or_else(|| remote_path.clone()), ), }; let file = self.client.open( &remote_path, OpenOptions::new().read(true), &Attributes::new(), )?; let result = (|| -> Result { let mut out = std::fs::File::create(&local_path)?; let mut offset: u64 = 0; const CHUNK: u32 = 32 * 1024; loop { match self.client.pread(&file, offset, CHUNK) { Ok(data) if data.is_empty() => break, Ok(data) => { out.write_all(&data)?; offset += data.len() as u64; } Err(SftpError::Eof(_, _)) => break, Err(e) => return Err(e), } } Ok(offset) })(); let _ = self.client.fclose(&file); let bytes = result?; println!("Fetched {} ({} bytes)", local_path.display(), bytes); Ok(()) } fn cmd_put(&self, local: &str, remote: Option<&str>) -> Result<(), SftpError> { let local_path = PathBuf::from(local); let remote_name = match remote { Some(p) => p.to_string(), None => local_path .file_name() .map(|s| s.to_string_lossy().into_owned()) .ok_or_else(|| { SftpError::Io(std::io::Error::new( std::io::ErrorKind::InvalidInput, "cannot derive remote name from local path", )) })?, }; let remote_path = self.resolve_remote(&remote_name); let mut input = std::fs::File::open(&local_path)?; let file = self.client.open( &remote_path, OpenOptions::new().write(true).create(true).truncate(true), &Attributes::new(), )?; let result = (|| -> Result { let mut offset: u64 = 0; let mut buf = vec![0u8; 32 * 1024]; loop { let n = input.read(&mut buf)?; if n == 0 { break; } self.client.pwrite(&file, offset, &buf[..n])?; offset += n as u64; } Ok(offset) })(); let _ = self.client.fclose(&file); let bytes = result?; println!("Uploaded {} ({} bytes)", remote_path, bytes); Ok(()) } fn cmd_lpwd() -> std::io::Result<()> { println!( "Local working directory: {}", std::env::current_dir()?.display() ); Ok(()) } fn cmd_lcd(path: Option<&str>) -> std::io::Result<()> { let target = match path { Some(p) => PathBuf::from(p), None => dirs_home().ok_or_else(|| { std::io::Error::new(std::io::ErrorKind::NotFound, "no home directory") })?, }; std::env::set_current_dir(&target)?; Ok(()) } fn cmd_lls(args: &[String]) -> std::io::Result<()> { let dir = args .first() .map(PathBuf::from) .unwrap_or_else(|| PathBuf::from(".")); for entry in std::fs::read_dir(&dir)? { let entry = entry?; println!("{}", entry.file_name().to_string_lossy()); } Ok(()) } } fn dirs_home() -> Option { std::env::var_os("HOME").map(PathBuf::from) } fn is_dir(attrs: &Attributes) -> bool { attrs .permissions .is_some_and(|p| (p & 0o170000) == 0o040000) } fn parse_ls_args(args: &[String]) -> (bool, Option) { let mut long = false; let mut target = None; for arg in args { if arg == "-l" || arg == "-la" || arg == "-al" { long = true; } else if arg.starts_with('-') { // ignore other flags for now } else { target = Some(arg.clone()); } } (long, target) } fn format_long(path: &str, attrs: &Attributes) -> String { let perms = attrs .permissions .map(|p| format!("{:o}", p)) .unwrap_or_else(|| "?".to_string()); let size = attrs .size .map(|s| s.to_string()) .unwrap_or_else(|| "-".to_string()); format!("{} {} {}", perms, size, path) } fn print_attrs(path: &str, attrs: &Attributes) { println!("{}:", path); if let Some(s) = attrs.size { println!(" size: {}", s); } if let Some(p) = attrs.permissions { println!(" permissions: {:o}", p); } if let (Some(uid), Some(gid)) = (attrs.uid, attrs.gid) { println!(" uid/gid: {}/{}", uid, gid); } if let (Some(owner), Some(group)) = (attrs.owner.as_deref(), attrs.group.as_deref()) { println!(" owner/group: {}/{}", owner, group); } if let Some((secs, _)) = attrs.modify_time { println!(" mtime: {}", secs); } } fn print_help() { println!("Available commands:"); println!(" cd [path] change remote directory"); println!(" pwd print remote working directory"); println!(" ls [-l] [path] list remote directory"); println!(" get remote [local] download file"); println!(" put local [remote] upload file"); println!(" mkdir path create remote directory"); println!(" rmdir path remove remote directory"); println!(" rm path remove remote file"); println!(" rename old new rename remote file"); println!(" ln [-s] target link create hard or symbolic link"); println!(" symlink target link create symbolic link"); println!(" chmod mode path change permissions (octal)"); println!(" stat path show file attributes"); println!(" lpwd print local working directory"); println!(" lcd [path] change local directory"); println!(" lls [path] list local directory"); println!(" help, ? show this help"); println!(" quit, exit, bye disconnect"); } fn dispatch(shell: &mut Shell, line: &str) -> bool { let tokens = match shell_words::split(line) { Ok(t) => t, Err(e) => { eprintln!("parse error: {}", e); return true; } }; if tokens.is_empty() { return true; } let cmd = tokens[0].as_str(); let args = &tokens[1..]; let result: Result<(), SftpError> = match cmd { "quit" | "exit" | "bye" => return false, "help" | "?" => { print_help(); Ok(()) } "pwd" => { shell.cmd_pwd(); Ok(()) } "cd" => shell.cmd_cd(args.first().map(String::as_str)), "ls" | "dir" => shell.cmd_ls(args), "mkdir" => need_arg(args, 1, "mkdir path").and_then(|_| shell.cmd_mkdir(&args[0])), "rmdir" => need_arg(args, 1, "rmdir path").and_then(|_| shell.cmd_rmdir(&args[0])), "rm" => need_arg(args, 1, "rm path").and_then(|_| shell.cmd_rm(&args[0])), "rename" => need_arg(args, 2, "rename old new") .and_then(|_| shell.cmd_rename(&args[0], &args[1])), "symlink" => need_arg(args, 2, "symlink target link") .and_then(|_| shell.cmd_symlink(&args[0], &args[1])), "ln" => { let symbolic = args.first().map(|s| s == "-s").unwrap_or(false); let rest: Vec<&String> = args.iter().filter(|a| a.as_str() != "-s").collect(); if rest.len() != 2 { Err(usage("ln [-s] target link")) } else { shell.cmd_ln(rest[0], rest[1], symbolic) } } "chmod" => need_arg(args, 2, "chmod mode path") .and_then(|_| shell.cmd_chmod(&args[0], &args[1])), "stat" => need_arg(args, 1, "stat path").and_then(|_| shell.cmd_stat(&args[0])), "get" => need_arg(args, 1, "get remote [local]") .and_then(|_| shell.cmd_get(&args[0], args.get(1).map(String::as_str))), "put" => need_arg(args, 1, "put local [remote]") .and_then(|_| shell.cmd_put(&args[0], args.get(1).map(String::as_str))), "lpwd" => Shell::cmd_lpwd().map_err(SftpError::Io), "lcd" => Shell::cmd_lcd(args.first().map(String::as_str)).map_err(SftpError::Io), "lls" => Shell::cmd_lls(args).map_err(SftpError::Io), other => { eprintln!("unknown command: {} (try 'help')", other); Ok(()) } }; if let Err(e) = result { eprintln!("error: {:?}", e); } true } fn need_arg(args: &[String], n: usize, usage_str: &str) -> Result<(), SftpError> { if args.len() < n { Err(usage(usage_str)) } else { Ok(()) } } fn usage(msg: &str) -> SftpError { SftpError::Io(std::io::Error::new( std::io::ErrorKind::InvalidInput, format!("usage: {}", msg), )) } fn history_path() -> Option { dirs_home().map(|h| h.join(".sftp_history")) } fn main() -> std::io::Result<()> { let destination = std::env::args().nth(1).unwrap_or_else(|| { eprintln!("Usage: sftp "); std::process::exit(1); }); let channel = SshChannel::spawn(&destination)?; let client = SftpClient::new(channel)?; println!("Connected. SFTP protocol version: {}", client.version()); let mut shell = Shell::new(client).map_err(|e| std::io::Error::other(format!("{:?}", e)))?; let mut rl = DefaultEditor::new().map_err(|e| std::io::Error::other(format!("readline init: {}", e)))?; let history = history_path(); if let Some(h) = &history { let _ = rl.load_history(h); } loop { match rl.readline("sftp> ") { Ok(line) => { let trimmed = line.trim(); if trimmed.is_empty() { continue; } let _ = rl.add_history_entry(trimmed); if !dispatch(&mut shell, trimmed) { break; } } Err(ReadlineError::Interrupted) => continue, Err(ReadlineError::Eof) => break, Err(e) => { eprintln!("readline error: {}", e); break; } } } if let Some(h) = &history { let _ = rl.save_history(h); } Ok(()) } sftp-0.3.0/src/lib.rs000064400000000000000000000014721046102023000125070ustar 00000000000000pub mod protocol; pub mod sync; #[cfg(feature = "async")] pub mod r#async; #[cfg(feature = "russh")] pub mod russh; pub use protocol::{ Attributes, Directory, Error, File, Kind, OpenOptions, Result, TextHint, SSH_FILEXFER_ATTR_ACCESSTIME, SSH_FILEXFER_ATTR_ACL, SSH_FILEXFER_ATTR_ALLOCATION_SIZE, SSH_FILEXFER_ATTR_BITS, SSH_FILEXFER_ATTR_CREATETIME, SSH_FILEXFER_ATTR_CTIME, SSH_FILEXFER_ATTR_EXTENDED, SSH_FILEXFER_ATTR_LINK_COUNT, SSH_FILEXFER_ATTR_MIME_TYPE, SSH_FILEXFER_ATTR_MODIFYTIME, SSH_FILEXFER_ATTR_OWNERGROUP, SSH_FILEXFER_ATTR_PERMISSIONS, SSH_FILEXFER_ATTR_SIZE, SSH_FILEXFER_ATTR_SUBSECOND_TIMES, SSH_FILEXFER_ATTR_TEXT_HINT, SSH_FILEXFER_ATTR_UIDGID, SSH_FILEXFER_ATTR_UNTRANSLATED_NAME, }; pub use sync::SftpClient; #[cfg(feature = "async")] pub use r#async::AsyncSftpClient; sftp-0.3.0/src/protocol.rs000064400000000000000000001566241046102023000136140ustar 00000000000000//! Pure SFTP wire-protocol codec: types, request builders, response parsers. //! //! No I/O. No transport. Bytes in, bytes out. Shared by the sync and async //! client implementations. #![allow(dead_code)] use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use std::io::{Cursor, Read, Seek, SeekFrom, Write}; #[derive(Debug)] pub enum Error { Io(std::io::Error), Utf8(std::str::Utf8Error), Other(u32, String, String), Eof(String, String), NoSuchFile(String, String), PermissionDenied(String, String), Failure(String, String), BadMessage(String, String), NoConnection(String, String), ConnectionLost(String, String), OpUnsupported(String, String), InvalidHandle(String, String), NoSuchPath(String, String), FileAlreadyExists(String, String), WriteProtect(String, String), NoMedia(String, String), NoSpaceOnFilesystem(String, String), QuotaExceeded(String, String), UnknownPrincipal(String, String), LockConflict(String, String), DirNotEmpty(String, String), NotADirectory(String, String), InvalidFilename(String, String), LinkLoop(String, String), CannotDelete(String, String), InvalidParameter(String, String), FileIsADirectory(String, String), ByteRangeLockConflict(String, String), ByteRangeLockRefused(String, String), DeletePending(String, String), FileCorrupt(String, String), OwnerInvalid(String, String), GroupInvalid(String, String), NoMatchingByteRangeLock(String, String), } impl From for Error { fn from(err: std::io::Error) -> Self { Error::Io(err) } } impl From for Error { fn from(err: std::str::Utf8Error) -> Self { Error::Utf8(err) } } impl From for std::io::Error { fn from(err: Error) -> Self { match err { Error::Io(err) => err, Error::Eof(_, _) => std::io::Error::new(std::io::ErrorKind::UnexpectedEof, ""), Error::NoSuchFile(_, m) => std::io::Error::new(std::io::ErrorKind::NotFound, m), Error::PermissionDenied(_, m) => { std::io::Error::new(std::io::ErrorKind::PermissionDenied, m) } Error::NoConnection(_, m) => std::io::Error::new(std::io::ErrorKind::NotConnected, m), Error::ConnectionLost(_, m) => { std::io::Error::new(std::io::ErrorKind::ConnectionReset, m) } Error::InvalidHandle(_, m) => std::io::Error::new(std::io::ErrorKind::InvalidInput, m), Error::NoSuchPath(_, m) => std::io::Error::new(std::io::ErrorKind::NotFound, m), Error::FileAlreadyExists(_, m) => { std::io::Error::new(std::io::ErrorKind::AlreadyExists, m) } Error::WriteProtect(_, m) => { std::io::Error::new(std::io::ErrorKind::PermissionDenied, m) } Error::NoMedia(_, m) => std::io::Error::new(std::io::ErrorKind::NotFound, m), Error::QuotaExceeded(_, m) => { std::io::Error::new(std::io::ErrorKind::PermissionDenied, m) } Error::LockConflict(_, m) => { std::io::Error::new(std::io::ErrorKind::PermissionDenied, m) } Error::InvalidFilename(_, m) => { std::io::Error::new(std::io::ErrorKind::InvalidInput, m) } _ => std::io::Error::other(format!("{:?}", err)), } } } pub type Result = std::result::Result; pub const SSH_FILEXFER_ATTR_SIZE: u32 = 0x00000001; pub const SSH_FILEXFER_ATTR_UIDGID: u32 = 0x00000002; pub const SSH_FILEXFER_ATTR_PERMISSIONS: u32 = 0x00000004; pub const SSH_FILEXFER_ATTR_ACCESSTIME: u32 = 0x00000008; pub const SSH_FILEXFER_ATTR_CREATETIME: u32 = 0x00000010; pub const SSH_FILEXFER_ATTR_MODIFYTIME: u32 = 0x00000020; pub const SSH_FILEXFER_ATTR_ACL: u32 = 0x00000040; pub const SSH_FILEXFER_ATTR_OWNERGROUP: u32 = 0x00000080; pub const SSH_FILEXFER_ATTR_SUBSECOND_TIMES: u32 = 0x00000100; pub const SSH_FILEXFER_ATTR_BITS: u32 = 0x00000200; pub const SSH_FILEXFER_ATTR_ALLOCATION_SIZE: u32 = 0x00000400; pub const SSH_FILEXFER_ATTR_TEXT_HINT: u32 = 0x00000800; pub const SSH_FILEXFER_ATTR_MIME_TYPE: u32 = 0x00001000; pub const SSH_FILEXFER_ATTR_LINK_COUNT: u32 = 0x00002000; pub const SSH_FILEXFER_ATTR_UNTRANSLATED_NAME: u32 = 0x00004000; pub const SSH_FILEXFER_ATTR_CTIME: u32 = 0x00008000; pub const SSH_FILEXFER_ATTR_EXTENDED: u32 = 0x80000000; const SSH_FILEXFER_ATTR_KNOWN_TEXT: u8 = 0x00; const SSH_FILEXFER_ATTR_GUESSED_TEXT: u8 = 0x01; const SSH_FILEXFER_ATTR_KNOWN_BINARY: u8 = 0x02; const SSH_FILEXFER_ATTR_GUESSED_BINARY: u8 = 0x03; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum TextHint { KnownText, GuessedText, KnownBinary, GuessedBinary, } impl From for u8 { fn from(hint: TextHint) -> Self { match hint { TextHint::KnownText => SSH_FILEXFER_ATTR_KNOWN_TEXT, TextHint::GuessedText => SSH_FILEXFER_ATTR_GUESSED_TEXT, TextHint::KnownBinary => SSH_FILEXFER_ATTR_KNOWN_BINARY, TextHint::GuessedBinary => SSH_FILEXFER_ATTR_GUESSED_BINARY, } } } impl From for TextHint { fn from(hint: u8) -> Self { match hint { SSH_FILEXFER_ATTR_KNOWN_TEXT => TextHint::KnownText, SSH_FILEXFER_ATTR_GUESSED_TEXT => TextHint::GuessedText, SSH_FILEXFER_ATTR_KNOWN_BINARY => TextHint::KnownBinary, SSH_FILEXFER_ATTR_GUESSED_BINARY => TextHint::GuessedBinary, _ => panic!("Invalid text hint"), } } } pub const SSH_FILEXFER_ATTR_FLAGS_READONLY: u32 = 0x00000001; pub const SSH_FILEXFER_ATTR_FLAGS_SYSTEM: u32 = 0x00000002; pub const SSH_FILEXFER_ATTR_FLAGS_HIDDEN: u32 = 0x00000004; pub const SSH_FILEXFER_ATTR_FLAGS_CASE_INSENSITIVE: u32 = 0x00000008; pub const SSH_FILEXFER_ATTR_FLAGS_ARCHIVE: u32 = 0x00000010; pub const SSH_FILEXFER_ATTR_FLAGS_ENCRYPTED: u32 = 0x00000020; pub const SSH_FILEXFER_ATTR_FLAGS_COMPRESSED: u32 = 0x00000040; pub const SSH_FILEXFER_ATTR_FLAGS_SPARSE: u32 = 0x00000080; pub const SSH_FILEXFER_ATTR_FLAGS_APPEND_ONLY: u32 = 0x00000100; pub const SSH_FILEXFER_ATTR_FLAGS_IMMUTABLE: u32 = 0x00000200; pub const SSH_FILEXFER_ATTR_FLAGS_SYNC: u32 = 0x00000400; pub const SSH_FILEXFER_ATTR_FLAGS_TRANSLATION_ERR: u32 = 0x00000800; const SSH_FILEXFER_TYPE_REGULAR: u8 = 1; const SSH_FILEXFER_TYPE_DIRECTORY: u8 = 2; const SSH_FILEXFER_TYPE_SYMLINK: u8 = 3; const SSH_FILEXFER_TYPE_SPECIAL: u8 = 4; const SSH_FILEXFER_TYPE_UNKNOWN: u8 = 5; const SSH_FILEXFER_TYPE_SOCKET: u8 = 6; const SSH_FILEXFER_TYPE_CHAR_DEVICE: u8 = 7; const SSH_FILEXFER_TYPE_BLOCK_DEVICE: u8 = 8; const SSH_FILEXFER_TYPE_FIFO: u8 = 9; #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Kind { Regular, Directory, Symlink, Special, #[default] Unknown, Socket, CharDevice, BlockDevice, Fifo, } impl From for u8 { fn from(val: Kind) -> Self { match val { Kind::Regular => SSH_FILEXFER_TYPE_REGULAR, Kind::Directory => SSH_FILEXFER_TYPE_DIRECTORY, Kind::Symlink => SSH_FILEXFER_TYPE_SYMLINK, Kind::Special => SSH_FILEXFER_TYPE_SPECIAL, Kind::Unknown => SSH_FILEXFER_TYPE_UNKNOWN, Kind::Socket => SSH_FILEXFER_TYPE_SOCKET, Kind::CharDevice => SSH_FILEXFER_TYPE_CHAR_DEVICE, Kind::BlockDevice => SSH_FILEXFER_TYPE_BLOCK_DEVICE, Kind::Fifo => SSH_FILEXFER_TYPE_FIFO, } } } impl From for Kind { fn from(kind: u8) -> Self { match kind { SSH_FILEXFER_TYPE_REGULAR => Kind::Regular, SSH_FILEXFER_TYPE_DIRECTORY => Kind::Directory, SSH_FILEXFER_TYPE_SYMLINK => Kind::Symlink, SSH_FILEXFER_TYPE_SPECIAL => Kind::Special, SSH_FILEXFER_TYPE_UNKNOWN => Kind::Unknown, SSH_FILEXFER_TYPE_SOCKET => Kind::Socket, SSH_FILEXFER_TYPE_CHAR_DEVICE => Kind::CharDevice, SSH_FILEXFER_TYPE_BLOCK_DEVICE => Kind::BlockDevice, SSH_FILEXFER_TYPE_FIFO => Kind::Fifo, f => panic!("Unknown file type {}", f), } } } pub const SSH_FXP_INIT: u8 = 1; pub const SSH_FXP_VERSION: u8 = 2; pub const SSH_FXP_OPEN: u8 = 3; pub const SSH_FXP_CLOSE: u8 = 4; pub const SSH_FXP_READ: u8 = 5; pub const SSH_FXP_WRITE: u8 = 6; pub const SSH_FXP_LSTAT: u8 = 7; pub const SSH_FXP_FSTAT: u8 = 8; pub const SSH_FXP_SETSTAT: u8 = 9; pub const SSH_FXP_FSETSTAT: u8 = 10; pub const SSH_FXP_OPENDIR: u8 = 11; pub const SSH_FXP_READDIR: u8 = 12; pub const SSH_FXP_REMOVE: u8 = 13; pub const SSH_FXP_MKDIR: u8 = 14; pub const SSH_FXP_RMDIR: u8 = 15; pub const SSH_FXP_REALPATH: u8 = 16; pub const SSH_FXP_STAT: u8 = 17; pub const SSH_FXP_RENAME: u8 = 18; pub const SSH_FXP_READLINK: u8 = 19; pub const SSH_FXP_SYMLINK: u8 = 20; pub const SSH_FXP_LINK: u8 = 21; pub const SSH_FXP_BLOCK: u8 = 22; pub const SSH_FXP_UNBLOCK: u8 = 23; pub const SSH_FXP_STATUS: u8 = 101; pub const SSH_FXP_HANDLE: u8 = 102; pub const SSH_FXP_DATA: u8 = 103; pub const SSH_FXP_NAME: u8 = 104; pub const SSH_FXP_ATTRS: u8 = 105; pub const SSH_FXP_EXTENDED: u8 = 200; pub const SSH_FXP_EXTENDED_REPLY: u8 = 201; pub const SSH_FX_OK: u32 = 0; pub const SSH_FX_EOF: u32 = 1; pub const SSH_FX_NO_SUCH_FILE: u32 = 2; pub const SSH_FX_PERMISSION_DENIED: u32 = 3; pub const SSH_FX_FAILURE: u32 = 4; pub const SSH_FX_BAD_MESSAGE: u32 = 5; pub const SSH_FX_NO_CONNECTION: u32 = 6; pub const SSH_FX_CONNECTION_LOST: u32 = 7; pub const SSH_FX_OP_UNSUPPORTED: u32 = 8; pub const SSH_FX_INVALID_HANDLE: u32 = 9; pub const SSH_FX_NO_SUCH_PATH: u32 = 10; pub const SSH_FX_FILE_ALREADY_EXISTS: u32 = 11; pub const SSH_FX_WRITE_PROTECT: u32 = 12; pub const SSH_FX_NO_MEDIA: u32 = 13; pub const SSH_FX_NO_SPACE_ON_FILESYSTEM: u32 = 14; pub const SSH_FX_QUOTA_EXCEEDED: u32 = 15; pub const SSH_FX_UNKNOWN_PRINCIPAL: u32 = 16; pub const SSH_FX_LOCK_CONFLICT: u32 = 17; pub const SSH_FX_DIR_NOT_EMPTY: u32 = 18; pub const SSH_FX_NOT_A_DIRECTORY: u32 = 19; pub const SSH_FX_INVALID_FILENAME: u32 = 20; pub const SSH_FX_LINK_LOOP: u32 = 21; pub const SSH_FX_CANNOT_DELETE: u32 = 22; pub const SSH_FX_INVALID_PARAMETER: u32 = 23; pub const SSH_FX_FILE_IS_A_DIRECTORY: u32 = 24; pub const SSH_FX_BYTE_RANGE_LOCK_CONFLICT: u32 = 25; pub const SSH_FX_BYTE_RANGE_LOCK_REFUSED: u32 = 26; pub const SSH_FX_DELETE_PENDING: u32 = 27; pub const SSH_FX_FILE_CORRUPT: u32 = 28; pub const SSH_FX_OWNER_INVALID: u32 = 29; pub const SSH_FX_GROUP_INVALID: u32 = 30; pub const SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK: u32 = 31; pub const SFTP_FLAG_READ: u32 = 0x00000001; pub const SFTP_FLAG_WRITE: u32 = 0x00000002; pub const SFTP_FLAG_APPEND: u32 = 0x00000004; pub const SFTP_FLAG_CREAT: u32 = 0x00000008; pub const SFTP_FLAG_TRUNC: u32 = 0x00000010; pub const SFTP_FLAG_EXCL: u32 = 0x00000020; #[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] pub struct OpenOptions(u32); impl OpenOptions { pub fn new() -> OpenOptions { OpenOptions(0) } pub fn read(mut self, read: bool) -> OpenOptions { if read { self.0 |= SFTP_FLAG_READ; } else { self.0 &= !SFTP_FLAG_READ; } self } pub fn write(mut self, write: bool) -> OpenOptions { if write { self.0 |= SFTP_FLAG_WRITE; } else { self.0 &= !SFTP_FLAG_WRITE; } self } pub fn append(mut self, append: bool) -> OpenOptions { if append { self.0 |= SFTP_FLAG_APPEND; } else { self.0 &= !SFTP_FLAG_APPEND; } self } pub fn create(mut self, create: bool) -> OpenOptions { if create { self.0 |= SFTP_FLAG_CREAT; } else { self.0 &= !SFTP_FLAG_CREAT; } self } pub fn truncate(mut self, truncate: bool) -> OpenOptions { if truncate { self.0 |= SFTP_FLAG_TRUNC; } else { self.0 &= !SFTP_FLAG_TRUNC; } self } pub fn excl(mut self, excl: bool) -> OpenOptions { if excl { self.0 |= SFTP_FLAG_EXCL; } else { self.0 &= !SFTP_FLAG_EXCL; } self } pub fn mode(&mut self, mode: u32) -> &mut OpenOptions { self.0 |= mode; self } pub fn get(&self) -> u32 { self.0 } } pub const SSH_FXF_RENAME_OVERWRITE: u32 = 0x00000001; pub const SSH_FXF_RENAME_ATOMIC: u32 = 0x00000002; pub const SSH_FXF_RENAME_NATIVE: u32 = 0x00000004; pub const SSH_FXF_ACCESS_DISPOSITION: u32 = 0x00000007; pub const SSH_FXF_CREATE_NEW: u32 = 0x00000000; pub const SSH_FXF_CREATE_TRUNCATE: u32 = 0x00000001; pub const SSH_FXF_OPEN_EXISTING: u32 = 0x00000002; pub const SSH_FXF_OPEN_OR_CREATE: u32 = 0x00000003; pub const SSH_FXF_TRUNCATE_EXISTING: u32 = 0x00000004; pub const SSH_FXF_APPEND_DATA: u32 = 0x00000008; pub const SSH_FXF_APPEND_DATA_ATOMIC: u32 = 0x00000010; pub const SSH_FXF_TEXT_MODE: u32 = 0x00000020; pub const SSH_FXF_BLOCK_READ: u32 = 0x00000040; pub const SSH_FXF_BLOCK_WRITE: u32 = 0x00000080; pub const SSH_FXF_BLOCK_DELETE: u32 = 0x00000100; pub const SSH_FXF_BLOCK_ADVISORY: u32 = 0x00000200; pub const SSH_FXF_NOFOLLOW: u32 = 0x00000400; pub const SSH_FXF_DELETE_ON_CLOSE: u32 = 0x00000800; pub const SSH_FXF_ACCESS_AUDIT_ALARM_INFO: u32 = 0x00001000; pub const SSH_FXF_ACCESS_BACKUP: u32 = 0x00002000; pub const SSH_FXF_BACKUP_STREAM: u32 = 0x00004000; pub const SSH_FXF_OVERRIDE_OWNER: u32 = 0x00008000; #[derive(Debug, PartialEq, Eq, Clone, Default)] pub struct Attributes { pub size: Option, pub uid: Option, pub gid: Option, pub allocation_size: Option, pub owner: Option, pub group: Option, pub permissions: Option, pub access_time: Option<(u64, Option)>, pub create_time: Option<(u64, Option)>, pub modify_time: Option<(u64, Option)>, pub ctime: Option<(u64, Option)>, pub acl: Option>, pub attrib_bits: Option, pub attrib_bits_valid: Option, pub text_hint: Option, pub mime_type: Option, pub link_count: Option, pub untranslated_name: Option>, pub extended: Option>, } impl Attributes { pub fn new() -> Self { Self::default() } pub fn serialize(&self) -> std::io::Result> { let mut valid_attribute_flags: u32 = 0; let buf = Vec::new(); let mut writer = Cursor::new(buf); writer.write_u32::(valid_attribute_flags)?; if let Some(size) = self.size { writer.write_u64::(size)?; valid_attribute_flags |= SSH_FILEXFER_ATTR_SIZE; } if let Some(uid) = self.uid { writer.write_u32::(uid)?; valid_attribute_flags |= SSH_FILEXFER_ATTR_UIDGID; } if let Some(gid) = self.gid { writer.write_u32::(gid)?; assert!(valid_attribute_flags & SSH_FILEXFER_ATTR_UIDGID != 0); } else { assert!(valid_attribute_flags & SSH_FILEXFER_ATTR_UIDGID == 0); } if let Some(allocation_size) = self.allocation_size { writer.write_u64::(allocation_size)?; valid_attribute_flags |= SSH_FILEXFER_ATTR_ALLOCATION_SIZE; } if let Some(owner) = self.owner.as_ref() { writer.write_u32::(owner.len() as u32)?; writer.write_all(owner.as_bytes())?; valid_attribute_flags |= SSH_FILEXFER_ATTR_OWNERGROUP; } if let Some(group) = self.group.as_ref() { writer.write_u32::(group.len() as u32)?; writer.write_all(group.as_bytes())?; valid_attribute_flags |= SSH_FILEXFER_ATTR_OWNERGROUP; } if let Some(permissions) = self.permissions { writer.write_u32::(permissions)?; valid_attribute_flags |= SSH_FILEXFER_ATTR_PERMISSIONS; } if let Some(access_time) = self.access_time { writer.write_u64::(access_time.0)?; if let Some(ns) = access_time.1 { writer.write_u32::(ns)?; valid_attribute_flags |= SSH_FILEXFER_ATTR_SUBSECOND_TIMES; } valid_attribute_flags |= SSH_FILEXFER_ATTR_ACCESSTIME; } if let Some(create_time) = self.create_time { writer.write_u64::(create_time.0)?; if let Some(ns) = create_time.1 { assert!(valid_attribute_flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES != 0); writer.write_u32::(ns)?; } else { assert!(valid_attribute_flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES == 0); } valid_attribute_flags |= SSH_FILEXFER_ATTR_CREATETIME; } if let Some(modify_time) = self.modify_time { writer.write_u64::(modify_time.0)?; if let Some(ns) = modify_time.1 { assert!(valid_attribute_flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES != 0); writer.write_u32::(ns)?; } else { assert!(valid_attribute_flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES == 0); } valid_attribute_flags |= SSH_FILEXFER_ATTR_MODIFYTIME; } if let Some(ctime) = self.ctime { writer.write_u64::(ctime.0)?; if let Some(ns) = ctime.1 { assert!(valid_attribute_flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES != 0); writer.write_u32::(ns)?; } else { assert!(valid_attribute_flags & SSH_FILEXFER_ATTR_SUBSECOND_TIMES == 0); } valid_attribute_flags |= SSH_FILEXFER_ATTR_CTIME; } if let Some(acl) = self.acl.as_ref() { writer.write_u32::(acl.len() as u32)?; writer.write_all(acl.as_slice())?; valid_attribute_flags |= SSH_FILEXFER_ATTR_ACL; } if let Some(attrib_bits) = self.attrib_bits { writer.write_u32::(attrib_bits)?; valid_attribute_flags |= SSH_FILEXFER_ATTR_BITS; } if let Some(attrib_bits_valid) = self.attrib_bits_valid { writer.write_u32::(attrib_bits_valid)?; valid_attribute_flags |= SSH_FILEXFER_ATTR_BITS; } if let Some(text_hint) = self.text_hint { writer.write_u8(text_hint.into())?; valid_attribute_flags |= SSH_FILEXFER_ATTR_TEXT_HINT; } if let Some(mime_type) = self.mime_type.as_ref() { writer.write_u32::(mime_type.len() as u32)?; writer.write_all(mime_type.as_bytes())?; valid_attribute_flags |= SSH_FILEXFER_ATTR_MIME_TYPE; } if let Some(link_count) = self.link_count { writer.write_u32::(link_count)?; valid_attribute_flags |= SSH_FILEXFER_ATTR_LINK_COUNT; } if let Some(untranslated_name) = self.untranslated_name.as_ref() { writer.write_u32::(untranslated_name.len() as u32)?; writer.write_all(untranslated_name.as_slice())?; valid_attribute_flags |= SSH_FILEXFER_ATTR_UNTRANSLATED_NAME; } if let Some(extended) = self.extended.as_ref() { writer.write_u32::(extended.len() as u32)?; for (key, value) in extended.iter() { writer.write_u32::(key.len() as u32)?; writer.write_all(key.as_bytes())?; writer.write_u32::(value.len() as u32)?; writer.write_all(value.as_bytes())?; } valid_attribute_flags |= SSH_FILEXFER_ATTR_EXTENDED; } writer.seek(SeekFrom::Start(0))?; writer.write_u32::(valid_attribute_flags)?; Ok(writer.into_inner()) } pub fn deserialize(reader: &mut Cursor<&[u8]>) -> std::io::Result { let valid = reader.read_u32::()?; let size = if valid & SSH_FILEXFER_ATTR_SIZE != 0 { Some(reader.read_u64::()?) } else { None }; let (uid, gid) = if valid & SSH_FILEXFER_ATTR_UIDGID != 0 { ( Some(reader.read_u32::()?), Some(reader.read_u32::()?), ) } else { (None, None) }; let allocation_size = if valid & SSH_FILEXFER_ATTR_ALLOCATION_SIZE != 0 { Some(reader.read_u64::()?) } else { None }; let owner = if valid & SSH_FILEXFER_ATTR_OWNERGROUP != 0 { Some(read_string(reader, "owner")?) } else { None }; let group = if valid & SSH_FILEXFER_ATTR_OWNERGROUP != 0 { Some(read_string(reader, "group")?) } else { None }; let permissions = if valid & SSH_FILEXFER_ATTR_PERMISSIONS != 0 { Some(reader.read_u32::()?) } else { None }; let access_time = if valid & SSH_FILEXFER_ATTR_ACCESSTIME != 0 { let secs = reader.read_u64::()?; let ns = if valid & SSH_FILEXFER_ATTR_SUBSECOND_TIMES != 0 { Some(reader.read_u32::()?) } else { None }; Some((secs, ns)) } else { None }; let create_time = if valid & SSH_FILEXFER_ATTR_CREATETIME != 0 { let secs = reader.read_u64::()?; let ns = if valid & SSH_FILEXFER_ATTR_SUBSECOND_TIMES != 0 { Some(reader.read_u32::()?) } else { None }; Some((secs, ns)) } else { None }; let modify_time = if valid & SSH_FILEXFER_ATTR_MODIFYTIME != 0 { let secs = reader.read_u64::()?; let ns = if valid & SSH_FILEXFER_ATTR_SUBSECOND_TIMES != 0 { Some(reader.read_u32::()?) } else { None }; Some((secs, ns)) } else { None }; let ctime = if valid & SSH_FILEXFER_ATTR_CTIME != 0 { let secs = reader.read_u64::()?; let ns = if valid & SSH_FILEXFER_ATTR_SUBSECOND_TIMES != 0 { Some(reader.read_u32::()?) } else { None }; Some((secs, ns)) } else { None }; let acl = if valid & SSH_FILEXFER_ATTR_ACL != 0 { let len = reader.read_u32::()?; let mut buf = vec![0; len as usize]; reader.read_exact(&mut buf)?; Some(buf) } else { None }; let attrib_bits = if valid & SSH_FILEXFER_ATTR_BITS != 0 { Some(reader.read_u32::()?) } else { None }; let attrib_bits_valid = if valid & SSH_FILEXFER_ATTR_BITS != 0 { Some(reader.read_u32::()?) } else { None }; let text_hint = if valid & SSH_FILEXFER_ATTR_TEXT_HINT != 0 { Some(reader.read_u8()?) } else { None }; let mime_type = if valid & SSH_FILEXFER_ATTR_MIME_TYPE != 0 { Some(read_string(reader, "mime type")?) } else { None }; let link_count = if valid & SSH_FILEXFER_ATTR_LINK_COUNT != 0 { Some(reader.read_u32::()?) } else { None }; let untranslated_name = if valid & SSH_FILEXFER_ATTR_UNTRANSLATED_NAME != 0 { let len = reader.read_u32::()?; let mut buf = vec![0; len as usize]; reader.read_exact(&mut buf)?; Some(buf) } else { None }; let extended = if valid & SSH_FILEXFER_ATTR_EXTENDED != 0 { let len = reader.read_u32::()?; let mut ext = Vec::with_capacity(len as usize); for _ in 0..len { let k = read_string(reader, "extended key")?; let v = read_string(reader, "extended value")?; ext.push((k, v)); } Some(ext) } else { None }; Ok(Self { size, uid, gid, allocation_size, owner, group, permissions, access_time, create_time, modify_time, ctime, acl, attrib_bits, attrib_bits_valid, text_hint: text_hint.map(|h| h.into()), mime_type, link_count, untranslated_name, extended, }) } } fn read_string(reader: &mut Cursor<&[u8]>, what: &str) -> std::io::Result { let len = reader.read_u32::()?; let mut buf = vec![0; len as usize]; reader.read_exact(&mut buf)?; String::from_utf8(buf).map_err(|e| { std::io::Error::new( std::io::ErrorKind::InvalidData, format!("Invalid {}: {}", what, e), ) }) } #[derive(Debug, Clone)] pub struct File(pub Vec); #[derive(Debug, Clone)] pub struct Directory(pub Vec); pub fn build_init() -> Vec { let mut buf = Vec::with_capacity(4); buf.write_u32::(3).unwrap(); buf } pub fn parse_version(body: &[u8]) -> std::io::Result<(u32, Vec<(String, String)>)> { let mut reader = Cursor::new(body); let version = reader.read_u32::()?; if version != 3 { return Err(std::io::Error::other(format!( "SFTP version mismatch (expected 3, got: {})", version ))); } let mut extensions = Vec::new(); while reader.position() < reader.get_ref().len() as u64 { let key = read_string(&mut reader, "extension key")?; let value = read_string(&mut reader, "extension value")?; extensions.push((key, value)); } Ok((version, extensions)) } fn put_str(buf: &mut Vec, s: &str) { buf.write_u32::(s.len() as u32).unwrap(); buf.extend_from_slice(s.as_bytes()); } fn put_bytes(buf: &mut Vec, b: &[u8]) { buf.write_u32::(b.len() as u32).unwrap(); buf.extend_from_slice(b); } /// Build a request body containing only a single path field. pub fn build_path_only(path: &str) -> Vec { let mut buf = Vec::with_capacity(4 + path.len()); put_str(&mut buf, path); buf } pub fn build_handle_only(handle: &[u8]) -> Vec { let mut buf = Vec::with_capacity(4 + handle.len()); put_bytes(&mut buf, handle); buf } pub fn build_path_and_attrs(path: &str, attr: &Attributes) -> std::io::Result> { let attrs = attr.serialize()?; let mut buf = Vec::with_capacity(4 + path.len() + attrs.len()); put_str(&mut buf, path); buf.extend_from_slice(&attrs); Ok(buf) } pub fn build_handle_and_attrs(handle: &[u8], attr: &Attributes) -> std::io::Result> { let attrs = attr.serialize()?; let mut buf = Vec::with_capacity(4 + handle.len() + attrs.len()); put_bytes(&mut buf, handle); buf.extend_from_slice(&attrs); Ok(buf) } pub fn build_path_and_flags(path: &str, flags: u32) -> Vec { let mut buf = Vec::with_capacity(8 + path.len()); put_str(&mut buf, path); buf.write_u32::(flags).unwrap(); buf } pub fn build_handle_and_flags(handle: &[u8], flags: u32) -> Vec { let mut buf = Vec::with_capacity(8 + handle.len()); put_bytes(&mut buf, handle); buf.write_u32::(flags).unwrap(); buf } pub fn build_two_paths(a: &str, b: &str) -> Vec { let mut buf = Vec::with_capacity(8 + a.len() + b.len()); put_str(&mut buf, a); put_str(&mut buf, b); buf } pub fn build_link(path: &str, target: &str, symlink: bool) -> Vec { let mut buf = build_two_paths(path, target); buf.push(if symlink { 1 } else { 0 }); buf } pub fn build_open(path: &str, options: u32, attr: &Attributes) -> std::io::Result> { let attrs = attr.serialize()?; let mut buf = Vec::with_capacity(8 + path.len() + attrs.len()); put_str(&mut buf, path); buf.write_u32::(options).unwrap(); buf.extend_from_slice(&attrs); Ok(buf) } pub fn build_realpath(path: &str, control_byte: Option, compose: Option<&str>) -> Vec { let mut buf = build_path_only(path); if let Some(b) = control_byte { buf.push(b); } if let Some(c) = compose { put_str(&mut buf, c); } buf } pub fn build_rename(oldpath: &str, newpath: &str, flags: Option) -> Vec { let mut buf = build_two_paths(oldpath, newpath); buf.write_u32::( flags.unwrap_or(SSH_FXF_RENAME_ATOMIC | SSH_FXF_RENAME_NATIVE | SSH_FXF_RENAME_OVERWRITE), ) .unwrap(); buf } pub fn build_extended(request: &str, data: &[u8]) -> Vec { let mut buf = Vec::with_capacity(4 + request.len() + data.len()); put_str(&mut buf, request); buf.extend_from_slice(data); buf } pub fn build_block(handle: &[u8], offset: u64, length: u64, lockmask: u32) -> Vec { let mut buf = Vec::with_capacity(4 + handle.len() + 8 + 8 + 4); put_bytes(&mut buf, handle); buf.write_u64::(offset).unwrap(); buf.write_u64::(length).unwrap(); buf.write_u32::(lockmask).unwrap(); buf } pub fn build_unblock(handle: &[u8], offset: u64, length: u64) -> Vec { let mut buf = Vec::with_capacity(4 + handle.len() + 8 + 8); put_bytes(&mut buf, handle); buf.write_u64::(offset).unwrap(); buf.write_u64::(length).unwrap(); buf } pub fn build_pwrite(handle: &[u8], offset: u64, data: &[u8]) -> Vec { let mut buf = Vec::with_capacity(4 + handle.len() + 8 + 4 + data.len()); put_bytes(&mut buf, handle); buf.write_u64::(offset).unwrap(); buf.write_u32::(data.len() as u32).unwrap(); buf.extend_from_slice(data); buf } pub fn build_pread(handle: &[u8], offset: u64, length: u32) -> Vec { let mut buf = Vec::with_capacity(4 + handle.len() + 8 + 4); put_bytes(&mut buf, handle); buf.write_u64::(offset).unwrap(); buf.write_u32::(length).unwrap(); buf } pub fn parse_status(respdata: &[u8]) -> Result<()> { let mut reader = Cursor::new(respdata); let status = reader.read_u32::()?; let err_msg = read_string(&mut reader, "error message")?; let lang_tag = read_string(&mut reader, "lang tag")?; match status { SSH_FX_OK => Ok(()), SSH_FX_EOF => Err(Error::Eof(err_msg, lang_tag)), SSH_FX_NO_SUCH_FILE => Err(Error::NoSuchFile(err_msg, lang_tag)), SSH_FX_PERMISSION_DENIED => Err(Error::PermissionDenied(err_msg, lang_tag)), SSH_FX_FAILURE => Err(Error::Failure(err_msg, lang_tag)), SSH_FX_BAD_MESSAGE => Err(Error::BadMessage(err_msg, lang_tag)), SSH_FX_NO_CONNECTION => Err(Error::NoConnection(err_msg, lang_tag)), SSH_FX_CONNECTION_LOST => Err(Error::ConnectionLost(err_msg, lang_tag)), SSH_FX_OP_UNSUPPORTED => Err(Error::OpUnsupported(err_msg, lang_tag)), SSH_FX_INVALID_HANDLE => Err(Error::InvalidHandle(err_msg, lang_tag)), SSH_FX_NO_SUCH_PATH => Err(Error::NoSuchPath(err_msg, lang_tag)), SSH_FX_FILE_ALREADY_EXISTS => Err(Error::FileAlreadyExists(err_msg, lang_tag)), SSH_FX_WRITE_PROTECT => Err(Error::WriteProtect(err_msg, lang_tag)), SSH_FX_NO_MEDIA => Err(Error::NoMedia(err_msg, lang_tag)), SSH_FX_NO_SPACE_ON_FILESYSTEM => Err(Error::NoSpaceOnFilesystem(err_msg, lang_tag)), SSH_FX_QUOTA_EXCEEDED => Err(Error::QuotaExceeded(err_msg, lang_tag)), SSH_FX_UNKNOWN_PRINCIPAL => Err(Error::UnknownPrincipal(err_msg, lang_tag)), SSH_FX_LOCK_CONFLICT => Err(Error::LockConflict(err_msg, lang_tag)), SSH_FX_DIR_NOT_EMPTY => Err(Error::DirNotEmpty(err_msg, lang_tag)), SSH_FX_NOT_A_DIRECTORY => Err(Error::NotADirectory(err_msg, lang_tag)), SSH_FX_INVALID_FILENAME => Err(Error::InvalidFilename(err_msg, lang_tag)), SSH_FX_LINK_LOOP => Err(Error::LinkLoop(err_msg, lang_tag)), SSH_FX_CANNOT_DELETE => Err(Error::CannotDelete(err_msg, lang_tag)), SSH_FX_INVALID_PARAMETER => Err(Error::InvalidParameter(err_msg, lang_tag)), SSH_FX_FILE_IS_A_DIRECTORY => Err(Error::FileIsADirectory(err_msg, lang_tag)), SSH_FX_BYTE_RANGE_LOCK_CONFLICT => Err(Error::ByteRangeLockConflict(err_msg, lang_tag)), SSH_FX_BYTE_RANGE_LOCK_REFUSED => Err(Error::ByteRangeLockRefused(err_msg, lang_tag)), SSH_FX_DELETE_PENDING => Err(Error::DeletePending(err_msg, lang_tag)), SSH_FX_FILE_CORRUPT => Err(Error::FileCorrupt(err_msg, lang_tag)), SSH_FX_OWNER_INVALID => Err(Error::OwnerInvalid(err_msg, lang_tag)), SSH_FX_GROUP_INVALID => Err(Error::GroupInvalid(err_msg, lang_tag)), SSH_FX_NO_MATCHING_BYTE_RANGE_LOCK => { Err(Error::NoMatchingByteRangeLock(err_msg, lang_tag)) } _ => Err(Error::Other(status, err_msg, lang_tag)), } } pub fn parse_handle(respdata: &[u8]) -> Result> { let mut reader = Cursor::new(respdata); let handle_len = reader.read_u32::()?; let mut handle = vec![0u8; handle_len as usize]; reader.read_exact(&mut handle)?; Ok(handle) } pub fn parse_data(respdata: &[u8]) -> Result> { let mut reader = Cursor::new(respdata); let len = reader.read_u32::()?; let mut data = vec![0; len as usize]; reader.read_exact(&mut data)?; Ok(data) } pub fn parse_attrs(respdata: &[u8]) -> Result { let mut reader = Cursor::new(respdata); Attributes::deserialize(&mut reader).map_err(Error::Io) } pub fn parse_name(respdata: &[u8]) -> Result> { let mut reader = Cursor::new(respdata); let count = reader.read_u32::()?; let mut files = Vec::with_capacity(count as usize); for _ in 0..count { let filename = read_string(&mut reader, "filename")?; let attrs = Attributes::deserialize(&mut reader)?; files.push((filename, attrs)); } Ok(files) } pub fn parse_readdir(respdata: &[u8]) -> Result> { let mut reader = Cursor::new(respdata); let count = reader.read_u32::()?; let mut files = Vec::with_capacity(count as usize); for _ in 0..count { let filename = read_string(&mut reader, "filename")?; let longname = read_string(&mut reader, "longname")?; let attrs = Attributes::deserialize(&mut reader)?; files.push((filename, longname, attrs)); } Ok(files) } fn unexpected(cmd: u8) -> Error { Error::Io(std::io::Error::other(format!( "Unexpected response: {}", cmd ))) } /// Interpret an SSH_FXP_STATUS reply received in place of a data-bearing response. /// Any non-OK status becomes its matching Error; SSH_FX_OK is itself a protocol /// violation (the server should have sent the requested handle/attrs/data/name). fn status_as_error(data: &[u8]) -> Error { match parse_status(data) { Ok(()) => Error::Io(std::io::Error::other( "Server returned SSH_FX_OK where a data-bearing response was expected", )), Err(e) => e, } } pub fn expect_status(cmd: u8, data: &[u8]) -> Result<()> { match cmd { SSH_FXP_STATUS => parse_status(data), _ => Err(unexpected(cmd)), } } pub fn expect_handle(cmd: u8, data: &[u8]) -> Result> { match cmd { SSH_FXP_HANDLE => parse_handle(data), SSH_FXP_STATUS => Err(status_as_error(data)), _ => Err(unexpected(cmd)), } } pub fn expect_attrs(cmd: u8, data: &[u8]) -> Result { match cmd { SSH_FXP_ATTRS => parse_attrs(data), SSH_FXP_STATUS => Err(status_as_error(data)), _ => Err(unexpected(cmd)), } } pub fn expect_data(cmd: u8, data: &[u8]) -> Result> { match cmd { SSH_FXP_DATA => parse_data(data), SSH_FXP_STATUS => Err(status_as_error(data)), _ => Err(unexpected(cmd)), } } pub fn expect_name(cmd: u8, data: &[u8]) -> Result> { match cmd { SSH_FXP_NAME => parse_name(data), SSH_FXP_STATUS => Err(status_as_error(data)), _ => Err(unexpected(cmd)), } } pub fn expect_readdir(cmd: u8, data: &[u8]) -> Result> { match cmd { SSH_FXP_NAME => parse_readdir(data), SSH_FXP_STATUS => Err(status_as_error(data)), _ => Err(unexpected(cmd)), } } pub fn expect_extended(cmd: u8, data: Vec) -> Result>> { match cmd { SSH_FXP_EXTENDED_REPLY => Ok(Some(data)), SSH_FXP_STATUS => parse_status(&data).map(|_| None), _ => Err(unexpected(cmd)), } } pub fn read_raw_packet(channel: &mut C) -> std::io::Result<(u8, Vec)> { let mut buf = [0u8; 4]; channel.read_exact(&mut buf)?; let len = i32::from_be_bytes(buf); let mut buf = vec![0u8; len as usize]; channel.read_exact(&mut buf)?; let kind = buf[0]; Ok((kind, buf[1..].to_vec())) } pub fn write_raw_packet(channel: &mut C, kind: u8, buf: &[u8]) -> std::io::Result<()> { let mut channel = std::io::BufWriter::new(channel); channel.write_u32::(buf.len() as u32 + 1)?; channel.write_u8(kind)?; channel.write_all(buf)?; channel.flush()?; Ok(()) } /// Wrap a request body with the request-id prefix used by all numbered requests. pub fn with_request_id(request_id: u32, body: &[u8]) -> Vec { let mut buf = Vec::with_capacity(4 + body.len()); buf.write_u32::(request_id).unwrap(); buf.extend_from_slice(body); buf } /// Strip the request-id prefix from a response body. Returns (request_id, payload). pub fn split_request_id(buf: &[u8]) -> std::io::Result<(u32, &[u8])> { if buf.len() < 4 { return Err(std::io::Error::other( "response too short to contain request id", )); } let request_id = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]); Ok((request_id, &buf[4..])) } #[cfg(test)] mod tests { use super::*; #[test] fn attributes_roundtrip_size_perms() { let mut a = Attributes::new(); a.size = Some(12345); a.permissions = Some(0o100644); let bytes = a.serialize().unwrap(); let mut cursor = Cursor::new(bytes.as_slice()); let b = Attributes::deserialize(&mut cursor).unwrap(); assert_eq!(a, b); } #[test] fn attributes_roundtrip_uidgid() { let mut a = Attributes::new(); a.uid = Some(1000); a.gid = Some(1000); let bytes = a.serialize().unwrap(); let mut cursor = Cursor::new(bytes.as_slice()); let b = Attributes::deserialize(&mut cursor).unwrap(); assert_eq!(a, b); } #[test] fn build_path_only_layout() { let body = build_path_only("hello"); assert_eq!(body, b"\x00\x00\x00\x05hello".to_vec()); } #[test] fn build_two_paths_layout() { let body = build_two_paths("a", "bc"); assert_eq!(body, b"\x00\x00\x00\x01a\x00\x00\x00\x02bc".to_vec()); } #[test] fn request_id_roundtrip() { let body = b"hello".to_vec(); let wrapped = with_request_id(0x12345678, &body); let (id, rest) = split_request_id(&wrapped).unwrap(); assert_eq!(id, 0x12345678); assert_eq!(rest, body.as_slice()); } #[test] fn parse_status_ok() { let mut buf = Vec::new(); buf.extend_from_slice(&SSH_FX_OK.to_be_bytes()); buf.extend_from_slice(&0u32.to_be_bytes()); buf.extend_from_slice(&0u32.to_be_bytes()); parse_status(&buf).unwrap(); } #[test] fn parse_status_eof() { let mut buf = Vec::new(); buf.extend_from_slice(&SSH_FX_EOF.to_be_bytes()); let msg = b"end"; buf.extend_from_slice(&(msg.len() as u32).to_be_bytes()); buf.extend_from_slice(msg); buf.extend_from_slice(&0u32.to_be_bytes()); match parse_status(&buf) { Err(Error::Eof(m, _)) => assert_eq!(m, "end"), other => panic!("expected Eof, got {:?}", other), } } fn status_payload(code: u32) -> Vec { let mut buf = Vec::new(); buf.extend_from_slice(&code.to_be_bytes()); buf.extend_from_slice(&0u32.to_be_bytes()); buf.extend_from_slice(&0u32.to_be_bytes()); buf } #[test] fn expect_handle_passes_through_status_error() { let payload = status_payload(SSH_FX_NO_SUCH_FILE); match expect_handle(SSH_FXP_STATUS, &payload) { Err(Error::NoSuchFile(_, _)) => {} other => panic!("expected NoSuchFile, got {:?}", other), } } #[test] fn expect_handle_rejects_ok_status_without_panic() { let payload = status_payload(SSH_FX_OK); match expect_handle(SSH_FXP_STATUS, &payload) { Err(Error::Io(e)) => { assert!( e.to_string().contains("SSH_FX_OK"), "unexpected message: {}", e ); } other => panic!("expected Io error, got {:?}", other), } } #[test] fn expect_attrs_rejects_ok_status_without_panic() { let payload = status_payload(SSH_FX_OK); assert!(matches!( expect_attrs(SSH_FXP_STATUS, &payload), Err(Error::Io(_)) )); } #[test] fn expect_data_rejects_ok_status_without_panic() { let payload = status_payload(SSH_FX_OK); assert!(matches!( expect_data(SSH_FXP_STATUS, &payload), Err(Error::Io(_)) )); } #[test] fn expect_name_rejects_ok_status_without_panic() { let payload = status_payload(SSH_FX_OK); assert!(matches!( expect_name(SSH_FXP_STATUS, &payload), Err(Error::Io(_)) )); } #[test] fn expect_readdir_rejects_ok_status_without_panic() { let payload = status_payload(SSH_FX_OK); assert!(matches!( expect_readdir(SSH_FXP_STATUS, &payload), Err(Error::Io(_)) )); } #[test] fn expect_handle_rejects_unexpected_cmd() { assert!(matches!( expect_handle(SSH_FXP_DATA, &[]), Err(Error::Io(_)) )); } fn roundtrip_attrs(a: &Attributes) { let bytes = a.serialize().unwrap(); let mut cursor = Cursor::new(bytes.as_slice()); let b = Attributes::deserialize(&mut cursor).unwrap(); assert_eq!(*a, b); } #[test] fn attributes_roundtrip_empty() { roundtrip_attrs(&Attributes::new()); } #[test] fn attributes_roundtrip_ownergroup() { let mut a = Attributes::new(); a.owner = Some("alice".into()); a.group = Some("staff".into()); roundtrip_attrs(&a); } #[test] fn attributes_roundtrip_allocation_size() { let mut a = Attributes::new(); a.allocation_size = Some(4096); roundtrip_attrs(&a); } #[test] fn attributes_roundtrip_times_seconds_only() { let mut a = Attributes::new(); a.access_time = Some((1_700_000_000, None)); a.modify_time = Some((1_700_000_001, None)); a.ctime = Some((1_700_000_002, None)); a.create_time = Some((1_700_000_003, None)); roundtrip_attrs(&a); } #[test] fn attributes_roundtrip_times_with_subseconds() { let mut a = Attributes::new(); // Subseconds on access_time turns the SUBSECOND_TIMES flag on, so every // other timestamp in this message must also carry nanoseconds. a.access_time = Some((1_700_000_000, Some(100))); a.modify_time = Some((1_700_000_001, Some(200))); a.ctime = Some((1_700_000_002, Some(300))); a.create_time = Some((1_700_000_003, Some(400))); roundtrip_attrs(&a); } #[test] fn attributes_roundtrip_bits() { let mut a = Attributes::new(); a.attrib_bits = Some(SSH_FILEXFER_ATTR_FLAGS_READONLY); a.attrib_bits_valid = Some(SSH_FILEXFER_ATTR_FLAGS_READONLY); roundtrip_attrs(&a); } #[test] fn attributes_roundtrip_text_and_mime() { let mut a = Attributes::new(); a.text_hint = Some(TextHint::KnownBinary); a.mime_type = Some("application/octet-stream".into()); roundtrip_attrs(&a); } #[test] fn attributes_roundtrip_link_count_untranslated() { let mut a = Attributes::new(); a.link_count = Some(3); a.untranslated_name = Some(vec![0xff, 0x00, 0x7f]); roundtrip_attrs(&a); } #[test] fn attributes_roundtrip_acl() { let mut a = Attributes::new(); a.acl = Some(vec![1, 2, 3, 4, 5]); roundtrip_attrs(&a); } #[test] fn attributes_roundtrip_extended() { let mut a = Attributes::new(); a.extended = Some(vec![ ("vendor@example".into(), "value-1".into()), ("other".into(), "value-2".into()), ]); roundtrip_attrs(&a); } #[test] fn attributes_roundtrip_all_fields() { let mut a = Attributes::new(); a.size = Some(9_999_999); a.uid = Some(501); a.gid = Some(20); a.allocation_size = Some(12_288); a.owner = Some("alice".into()); a.group = Some("staff".into()); a.permissions = Some(0o100755); a.access_time = Some((1_700_000_000, Some(1))); a.modify_time = Some((1_700_000_001, Some(2))); a.ctime = Some((1_700_000_002, Some(3))); a.create_time = Some((1_700_000_003, Some(4))); a.acl = Some(vec![7, 8, 9]); a.attrib_bits = Some(SSH_FILEXFER_ATTR_FLAGS_ARCHIVE); a.attrib_bits_valid = Some(SSH_FILEXFER_ATTR_FLAGS_ARCHIVE); a.text_hint = Some(TextHint::GuessedText); a.mime_type = Some("text/plain".into()); a.link_count = Some(1); a.untranslated_name = Some(b"raw-name".to_vec()); a.extended = Some(vec![("x".into(), "y".into())]); roundtrip_attrs(&a); } #[test] fn build_handle_only_layout() { let body = build_handle_only(&[0xaa, 0xbb]); assert_eq!(body, vec![0x00, 0x00, 0x00, 0x02, 0xaa, 0xbb]); } #[test] fn build_path_and_flags_layout() { let body = build_path_and_flags("f", 0x11223344); assert_eq!(body, vec![0, 0, 0, 1, b'f', 0x11, 0x22, 0x33, 0x44]); } #[test] fn build_handle_and_flags_layout() { let body = build_handle_and_flags(&[0xa], 0xdeadbeef); assert_eq!(body, vec![0, 0, 0, 1, 0xa, 0xde, 0xad, 0xbe, 0xef]); } #[test] fn build_link_sets_flag_byte() { assert_eq!(build_link("a", "b", true).last().copied(), Some(1)); assert_eq!(build_link("a", "b", false).last().copied(), Some(0)); } #[test] fn build_realpath_optional_fields() { let body = build_realpath("p", None, None); assert_eq!(body, vec![0, 0, 0, 1, b'p']); let body = build_realpath("p", Some(0x5), None); assert_eq!(body, vec![0, 0, 0, 1, b'p', 0x5]); let body = build_realpath("p", Some(0x5), Some("q")); assert_eq!(body, vec![0, 0, 0, 1, b'p', 0x5, 0, 0, 0, 1, b'q']); } #[test] fn build_rename_default_flags() { let body = build_rename("a", "b", None); // Last 4 bytes are the flags: ATOMIC | NATIVE | OVERWRITE = 7. let tail = &body[body.len() - 4..]; let flags = u32::from_be_bytes([tail[0], tail[1], tail[2], tail[3]]); assert_eq!( flags, SSH_FXF_RENAME_ATOMIC | SSH_FXF_RENAME_NATIVE | SSH_FXF_RENAME_OVERWRITE ); } #[test] fn build_rename_honours_explicit_flags() { let body = build_rename("a", "b", Some(SSH_FXF_RENAME_OVERWRITE)); let tail = &body[body.len() - 4..]; let flags = u32::from_be_bytes([tail[0], tail[1], tail[2], tail[3]]); assert_eq!(flags, SSH_FXF_RENAME_OVERWRITE); } #[test] fn build_pread_layout() { let body = build_pread(b"h", 0x1122334455667788, 0x10); let expected: Vec = vec![ 0, 0, 0, 1, b'h', // handle 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, // offset 0x00, 0x00, 0x00, 0x10, // length ]; assert_eq!(body, expected); } #[test] fn build_pwrite_layout() { let body = build_pwrite(b"h", 1, b"abc"); let expected: Vec = vec![ 0, 0, 0, 1, b'h', 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 3, b'a', b'b', b'c', ]; assert_eq!(body, expected); } #[test] fn build_block_unblock_layout() { let body = build_block(b"h", 1, 2, 0xff); assert_eq!( body, vec![0, 0, 0, 1, b'h', 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0xff,] ); let body = build_unblock(b"h", 3, 4); assert_eq!( body, vec![0, 0, 0, 1, b'h', 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 4] ); } #[test] fn build_extended_layout() { let body = build_extended("ext@ex", b"payload"); assert_eq!( body, vec![ 0, 0, 0, 6, b'e', b'x', b't', b'@', b'e', b'x', b'p', b'a', b'y', b'l', b'o', b'a', b'd' ] ); } #[test] fn build_open_layout_includes_flags_and_attrs() { let body = build_open("p", 0x9, &Attributes::new()).unwrap(); // path prefix assert_eq!(&body[..5], &[0, 0, 0, 1, b'p']); // flags assert_eq!(&body[5..9], &[0, 0, 0, 0x9]); // attrs: empty => valid=0 assert_eq!(&body[9..], &[0, 0, 0, 0]); } #[test] fn parse_name_roundtrip() { // Build a NAME body with two entries. let mut body = Vec::new(); body.extend_from_slice(&2u32.to_be_bytes()); for name in ["foo", "bar"] { body.extend_from_slice(&(name.len() as u32).to_be_bytes()); body.extend_from_slice(name.as_bytes()); // empty attrs body.extend_from_slice(&0u32.to_be_bytes()); } let out = parse_name(&body).unwrap(); assert_eq!(out.len(), 2); assert_eq!(out[0].0, "foo"); assert_eq!(out[1].0, "bar"); assert_eq!(out[0].1, Attributes::new()); } #[test] fn parse_readdir_roundtrip() { let mut body = Vec::new(); body.extend_from_slice(&1u32.to_be_bytes()); let name = "f.txt"; let long = "-rw-r--r-- 1 u g 0 Jan 1 1970 f.txt"; body.extend_from_slice(&(name.len() as u32).to_be_bytes()); body.extend_from_slice(name.as_bytes()); body.extend_from_slice(&(long.len() as u32).to_be_bytes()); body.extend_from_slice(long.as_bytes()); body.extend_from_slice(&0u32.to_be_bytes()); let out = parse_readdir(&body).unwrap(); assert_eq!(out.len(), 1); assert_eq!(out[0].0, name); assert_eq!(out[0].1, long); } #[test] fn parse_handle_roundtrip() { let mut body = Vec::new(); body.extend_from_slice(&4u32.to_be_bytes()); body.extend_from_slice(b"abcd"); assert_eq!(parse_handle(&body).unwrap(), b"abcd".to_vec()); } #[test] fn parse_data_roundtrip() { let mut body = Vec::new(); body.extend_from_slice(&3u32.to_be_bytes()); body.extend_from_slice(b"xyz"); assert_eq!(parse_data(&body).unwrap(), b"xyz".to_vec()); } #[test] fn parse_version_reads_extensions() { let mut body = Vec::new(); body.extend_from_slice(&3u32.to_be_bytes()); for (k, v) in [ ("posix-rename@openssh.com", "1"), ("statvfs@openssh.com", "2"), ] { body.extend_from_slice(&(k.len() as u32).to_be_bytes()); body.extend_from_slice(k.as_bytes()); body.extend_from_slice(&(v.len() as u32).to_be_bytes()); body.extend_from_slice(v.as_bytes()); } let (version, exts) = parse_version(&body).unwrap(); assert_eq!(version, 3); assert_eq!(exts.len(), 2); assert_eq!(exts[0].0, "posix-rename@openssh.com"); assert_eq!(exts[1].1, "2"); } #[test] fn parse_version_rejects_non_3() { let body = 4u32.to_be_bytes().to_vec(); assert!(parse_version(&body).is_err()); } #[test] fn split_request_id_rejects_short_buffer() { assert!(split_request_id(&[0, 0]).is_err()); } #[test] fn error_conversion_preserves_kind() { use std::io::ErrorKind::*; let cases: &[(Error, std::io::ErrorKind)] = &[ (Error::NoSuchFile("m".into(), "".into()), NotFound), (Error::NoSuchPath("m".into(), "".into()), NotFound), (Error::NoMedia("m".into(), "".into()), NotFound), ( Error::PermissionDenied("m".into(), "".into()), PermissionDenied, ), (Error::WriteProtect("m".into(), "".into()), PermissionDenied), ( Error::QuotaExceeded("m".into(), "".into()), PermissionDenied, ), (Error::LockConflict("m".into(), "".into()), PermissionDenied), (Error::NoConnection("m".into(), "".into()), NotConnected), ( Error::ConnectionLost("m".into(), "".into()), ConnectionReset, ), (Error::InvalidHandle("m".into(), "".into()), InvalidInput), (Error::InvalidFilename("m".into(), "".into()), InvalidInput), ( Error::FileAlreadyExists("m".into(), "".into()), AlreadyExists, ), (Error::Eof("m".into(), "".into()), UnexpectedEof), ]; for (err, expected) in cases { let io_err: std::io::Error = err.clone_for_test().into(); assert_eq!( io_err.kind(), *expected, "wrong kind for {:?}: got {:?}", err, io_err.kind() ); } } impl Error { /// Test-only clone. The `Error` variant carrying `io::Error` cannot be /// cloned, but every variant used in `error_conversion_preserves_kind` /// is string-based and safe to duplicate. fn clone_for_test(&self) -> Error { match self { Error::NoSuchFile(a, b) => Error::NoSuchFile(a.clone(), b.clone()), Error::NoSuchPath(a, b) => Error::NoSuchPath(a.clone(), b.clone()), Error::NoMedia(a, b) => Error::NoMedia(a.clone(), b.clone()), Error::PermissionDenied(a, b) => Error::PermissionDenied(a.clone(), b.clone()), Error::WriteProtect(a, b) => Error::WriteProtect(a.clone(), b.clone()), Error::QuotaExceeded(a, b) => Error::QuotaExceeded(a.clone(), b.clone()), Error::LockConflict(a, b) => Error::LockConflict(a.clone(), b.clone()), Error::NoConnection(a, b) => Error::NoConnection(a.clone(), b.clone()), Error::ConnectionLost(a, b) => Error::ConnectionLost(a.clone(), b.clone()), Error::InvalidHandle(a, b) => Error::InvalidHandle(a.clone(), b.clone()), Error::InvalidFilename(a, b) => Error::InvalidFilename(a.clone(), b.clone()), Error::FileAlreadyExists(a, b) => Error::FileAlreadyExists(a.clone(), b.clone()), Error::Eof(a, b) => Error::Eof(a.clone(), b.clone()), _ => panic!("clone_for_test only handles string-based variants"), } } } } sftp-0.3.0/src/russh.rs000064400000000000000000000032301046102023000130770ustar 00000000000000//! russh transport glue for [`AsyncSftpClient`]. //! //! The caller is responsible for establishing the SSH session (host-key //! verification, authentication, proxy jumps, etc.) and opening a channel. //! This module only takes the already-open channel, requests the `sftp` //! subsystem, and wraps the resulting byte stream in an //! [`AsyncSftpClient`]. use crate::r#async::AsyncSftpClient; use russh::client::Msg; use russh::{Channel, ChannelStream}; use tokio::io::WriteHalf; /// The concrete [`AsyncSftpClient`] type returned when running over a russh /// channel. pub type RusshSftpClient = AsyncSftpClient>>; /// Request the `sftp` subsystem on an already-open russh session channel and /// return a ready-to-use async SFTP client over it. /// /// The channel must have been obtained from a session you opened yourself, /// e.g. via `session.channel_open_session().await?`. pub async fn from_channel(channel: Channel) -> std::io::Result { channel .request_subsystem(true, "sftp") .await .map_err(|e| std::io::Error::other(format!("sftp subsystem request failed: {:?}", e)))?; from_subsystem_channel(channel).await } /// Wrap a russh channel on which the `sftp` subsystem has already been /// requested. Use this if you manage the subsystem request yourself (for /// example to pass custom environment or to negotiate a non-standard /// subsystem name). pub async fn from_subsystem_channel(channel: Channel) -> std::io::Result { let stream = channel.into_stream(); let (reader, writer) = tokio::io::split(stream); AsyncSftpClient::new(reader, writer).await } sftp-0.3.0/src/sync.rs000064400000000000000000000501501046102023000127120ustar 00000000000000//! Synchronous SFTP client over a `Read + Write` channel. use crate::protocol::*; use std::io::{Read, Write}; #[cfg(unix)] use std::os::unix::io::FromRawFd; #[cfg(windows)] use std::os::windows::io::{FromRawHandle, RawHandle}; use std::sync::Mutex; pub struct SftpClient { channel: Mutex, last_request_id: std::sync::atomic::AtomicU32, version: u32, extensions: Vec<(String, String)>, } impl SftpClient { #[cfg(unix)] pub fn from_fd(fd: i32) -> std::io::Result> { let file = unsafe { std::fs::File::from_raw_fd(fd) }; SftpClient::new(file) } #[cfg(windows)] pub fn from_handle(handle: RawHandle) -> std::io::Result> { let file = unsafe { std::fs::File::from_raw_handle(handle) }; SftpClient::new(file) } } impl SftpClient { pub fn new(mut channel: C) -> std::io::Result { write_raw_packet(&mut channel, SSH_FXP_INIT, &build_init())?; channel.flush()?; let (kind, body) = read_raw_packet(&mut channel)?; if kind != SSH_FXP_VERSION { return Err(std::io::Error::other(format!( "Unexpected response to init: {}", kind ))); } let (version, extensions) = parse_version(&body)?; Ok(Self { channel: Mutex::new(channel), version, extensions, last_request_id: std::sync::atomic::AtomicU32::new(0), }) } pub fn extensions(&self) -> &[(String, String)] { &self.extensions } pub fn version(&self) -> u32 { self.version } fn process(&self, cmd: u8, body: &[u8]) -> std::io::Result<(u8, Vec)> { let request_id = self .last_request_id .fetch_add(1, std::sync::atomic::Ordering::SeqCst); let body = with_request_id(request_id, body); write_raw_packet(&mut *self.channel.lock().unwrap(), cmd, body.as_slice())?; let (cmd, buf) = read_raw_packet(&mut *self.channel.lock().unwrap())?; let (resp_id, payload) = split_request_id(&buf)?; assert_eq!(resp_id, request_id); Ok((cmd, payload.to_vec())) } /// Create a new directory. pub fn mkdir(&self, path: &str, attr: &Attributes) -> Result<()> { let (cmd, data) = self.process(SSH_FXP_MKDIR, &build_path_and_attrs(path, attr)?)?; expect_status(cmd, &data) } /// Remove a directory. pub fn rmdir(&self, path: &str) -> Result<()> { let (cmd, data) = self.process(SSH_FXP_RMDIR, &build_path_only(path))?; expect_status(cmd, &data) } pub fn readlink(&self, path: &str) -> Result { let (cmd, data) = self.process(SSH_FXP_READLINK, &build_path_only(path))?; let names = expect_name(cmd, &data)?; Ok(names[0].0.clone()) } pub fn symlink(&self, path: &str, target: &str) -> Result<()> { let (cmd, data) = self.process(SSH_FXP_SYMLINK, &build_two_paths(path, target))?; expect_status(cmd, &data) } pub fn hardlink(&self, path: &str, target: &str) -> Result<()> { self.link(path, target, false) } pub fn link(&self, path: &str, target: &str, symlink: bool) -> Result<()> { let (cmd, data) = self.process(SSH_FXP_LINK, &build_link(path, target, symlink))?; expect_status(cmd, &data) } pub fn open(&self, path: &str, options: OpenOptions, attr: &Attributes) -> Result { let (cmd, data) = self.process(SSH_FXP_OPEN, &build_open(path, options.get(), attr)?)?; Ok(File(expect_handle(cmd, &data)?)) } pub fn realpath( &self, path: &str, control_byte: Option, compose_path: Option<&str>, ) -> Result { let (cmd, data) = self.process( SSH_FXP_REALPATH, &build_realpath(path, control_byte, compose_path), )?; let names = expect_name(cmd, &data)?; Ok(names[0].0.clone()) } pub fn setstat(&self, path: &str, attr: &Attributes) -> Result<()> { let (cmd, data) = self.process(SSH_FXP_SETSTAT, &build_path_and_attrs(path, attr)?)?; expect_status(cmd, &data) } pub fn stat(&self, path: &str, flags: Option) -> Result { let (cmd, data) = self.process( SSH_FXP_STAT, &build_path_and_flags(path, flags.unwrap_or(0)), )?; expect_attrs(cmd, &data) } pub fn remove(&self, path: &str) -> Result<()> { let (cmd, data) = self.process(SSH_FXP_REMOVE, &build_path_only(path))?; expect_status(cmd, &data) } pub fn rename(&self, oldpath: &str, newpath: &str, flags: Option) -> Result<()> { let (cmd, data) = self.process(SSH_FXP_RENAME, &build_rename(oldpath, newpath, flags))?; expect_status(cmd, &data) } pub fn lstat(&self, path: &str, flags: Option) -> Result { let (cmd, data) = self.process( SSH_FXP_LSTAT, &build_path_and_flags(path, flags.unwrap_or(0)), )?; expect_attrs(cmd, &data) } pub fn opendir(&self, path: &str) -> Result { let (cmd, data) = self.process(SSH_FXP_OPENDIR, &build_path_only(path))?; Ok(Directory(expect_handle(cmd, &data)?)) } pub fn extended(&self, request: &str, data: &[u8]) -> Result>> { let (cmd, payload) = self.process(SSH_FXP_EXTENDED, &build_extended(request, data))?; expect_extended(cmd, payload) } pub fn block(&self, file: &File, offset: u64, length: u64, lockmask: u32) -> Result<()> { let (cmd, data) = self.process( SSH_FXP_BLOCK, &build_block(&file.0, offset, length, lockmask), )?; expect_status(cmd, &data) } pub fn unblock(&self, file: &File, offset: u64, length: u64) -> Result<()> { let (cmd, data) = self.process(SSH_FXP_UNBLOCK, &build_unblock(&file.0, offset, length))?; expect_status(cmd, &data) } pub fn fsetstat(&self, file: &File, attr: &Attributes) -> Result<()> { let (cmd, data) = self.process(SSH_FXP_FSETSTAT, &build_handle_and_attrs(&file.0, attr)?)?; expect_status(cmd, &data) } pub fn fstat(&self, file: &File, flags: Option) -> Result { let (cmd, data) = self.process( SSH_FXP_FSTAT, &build_handle_and_flags(&file.0, flags.unwrap_or(0)), )?; expect_attrs(cmd, &data) } pub fn pwrite(&self, file: &File, offset: u64, data: &[u8]) -> Result<()> { let (cmd, payload) = self.process(SSH_FXP_WRITE, &build_pwrite(&file.0, offset, data))?; expect_status(cmd, &payload) } pub fn pread(&self, file: &File, offset: u64, length: u32) -> Result> { let (cmd, data) = self.process(SSH_FXP_READ, &build_pread(&file.0, offset, length))?; expect_data(cmd, &data) } pub fn fclose(&self, file: &File) -> Result<()> { let (cmd, data) = self.process(SSH_FXP_CLOSE, &build_handle_only(&file.0))?; expect_status(cmd, &data) } pub fn flineseek(&self, file: &File, lineno: u64) -> Result<()> { use byteorder::{BigEndian, WriteBytesExt}; let mut buf = build_handle_only(&file.0); buf.write_u64::(lineno)?; self.extended("text-seek", buf.as_slice())?; Ok(()) } pub fn closedir(&self, dir: &Directory) -> Result<()> { let (cmd, data) = self.process(SSH_FXP_CLOSE, &build_handle_only(&dir.0))?; expect_status(cmd, &data) } pub fn readdir(&self, dir: &Directory) -> Result> { let (cmd, data) = self.process(SSH_FXP_READDIR, &build_handle_only(&dir.0))?; expect_readdir(cmd, &data) } } #[cfg(feature = "ssh2")] impl TryFrom for SftpClient { type Error = std::io::Error; fn try_from(mut channel: ssh2::Channel) -> std::result::Result { channel.subsystem("sftp").map_err(std::io::Error::other)?; SftpClient::new(channel) } } #[cfg(test)] mod tests { use super::*; use crate::protocol::{ read_raw_packet, split_request_id, write_raw_packet, SSH_FXP_ATTRS, SSH_FXP_CLOSE, SSH_FXP_DATA, SSH_FXP_EXTENDED, SSH_FXP_EXTENDED_REPLY, SSH_FXP_FSETSTAT, SSH_FXP_FSTAT, SSH_FXP_HANDLE, SSH_FXP_INIT, SSH_FXP_LSTAT, SSH_FXP_MKDIR, SSH_FXP_NAME, SSH_FXP_OPEN, SSH_FXP_OPENDIR, SSH_FXP_READ, SSH_FXP_READDIR, SSH_FXP_READLINK, SSH_FXP_REALPATH, SSH_FXP_REMOVE, SSH_FXP_RENAME, SSH_FXP_RMDIR, SSH_FXP_SETSTAT, SSH_FXP_STAT, SSH_FXP_STATUS, SSH_FXP_SYMLINK, SSH_FXP_VERSION, SSH_FXP_WRITE, SSH_FX_EOF, SSH_FX_NO_SUCH_FILE, SSH_FX_OK, }; use std::net::{TcpListener, TcpStream}; use std::thread::JoinHandle; /// Start a background thread that acts as a stub SFTP server on a loopback /// socket. Returns a connected client-side TcpStream plus the server's join /// handle. The handler is called with `(cmd, body_without_req_id)` and must /// return `(response_cmd, response_body_without_req_id)`. fn spawn_stub(mut handler: F) -> (TcpStream, JoinHandle<()>) where F: FnMut(u8, &[u8]) -> (u8, Vec) + Send + 'static, { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = listener.local_addr().unwrap(); let server = std::thread::spawn(move || { let (mut srv, _) = listener.accept().unwrap(); // INIT → VERSION handshake. let (kind, _) = read_raw_packet(&mut srv).unwrap(); assert_eq!(kind, SSH_FXP_INIT); write_raw_packet(&mut srv, SSH_FXP_VERSION, &3u32.to_be_bytes()).unwrap(); loop { let (cmd, body) = match read_raw_packet(&mut srv) { Ok(p) => p, Err(_) => return, }; let (req_id, payload) = split_request_id(&body).unwrap(); let (resp_cmd, resp_body) = handler(cmd, payload); let mut wire = req_id.to_be_bytes().to_vec(); wire.extend_from_slice(&resp_body); if write_raw_packet(&mut srv, resp_cmd, &wire).is_err() { return; } } }); let client = TcpStream::connect(addr).unwrap(); // Disable Nagle so small test packets flush promptly. client.set_nodelay(true).unwrap(); (client, server) } fn ok_status() -> (u8, Vec) { let mut body = Vec::new(); body.extend_from_slice(&SSH_FX_OK.to_be_bytes()); body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&0u32.to_be_bytes()); (SSH_FXP_STATUS, body) } fn err_status(code: u32) -> (u8, Vec) { let mut body = Vec::new(); body.extend_from_slice(&code.to_be_bytes()); body.extend_from_slice(&0u32.to_be_bytes()); body.extend_from_slice(&0u32.to_be_bytes()); (SSH_FXP_STATUS, body) } fn handle_body(handle: &[u8]) -> (u8, Vec) { let mut body = Vec::with_capacity(4 + handle.len()); body.extend_from_slice(&(handle.len() as u32).to_be_bytes()); body.extend_from_slice(handle); (SSH_FXP_HANDLE, body) } fn attrs_body(a: &Attributes) -> (u8, Vec) { (SSH_FXP_ATTRS, a.serialize().unwrap()) } fn data_body(payload: &[u8]) -> (u8, Vec) { let mut body = Vec::with_capacity(4 + payload.len()); body.extend_from_slice(&(payload.len() as u32).to_be_bytes()); body.extend_from_slice(payload); (SSH_FXP_DATA, body) } fn name_body(entries: &[(&str, Attributes)]) -> (u8, Vec) { let mut body = Vec::new(); body.extend_from_slice(&(entries.len() as u32).to_be_bytes()); for (name, attrs) in entries { body.extend_from_slice(&(name.len() as u32).to_be_bytes()); body.extend_from_slice(name.as_bytes()); body.extend_from_slice(&attrs.serialize().unwrap()); } (SSH_FXP_NAME, body) } fn readdir_body(entries: &[(&str, &str, Attributes)]) -> (u8, Vec) { let mut body = Vec::new(); body.extend_from_slice(&(entries.len() as u32).to_be_bytes()); for (name, long, attrs) in entries { body.extend_from_slice(&(name.len() as u32).to_be_bytes()); body.extend_from_slice(name.as_bytes()); body.extend_from_slice(&(long.len() as u32).to_be_bytes()); body.extend_from_slice(long.as_bytes()); body.extend_from_slice(&attrs.serialize().unwrap()); } (SSH_FXP_NAME, body) } fn client(client_io: TcpStream) -> SftpClient { SftpClient::new(client_io).unwrap() } #[test] fn sync_handshake_reads_version() { let (io, srv) = spawn_stub(|_, _| ok_status()); let c = client(io); assert_eq!(c.version(), 3); drop(c); let _ = srv.join(); } #[test] fn sync_version_helper() { // A handshake where the server also advertises extensions. let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = listener.local_addr().unwrap(); let server = std::thread::spawn(move || { let (mut srv, _) = listener.accept().unwrap(); let (kind, _) = read_raw_packet(&mut srv).unwrap(); assert_eq!(kind, SSH_FXP_INIT); let mut body = 3u32.to_be_bytes().to_vec(); for (k, v) in [("ext1", "v1"), ("ext2", "v2")] { body.extend_from_slice(&(k.len() as u32).to_be_bytes()); body.extend_from_slice(k.as_bytes()); body.extend_from_slice(&(v.len() as u32).to_be_bytes()); body.extend_from_slice(v.as_bytes()); } write_raw_packet(&mut srv, SSH_FXP_VERSION, &body).unwrap(); }); let io = TcpStream::connect(addr).unwrap(); let c = SftpClient::new(io).unwrap(); assert_eq!(c.version(), 3); assert_eq!(c.extensions().len(), 2); assert_eq!(c.extensions()[0], ("ext1".to_string(), "v1".to_string())); server.join().unwrap(); } #[test] fn sync_init_fails_on_wrong_response() { let listener = TcpListener::bind("127.0.0.1:0").unwrap(); let addr = listener.local_addr().unwrap(); std::thread::spawn(move || { let (mut srv, _) = listener.accept().unwrap(); let _ = read_raw_packet(&mut srv); // Wrong reply kind. let _ = write_raw_packet(&mut srv, SSH_FXP_STATUS, &[]); }); let io = TcpStream::connect(addr).unwrap(); assert!(SftpClient::new(io).is_err()); } #[test] fn sync_open_returns_handle() { let (io, srv) = spawn_stub(|cmd, _| { assert_eq!(cmd, SSH_FXP_OPEN); handle_body(b"HSYNC") }); let c = client(io); let f = c .open("/x", OpenOptions::new().read(true), &Attributes::new()) .unwrap(); assert_eq!(f.0, b"HSYNC".to_vec()); drop(c); let _ = srv.join(); } #[test] fn sync_open_propagates_no_such_file() { let (io, srv) = spawn_stub(|cmd, _| { assert_eq!(cmd, SSH_FXP_OPEN); err_status(SSH_FX_NO_SUCH_FILE) }); let c = client(io); match c.open("/x", OpenOptions::new().read(true), &Attributes::new()) { Err(Error::NoSuchFile(_, _)) => {} other => panic!("expected NoSuchFile, got {:?}", other), } drop(c); let _ = srv.join(); } #[test] fn sync_stat_returns_attributes() { let mut a = Attributes::new(); a.size = Some(42); let a_clone = a.clone(); let (io, srv) = spawn_stub(move |cmd, _| { assert_eq!(cmd, SSH_FXP_STAT); attrs_body(&a_clone) }); let c = client(io); assert_eq!(c.stat("/x", None).unwrap(), a); drop(c); let _ = srv.join(); } #[test] fn sync_lstat_and_fstat() { let (io, srv) = spawn_stub(|cmd, _| match cmd { SSH_FXP_LSTAT | SSH_FXP_FSTAT => attrs_body(&Attributes::new()), other => panic!("unexpected cmd {}", other), }); let c = client(io); c.lstat("/x", None).unwrap(); c.fstat(&File(b"h".to_vec()), None).unwrap(); drop(c); let _ = srv.join(); } #[test] fn sync_pread_returns_data() { let (io, srv) = spawn_stub(|cmd, _| { assert_eq!(cmd, SSH_FXP_READ); data_body(b"abcd") }); let c = client(io); let data = c.pread(&File(b"h".to_vec()), 0, 4).unwrap(); assert_eq!(data, b"abcd".to_vec()); drop(c); let _ = srv.join(); } #[test] fn sync_pread_eof_surfaces() { let (io, srv) = spawn_stub(|cmd, _| { assert_eq!(cmd, SSH_FXP_READ); err_status(SSH_FX_EOF) }); let c = client(io); match c.pread(&File(b"h".to_vec()), 0, 4) { Err(Error::Eof(_, _)) => {} other => panic!("expected Eof, got {:?}", other), } drop(c); let _ = srv.join(); } #[test] fn sync_pwrite_ok() { let (io, srv) = spawn_stub(|cmd, _| { assert_eq!(cmd, SSH_FXP_WRITE); ok_status() }); let c = client(io); c.pwrite(&File(b"h".to_vec()), 0, b"xyz").unwrap(); drop(c); let _ = srv.join(); } #[test] fn sync_opendir_readdir() { let (io, srv) = spawn_stub(|cmd, _| match cmd { SSH_FXP_OPENDIR => handle_body(b"D"), SSH_FXP_READDIR => readdir_body(&[ ("a", "-rw-r--r-- a", Attributes::new()), ("b", "-rw-r--r-- b", Attributes::new()), ]), other => panic!("unexpected cmd {}", other), }); let c = client(io); let dir = c.opendir("/d").unwrap(); let entries = c.readdir(&dir).unwrap(); assert_eq!(entries.len(), 2); assert_eq!(entries[0].0, "a"); drop(c); let _ = srv.join(); } #[test] fn sync_realpath_and_readlink() { let (io, srv) = spawn_stub(|cmd, _| match cmd { SSH_FXP_REALPATH => name_body(&[("/abs", Attributes::new())]), SSH_FXP_READLINK => name_body(&[("/tgt", Attributes::new())]), other => panic!("unexpected cmd {}", other), }); let c = client(io); assert_eq!(c.realpath(".", None, None).unwrap(), "/abs"); assert_eq!(c.readlink("/l").unwrap(), "/tgt"); drop(c); let _ = srv.join(); } #[test] fn sync_status_mutators() { let (io, srv) = spawn_stub(|cmd, _| match cmd { SSH_FXP_MKDIR | SSH_FXP_RMDIR | SSH_FXP_REMOVE | SSH_FXP_RENAME | SSH_FXP_SYMLINK | SSH_FXP_SETSTAT | SSH_FXP_FSETSTAT | SSH_FXP_CLOSE => ok_status(), other => panic!("unexpected cmd {}", other), }); let c = client(io); let attrs = Attributes::new(); let h = File(b"h".to_vec()); let d = Directory(b"d".to_vec()); c.mkdir("/a", &attrs).unwrap(); c.rmdir("/a").unwrap(); c.remove("/a").unwrap(); c.rename("/a", "/b", None).unwrap(); c.symlink("/b", "/a").unwrap(); c.setstat("/a", &attrs).unwrap(); c.fsetstat(&h, &attrs).unwrap(); c.fclose(&h).unwrap(); c.closedir(&d).unwrap(); drop(c); let _ = srv.join(); } #[test] fn sync_extended_payload_and_none() { // Two consecutive extended calls: first with a reply, second with OK // status to exercise the `None` branch. let mut call = 0; let (io, srv) = spawn_stub(move |cmd, _| { assert_eq!(cmd, SSH_FXP_EXTENDED); call += 1; if call == 1 { (SSH_FXP_EXTENDED_REPLY, b"P".to_vec()) } else { ok_status() } }); let c = client(io); assert_eq!(c.extended("x", b"").unwrap(), Some(b"P".to_vec())); assert_eq!(c.extended("x", b"").unwrap(), None); drop(c); let _ = srv.join(); } }