openssh-sftp-client-lowlevel-0.7.0/.cargo_vcs_info.json0000644000000001720000000000100165740ustar { "git": { "sha1": "6995b4a48ed0913cda1c9cb76df271db21f6e0bd" }, "path_in_vcs": "openssh-sftp-client-lowlevel" }openssh-sftp-client-lowlevel-0.7.0/CHANGELOG.md000064400000000000000000000032651046102023000172030ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.7.0](https://github.com/openssh-rust/openssh-sftp-client/compare/openssh-sftp-client-lowlevel-v0.6.0...openssh-sftp-client-lowlevel-v0.7.0) - 2024-08-10 ### Other - updated the following local packages: openssh-sftp-error ## [0.14.6](https://github.com/openssh-rust/openssh-sftp-client/compare/openssh-sftp-client-v0.14.5...openssh-sftp-client-v0.14.6) - 2024-07-25 ### Other - Fix panic when flush_interval is set to 0 ([#136](https://github.com/openssh-rust/openssh-sftp-client/pull/136)) ## [0.14.5](https://github.com/openssh-rust/openssh-sftp-client/compare/openssh-sftp-client-v0.14.4...openssh-sftp-client-v0.14.5) - 2024-07-11 ### Other - Implement `Sftp::from_clonable_session*` ([#131](https://github.com/openssh-rust/openssh-sftp-client/pull/131)) ## [0.14.4](https://github.com/openssh-rust/openssh-sftp-client/compare/openssh-sftp-client-v0.14.3...openssh-sftp-client-v0.14.4) - 2024-06-27 ### Other - Run rust.yml on merge_queue ([#128](https://github.com/openssh-rust/openssh-sftp-client/pull/128)) - Impl `Default` for `Permissions` ([#126](https://github.com/openssh-rust/openssh-sftp-client/pull/126)) - Use release-plz in publish.yml ([#125](https://github.com/openssh-rust/openssh-sftp-client/pull/125)) - Support setting time in MetaDataBuilder ([#124](https://github.com/openssh-rust/openssh-sftp-client/pull/124)) The changelog for this crate is kept in the project's Rust documentation in the changelog module. openssh-sftp-client-lowlevel-0.7.0/Cargo.toml0000644000000035300000000000100145730ustar # 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 = "2018" name = "openssh-sftp-client-lowlevel" version = "0.7.0" authors = ["Jiahao XU "] build = false autobins = false autoexamples = false autotests = false autobenches = false description = "Lowlevel utilities to communicate with openssh sftp server." readme = false keywords = [ "ssh", "multiplex", "async", "network", "sftp", ] categories = [ "asynchronous", "network-programming", "api-bindings", ] license = "MIT" repository = "https://github.com/openssh-rust/openssh-sftp-client" resolver = "2" [lib] name = "openssh_sftp_client_lowlevel" path = "src/lib.rs" [[test]] name = "lowlevel" path = "tests/lowlevel.rs" [[test]] name = "queue" path = "tests/queue.rs" [dependencies.awaitable] version = "0.4.0" [dependencies.bytes] version = "1.2.1" [dependencies.concurrent_arena] version = "0.1.7" [dependencies.derive_destructure2] version = "0.1.0" [dependencies.openssh-sftp-error] version = "0.5.0" [dependencies.openssh-sftp-protocol] version = "0.24.0" features = ["bytes"] [dependencies.pin-project] version = "1.0.10" [dependencies.tokio] version = "1.11.0" features = [ "io-util", "sync", ] [dependencies.tokio-io-utility] version = "0.7.1" features = ["read-exact-to-bytes"] [dev-dependencies.tempfile] version = "3.1.0" [dev-dependencies.tokio] version = "1.11.0" features = [ "rt", "macros", ] openssh-sftp-client-lowlevel-0.7.0/Cargo.toml.orig000064400000000000000000000017371046102023000202630ustar 00000000000000[package] name = "openssh-sftp-client-lowlevel" version = "0.7.0" edition = "2018" authors = ["Jiahao XU "] license = "MIT" description = "Lowlevel utilities to communicate with openssh sftp server." repository = "https://github.com/openssh-rust/openssh-sftp-client" keywords = ["ssh", "multiplex", "async", "network", "sftp"] categories = ["asynchronous", "network-programming", "api-bindings"] [dependencies] awaitable = "0.4.0" openssh-sftp-protocol = { version = "0.24.0", features = ["bytes"] } openssh-sftp-error = { version = "0.5.0", path = "../openssh-sftp-error" } concurrent_arena = "0.1.7" derive_destructure2 = "0.1.0" tokio = { version = "1.11.0", features = ["io-util", "sync"] } bytes = "1.2.1" tokio-io-utility = { version = "0.7.1", features = ["read-exact-to-bytes"] } pin-project = "1.0.10" [dev-dependencies] tokio = { version = "1.11.0", features = ["rt", "macros"] } tempfile = "3.1.0" sftp-test-common = { path = "../sftp-test-common" } openssh-sftp-client-lowlevel-0.7.0/LICENSE000064400000000000000000000020521046102023000163700ustar 00000000000000MIT License Copyright (c) 2021 Jiahao XU Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. openssh-sftp-client-lowlevel-0.7.0/run_tests.sh000075500000000000000000000002421046102023000177470ustar 00000000000000#!/bin/bash set -euxo pipefail cd "$(dirname "$(realpath "$0")")" export RUNTIME_DIR=${XDG_RUNTIME_DIR:-/tmp} exec cargo test --all-features --workspace "$@" openssh-sftp-client-lowlevel-0.7.0/src/awaitable_responses.rs000064400000000000000000000046241046102023000225610ustar 00000000000000#![forbid(unsafe_code)] use super::Error; use concurrent_arena::Arena; use derive_destructure2::destructure; use openssh_sftp_protocol::response::ResponseInner; #[derive(Debug)] pub(crate) enum Response { Header(ResponseInner), /// The buffer that stores the response of Read. /// /// It will be returned if you provided a buffer to /// [`crate::lowlevel::WriteEnd::send_read_request`]. Buffer(Buffer), /// This is a fallback that is returned /// if `Buffer` isn't provided or it isn't large enough. AllocatedBox(Box<[u8]>), /// Extended reply ExtendedReply(Box<[u8]>), } pub(crate) type Awaitable = awaitable::Awaitable>; /// BITARRAY_LEN must be LEN / usize::BITS and LEN must be divisble by usize::BITS. const BITARRAY_LEN: usize = 2; const LEN: usize = 128; pub(crate) type ArenaArc = concurrent_arena::ArenaArc, BITARRAY_LEN, LEN>; /// Check `concurrent_arena::Arena` for `BITARRAY_LEN` and `LEN`. #[derive(Debug)] #[repr(transparent)] pub(crate) struct AwaitableResponses(Arena, BITARRAY_LEN, LEN>); impl AwaitableResponses { #[inline(always)] pub(crate) fn new() -> Self { Self(Arena::with_capacity(3)) } /// Return (slot_id, awaitable_response) pub(crate) fn insert(&self) -> Id { Id::new(self.0.insert(Awaitable::new())) } #[inline] pub(crate) fn try_reserve(&self, new_id_cnt: u32) -> bool { self.0.try_reserve(new_id_cnt / (LEN as u32)) } #[inline] pub(crate) fn reserve(&self, new_id_cnt: u32) { self.0.reserve(new_id_cnt / (LEN as u32)); } #[inline] pub(crate) fn get(&self, slot: u32) -> Result, Error> { self.0 .get(slot) .ok_or(Error::InvalidResponseId { response_id: slot }) } } /// Request Id #[repr(transparent)] #[derive(Debug, destructure)] pub struct Id(pub(crate) ArenaArc); impl Id { #[inline(always)] pub(crate) fn new(arc: ArenaArc) -> Self { Id(arc) } #[inline(always)] pub(crate) fn into_inner(self) -> ArenaArc { self.destructure().0 } } impl Drop for Id { #[inline(always)] fn drop(&mut self) { ArenaArc::remove(&self.0); } } openssh-sftp-client-lowlevel-0.7.0/src/awaitables.rs000064400000000000000000000205061046102023000206400ustar 00000000000000#![forbid(unsafe_code)] use super::*; use awaitable_responses::{ArenaArc, Response}; use std::{ future::Future, mem::replace, path::Path, pin::Pin, task::{Context, Poll}, }; use derive_destructure2::destructure; use openssh_sftp_protocol::{ file_attrs::FileAttrs, response::{NameEntry, ResponseInner, StatusCode}, ssh_format, HandleOwned, }; /// The data returned by [`WriteEnd::send_read_request`]. #[derive(Debug, Clone)] pub enum Data { /// The buffer that stores the response of Read. /// /// It will be returned if you provided a buffer to /// [`crate::WriteEnd::send_read_request`]. Buffer(Buffer), /// This is a fallback that is returned /// if `Buffer` isn't provided or it isn't large enough. AllocatedBox(Box<[u8]>), /// EOF is reached before any data can be read. Eof, } type AwaitableInnerRes = (Id, Response); #[repr(transparent)] #[derive(Debug)] struct AwaitableInnerFuture(Option>); impl AwaitableInnerFuture { fn new(awaitable_inner: AwaitableInner) -> Self { Self(Some(awaitable_inner)) } fn poll(&mut self, cx: &mut Context<'_>) -> Poll, Error>> { let errmsg = "AwaitableInnerFuture::poll is called after completed"; let waker = cx.waker().clone(); let res = self .0 .as_ref() .expect(errmsg) .0 .install_waker(waker) .expect("AwaitableResponse should either in state Ongoing or Done"); if !res { return Poll::Pending; } let awaitable = self.0.take().expect(errmsg); let response = awaitable .0 .take_output() .expect("The request should be done by now"); // Reconstruct Id here so that it will be automatically // released on error. let id = Id::new(awaitable.destructure().0); // Propagate failure Poll::Ready(match response { Response::Header(ResponseInner::Status { status_code: StatusCode::Failure(err_code), err_msg, }) => Err(Error::SftpError(err_code, err_msg)), response => Ok((id, response)), }) } } /// Provides drop impl /// /// Store `ArenaArc` instead of `Id` or `IdInner` to have more control /// over removal of `ArenaArc`. #[repr(transparent)] #[derive(Debug, destructure)] struct AwaitableInner(ArenaArc); impl Drop for AwaitableInner { fn drop(&mut self) { // Remove ArenaArc only if the `AwaitableResponse` is done. // // If the ArenaArc is in `Consumed` state, then the user cannot have the future // cancelled unless they played with fire (`unsafe`). if self.0.is_done() { ArenaArc::remove(&self.0); } } } macro_rules! def_awaitable { ($name:ident, $future_name:ident, $res:ty, | $response_name:ident | $post_processing:block) => { /// Return (id, res). /// /// id can be reused in the next request. /// /// # Cancel Safety /// /// It is perfectly safe to cancel the future. #[repr(transparent)] #[derive(Debug)] pub struct $future_name(AwaitableInnerFuture); impl Future for $future_name { type Output = Result<(Id, $res), Error>; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let post_processing = |$response_name: Response| $post_processing; self.0.poll(cx).map(|res| { let (id, response) = res?; Ok((id, post_processing(response)?)) }) } } /// Awaitable /// /// You must call `wait()` and poll the return future to end, otherwise /// the request/response id might be dropped early and /// [`ReadEnd::read_in_one_packet_pinned`] or /// [`ReadEnd::read_in_one_packet`] might fail due to unexpected /// response id. /// /// Alternatively, you can choose to ignore these errors, but it's not /// recommended. #[repr(transparent)] #[derive(Debug)] pub struct $name(AwaitableInner); impl $name { #[inline(always)] pub(crate) fn new(arc: ArenaArc) -> Self { Self(AwaitableInner(arc)) } /// Return (id, res). /// /// id can be reused in the next request. /// /// # Cancel Safety /// /// It is perfectly safe to cancel the future. pub fn wait(self) -> $future_name { $future_name(AwaitableInnerFuture::new(self.0)) } } }; } def_awaitable!(AwaitableStatus, AwaitableStatusFuture, (), |response| { match response { Response::Header(ResponseInner::Status { status_code: StatusCode::Success, .. }) => Ok(()), _ => Err(Error::InvalidResponse(&"Expected Status response")), } }); def_awaitable!( AwaitableHandle, AwaitableHandleFuture, HandleOwned, |response| { match response { Response::Header(ResponseInner::Handle(handle)) => { if handle.into_inner().len() > 256 { Err(Error::HandleTooLong) } else { Ok(handle) } } _ => Err(Error::InvalidResponse( &"Expected Handle or err Status response", )), } } ); def_awaitable!( AwaitableData, AwaitableDataFuture, Data, |response| { match response { Response::Buffer(buffer) => Ok(Data::Buffer(buffer)), Response::AllocatedBox(allocated_box) => Ok(Data::AllocatedBox(allocated_box)), Response::Header(ResponseInner::Status { status_code: StatusCode::Eof, .. }) => Ok(Data::Eof), _ => Err(Error::InvalidResponse( &"Expected Buffer/AllocatedBox response", )), } } ); def_awaitable!( AwaitableNameEntries, AwaitableNameEntriesFuture, Box<[NameEntry]>, |response| { match response { Response::Header(response_inner) => match response_inner { ResponseInner::Name(name) => Ok(name), ResponseInner::Status { status_code: StatusCode::Eof, .. } => Ok(Vec::new().into_boxed_slice()), _ => Err(Error::InvalidResponse( &"Expected Name or err Status response", )), }, _ => Err(Error::InvalidResponse( &"Expected Name or err Status response", )), } } ); def_awaitable!( AwaitableAttrs, AwaitableAttrsFuture, FileAttrs, |response| { match response { Response::Header(ResponseInner::Attrs(attrs)) => Ok(attrs), _ => Err(Error::InvalidResponse( &"Expected Attrs or err Status response", )), } } ); def_awaitable!(AwaitableName, AwaitableNameFuture, Box, |response| { match response { Response::Header(ResponseInner::Name(mut names)) => { if names.len() != 1 { Err(Error::InvalidResponse( &"Got expected Name response, but it does not have exactly \ one and only one entry", )) } else { let name = &mut names[0]; Ok(replace(&mut name.filename, Path::new("").into())) } } _ => Err(Error::InvalidResponse( &"Expected Name or err Status response", )), } }); def_awaitable!(AwaitableLimits, AwaitableLimitsFuture, Limits, |response| { match response { Response::ExtendedReply(boxed) => Ok(ssh_format::from_bytes(&boxed)?.0), _ => Err(Error::InvalidResponse(&"Expected extended reply response")), } }); openssh-sftp-client-lowlevel-0.7.0/src/buffer.rs000064400000000000000000000020071046102023000177710ustar 00000000000000#![forbid(unsafe_code)] use bytes::BytesMut; /// Any type that can act as a buffer. pub trait ToBuffer { /// Returned the buffer. fn get_buffer(&mut self) -> Buffer<'_>; } /// Buffer that can be used to write data into. #[derive(Debug)] pub enum Buffer<'a> { /// A `Vec` acts as a buffer. Vector(&'a mut Vec), /// A byte slice acts as a buffer. Slice(&'a mut [u8]), /// A `BytesMut` acts as a buffer. Bytes(&'a mut BytesMut), } impl ToBuffer for Vec { #[inline(always)] fn get_buffer(&mut self) -> Buffer<'_> { Buffer::Vector(self) } } impl ToBuffer for BytesMut { #[inline(always)] fn get_buffer(&mut self) -> Buffer<'_> { Buffer::Bytes(self) } } impl ToBuffer for Box<[u8]> { #[inline(always)] fn get_buffer(&mut self) -> Buffer<'_> { Buffer::Slice(&mut *self) } } impl ToBuffer for [u8; LEN] { #[inline(always)] fn get_buffer(&mut self) -> Buffer<'_> { Buffer::Slice(self) } } openssh-sftp-client-lowlevel-0.7.0/src/changelog.rs000064400000000000000000000052171046102023000204550ustar 00000000000000#[allow(unused_imports)] use crate::*; #[doc(hidden)] pub mod unreleased {} /// # Changed /// - Bump dependency [`openssh-sftp-error`] to v0.4.0. pub mod v0_6_0 {} /// ## Changed /// - Fix: Leave error of exceeding buffer len in `ReaderBuffered::consume` to handle by `BytesMut` pub mod v0_5_1 {} /// ## Changed /// - Make `openssh_sftp_client_lowlevel::connect` regular fn /// - Make `WriteEnd::send_hello` regular fn pub mod v0_5_0 {} /// ## Other /// - Bump [`openssh-sftp-protocol`] to v0.24.0 pub mod v0_4_1 {} /// ## Changed /// - Fix [`openssh-sftp-error`]: Ensure stable api (#49) /// - Create newtype RecursiveError: Impls error::Error (#52) /// /// ## Other /// - Bump [`openssh-sftp-protocol`] to v0.23.0 /// - Bump dep awaitable to v0.4.0 (#48) pub mod v0_4_0 {} /// ## Internal /// - Rm WriteBuffer: ssh_format now supports BytesMut as SerOutput /// - Enable feature "bytes" of dep [`openssh-sftp-error`] which /// enables "bytes" of dep `ssh_format`. /// /// ## Other /// - Bump [`openssh-sftp-protocol`] to v0.22.1 pub mod v0_3_1 {} /// ## Other /// - Bump [`openssh-sftp-protocol`] to v0.22.0 pub mod v0_3_0 {} /// ## Added /// - new trait [`Queue`] /// - [`ReadEnd::new`] is now public /// /// ## Changed /// - [`connect`] now takes `queue` instead of `write_end_buffer_size` /// - [`connect`] does not take `reader` and `reader_buffer_len` and it /// does not return [`ReadEnd`] anymore. /// /// User has to manually call [`ReadEnd::new`] to create [`ReadEnd`]. /// /// This is done to give the user more freedom on how and when [`ReadEnd`] /// is created. /// - [`ReadEnd`], [`WriteEnd`] and [`SharedData`] now takes an additional generic /// parameter `Q`. pub mod v0_2_0 {} /// This is the first release! /// /// This crate has been extracted out from /// [openssh-sftp-client](https://docs.rs/openssh-sftp-client). /// /// # Changes from v0.10.2 of `openssh_sftp_client::lowlevel`: /// /// ## Added /// - `ReadEnd::receive_server_hello` /// - `ReadEnd::receive_server_hello_pinned` /// - `ReadEnd::read_in_one_packet_pinned` /// - `ReadEnd::ready_for_read_pinned` /// /// ## Changed /// /// - `lowlevel::WriteEnd` now does not require `W` /// - `lowlevel::ReadEnd` now does not require `W` /// - `lowlevel::SharedData` now does not require `W` /// - `lowlevel::connect` removed parameter `writer` and generic paramter `W`, /// it now also requires user to call `ReadEnd::receive_server_hello` /// and flush the buffer themselves. /// /// ## Removed /// - `SharedData::get_auxiliary_mut` /// - `SharedData::strong_count` /// - `ReadEnd::wait_for_new_request` /// - `lowlevel::connect_with_auxiliary` pub mod v0_1_0 {} openssh-sftp-client-lowlevel-0.7.0/src/connection.rs000064400000000000000000000060531046102023000206640ustar 00000000000000#![forbid(unsafe_code)] use super::{awaitable_responses::AwaitableResponses, *}; use std::{fmt, sync::Arc}; use openssh_sftp_protocol::constants::SSH2_FILEXFER_VERSION; // TODO: // - Support for zero copy syscalls #[derive(Debug)] struct SharedDataInner { queue: Q, responses: AwaitableResponses, auxiliary: Auxiliary, } /// SharedData contains both the writer and the responses because: /// - The overhead of `Arc` and a separate allocation; /// - If the write end of a connection is closed, then openssh implementation /// of sftp-server would close the read end right away, discarding /// any unsent but processed or unprocessed responses. #[derive(Debug)] pub struct SharedData(Arc>); impl fmt::Pointer for SharedData { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Pointer::fmt(&self.0, f) } } impl Clone for SharedData { fn clone(&self) -> Self { Self(self.0.clone()) } } impl SharedData { fn new(queue: Q, auxiliary: Auxiliary) -> Self { SharedData(Arc::new(SharedDataInner { responses: AwaitableResponses::new(), queue, auxiliary, })) } } impl SharedData { pub fn queue(&self) -> &Q { &self.0.queue } pub(crate) fn responses(&self) -> &AwaitableResponses { &self.0.responses } /// Returned the auxiliary data. pub fn get_auxiliary(&self) -> &Auxiliary { &self.0.auxiliary } } impl SharedData { /// Create a useable response id. #[inline(always)] pub fn create_response_id(&self) -> Id { self.responses().insert() } /// Return true if reserve succeeds, false otherwise. #[inline(always)] pub fn try_reserve_id(&self, new_id_cnt: u32) -> bool { self.responses().try_reserve(new_id_cnt) } /// Return true if reserve succeeds, false otherwise. #[inline(always)] pub fn reserve_id(&self, new_id_cnt: u32) { self.responses().reserve(new_id_cnt); } } /// Initialize connection to remote sftp server and /// negotiate the sftp version. /// /// User of this function must manually create [`ReadEnd`] /// and manually flush the buffer. /// /// # Cancel Safety /// /// This function is not cancel safe. /// /// After dropping the future, the connection would be in a undefined state. pub fn connect( queue: Q, auxiliary: Auxiliary, ) -> Result, Error> where Buffer: ToBuffer + Send + Sync + 'static, Q: Queue, { let shared_data = SharedData::new(queue, auxiliary); // Send hello message let mut write_end = WriteEnd::new(shared_data); write_end.send_hello(SSH2_FILEXFER_VERSION)?; Ok(write_end) } openssh-sftp-client-lowlevel-0.7.0/src/lib.rs000064400000000000000000000051051046102023000172700ustar 00000000000000//! This crate provides a set of APIs to access the remote filesystem using //! the sftp protocol and is implemented in pure Rust. //! //! It supports sending multiple requests concurrently using [`WriteEnd`] //! (it can be [`WriteEnd::clone`]d), however receiving responses have to be done //! sequentially using [`ReadEnd::read_in_one_packet`]. //! //! To create [`WriteEnd`] and [`ReadEnd`], simply pass the `stdin` and `stdout` of //! the `sftp-server` launched at remote to [`connect`]. //! //! This crate supports all operations supported by sftp v3, in additional to //! the following extensions: //! - [`WriteEnd::send_limits_request`] //! - [`WriteEnd::send_expand_path_request`] //! - [`WriteEnd::send_fsync_request`] //! - [`WriteEnd::send_hardlink_request`] //! - [`WriteEnd::send_posix_rename_request`] pub use openssh_sftp_error::{Error, SftpErrMsg, SftpErrorKind, UnixTimeStampError}; pub use openssh_sftp_protocol::{ file_attrs::{FileAttrs, FileType, Permissions, UnixTimeStamp}, open_options::{CreateFlags, OpenOptions}, request::OpenFileRequest, response::{Extensions, Limits, NameEntry}, Handle, HandleOwned, }; /// Default size of buffer for up/download in openssh-portable pub const OPENSSH_PORTABLE_DEFAULT_COPY_BUFLEN: usize = 32768; /// Default number of concurrent outstanding requests in openssh-portable pub const OPENSSH_PORTABLE_DEFAULT_NUM_REQUESTS: usize = 64; /// Minimum amount of data to read at a time in openssh-portable pub const OPENSSH_PORTABLE_MIN_READ_SIZE: usize = 512; /// Maximum depth to descend in directory trees in openssh-portable pub const OPENSSH_PORTABLE_MAX_DIR_DEPTH: usize = 64; /// Default length of download buffer in openssh-portable pub const OPENSSH_PORTABLE_DEFAULT_DOWNLOAD_BUFLEN: usize = 20480; /// Default length of upload buffer in openssh-portable pub const OPENSSH_PORTABLE_DEFAULT_UPLOAD_BUFLEN: usize = 20480; #[cfg(doc)] /// Changelog for this crate. pub mod changelog; mod awaitable_responses; pub use awaitable_responses::Id; mod awaitables; pub use awaitables::{ AwaitableAttrs, AwaitableAttrsFuture, AwaitableData, AwaitableDataFuture, AwaitableHandle, AwaitableHandleFuture, AwaitableLimits, AwaitableLimitsFuture, AwaitableName, AwaitableNameEntries, AwaitableNameEntriesFuture, AwaitableNameFuture, AwaitableStatus, AwaitableStatusFuture, Data, }; mod buffer; pub use buffer::{Buffer, ToBuffer}; mod connection; pub use connection::{connect, SharedData}; mod queue; pub use queue::Queue; mod read_end; pub use read_end::ReadEnd; mod reader_buffered; mod write_end; pub use write_end::WriteEnd; openssh-sftp-client-lowlevel-0.7.0/src/queue.rs000064400000000000000000000003031046102023000176410ustar 00000000000000use bytes::Bytes; pub trait Queue { /// Push one `bytes`. fn push(&self, bytes: Bytes); /// Push multiple bytes atomically fn extend(&self, header: Bytes, body: &[&[Bytes]]); } openssh-sftp-client-lowlevel-0.7.0/src/read_end.rs000064400000000000000000000266171046102023000202760ustar 00000000000000#![forbid(unsafe_code)] use super::{ awaitable_responses::ArenaArc, awaitable_responses::Response, connection::SharedData, reader_buffered::ReaderBuffered, Error, Extensions, ToBuffer, }; use std::{io, num::NonZeroUsize, pin::Pin}; use openssh_sftp_error::RecursiveError; use openssh_sftp_protocol::{ constants::SSH2_FILEXFER_VERSION, response::{self, ServerVersion}, serde::de::DeserializeOwned, ssh_format::{self, from_bytes}, }; use pin_project::pin_project; use tokio::io::{copy_buf, sink, AsyncBufReadExt, AsyncRead, AsyncReadExt}; use tokio_io_utility::{read_exact_to_bytes, read_exact_to_vec}; /// The ReadEnd for the lowlevel API. #[derive(Debug)] #[pin_project] pub struct ReadEnd { #[pin] reader: ReaderBuffered, shared_data: SharedData, } impl ReadEnd where R: AsyncRead, Buffer: ToBuffer + 'static + Send + Sync, { /// Must call [`ReadEnd::receive_server_hello_pinned`] /// or [`ReadEnd::receive_server_hello`] after this /// function call. pub fn new( reader: R, reader_buffer_len: NonZeroUsize, shared_data: SharedData, ) -> Self { Self { reader: ReaderBuffered::new(reader, reader_buffer_len), shared_data, } } /// Must be called once right after [`ReadEnd::new`] /// to receive the hello message from the server. pub async fn receive_server_hello_pinned( mut self: Pin<&mut Self>, ) -> Result { // Receive server version let len: u32 = self.as_mut().read_and_deserialize(4).await?; if (len as usize) > 4096 { return Err(Error::SftpServerHelloMsgTooLong { len }); } let drain = self .project() .reader .read_exact_into_buffer(len as usize) .await?; let server_version = ServerVersion::deserialize(&mut ssh_format::Deserializer::from_bytes(&drain))?; if server_version.version != SSH2_FILEXFER_VERSION { Err(Error::UnsupportedSftpProtocol { version: server_version.version, }) } else { Ok(server_version.extensions) } } async fn read_and_deserialize( self: Pin<&mut Self>, size: usize, ) -> Result { let drain = self.project().reader.read_exact_into_buffer(size).await?; Ok(from_bytes(&drain)?.0) } async fn consume_packet(self: Pin<&mut Self>, len: u32, err: Error) -> Result<(), Error> { let reader = self.project().reader; if let Err(consumption_err) = copy_buf(&mut reader.take(len as u64), &mut sink()).await { Err(Error::RecursiveErrors(Box::new(RecursiveError { original_error: err, occuring_error: consumption_err.into(), }))) } else { Err(err) } } async fn read_into_box(self: Pin<&mut Self>, len: usize) -> Result, Error> { let mut vec = Vec::new(); read_exact_to_vec(&mut self.project().reader, &mut vec, len).await?; Ok(vec.into_boxed_slice()) } async fn read_in_data_packet_fallback( self: Pin<&mut Self>, len: usize, ) -> Result, Error> { self.read_into_box(len).await.map(Response::AllocatedBox) } /// * `len` - excludes packet_type and request_id. async fn read_in_data_packet( mut self: Pin<&mut Self>, len: u32, buffer: Option, ) -> Result, Error> { // Since the data is sent as a string, we need to consume the 4-byte length first. self.as_mut() .project() .reader .read_exact_into_buffer(4) .await?; let len = (len - 4) as usize; if let Some(mut buffer) = buffer { match buffer.get_buffer() { super::Buffer::Vector(vec) => { read_exact_to_vec(&mut self.project().reader, vec, len).await?; Ok(Response::Buffer(buffer)) } super::Buffer::Slice(slice) => { if slice.len() >= len { self.project().reader.read_exact(slice).await?; Ok(Response::Buffer(buffer)) } else { self.read_in_data_packet_fallback(len).await } } super::Buffer::Bytes(bytes) => { read_exact_to_bytes(&mut self.project().reader, bytes, len).await?; Ok(Response::Buffer(buffer)) } } } else { self.read_in_data_packet_fallback(len).await } } /// * `len` - includes packet_type and request_id. async fn read_in_packet(self: Pin<&mut Self>, len: u32) -> Result, Error> { let response: response::Response = self.read_and_deserialize(len as usize).await?; Ok(Response::Header(response.response_inner)) } /// * `len` - excludes packet_type and request_id. async fn read_in_extended_reply( self: Pin<&mut Self>, len: u32, ) -> Result, Error> { self.read_into_box(len as usize) .await .map(Response::ExtendedReply) } /// # Restart on Error /// /// Only when the returned error is [`Error::InvalidResponseId`] or /// [`Error::AwaitableError`], can the function be restarted. /// /// Upon other errors [`Error::IOError`], [`Error::FormatError`] and /// [`Error::RecursiveErrors`], the sftp session has to be discarded. /// /// # Example /// /// ```rust,ignore /// let readend = ...; /// loop { /// let new_requests_submit = readend.wait_for_new_request().await; /// if new_requests_submit == 0 { /// break; /// } /// /// // If attempt to read in more than new_requests_submit, then /// // `read_in_one_packet` might block forever. /// for _ in 0..new_requests_submit { /// readend.read_in_one_packet().await.unwrap(); /// } /// } /// ``` /// # Cancel Safety /// /// This function is not cancel safe. /// /// Dropping the future might cause the response packet to be partially read, /// and the next read would treat the partial response as a new response. pub async fn read_in_one_packet_pinned(mut self: Pin<&mut Self>) -> Result<(), Error> { let this = self.as_mut().project(); let drain = this.reader.read_exact_into_buffer(9).await?; let (len, packet_type, response_id): (u32, u8, u32) = from_bytes(&drain)?.0; let len = len - 5; let res = this.shared_data.responses().get(response_id); let callback = match res { Ok(callback) => callback, // Invalid response_id Err(err) => { drop(drain); // Consume the invalid data to return self to a valid state // where read_in_one_packet can be called again. return self.consume_packet(len, err).await; } }; let response = if response::Response::is_data(packet_type) { drop(drain); let buffer = match callback.take_input() { Ok(buffer) => buffer, Err(err) => { // Consume the invalid data to return self to a valid state // where read_in_one_packet can be called again. return self.consume_packet(len, err.into()).await; } }; self.read_in_data_packet(len, buffer).await? } else if response::Response::is_extended_reply(packet_type) { drop(drain); self.read_in_extended_reply(len).await? } else { // Consumes 4 bytes and put back the rest, since // read_in_packet needs the packet_type and response_id. drain.subdrain(4); self.read_in_packet(len + 5).await? }; let res = callback.done(response); // If counter == 2, then it must be one of the following situation: // - `ReadEnd` is the only holder other than the `Arena` itself; // - `ReadEnd` and the `AwaitableInner` is the holder and `AwaitableInner::drop` // has already `ArenaArc::remove`d it. // // In case 1, since there is no `AwaitableInner` holding reference to it, // it can be removed safely. // // In case 2, since it is already removed, remove it again is a no-op. // // NOTE that if the arc is dropped after this call while having the // `Awaitable*::drop` executed before `callback.done`, then the callback // would not be removed. // // Though this kind of situation is rare. if ArenaArc::strong_count(&callback) == 2 { ArenaArc::remove(&callback); } Ok(res?) } /// Wait for next packet to be readable. /// /// Return `Ok(())` if next packet is ready and readable, `Error::IOError(io_error)` /// where `io_error.kind() == ErrorKind::UnexpectedEof` if `EOF` is met. /// /// # Cancel Safety /// /// This function is cancel safe. pub async fn ready_for_read_pinned(self: Pin<&mut Self>) -> Result<(), io::Error> { if self.project().reader.fill_buf().await?.is_empty() { // Empty buffer means EOF Err(io::Error::new(io::ErrorKind::UnexpectedEof, "")) } else { Ok(()) } } } impl ReadEnd where Self: Unpin, R: AsyncRead, Buffer: ToBuffer + 'static + Send + Sync, { /// Must be called once right after [`super::connect`] /// to receive the hello message from the server. pub async fn receive_server_hello(&mut self) -> Result { Pin::new(self).receive_server_hello_pinned().await } /// # Restart on Error /// /// Only when the returned error is [`Error::InvalidResponseId`] or /// [`Error::AwaitableError`], can the function be restarted. /// /// Upon other errors [`Error::IOError`], [`Error::FormatError`] and /// [`Error::RecursiveErrors`], the sftp session has to be discarded. /// /// # Cancel Safety /// /// This function is not cancel safe. /// /// Dropping the future might cause the response packet to be partially read, /// and the next read would treat the partial response as a new response. pub async fn read_in_one_packet(&mut self) -> Result<(), Error> { Pin::new(self).read_in_one_packet_pinned().await } /// Wait for next packet to be readable. /// /// Return `Ok(())` if next packet is ready and readable, `Error::IOError(io_error)` /// where `io_error.kind() == ErrorKind::UnexpectedEof` if `EOF` is met. /// /// # Cancel Safety /// /// This function is cancel safe. pub async fn ready_for_read(&mut self) -> Result<(), io::Error> { Pin::new(self).ready_for_read_pinned().await } } impl ReadEnd { /// Return the [`SharedData`] held by [`ReadEnd`]. pub fn get_shared_data(&self) -> &SharedData { &self.shared_data } } openssh-sftp-client-lowlevel-0.7.0/src/reader_buffered.rs000064400000000000000000000105731046102023000216330ustar 00000000000000use std::{ cmp::min, future::Future, io, num::NonZeroUsize, ops::Deref, pin::Pin, task::{Context, Poll}, }; use bytes::{Buf, BytesMut}; use pin_project::pin_project; use tokio::io::{AsyncBufRead, AsyncRead, ReadBuf}; use tokio_io_utility::{read_exact_to_bytes, read_to_bytes_rng, ready}; #[derive(Debug)] #[pin_project] pub(super) struct ReaderBuffered { #[pin] reader: R, /// Use `BytesMut` here to avoid frequent copy when consuming. /// /// If we use `Vec` here, then every consume would have to /// copy the buffer back to the start, which is very inefficient. /// /// `BytesMut` avoids this as consuming is simply bumping the start /// counter, and the content is still stored continously. buffer: BytesMut, } impl ReaderBuffered { pub(super) fn new(reader: R, reader_buffer_len: NonZeroUsize) -> Self { Self { reader, buffer: BytesMut::with_capacity(reader_buffer_len.get()), } } pub(super) async fn read_exact_into_buffer( self: Pin<&mut Self>, size: usize, ) -> io::Result> { let mut this = self.project(); let n = this.buffer.len(); if n < size { // buffer does not contain enough data, read more let cap = this.buffer.capacity(); if size < cap { read_to_bytes_rng(&mut this.reader, this.buffer, (size - n)..(cap - n)).await?; } else { read_exact_to_bytes(&mut this.reader, this.buffer, size - n).await?; } } Ok(Drain { buffer: this.buffer, n: size, }) } } impl AsyncBufRead for ReaderBuffered { fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let mut this = self.project(); let buffer = &mut this.buffer; let reader = &mut this.reader; // If we've reached the end of our internal buffer then we need to fetch // some more data from the underlying reader. // Branch using `>=` instead of the more correct `==` // to tell the compiler that the pos..cap slice is always valid. if buffer.is_empty() { let cap = buffer.capacity(); let mut future = read_to_bytes_rng(reader, buffer, ..cap); let future = Pin::new(&mut future); match ready!(future.poll(cx)) { Ok(()) => (), Err(error) => match error.kind() { io::ErrorKind::UnexpectedEof => (), _ => return Poll::Ready(Err(error)), }, } } Poll::Ready(Ok(this.buffer)) } fn consume(self: Pin<&mut Self>, amt: usize) { let buffer = &mut self.project().buffer; buffer.advance(amt); } } impl AsyncRead for ReaderBuffered { fn poll_read( mut self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { let this = self.as_mut().project(); let buffer = this.buffer; let reader = this.reader; // If we don't have any buffered data and we're doing a massive read // (larger than our internal buffer), bypass our internal buffer // entirely. let cap = buffer.capacity(); if buffer.is_empty() && buf.remaining() >= cap { let res = ready!(reader.poll_read(cx, buf)); return Poll::Ready(res); } let rem = ready!(self.as_mut().poll_fill_buf(cx))?; let amt = std::cmp::min(rem.len(), buf.remaining()); buf.put_slice(&rem[..amt]); self.consume(amt); Poll::Ready(Ok(())) } } /// Similar to [`std::vec::Drain`], but can be safely [`mem::forget`] /// or create a sub[`Drain`]. #[derive(Debug)] pub(super) struct Drain<'a> { buffer: &'a mut BytesMut, /// Number of bytes to remove on drop n: usize, } impl Drain<'_> { /// Create a new `Drain` that contains `0..min(self.n, n)`. pub(super) fn subdrain(mut self, n: usize) -> Self { self.n = min(self.n, n); self } } impl Deref for Drain<'_> { type Target = [u8]; fn deref(&self) -> &Self::Target { &self.buffer[..self.n] } } impl Drop for Drain<'_> { fn drop(&mut self) { self.buffer.advance(self.n); } } openssh-sftp-client-lowlevel-0.7.0/src/write_end.rs000064400000000000000000000471531046102023000205130ustar 00000000000000#![forbid(unsafe_code)] use super::*; use awaitable_responses::ArenaArc; use connection::SharedData; use std::{ borrow::Cow, convert::TryInto, fmt::Debug, io::IoSlice, ops::{Deref, DerefMut}, path::Path, }; use bytes::{BufMut, Bytes, BytesMut}; use openssh_sftp_protocol::{ file_attrs::FileAttrs, request::*, serde::Serialize, ssh_format::Serializer, Handle, }; /// It is recommended to create at most one `WriteEnd` per thread /// using [`WriteEnd::clone`]. #[derive(Debug)] pub struct WriteEnd { serializer: Serializer, shared_data: SharedData, } impl Clone for WriteEnd { fn clone(&self) -> Self { Self::new(self.shared_data.clone()) } } impl WriteEnd { /// Create a [`WriteEnd`] from [`SharedData`]. pub fn new(shared_data: SharedData) -> Self { Self { serializer: Serializer::default(), shared_data, } } /// Consume the [`WriteEnd`] and return the stored [`SharedData`]. pub fn into_shared_data(self) -> SharedData { self.shared_data } } impl Deref for WriteEnd { type Target = SharedData; fn deref(&self) -> &Self::Target { &self.shared_data } } impl DerefMut for WriteEnd { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.shared_data } } impl WriteEnd where Buffer: Send + Sync, Q: Queue, { pub(crate) fn send_hello(&mut self, version: u32) -> Result<(), Error> { self.shared_data .queue() .push(Self::serialize(&mut self.serializer, Hello { version })?); Ok(()) } fn reset(serializer: &mut Serializer) { serializer.reset_counter(); // Reserve for the header serializer.output.resize(4, 0); } fn serialize(serializer: &mut Serializer, value: T) -> Result where T: Serialize, { Self::reset(serializer); value.serialize(&mut *serializer)?; let header = serializer.create_header(0)?; // Write the header serializer.output[..4].copy_from_slice(&header); Ok(serializer.output.split().freeze()) } /// Send requests. /// /// NOTE that this merely add the request to the buffer, you need to call /// [`SharedData::flush`] to actually send the requests. fn send_request( &mut self, id: Id, request: RequestInner<'_>, buffer: Option, ) -> Result, Error> { let serialized = Self::serialize( &mut self.serializer, Request { request_id: ArenaArc::slot(&id.0), inner: request, }, )?; id.0.reset(buffer); self.shared_data.queue().push(serialized); Ok(id.into_inner()) } pub fn send_open_file_request( &mut self, id: Id, params: OpenFileRequest<'_>, ) -> Result, Error> { self.send_request(id, RequestInner::Open(params), None) .map(AwaitableHandle::new) } pub fn send_close_request( &mut self, id: Id, handle: Cow<'_, Handle>, ) -> Result, Error> { self.send_request(id, RequestInner::Close(handle), None) .map(AwaitableStatus::new) } /// - `buffer` - If set to `None` or the buffer is not long enough, /// then [`crate::Data::AllocatedBox`] will be returned. /// /// Return [`crate::Data::Buffer`] or /// [`crate::Data::AllocatedBox`] if not EOF, otherwise returns /// [`crate::Data::Eof`]. pub fn send_read_request( &mut self, id: Id, handle: Cow<'_, Handle>, offset: u64, len: u32, buffer: Option, ) -> Result, Error> { self.send_request( id, RequestInner::Read { handle, offset, len, }, buffer, ) .map(AwaitableData::new) } pub fn send_remove_request( &mut self, id: Id, path: Cow<'_, Path>, ) -> Result, Error> { self.send_request(id, RequestInner::Remove(path), None) .map(AwaitableStatus::new) } pub fn send_rename_request( &mut self, id: Id, oldpath: Cow<'_, Path>, newpath: Cow<'_, Path>, ) -> Result, Error> { self.send_request(id, RequestInner::Rename { oldpath, newpath }, None) .map(AwaitableStatus::new) } /// * `attrs` - [`FileAttrs::get_size`] must be equal to `None`. pub fn send_mkdir_request( &mut self, id: Id, path: Cow<'_, Path>, attrs: FileAttrs, ) -> Result, Error> { self.send_request(id, RequestInner::Mkdir { path, attrs }, None) .map(AwaitableStatus::new) } pub fn send_rmdir_request( &mut self, id: Id, path: Cow<'_, Path>, ) -> Result, Error> { self.send_request(id, RequestInner::Rmdir(path), None) .map(AwaitableStatus::new) } pub fn send_opendir_request( &mut self, id: Id, path: Cow<'_, Path>, ) -> Result, Error> { self.send_request(id, RequestInner::Opendir(path), None) .map(AwaitableHandle::new) } /// Return entries in the directory specified by the `handle`, including /// `.` and `..`. /// /// The `filename` only contains the basename. /// /// **NOTE that it does not return all entries in one response.** /// You would have to keep calling `send_readdir_request` until it returns /// an empty `Box`. pub fn send_readdir_request( &mut self, id: Id, handle: Cow<'_, Handle>, ) -> Result, Error> { self.send_request(id, RequestInner::Readdir(handle), None) .map(AwaitableNameEntries::new) } pub fn send_stat_request( &mut self, id: Id, path: Cow<'_, Path>, ) -> Result, Error> { self.send_request(id, RequestInner::Stat(path), None) .map(AwaitableAttrs::new) } /// Does not follow symlink pub fn send_lstat_request( &mut self, id: Id, path: Cow<'_, Path>, ) -> Result, Error> { self.send_request(id, RequestInner::Lstat(path), None) .map(AwaitableAttrs::new) } /// * `handle` - Must be opened with `FileMode::READ`. pub fn send_fstat_request( &mut self, id: Id, handle: Cow<'_, Handle>, ) -> Result, Error> { self.send_request(id, RequestInner::Fstat(handle), None) .map(AwaitableAttrs::new) } pub fn send_setstat_request( &mut self, id: Id, path: Cow<'_, Path>, attrs: FileAttrs, ) -> Result, Error> { self.send_request(id, RequestInner::Setstat { path, attrs }, None) .map(AwaitableStatus::new) } /// * `handle` - Must be opened with `OpenOptions::write` set. pub fn send_fsetstat_request( &mut self, id: Id, handle: Cow<'_, Handle>, attrs: FileAttrs, ) -> Result, Error> { self.send_request(id, RequestInner::Fsetstat { handle, attrs }, None) .map(AwaitableStatus::new) } pub fn send_readlink_request( &mut self, id: Id, path: Cow<'_, Path>, ) -> Result, Error> { self.send_request(id, RequestInner::Readlink(path), None) .map(AwaitableName::new) } pub fn send_realpath_request( &mut self, id: Id, path: Cow<'_, Path>, ) -> Result, Error> { self.send_request(id, RequestInner::Realpath(path), None) .map(AwaitableName::new) } /// Create symlink pub fn send_symlink_request( &mut self, id: Id, targetpath: Cow<'_, Path>, linkpath: Cow<'_, Path>, ) -> Result, Error> { self.send_request( id, RequestInner::Symlink { linkpath, targetpath, }, None, ) .map(AwaitableStatus::new) } /// Return limits of the server /// /// # Precondition /// /// Requires `extensions::contains(Extensions::LIMITS)` to be true. pub fn send_limits_request( &mut self, id: Id, ) -> Result, Error> { self.send_request(id, RequestInner::Limits, None) .map(AwaitableLimits::new) } /// This supports canonicalisation of relative paths and those that need /// tilde-expansion, i.e. "~", "~/..." and "~user/...". /// /// These paths are expanded using shell-like rules and the resultant path /// is canonicalised similarly to [`WriteEnd::send_realpath_request`]. /// /// # Precondition /// /// Requires `extensions::contains(Extensions::EXPAND_PATH)` to be true. pub fn send_expand_path_request( &mut self, id: Id, path: Cow<'_, Path>, ) -> Result, Error> { self.send_request(id, RequestInner::ExpandPath(path), None) .map(AwaitableName::new) } /// # Precondition /// /// Requires `extensions::contains(Extensions::FSYNC)` to be true. pub fn send_fsync_request( &mut self, id: Id, handle: Cow<'_, Handle>, ) -> Result, Error> { self.send_request(id, RequestInner::Fsync(handle), None) .map(AwaitableStatus::new) } /// # Precondition /// /// Requires `extensions::contains(Extensions::HARDLINK)` to be true. pub fn send_hardlink_request( &mut self, id: Id, oldpath: Cow<'_, Path>, newpath: Cow<'_, Path>, ) -> Result, Error> { self.send_request(id, RequestInner::HardLink { oldpath, newpath }, None) .map(AwaitableStatus::new) } /// # Precondition /// /// Requires `extensions::contains(Extensions::POSIX_RENAME)` to be true. pub fn send_posix_rename_request( &mut self, id: Id, oldpath: Cow<'_, Path>, newpath: Cow<'_, Path>, ) -> Result, Error> { self.send_request(id, RequestInner::PosixRename { oldpath, newpath }, None) .map(AwaitableStatus::new) } /// The server MUST copy the data exactly as if the client had issued a /// series of [`RequestInner::Read`] requests on the `read_from_handle` /// starting at `read_from_offset` and totaling `read_data_length` bytes, /// and issued a series of [`RequestInner::Write`] packets on the /// `write_to_handle`, starting at the `write_from_offset`, and totaling /// the total number of bytes read by the [`RequestInner::Read`] packets. /// /// The server SHOULD allow `read_from_handle` and `write_to_handle` to /// be the same handle as long as the range of data is not overlapping. /// This allows data to efficiently be moved within a file. /// /// If `data_length` is `0`, this imples data should be read until EOF is /// encountered. /// /// There are no protocol restictions on this operation; however, the /// server MUST ensure that the user does not exceed quota, etc. The /// server is, as always, free to complete this operation out of order if /// it is too large to complete immediately, or to refuse a request that /// is too large. /// /// # Precondition /// /// Requires `extensions::contains(Extensions::COPY_DATA)` to be true. /// /// For [openssh-portable], this is available from V_9_0_P1. /// /// [openssh-portable]: https://github.com/openssh/openssh-portable pub fn send_copy_data_request( &mut self, id: Id, read_from_handle: Cow<'_, Handle>, read_from_offset: u64, read_data_length: u64, write_to_handle: Cow<'_, Handle>, write_to_offset: u64, ) -> Result, Error> { self.send_request( id, RequestInner::Cp { read_from_handle, read_from_offset, read_data_length, write_to_handle, write_to_offset, }, None, ) .map(AwaitableStatus::new) } } impl WriteEnd where Buffer: ToBuffer + Send + Sync + 'static, Q: Queue, { /// Write will extend the file if writing beyond the end of the file. /// /// It is legal to write way beyond the end of the file, the semantics /// are to write zeroes from the end of the file to the specified offset /// and then the data. /// /// On most operating systems, such writes do not allocate disk space but /// instead leave "holes" in the file. /// /// This function is only suitable for writing small data since it needs to copy the /// entire `data` into buffer. pub fn send_write_request_buffered( &mut self, id: Id, handle: Cow<'_, Handle>, offset: u64, data: Cow<'_, [u8]>, ) -> Result, Error> { let len: u32 = data.len().try_into()?; self.serializer.reserve( // 9 bytes for the 4-byte len of packet, 1-byte packet type and // 4-byte request id 9 + handle.into_inner().len() + // 8 bytes for the offset 8 + // 4 bytes for the lenght of the data to be sent 4 + // len of the data len as usize, ); self.send_request( id, RequestInner::Write { handle, offset, data, }, None, ) .map(AwaitableStatus::new) } /// Write will extend the file if writing beyond the end of the file. /// /// It is legal to write way beyond the end of the file, the semantics /// are to write zeroes from the end of the file to the specified offset /// and then the data. /// /// On most operating systems, such writes do not allocate disk space but /// instead leave "holes" in the file. /// /// This function is only suitable for writing small data since it needs to copy the /// entire `data` into buffer. pub fn send_write_request_buffered_vectored( &mut self, id: Id, handle: Cow<'_, Handle>, offset: u64, io_slices: &[IoSlice<'_>], ) -> Result, Error> { self.send_write_request_buffered_vectored2(id, handle, offset, &[io_slices]) } fn serialize_write_request<'a>( serializer: &'a mut Serializer, request_id: u32, handle: Cow<'_, Handle>, offset: u64, len: u32, ) -> Result<&'a mut BytesMut, Error> { Self::reset(serializer); let header = Request::serialize_write_request(serializer, request_id, handle, offset, len)?; let buffer = &mut serializer.output; // Write the header buffer[..4].copy_from_slice(&header); Ok(buffer) } /// Write will extend the file if writing beyond the end of the file. /// /// It is legal to write way beyond the end of the file, the semantics /// are to write zeroes from the end of the file to the specified offset /// and then the data. /// /// On most operating systems, such writes do not allocate disk space but /// instead leave "holes" in the file. /// /// This function is only suitable for writing small data since it needs to copy the /// entire `data` into buffer. pub fn send_write_request_buffered_vectored2( &mut self, id: Id, handle: Cow<'_, Handle>, offset: u64, bufs: &[&[IoSlice<'_>]], ) -> Result, Error> { let len: usize = bufs .iter() .flat_map(Deref::deref) .map(|io_slice| io_slice.len()) .sum(); let len: u32 = len.try_into()?; self.serializer.reserve( // 9 bytes for the 4-byte len of packet, 1-byte packet type and // 4-byte request id 9 + handle.into_inner().len() + // 8 bytes for the offset 8 + // 4 bytes for the lenght of the data to be sent 4 + // len of the data len as usize, ); let buffer = Self::serialize_write_request( &mut self.serializer, ArenaArc::slot(&id.0), handle, offset, len, )?; bufs.iter() .flat_map(|io_slices| io_slices.iter()) .for_each(|io_slice| { buffer.put_slice(io_slice); }); id.0.reset(None); self.shared_data.queue().push(buffer.split().freeze()); Ok(AwaitableStatus::new(id.into_inner())) } /// Write will extend the file if writing beyond the end of the file. /// /// It is legal to write way beyond the end of the file, the semantics /// are to write zeroes from the end of the file to the specified offset /// and then the data. /// /// On most operating systems, such writes do not allocate disk space but /// instead leave "holes" in the file. /// /// This function is zero-copy. pub fn send_write_request_zero_copy( &mut self, id: Id, handle: Cow<'_, Handle>, offset: u64, data: &[Bytes], ) -> Result, Error> { self.send_write_request_zero_copy2(id, handle, offset, &[data]) } /// Write will extend the file if writing beyond the end of the file. /// /// It is legal to write way beyond the end of the file, the semantics /// are to write zeroes from the end of the file to the specified offset /// and then the data. /// /// On most operating systems, such writes do not allocate disk space but /// instead leave "holes" in the file. /// /// This function is zero-copy. pub fn send_write_request_zero_copy2( &mut self, id: Id, handle: Cow<'_, Handle>, offset: u64, data_slice: &[&[Bytes]], ) -> Result, Error> { let len: usize = data_slice .iter() .flat_map(Deref::deref) .map(Bytes::len) .sum(); let len: u32 = len.try_into()?; let header = Self::serialize_write_request( &mut self.serializer, ArenaArc::slot(&id.0), handle, offset, len, )? .split() .freeze(); id.0.reset(None); self.shared_data.queue().extend(header, data_slice); Ok(AwaitableStatus::new(id.into_inner())) } } openssh-sftp-client-lowlevel-0.7.0/tests/lowlevel.rs000064400000000000000000000634231046102023000207350ustar 00000000000000use lowlevel::*; use openssh_sftp_client_lowlevel as lowlevel; use std::{ borrow::Cow, env, fs, io, io::IoSlice, num::NonZeroUsize, ops::Deref, os::unix::fs::symlink, path, }; use sftp_test_common::*; use bytes::Bytes; use tempfile::{Builder, TempDir}; use tokio::{io::AsyncWriteExt, sync::Mutex}; mod queue; use queue::MpscQueue; type Id = lowlevel::Id>; type WriteEnd = lowlevel::WriteEnd, MpscQueue, Mutex>; type ReadEnd = lowlevel::ReadEnd, MpscQueue, Mutex>; fn assert_not_found(err: io::Error) { assert!(matches!(err.kind(), io::ErrorKind::NotFound), "{:#?}", err); } async fn flush(read_end: &mut ReadEnd) { let shared_data = read_end.get_shared_data(); let stdin = shared_data.get_auxiliary(); let bytes = shared_data.queue().consume(); let mut stdin_locked = stdin.lock().await; for byte in bytes { stdin_locked.write_all(&byte).await.unwrap(); } } async fn connect_with_extensions() -> (WriteEnd, ReadEnd, process::Child, Extensions) { let (child, stdin, stdout) = launch_sftp().await; let buffer_size = NonZeroUsize::new(1000).unwrap(); let write_end = lowlevel::connect(MpscQueue::default(), Mutex::new(stdin)).unwrap(); let mut read_end = ReadEnd::new(stdout, buffer_size, write_end.deref().clone()); flush(&mut read_end).await; let extensions = read_end.receive_server_hello().await.unwrap(); (write_end, read_end, child, extensions) } async fn connect() -> (WriteEnd, ReadEnd, process::Child) { let (write_end, read_end, child, _extensions) = connect_with_extensions().await; (write_end, read_end, child) } #[tokio::test] async fn test_connect() { let mut child = connect().await.2; assert!(child.wait().await.unwrap().success()); } fn create_tmpdir() -> TempDir { let path = get_path_for_tmp_files(); fs::create_dir_all(&path).unwrap(); Builder::new() .prefix("lowlevel-test") .tempdir_in(path) .unwrap() } async fn read_one_packet(read_end: &mut ReadEnd) { flush(read_end).await; eprintln!("Wait for new request"); eprintln!("Read in one packet"); read_end.read_in_one_packet().await.unwrap(); } #[tokio::test] async fn test_file_desc() { let (mut write_end, mut read_end, mut child) = connect().await; let id = write_end.create_response_id(); let tempdir = create_tmpdir(); let filename = tempdir.path().join("file"); // Create file let mut file_attrs = FileAttrs::new(); file_attrs.set_size(2000); file_attrs.set_permissions(Permissions::READ_BY_OWNER | Permissions::WRITE_BY_OWNER); let file_attrs = file_attrs; let awaitable = write_end .send_open_file_request( id, OpenOptions::new().read(true).write(true).create( Cow::Borrowed(&filename), CreateFlags::Excl, file_attrs, ), ) .unwrap(); read_one_packet(&mut read_end).await; let (id, handle) = awaitable.wait().await.unwrap(); eprintln!("handle = {:#?}", handle); // Write msg into it let msg = "Hello, world!".as_bytes(); let awaitable = write_end .send_write_request_buffered(id, Cow::Borrowed(&handle), 0, Cow::Borrowed(msg)) .unwrap(); eprintln!("Waiting for write response"); read_one_packet(&mut read_end).await; let id = awaitable.wait().await.unwrap().0; // Close it let awaitable = write_end .send_close_request(id, Cow::Borrowed(&handle)) .unwrap(); read_one_packet(&mut read_end).await; let id = awaitable.wait().await.unwrap().0; // Open it again let awaitable = write_end .send_open_file_request(id, OpenFileRequest::open(Cow::Borrowed(&filename))) .unwrap(); read_one_packet(&mut read_end).await; let (id, handle) = awaitable.wait().await.unwrap(); eprintln!("handle = {:#?}", handle); // Read from it let awaitable = write_end .send_read_request(id, Cow::Borrowed(&handle), 0, msg.len() as u32, None) .unwrap(); read_one_packet(&mut read_end).await; let (id, data) = awaitable.wait().await.unwrap(); match data { Data::AllocatedBox(data) => assert_eq!(&*data, msg), _ => panic!("Unexpected data"), }; drop(id); drop(write_end); drop(read_end); assert!(child.wait().await.unwrap().success()); } async fn test_write_impl( write: impl FnOnce(&mut WriteEnd, Id, Cow<'_, Handle>, &[u8]) -> AwaitableStatus>, ) { let (mut write_end, mut read_end, mut child) = connect().await; let id = write_end.create_response_id(); let tempdir = create_tmpdir(); let filename = tempdir.path().join("file"); // Create one file and write to it let mut file_attrs = FileAttrs::new(); file_attrs.set_size(2000); file_attrs.set_permissions(Permissions::READ_BY_OWNER | Permissions::WRITE_BY_OWNER); let file_attrs = file_attrs; let awaitable = write_end .send_open_file_request( id, OpenOptions::new().read(true).write(true).create( Cow::Borrowed(&filename), CreateFlags::Excl, file_attrs, ), ) .unwrap(); read_one_packet(&mut read_end).await; let (id, handle) = awaitable.wait().await.unwrap(); eprintln!("handle = {:#?}", handle); let msg = "Hello, world!".as_bytes(); let awaitable = write(&mut write_end, id, Cow::Borrowed(&handle), msg); eprintln!("Waiting for write response"); read_one_packet(&mut read_end).await; let id = awaitable.wait().await.unwrap().0; // Read from it let awaitable = write_end .send_read_request(id, Cow::Borrowed(&handle), 0, msg.len() as u32, None) .unwrap(); read_one_packet(&mut read_end).await; let (id, data) = awaitable.wait().await.unwrap(); match data { Data::AllocatedBox(data) => assert_eq!(&*data, msg), _ => panic!("Unexpected data"), }; drop(id); drop(write_end); drop(read_end); assert!(child.wait().await.unwrap().success()); } #[tokio::test] async fn test_write_buffered() { test_write_impl(|write_end, id, handle, msg| { write_end .send_write_request_buffered(id, handle, 0, Cow::Borrowed(msg)) .unwrap() }) .await; } #[tokio::test] async fn test_write_buffered_vectored() { test_write_impl(|write_end, id, handle, msg| { write_end .send_write_request_buffered_vectored( id, handle, 0, &[IoSlice::new(&msg[..3]), IoSlice::new(&msg[3..])], ) .unwrap() }) .await; } #[tokio::test] async fn test_write_buffered_vectored2() { test_write_impl(|write_end, id, handle, msg| { write_end .send_write_request_buffered_vectored2( id, handle, 0, &[ [IoSlice::new(&msg[..3])].as_slice(), [IoSlice::new(&msg[3..])].as_slice(), ], ) .unwrap() }) .await; } #[tokio::test] async fn test_write_zero_copy() { test_write_impl(|write_end, id, handle, msg| { write_end .send_write_request_zero_copy( id, handle, 0, &[ Bytes::copy_from_slice(&msg[..3]), Bytes::copy_from_slice(&msg[3..]), ], ) .unwrap() }) .await; } #[tokio::test] async fn test_write_zero_copy2() { test_write_impl(|write_end, id, handle, msg| { write_end .send_write_request_zero_copy2( id, handle, 0, &[ [Bytes::copy_from_slice(&msg[..3])].as_slice(), [Bytes::copy_from_slice(&msg[3..])].as_slice(), ], ) .unwrap() }) .await; } #[tokio::test] async fn test_file_remove() { let (mut write_end, mut read_end, mut child) = connect().await; let id = write_end.create_response_id(); let tempdir = create_tmpdir(); let filename = tempdir.path().join("file"); fs::File::create(&filename).unwrap().set_len(2000).unwrap(); // remove it let awaitable = write_end .send_remove_request(id, Cow::Borrowed(&filename)) .unwrap(); read_one_packet(&mut read_end).await; let id = awaitable.wait().await.unwrap().0; // Try open it again let err = fs::File::open(&filename).unwrap_err(); assert_not_found(err); drop(id); drop(write_end); drop(read_end); assert!(child.wait().await.unwrap().success()); } #[tokio::test] async fn test_file_rename() { let (mut write_end, mut read_end, mut child) = connect().await; let id = write_end.create_response_id(); let tempdir = create_tmpdir(); let filename = tempdir.path().join("file"); fs::File::create(&filename).unwrap().set_len(2000).unwrap(); // rename it let new_filename = tempdir.path().join("file2"); let awaitable = write_end .send_rename_request(id, Cow::Borrowed(&filename), Cow::Borrowed(&new_filename)) .unwrap(); read_one_packet(&mut read_end).await; let id = awaitable.wait().await.unwrap().0; // Open it again let metadata = fs::File::open(&new_filename).unwrap().metadata().unwrap(); assert!(metadata.is_file()); assert_eq!(metadata.len(), 2000); drop(id); drop(write_end); drop(read_end); assert!(child.wait().await.unwrap().success()); } #[tokio::test] async fn test_mkdir() { let (mut write_end, mut read_end, mut child) = connect().await; let id = write_end.create_response_id(); let tempdir = create_tmpdir(); let dirname = tempdir.path().join("dir"); // mkdir it let awaitable = write_end .send_mkdir_request(id, Cow::Borrowed(&dirname), FileAttrs::default()) .unwrap(); read_one_packet(&mut read_end).await; let id = awaitable.wait().await.unwrap().0; // Open it assert!(fs::read_dir(&dirname).unwrap().next().is_none()); drop(id); drop(write_end); drop(read_end); assert!(child.wait().await.unwrap().success()); } #[tokio::test] async fn test_rmdir() { let (mut write_end, mut read_end, mut child) = connect().await; let id = write_end.create_response_id(); let tempdir = create_tmpdir(); let dirname = tempdir.path().join("dir"); fs::DirBuilder::new().create(&dirname).unwrap(); // rmdir it let awaitable = write_end .send_rmdir_request(id, Cow::Borrowed(&dirname)) .unwrap(); read_one_packet(&mut read_end).await; let id = awaitable.wait().await.unwrap().0; // Try open it let err = fs::read_dir(&dirname).unwrap_err(); assert_not_found(err); drop(id); drop(write_end); drop(read_end); assert!(child.wait().await.unwrap().success()); } #[tokio::test] async fn test_dir_desc() { let (mut write_end, mut read_end, mut child) = connect().await; let id = write_end.create_response_id(); let tempdir = create_tmpdir(); let dirname = tempdir.path().join("dir"); let subdir = dirname.join("subdir"); fs::DirBuilder::new() .recursive(true) .create(&subdir) .unwrap(); let file = dirname.join("file"); fs::File::create(&file).unwrap().set_len(2000).unwrap(); // open it let awaitable = write_end .send_opendir_request(id, Cow::Borrowed(&dirname)) .unwrap(); read_one_packet(&mut read_end).await; let (id, handle) = awaitable.wait().await.unwrap(); // read it let awaitable = write_end .send_readdir_request(id, Cow::Borrowed(&*handle)) .unwrap(); read_one_packet(&mut read_end).await; let (id, entries) = awaitable.wait().await.unwrap(); for entry in entries.iter() { let filename = &*entry.filename; if filename == path::Path::new(".") || filename == path::Path::new("..") { continue; } assert!( filename == path::Path::new("subdir") || filename == path::Path::new("file"), "{:#?}", filename ); if filename == file { assert_eq!(entry.attrs.get_size().unwrap(), 2000); } } drop(id); drop(write_end); drop(read_end); assert!(child.wait().await.unwrap().success()); } #[tokio::test] async fn test_stat() { let (mut write_end, mut read_end, mut child) = connect().await; let id = write_end.create_response_id(); let tempdir = create_tmpdir(); let filename = tempdir.path().join("file"); fs::File::create(&filename).unwrap().set_len(2000).unwrap(); let linkname = tempdir.path().join("symlink"); symlink(&filename, &linkname).unwrap(); // stat let awaitable = write_end .send_stat_request(id, Cow::Borrowed(&linkname)) .unwrap(); read_one_packet(&mut read_end).await; let (id, attrs) = awaitable.wait().await.unwrap(); assert_eq!(attrs.get_size().unwrap(), 2000); assert_eq!(attrs.get_filetype().unwrap(), FileType::RegularFile); drop(id); drop(write_end); drop(read_end); assert!(child.wait().await.unwrap().success()); } #[tokio::test] async fn test_lstat() { let (mut write_end, mut read_end, mut child) = connect().await; let id = write_end.create_response_id(); let tempdir = create_tmpdir(); let filename = tempdir.path().join("file"); fs::File::create(&filename).unwrap().set_len(2000).unwrap(); let linkname = tempdir.path().join("symlink"); symlink(&filename, &linkname).unwrap(); // lstat let awaitable = write_end .send_lstat_request(id, Cow::Borrowed(&linkname)) .unwrap(); read_one_packet(&mut read_end).await; let (id, attrs) = awaitable.wait().await.unwrap(); assert_eq!(attrs.get_filetype().unwrap(), FileType::Symlink); drop(id); drop(write_end); drop(read_end); assert!(child.wait().await.unwrap().success()); } #[tokio::test] async fn test_fstat() { let (mut write_end, mut read_end, mut child) = connect().await; let id = write_end.create_response_id(); let tempdir = create_tmpdir(); let filename = tempdir.path().join("file"); fs::File::create(&filename).unwrap().set_len(2000).unwrap(); // open let awaitable = write_end .send_open_file_request(id, OpenFileRequest::open(Cow::Borrowed(&filename))) .unwrap(); read_one_packet(&mut read_end).await; let (id, handle) = awaitable.wait().await.unwrap(); // fstat let awaitable = write_end .send_fstat_request(id, Cow::Borrowed(&handle)) .unwrap(); read_one_packet(&mut read_end).await; let (id, attrs) = awaitable.wait().await.unwrap(); assert_eq!(attrs.get_size().unwrap(), 2000); assert_eq!(attrs.get_filetype().unwrap(), FileType::RegularFile); drop(id); drop(write_end); drop(read_end); assert!(child.wait().await.unwrap().success()); } #[tokio::test] async fn test_setstat() { let (mut write_end, mut read_end, mut child) = connect().await; let id = write_end.create_response_id(); let tempdir = create_tmpdir(); let filename = tempdir.path().join("file"); fs::File::create(&filename).unwrap().set_len(2000).unwrap(); let mut fileattrs = FileAttrs::default(); fileattrs.set_size(10000); // setstat let awaitable = write_end .send_setstat_request(id, Cow::Borrowed(&filename), fileattrs) .unwrap(); read_one_packet(&mut read_end).await; let id = awaitable.wait().await.unwrap().0; // stat let awaitable = write_end .send_stat_request(id, Cow::Borrowed(&filename)) .unwrap(); read_one_packet(&mut read_end).await; let (id, attrs) = awaitable.wait().await.unwrap(); assert_eq!(attrs.get_size().unwrap(), 10000); assert_eq!(attrs.get_filetype().unwrap(), FileType::RegularFile); drop(id); drop(write_end); drop(read_end); assert!(child.wait().await.unwrap().success()); } #[tokio::test] async fn test_fsetstat() { let (mut write_end, mut read_end, mut child) = connect().await; let id = write_end.create_response_id(); let tempdir = create_tmpdir(); let filename = tempdir.path().join("file"); fs::File::create(&filename).unwrap().set_len(2000).unwrap(); // open let awaitable = write_end .send_open_file_request( id, OpenOptions::new() .read(true) .write(true) .open(Cow::Borrowed(&filename)), ) .unwrap(); read_one_packet(&mut read_end).await; let (id, handle) = awaitable.wait().await.unwrap(); // fsetstat let mut fileattrs = FileAttrs::default(); fileattrs.set_size(10000); let awaitable = write_end .send_fsetstat_request(id, Cow::Borrowed(&handle), fileattrs) .unwrap(); read_one_packet(&mut read_end).await; let id = awaitable.wait().await.unwrap().0; // fstat let awaitable = write_end .send_fstat_request(id, Cow::Borrowed(&handle)) .unwrap(); read_one_packet(&mut read_end).await; let (id, attrs) = awaitable.wait().await.unwrap(); assert_eq!(attrs.get_size().unwrap(), 10000); assert_eq!(attrs.get_filetype().unwrap(), FileType::RegularFile); drop(id); drop(write_end); drop(read_end); assert!(child.wait().await.unwrap().success()); } #[tokio::test] async fn test_readlink() { let (mut write_end, mut read_end, mut child) = connect().await; let id = write_end.create_response_id(); let tempdir = create_tmpdir(); let filename = tempdir.path().join("file"); fs::File::create(&filename).unwrap().set_len(2000).unwrap(); let linkname = tempdir.path().join("symlink"); symlink(&filename, &linkname).unwrap(); // readlink let awaitable = write_end .send_readlink_request(id, Cow::Borrowed(&linkname)) .unwrap(); read_one_packet(&mut read_end).await; let (id, path) = awaitable.wait().await.unwrap(); assert_eq!(&*path, &*filename); drop(id); drop(write_end); drop(read_end); assert!(child.wait().await.unwrap().success()); } #[tokio::test] async fn test_readpath() { let (mut write_end, mut read_end, mut child) = connect().await; let id = write_end.create_response_id(); let tempdir = create_tmpdir(); let filename = tempdir.path().join("file"); fs::File::create(&filename).unwrap().set_len(2000).unwrap(); let linkname = tempdir.path().join("symlink"); symlink(&filename, &linkname).unwrap(); // readpath let awaitable = write_end .send_realpath_request(id, Cow::Borrowed(&linkname)) .unwrap(); read_one_packet(&mut read_end).await; let (id, path) = awaitable.wait().await.unwrap(); assert_eq!(&*path, &*fs::canonicalize(&filename).unwrap()); drop(id); drop(write_end); drop(read_end); assert!(child.wait().await.unwrap().success()); } #[tokio::test] async fn test_symlink() { let (mut write_end, mut read_end, mut child) = connect().await; let id = write_end.create_response_id(); let tempdir = create_tmpdir(); let filename = tempdir.path().join("file"); fs::File::create(&filename).unwrap().set_len(2000).unwrap(); let linkname = tempdir.path().join("symlink"); // symlink let awaitable = write_end .send_symlink_request(id, Cow::Borrowed(&filename), Cow::Borrowed(&linkname)) .unwrap(); read_one_packet(&mut read_end).await; let id = awaitable.wait().await.unwrap().0; assert_eq!( &*fs::canonicalize(&linkname).unwrap(), &*fs::canonicalize(&filename).unwrap() ); drop(id); drop(write_end); drop(read_end); assert!(child.wait().await.unwrap().success()); } #[tokio::test] async fn test_limits() { let (mut write_end, mut read_end, mut child, extensions) = connect_with_extensions().await; let id = write_end.create_response_id(); assert!(extensions.contains(lowlevel::Extensions::LIMITS)); let awaitable = write_end.send_limits_request(id).unwrap(); read_one_packet(&mut read_end).await; let (id, limits) = awaitable.wait().await.unwrap(); eprintln!("{:#?}", limits); drop(id); drop(write_end); drop(read_end); assert!(child.wait().await.unwrap().success()); } #[tokio::test] async fn test_expand_path() { let home: path::PathBuf = env::var("HOME").unwrap().into(); env::set_current_dir(&home).unwrap(); let (mut write_end, mut read_end, mut child, extensions) = connect_with_extensions().await; let id = write_end.create_response_id(); assert!(extensions.contains(lowlevel::Extensions::EXPAND_PATH)); let awaitable = write_end .send_expand_path_request(id, Cow::Borrowed(path::Path::new("~"))) .unwrap(); read_one_packet(&mut read_end).await; let (id, expanded_path) = awaitable.wait().await.unwrap(); assert_eq!(&*expanded_path, &*home); drop(id); drop(write_end); drop(read_end); assert!(child.wait().await.unwrap().success()); } #[tokio::test] async fn test_fsync() { let (mut write_end, mut read_end, mut child, extensions) = connect_with_extensions().await; assert!(extensions.contains(lowlevel::Extensions::FSYNC)); let id = write_end.create_response_id(); let tempdir = create_tmpdir(); let filename = tempdir.path().join("file"); fs::File::create(&filename).unwrap().set_len(2000).unwrap(); // open let awaitable = write_end .send_open_file_request( id, OpenOptions::new() .read(true) .write(true) .open(Cow::Borrowed(&filename)), ) .unwrap(); read_one_packet(&mut read_end).await; let (id, handle) = awaitable.wait().await.unwrap(); // fsync let awaitable = write_end .send_fsync_request(id, Cow::Borrowed(&handle)) .unwrap(); read_one_packet(&mut read_end).await; let id = awaitable.wait().await.unwrap().0; drop(id); drop(write_end); drop(read_end); assert!(child.wait().await.unwrap().success()); } #[tokio::test] async fn test_hardlink() { let (mut write_end, mut read_end, mut child, extensions) = connect_with_extensions().await; assert!(extensions.contains(lowlevel::Extensions::HARDLINK)); let id = write_end.create_response_id(); let tempdir = create_tmpdir(); let filename = tempdir.path().join("file"); fs::File::create(&filename).unwrap().set_len(2000).unwrap(); // hardlink it let new_filename = tempdir.path().join("file2"); let awaitable = write_end .send_hardlink_request(id, Cow::Borrowed(&filename), Cow::Borrowed(&new_filename)) .unwrap(); read_one_packet(&mut read_end).await; let id = awaitable.wait().await.unwrap().0; // Open the new name and old name again for name in [filename, new_filename] { let metadata = fs::File::open(name).unwrap().metadata().unwrap(); assert!(metadata.is_file()); assert_eq!(metadata.len(), 2000); } drop(id); drop(write_end); drop(read_end); assert!(child.wait().await.unwrap().success()); } #[tokio::test] async fn test_posix_rename() { let (mut write_end, mut read_end, mut child, extensions) = connect_with_extensions().await; assert!(extensions.contains(lowlevel::Extensions::POSIX_RENAME)); let id = write_end.create_response_id(); let tempdir = create_tmpdir(); let filename = tempdir.path().join("file"); fs::File::create(&filename).unwrap().set_len(2000).unwrap(); // Posix rename it let new_filename = tempdir.path().join("file2"); let awaitable = write_end .send_posix_rename_request(id, Cow::Borrowed(&filename), Cow::Borrowed(&new_filename)) .unwrap(); read_one_packet(&mut read_end).await; let id = awaitable.wait().await.unwrap().0; // Open again let metadata = fs::File::open(new_filename).unwrap().metadata().unwrap(); assert!(metadata.is_file()); assert_eq!(metadata.len(), 2000); drop(id); drop(write_end); drop(read_end); assert!(child.wait().await.unwrap().success()); } #[tokio::test] async fn test_ext_copy_data() { let content = "Hello, World!\n"; let (mut write_end, mut read_end, mut child, extensions) = connect_with_extensions().await; assert!(extensions.contains(lowlevel::Extensions::COPY_DATA)); let id = write_end.create_response_id(); let tempdir = create_tmpdir(); let filename = tempdir.path().join("test_ext_copy_data"); fs::write(&filename, content).unwrap(); let awaitable = write_end .send_open_file_request( id, OpenOptions::new().read(true).open(Cow::Borrowed(&filename)), ) .unwrap(); read_one_packet(&mut read_end).await; let (id, handle) = awaitable.wait().await.unwrap(); // copy it let new_filename = tempdir.path().join("test_ext_copy_data_file2"); let awaitable = write_end .send_open_file_request( id, OpenOptions::new().read(true).write(true).create( Cow::Borrowed(&new_filename), CreateFlags::None, FileAttrs::default(), ), ) .unwrap(); read_one_packet(&mut read_end).await; let (id, new_handle) = awaitable.wait().await.unwrap(); let awaitable = write_end .send_copy_data_request( id, Cow::Borrowed(&handle), 0, 0, Cow::Borrowed(&new_handle), 0, ) .unwrap(); read_one_packet(&mut read_end).await; let id = awaitable.wait().await.unwrap().0; // Open the new name and old name again assert_eq!(fs::read_to_string(&new_filename).unwrap(), content); drop(id); drop(write_end); drop(read_end); assert!(child.wait().await.unwrap().success()); } openssh-sftp-client-lowlevel-0.7.0/tests/queue.rs000064400000000000000000000011221046102023000202140ustar 00000000000000use std::{mem, sync::Mutex}; use bytes::Bytes; use openssh_sftp_client_lowlevel::Queue; #[derive(Default)] pub struct MpscQueue(Mutex>); impl MpscQueue { pub fn consume(&self) -> Vec { mem::take(&mut *self.0.lock().unwrap()) } } impl Queue for MpscQueue { fn push(&self, bytes: Bytes) { self.0.lock().unwrap().push(bytes); } fn extend(&self, header: Bytes, body: &[&[Bytes]]) { let mut v = self.0.lock().unwrap(); v.push(header); for data in body { v.extend(data.iter().cloned()); } } }