openssh-sftp-protocol-0.24.0/.cargo_vcs_info.json0000644000000001360000000000100154070ustar { "git": { "sha1": "10347b28a4ce660a178bc77088a3c517550783ef" }, "path_in_vcs": "" }openssh-sftp-protocol-0.24.0/.github/dependabot.yml000064400000000000000000000004361046102023000203720ustar 00000000000000version: 2 updates: - package-ecosystem: "github-actions" # Workflow files stored in the # default location of `.github/workflows` directory: "/" schedule: interval: "daily" - package-ecosystem: "cargo" directory: "/" schedule: interval: "daily" openssh-sftp-protocol-0.24.0/.github/workflows/rust.yml000064400000000000000000000034641046102023000213230ustar 00000000000000name: Rust env: CARGO_TERM_COLOR: always on: push: paths-ignore: - 'README.md' - 'LICENSE' - '.gitignore' pull_request: paths-ignore: - 'README.md' - 'LICENSE' - '.gitignore' jobs: os-check: runs-on: ${{ matrix.os }} name: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [macos-latest, windows-latest] steps: - uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable - uses: actions/checkout@v3 - uses: actions/cache@v3 with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ key: ${{ github.event.repository.name }}-${{ runner.os }}-cargo-check-v2 - name: cargo check uses: actions-rs/cargo@v1 with: command: check args: --all-features check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/cache@v3 with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ key: ${{ github.event.repository.name }}-${{ runner.os }}-cargo-check-v2 - name: Run check run: | cargo fmt --all -- --check cargo clippy --all build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/cache@v3 with: path: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ key: ${{ github.event.repository.name }}-${{ runner.os }}-cargo-test-v2 - name: Run tests run: cargo test --all-features openssh-sftp-protocol-0.24.0/.gitignore000064400000000000000000000006441046102023000161730ustar 00000000000000# Generated by Cargo # will have compiled files and executables /target/ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk # Added by cargo # # already existing elements were commented out /target #Cargo.lock .DS_Store openssh-sftp-protocol-0.24.0/Cargo.toml0000644000000025530000000000100134120ustar # 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-protocol" version = "0.24.0" description = "Data format used to communicate with openssh mux server." readme = "README.md" keywords = [ "ssh", "multiplex", "async", "network", "sftp", ] categories = [ "asynchronous", "network-programming", "api-bindings", ] license = "MIT" repository = "https://github.com/openssh-rust/openssh-sftp-client" [dependencies.bitflags] version = "2.0.0" [dependencies.num-derive] version = "0.3" [dependencies.num-traits] version = "0.2" [dependencies.openssh-sftp-protocol-error] version = "0.1.0" [dependencies.serde] version = "1.0" features = ["derive"] [dependencies.ssh_format] version = "0.14.1" [dependencies.vec-strings] version = "0.4.5" features = ["serde"] [dev-dependencies.serde_json] version = "1.0" [dev-dependencies.serde_test] version = "1.0" [features] bytes = ["ssh_format/bytes"] openssh-sftp-protocol-0.24.0/Cargo.toml.orig000064400000000000000000000014571046102023000170750ustar 00000000000000[package] name = "openssh-sftp-protocol" version = "0.24.0" edition = "2018" license = "MIT" description = "Data format used to communicate with openssh mux server." repository = "https://github.com/openssh-rust/openssh-sftp-client" keywords = ["ssh", "multiplex", "async", "network", "sftp"] categories = ["asynchronous", "network-programming", "api-bindings"] [workspace] members = ["openssh-sftp-protocol-error"] [dependencies] openssh-sftp-protocol-error = { version = "0.1.0", path = "openssh-sftp-protocol-error" } num-traits = "0.2" num-derive = "0.3" serde = { version = "1.0", features = ["derive"] } ssh_format = "0.14.1" bitflags = "2.0.0" vec-strings = { version = "0.4.5", features = ["serde"] } [features] bytes = ["ssh_format/bytes"] [dev-dependencies] serde_json = "1.0" serde_test = "1.0" openssh-sftp-protocol-0.24.0/LICENSE000064400000000000000000000020521046102023000152030ustar 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-protocol-0.24.0/README.md000064400000000000000000000012341046102023000154560ustar 00000000000000# openssh-sftp-protocol [![Rust](https://github.com/openssh-rust/openssh-sftp-protocol/actions/workflows/rust.yml/badge.svg)](https://github.com/openssh-rust/openssh-sftp-protocol/actions/workflows/rust.yml) [![crate.io downloads](https://img.shields.io/crates/d/openssh-sftp-protocol)](https://crates.io/crates/openssh-sftp-protocol) [![crate.io version](https://img.shields.io/crates/v/openssh-sftp-protocol)](https://crates.io/crates/openssh-sftp-protocol) [![docs](https://docs.rs/openssh-sftp-protocol/badge.svg)](https://docs.rs/openssh-sftp-protocol) This crates implements [SFTP protocol v3](https://www.openssh.com/txt/draft-ietf-secsh-filexfer-02.txt). openssh-sftp-protocol-0.24.0/src/constants.rs000064400000000000000000000070131046102023000173510ustar 00000000000000#![forbid(unsafe_code)] macro_rules! def_packet_type { ( $name:ident, $val:literal ) => { pub const $name: u8 = $val; }; } macro_rules! def_u32_constants { ( $name:ident, $val:literal ) => { pub const $name: u32 = $val; }; } macro_rules! def_ext_constants { ( $valname:ident, $name:literal, $revision:literal ) => { pub const $valname: (&'static str, u64) = ($name, $revision); }; } /// version pub const SSH2_FILEXFER_VERSION: u32 = 3; // client to server def_packet_type!(SSH_FXP_INIT, 1); def_packet_type!(SSH_FXP_OPEN, 3); def_packet_type!(SSH_FXP_CLOSE, 4); def_packet_type!(SSH_FXP_READ, 5); def_packet_type!(SSH_FXP_WRITE, 6); def_packet_type!(SSH_FXP_LSTAT, 7); def_packet_type!(SSH_FXP_FSTAT, 8); def_packet_type!(SSH_FXP_SETSTAT, 9); def_packet_type!(SSH_FXP_FSETSTAT, 10); def_packet_type!(SSH_FXP_OPENDIR, 11); def_packet_type!(SSH_FXP_READDIR, 12); def_packet_type!(SSH_FXP_REMOVE, 13); def_packet_type!(SSH_FXP_MKDIR, 14); def_packet_type!(SSH_FXP_RMDIR, 15); def_packet_type!(SSH_FXP_REALPATH, 16); def_packet_type!(SSH_FXP_STAT, 17); def_packet_type!(SSH_FXP_RENAME, 18); def_packet_type!(SSH_FXP_READLINK, 19); def_packet_type!(SSH_FXP_SYMLINK, 20); // server to client def_packet_type!(SSH_FXP_VERSION, 2); def_packet_type!(SSH_FXP_STATUS, 101); def_packet_type!(SSH_FXP_HANDLE, 102); def_packet_type!(SSH_FXP_DATA, 103); def_packet_type!(SSH_FXP_NAME, 104); def_packet_type!(SSH_FXP_ATTRS, 105); def_packet_type!(SSH_FXP_EXTENDED, 200); def_packet_type!(SSH_FXP_EXTENDED_REPLY, 201); // status code def_u32_constants!(SSH_FX_OK, 0); def_u32_constants!(SSH_FX_EOF, 1); def_u32_constants!(SSH_FX_NO_SUCH_FILE, 2); def_u32_constants!(SSH_FX_PERMISSION_DENIED, 3); def_u32_constants!(SSH_FX_FAILURE, 4); def_u32_constants!(SSH_FX_BAD_MESSAGE, 5); def_u32_constants!(SSH_FX_NO_CONNECTION, 6); def_u32_constants!(SSH_FX_CONNECTION_LOST, 7); def_u32_constants!(SSH_FX_OP_UNSUPPORTED, 8); // attributes def_u32_constants!(SSH_FILEXFER_ATTR_SIZE, 0x00000001); def_u32_constants!(SSH_FILEXFER_ATTR_UIDGID, 0x00000002); def_u32_constants!(SSH_FILEXFER_ATTR_PERMISSIONS, 0x00000004); def_u32_constants!(SSH_FILEXFER_ATTR_ACMODTIME, 0x00000008); def_u32_constants!(SSH_FILEXFER_ATTR_EXTENDED, 0x80000000); // open modes def_u32_constants!(SSH_FXF_READ, 0x00000001); def_u32_constants!(SSH_FXF_WRITE, 0x00000002); def_u32_constants!(SSH_FXF_APPEND, 0x00000004); def_u32_constants!(SSH_FXF_CREAT, 0x00000008); def_u32_constants!(SSH_FXF_TRUNC, 0x00000010); def_u32_constants!(SSH_FXF_EXCL, 0x00000020); // extensions def_u32_constants!(SFTP_EXT_POSIX_RENAME, 0x00000001); def_u32_constants!(SFTP_EXT_STATVFS, 0x00000002); def_u32_constants!(SFTP_EXT_FSTATVFS, 0x00000004); def_u32_constants!(SFTP_EXT_HARDLINK, 0x00000008); def_u32_constants!(SFTP_EXT_FSYNC, 0x00000010); def_u32_constants!(SFTP_EXT_LSETSTAT, 0x00000020); def_u32_constants!(SFTP_EXT_LIMITS, 0x00000040); def_u32_constants!(SFTP_EXT_PATH_EXPAND, 0x00000080); // extension names def_ext_constants!(EXT_NAME_POSIX_RENAME, "posix-rename@openssh.com", 1); def_ext_constants!(EXT_NAME_STATVFS, "statvfs@openssh.com", 2); def_ext_constants!(EXT_NAME_FSTATVFS, "fstatvfs@openssh.com", 2); def_ext_constants!(EXT_NAME_HARDLINK, "hardlink@openssh.com", 1); def_ext_constants!(EXT_NAME_FSYNC, "fsync@openssh.com", 1); def_ext_constants!(EXT_NAME_LSETSTAT, "lsetstat@openssh.com", 1); def_ext_constants!(EXT_NAME_LIMITS, "limits@openssh.com", 1); def_ext_constants!(EXT_NAME_EXPAND_PATH, "expand-path@openssh.com", 1); def_ext_constants!(EXT_NAME_COPY_DATA, "copy-data", 1); openssh-sftp-protocol-0.24.0/src/file_attrs.rs000064400000000000000000000414241046102023000174750ustar 00000000000000#![forbid(unsafe_code)] use super::{ constants, {seq_iter::SeqIter, visitor::impl_visitor}, }; use std::{ convert::TryInto, time::{Duration, SystemTime}, }; use bitflags::bitflags; use num_derive::FromPrimitive; use num_traits::cast::FromPrimitive; use openssh_sftp_protocol_error::UnixTimeStampError; use serde::{ de::{Error, Unexpected}, ser::{SerializeTuple, Serializer}, Serialize, }; /// bit mask for the file type bit field const S_IFMT: u32 = 0o170000; bitflags! { #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] struct FileAttrsFlags: u8 { const SIZE = 1 << 0; const ID = 1 << 1; const PERMISSIONS = 1 << 2; const TIME = 1 << 3; const EXTENSIONS = 1 << 4; } } impl Serialize for FileAttrsFlags { fn serialize(&self, serializer: S) -> Result { use constants::{ SSH_FILEXFER_ATTR_ACMODTIME, SSH_FILEXFER_ATTR_PERMISSIONS, SSH_FILEXFER_ATTR_SIZE, SSH_FILEXFER_ATTR_UIDGID, }; let mut flags: u32 = 0; if self.intersects(FileAttrsFlags::SIZE) { flags |= SSH_FILEXFER_ATTR_SIZE; } if self.intersects(FileAttrsFlags::ID) { flags |= SSH_FILEXFER_ATTR_UIDGID; } if self.intersects(FileAttrsFlags::PERMISSIONS) { flags |= SSH_FILEXFER_ATTR_PERMISSIONS; } if self.intersects(FileAttrsFlags::TIME) { flags |= SSH_FILEXFER_ATTR_ACMODTIME; } flags.serialize(serializer) } } impl<'de> crate::visitor::Deserialize<'de> for FileAttrsFlags { fn deserialize>( deserializer: D, ) -> Result { use constants::{ SSH_FILEXFER_ATTR_ACMODTIME, SSH_FILEXFER_ATTR_EXTENDED, SSH_FILEXFER_ATTR_PERMISSIONS, SSH_FILEXFER_ATTR_SIZE, SSH_FILEXFER_ATTR_UIDGID, }; let flags = u32::deserialize(deserializer)?; let has_attr = |attr_mask| -> bool { (flags & attr_mask) != 0 }; let mut file_attrs_flags = FileAttrsFlags::empty(); if has_attr(SSH_FILEXFER_ATTR_SIZE) { file_attrs_flags |= FileAttrsFlags::SIZE; } if has_attr(SSH_FILEXFER_ATTR_UIDGID) { file_attrs_flags |= FileAttrsFlags::ID; } if has_attr(SSH_FILEXFER_ATTR_PERMISSIONS) { file_attrs_flags |= FileAttrsFlags::PERMISSIONS; } if has_attr(SSH_FILEXFER_ATTR_ACMODTIME) { file_attrs_flags |= FileAttrsFlags::TIME; } if has_attr(SSH_FILEXFER_ATTR_EXTENDED) { file_attrs_flags |= FileAttrsFlags::EXTENSIONS; } Ok(file_attrs_flags) } } bitflags! { #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Permissions: u32 { /// set-user-ID (set process effective user ID on execve(2)) const SET_UID = 0o4000; /// set-group-ID /// /// - set process effective group ID on execve(2) /// - mandatory locking, as described in fcntl(2) /// - take a new file's group from parent directory, as described in /// chown(2) and mkdir(2) const SET_GID = 0o2000; /// sticky bit (restricted deletion flag, as described in unlink(2)) const SET_VTX = 0o1000; /// read by owner const READ_BY_OWNER = 0o400; /// write by owner const WRITE_BY_OWNER = 0o200; /// execute file or search directory by owner const EXECUTE_BY_OWNER = 0o100; /// read by group const READ_BY_GROUP = 0o40; /// write by group const WRITE_BY_GROUP = 0o20; /// execute/search by group const EXECUTE_BY_GROUP = 0o10; /// read by others const READ_BY_OTHER = 0o4; /// write by others const WRITE_BY_OTHER = 0o2; /// execute/search by others const EXECUTE_BY_OTHER = 0o1; } } #[derive(Debug, Clone, Copy, FromPrimitive, Eq, PartialEq, Hash)] #[repr(u32)] pub enum FileType { Socket = 0o140000, Symlink = 0o120000, RegularFile = 0o100000, BlockDevice = 0o60000, Directory = 0o40000, CharacterDevice = 0o20000, FIFO = 0o10000, } /// Default value is 1970-01-01 00:00:00 UTC. /// /// UnixTimeStamp stores number of seconds elapsed since 1970-01-01 00:00:00 UTC /// as `u32`. #[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)] #[repr(transparent)] pub struct UnixTimeStamp(u32); impl UnixTimeStamp { pub fn new(system_time: SystemTime) -> Result { let duration = system_time.duration_since(SystemTime::UNIX_EPOCH)?; let seconds: u32 = duration.as_secs().try_into()?; Ok(Self(seconds)) } /// Return unix epoch, same as [`UnixTimeStamp::default`] pub const fn unix_epoch() -> Self { Self(0) } /// Return `None` if [`std::time::SystemTime`] cannot hold the timestamp. pub fn from_raw(elapsed: u32) -> Option { let this = Self(elapsed); let duration = this.as_duration(); SystemTime::UNIX_EPOCH.checked_add(duration)?; Some(this) } pub fn into_raw(self) -> u32 { self.0 } pub fn as_duration(self) -> Duration { Duration::from_secs(self.0 as u64) } pub fn as_system_time(self) -> SystemTime { SystemTime::UNIX_EPOCH + self.as_duration() } } impl_visitor!( UnixTimeStamp, UnixTimeStampVisitor, "Unix Timestamp", seq, { let mut iter = SeqIter::new(seq); let elapsed: u32 = iter.get_next()?; let timestamp = UnixTimeStamp::from_raw(elapsed).ok_or_else(|| { V::Error::invalid_value( Unexpected::Unsigned(elapsed as u64), &"Invalid UnixTimeStamp (seconds)", ) })?; Ok(timestamp) } ); #[derive(Debug, Default, Copy, Clone)] pub struct FileAttrs { flags: FileAttrsFlags, /// present only if flag SSH_FILEXFER_ATTR_SIZE size: u64, /// present only if flag SSH_FILEXFER_ATTR_UIDGID uid: u32, gid: u32, /// present only if flag SSH_FILEXFER_ATTR_PERMISSIONS st_mode: u32, /// present only if flag SSH_FILEXFER_ATTR_ACMODTIME atime: UnixTimeStamp, mtime: UnixTimeStamp, } impl PartialEq for FileAttrs { fn eq(&self, other: &Self) -> bool { self.get_size() == other.get_size() && self.get_id() == other.get_id() && self.get_permissions() == other.get_permissions() && self.get_filetype() == other.get_filetype() && self.get_time() == other.get_time() } } impl Eq for FileAttrs {} impl FileAttrs { pub const fn new() -> Self { Self { flags: FileAttrsFlags::empty(), size: 0, uid: 0, gid: 0, st_mode: 0, atime: UnixTimeStamp::unix_epoch(), mtime: UnixTimeStamp::unix_epoch(), } } pub fn set_size(&mut self, size: u64) { self.flags |= FileAttrsFlags::SIZE; self.size = size; } pub fn set_id(&mut self, uid: u32, gid: u32) { self.flags |= FileAttrsFlags::ID; self.uid = uid; self.gid = gid; } pub fn set_permissions(&mut self, permissions: Permissions) { self.flags |= FileAttrsFlags::PERMISSIONS; let filetype = self.st_mode & S_IFMT; self.st_mode = filetype | permissions.bits(); } pub fn set_time(&mut self, atime: UnixTimeStamp, mtime: UnixTimeStamp) { self.flags |= FileAttrsFlags::TIME; self.atime = atime; self.mtime = mtime; } fn has_attr(&self, flag: FileAttrsFlags) -> bool { self.flags.intersects(flag) } fn getter_impl(&self, flag: FileAttrsFlags, f: impl FnOnce() -> T) -> Option { if self.has_attr(flag) { Some(f()) } else { None } } pub fn get_size(&self) -> Option { self.getter_impl(FileAttrsFlags::SIZE, || self.size) } /// Return uid and gid pub fn get_id(&self) -> Option<(u32, u32)> { self.getter_impl(FileAttrsFlags::ID, || (self.uid, self.gid)) } pub fn get_permissions(&self) -> Option { self.getter_impl(FileAttrsFlags::PERMISSIONS, || { Permissions::from_bits_truncate(self.st_mode) }) } /// filetype is only set by the sftp-server. pub fn get_filetype(&self) -> Option { self.getter_impl(FileAttrsFlags::PERMISSIONS, || { let filetype = self.st_mode & S_IFMT; if filetype == 0 { None } else { Some(FileType::from_u32(filetype).unwrap()) } }) .flatten() } /// Return atime and mtime pub fn get_time(&self) -> Option<(UnixTimeStamp, UnixTimeStamp)> { self.getter_impl(FileAttrsFlags::TIME, || (self.atime, self.mtime)) } } impl Serialize for FileAttrs { fn serialize(&self, serializer: S) -> Result { // dummy size since ssh_format doesn't care let mut tuple_serializer = serializer.serialize_tuple(1)?; tuple_serializer.serialize_element(&self.flags)?; if let Some(size) = self.get_size() { tuple_serializer.serialize_element(&size)?; } if let Some((uid, gid)) = self.get_id() { tuple_serializer.serialize_element(&uid)?; tuple_serializer.serialize_element(&gid)?; } if let Some(perm) = self.get_permissions() { tuple_serializer.serialize_element(&perm.bits())?; } if let Some((atime, mtime)) = self.get_time() { tuple_serializer.serialize_element(&atime.into_raw())?; tuple_serializer.serialize_element(&mtime.into_raw())?; } tuple_serializer.end() } } impl_visitor!(FileAttrs, FileAttrVisitor, "File attributes", seq, { let mut iter = SeqIter::new(seq); let mut attrs = FileAttrs { flags: iter.get_next()?, ..Default::default() }; if attrs.has_attr(FileAttrsFlags::SIZE) { attrs.size = iter.get_next()?; } if attrs.has_attr(FileAttrsFlags::ID) { attrs.uid = iter.get_next()?; attrs.gid = iter.get_next()?; } if attrs.has_attr(FileAttrsFlags::PERMISSIONS) { attrs.st_mode = iter.get_next()?; let filetype = attrs.st_mode & S_IFMT; // If filetype is specified, then make sure it is valid. if filetype != 0 && FileType::from_u32(filetype).is_none() { return Err(V::Error::invalid_value( Unexpected::Unsigned(filetype as u64), &"Expected valid filetype specified in POSIX", )); } } let into_timestamp = |elapsed: u32| { let timestamp = UnixTimeStamp::from_raw(elapsed).ok_or_else(|| { V::Error::invalid_value( Unexpected::Unsigned(elapsed as u64), &"Invalid UnixTimeStamp (seconds)", ) })?; Ok(timestamp) }; if attrs.has_attr(FileAttrsFlags::TIME) { attrs.atime = into_timestamp(iter.get_next()?)?; attrs.mtime = into_timestamp(iter.get_next()?)?; } if attrs.has_attr(FileAttrsFlags::EXTENSIONS) { let extension_pairs: u32 = iter.get_next()?; for _i in 0..extension_pairs { let _name: &[u8] = iter.get_next()?; let _value: &[u8] = iter.get_next()?; } } attrs.flags.remove(FileAttrsFlags::EXTENSIONS); Ok(attrs) }); #[cfg(test)] mod tests { use super::{FileAttrs, FileAttrsFlags, FileType, Permissions, UnixTimeStamp}; use super::constants::{ SSH_FILEXFER_ATTR_ACMODTIME, SSH_FILEXFER_ATTR_PERMISSIONS, SSH_FILEXFER_ATTR_SIZE, SSH_FILEXFER_ATTR_UIDGID, }; // Test getter and setters fn get_unix_timestamps() -> (UnixTimeStamp, UnixTimeStamp) { ( UnixTimeStamp::from_raw(2).unwrap(), UnixTimeStamp::from_raw(150).unwrap(), ) } #[test] fn test_set_get_size() { let mut attrs = FileAttrs::default(); attrs.set_size(2333); assert_eq!(attrs.get_size().unwrap(), 2333); } #[test] fn test_set_get_id() { let mut attrs = FileAttrs::default(); attrs.set_id(u32::MAX, 1000); assert_eq!(attrs.get_id().unwrap(), (u32::MAX, 1000)); } #[test] fn test_set_get_permissions() { let mut attrs = FileAttrs::default(); attrs.set_permissions(Permissions::SET_GID); assert_eq!(attrs.get_permissions().unwrap(), Permissions::SET_GID); } #[test] fn test_set_get_time() { let (atime, mtime) = get_unix_timestamps(); let mut attrs = FileAttrs::default(); attrs.set_time(atime, mtime); assert_eq!(attrs.get_time().unwrap(), (atime, mtime)); } // Test Serialize and Deserialize use serde_test::{assert_de_tokens, assert_tokens, Token}; #[test] fn test_file_attr_flags() { assert_tokens(&FileAttrsFlags::empty(), &[Token::U32(0)]); assert_tokens(&FileAttrsFlags::SIZE, &[Token::U32(SSH_FILEXFER_ATTR_SIZE)]); assert_tokens(&FileAttrsFlags::ID, &[Token::U32(SSH_FILEXFER_ATTR_UIDGID)]); assert_tokens( &FileAttrsFlags::PERMISSIONS, &[Token::U32(SSH_FILEXFER_ATTR_PERMISSIONS)], ); assert_tokens( &FileAttrsFlags::TIME, &[Token::U32(SSH_FILEXFER_ATTR_ACMODTIME)], ); } fn init_attrs(f: impl FnOnce(&mut FileAttrs)) -> FileAttrs { let mut attrs = FileAttrs::default(); f(&mut attrs); attrs } #[test] fn test_ser_de_size() { assert_tokens( &init_attrs(|attrs| attrs.set_size(2333)), &[ Token::Tuple { len: 1 }, Token::U32(SSH_FILEXFER_ATTR_SIZE), Token::U64(2333), Token::TupleEnd, ], ); } #[test] fn test_ser_de_id() { assert_tokens( &init_attrs(|attrs| attrs.set_id(u32::MAX, 1000)), &[ Token::Tuple { len: 1 }, Token::U32(SSH_FILEXFER_ATTR_UIDGID), Token::U32(u32::MAX), Token::U32(1000), Token::TupleEnd, ], ); } #[test] fn test_ser_de_permissions_and_filetype() { assert_tokens( &init_attrs(|attrs| attrs.set_permissions(Permissions::WRITE_BY_OTHER)), &[ Token::Tuple { len: 1 }, Token::U32(SSH_FILEXFER_ATTR_PERMISSIONS), Token::U32(Permissions::WRITE_BY_OTHER.bits()), Token::TupleEnd, ], ); assert_de_tokens( &init_attrs(|attrs| { attrs.set_permissions(Permissions::WRITE_BY_OTHER); attrs.st_mode = Permissions::WRITE_BY_OTHER.bits() | FileType::Socket as u32; }), &[ Token::Tuple { len: 1 }, Token::U32(SSH_FILEXFER_ATTR_PERMISSIONS), Token::U32(Permissions::WRITE_BY_OTHER.bits() | FileType::Socket as u32), Token::TupleEnd, ], ); } #[test] fn test_ser_de_time() { let (atime, mtime) = get_unix_timestamps(); assert_tokens( &init_attrs(|attrs| attrs.set_time(atime, mtime)), &[ Token::Tuple { len: 1 }, Token::U32(SSH_FILEXFER_ATTR_ACMODTIME), Token::U32(atime.into_raw()), Token::U32(mtime.into_raw()), Token::TupleEnd, ], ); } #[test] fn test_ser_de_all() { let (atime, mtime) = get_unix_timestamps(); assert_tokens( &init_attrs(|attrs| { attrs.set_size(2333); attrs.set_id(u32::MAX, 1000); attrs.set_permissions(Permissions::READ_BY_OWNER); attrs.set_time(atime, mtime); }), &[ Token::Tuple { len: 1 }, Token::U32( SSH_FILEXFER_ATTR_SIZE | SSH_FILEXFER_ATTR_UIDGID | SSH_FILEXFER_ATTR_PERMISSIONS | SSH_FILEXFER_ATTR_ACMODTIME, ), Token::U64(2333), // size Token::U32(u32::MAX), // uid Token::U32(1000), // gid Token::U32(Permissions::READ_BY_OWNER.bits()), // permissions Token::U32(atime.into_raw()), // atime Token::U32(mtime.into_raw()), // mtime Token::TupleEnd, ], ); } } openssh-sftp-protocol-0.24.0/src/handle.rs000064400000000000000000000016751046102023000166000ustar 00000000000000use std::{ borrow::{Borrow, ToOwned}, convert::AsRef, ops::Deref, }; use vec_strings::SmallArrayBox; #[derive(Debug, Clone)] #[repr(transparent)] pub struct HandleOwned(pub(crate) SmallArrayBox); impl Deref for HandleOwned { type Target = Handle; fn deref(&self) -> &Self::Target { unsafe { &*(self.0.deref() as *const [u8] as *const Handle) } } } impl Borrow for HandleOwned { fn borrow(&self) -> &Handle { self.deref() } } impl AsRef for HandleOwned { fn as_ref(&self) -> &Handle { self.deref() } } #[derive(Debug, Eq, PartialEq, serde::Serialize)] #[repr(transparent)] pub struct Handle([u8]); impl Handle { pub const fn into_inner(&self) -> &[u8] { &self.0 } } impl ToOwned for Handle { type Owned = HandleOwned; fn to_owned(&self) -> Self::Owned { HandleOwned(SmallArrayBox::new(self.into_inner().iter().copied())) } } openssh-sftp-protocol-0.24.0/src/lib.rs000064400000000000000000000004311046102023000161000ustar 00000000000000pub extern crate serde; pub extern crate ssh_format; pub extern crate vec_strings; pub use openssh_sftp_protocol_error::*; mod handle; pub use handle::*; mod seq_iter; mod visitor; pub mod constants; pub mod file_attrs; pub mod open_options; pub mod request; pub mod response; openssh-sftp-protocol-0.24.0/src/open_options.rs000064400000000000000000000040671046102023000200570ustar 00000000000000#![forbid(unsafe_code)] use super::{constants, file_attrs::FileAttrs, request::OpenFileRequest}; use std::{borrow::Cow, path::Path}; #[derive(Debug, Copy, Clone)] pub struct OpenOptions { read: bool, write: bool, append: bool, } impl OpenOptions { pub const fn new() -> Self { Self { read: false, write: false, append: false, } } pub const fn read(mut self, read: bool) -> Self { self.read = read; self } pub const fn get_read(self) -> bool { self.read } pub const fn write(mut self, write: bool) -> Self { self.write = write; self } pub const fn get_write(self) -> bool { self.write || self.append } pub const fn append(mut self, append: bool) -> Self { self.append = append; self } pub const fn get_append(self) -> bool { self.append } pub const fn open(self, filename: Cow<'_, Path>) -> OpenFileRequest<'_> { let mut flags: u32 = 0; if self.read { flags |= constants::SSH_FXF_READ; } if self.write || self.append { flags |= constants::SSH_FXF_WRITE; } if self.append { flags |= constants::SSH_FXF_APPEND; } OpenFileRequest { filename, flags, attrs: FileAttrs::new(), } } pub const fn create( self, filename: Cow<'_, Path>, flags: CreateFlags, attrs: FileAttrs, ) -> OpenFileRequest<'_> { let mut openfile = self.open(filename); openfile.flags |= constants::SSH_FXF_CREAT | flags as u32; openfile.attrs = attrs; openfile } } #[derive(Debug, Copy, Clone)] #[repr(u32)] pub enum CreateFlags { None = 0, /// Forces an existing file with the same name to be truncated to zero /// length when creating a file. Trunc = constants::SSH_FXF_TRUNC, /// Causes the request to fail if the named file already exists. Excl = constants::SSH_FXF_EXCL, } openssh-sftp-protocol-0.24.0/src/request.rs000064400000000000000000000340551046102023000170330ustar 00000000000000#![forbid(unsafe_code)] use super::{constants, file_attrs::FileAttrs, open_options::OpenOptions, Handle}; use std::{borrow::Cow, path::Path}; use serde::{Serialize, Serializer}; use ssh_format::SerOutput; /// Response with `Response::Version`. pub struct Hello { pub version: u32, } impl Serialize for Hello { fn serialize(&self, serializer: S) -> Result { (constants::SSH_FXP_INIT, self.version).serialize(serializer) } } #[derive(Debug)] pub enum RequestInner<'a> { /// The response to this message will be either /// [`crate::response::ResponseInner::Handle`] (if the operation is successful) or /// [`crate::response::ResponseInner::Status`] /// (if the operation fails). Open(OpenFileRequest<'a>), /// Response will be [`crate::response::ResponseInner::Status`]. Close(Cow<'a, Handle>), /// In response to this request, the server will read as many bytes as it /// can from the file (up to `len'), and return them in a ResponseInner::Data /// message. /// /// If an error occurs or EOF is encountered before reading any /// data, the server will respond with [`crate::response::ResponseInner::Status`]. /// /// For normal disk files, it is guaranteed that this will read the specified /// number of bytes, or up to end of file. /// /// For e.g. device files this may return fewer bytes than requested. Read { handle: Cow<'a, Handle>, offset: u64, len: u32, }, /// Responds with a [`crate::response::ResponseInner::Status`] message. Remove(Cow<'a, Path>), /// Responds with a [`crate::response::ResponseInner::Status`] message. Rename { oldpath: Cow<'a, Path>, newpath: Cow<'a, Path>, }, /// Responds with a [`crate::response::ResponseInner::Status`] message. Mkdir { path: Cow<'a, Path>, attrs: FileAttrs, }, /// Responds with a [`crate::response::ResponseInner::Status`] message. Rmdir(Cow<'a, Path>), /// Responds with a [`crate::response::ResponseInner::Handle`] /// or a [`crate::response::ResponseInner::Status`] message. Opendir(Cow<'a, Path>), /// Responds with a [`crate::response::ResponseInner::Name`] or /// a [`crate::response::ResponseInner::Status`] message Readdir(Cow<'a, Handle>), /// Responds with [`crate::response::ResponseInner::Attrs`] or /// [`crate::response::ResponseInner::Status`]. Stat(Cow<'a, Path>), /// Responds with [`crate::response::ResponseInner::Attrs`] or /// [`crate::response::ResponseInner::Status`]. /// /// Does not follow symlink. Lstat(Cow<'a, Path>), /// Responds with [`crate::response::ResponseInner::Attrs`] or /// [`crate::response::ResponseInner::Status`]. Fstat(Cow<'a, Handle>), /// Responds with a [`crate::response::ResponseInner::Status`] message. Setstat { path: Cow<'a, Path>, attrs: FileAttrs, }, /// Responds with a [`crate::response::ResponseInner::Status`] message. Fsetstat { handle: Cow<'a, Handle>, attrs: FileAttrs, }, /// Responds with [`crate::response::ResponseInner::Name`] with a name and /// dummy attribute value or [`crate::response::ResponseInner::Status`] on error. Readlink(Cow<'a, Path>), /// Responds with a [`crate::response::ResponseInner::Status`] message. Symlink { linkpath: Cow<'a, Path>, targetpath: Cow<'a, Path>, }, /// Responds with [`crate::response::ResponseInner::Name`] with a name and /// dummy attribute value or [`crate::response::ResponseInner::Status`] on error. Realpath(Cow<'a, Path>), /// Responds with extended reply, with payload [`crate::response::Limits`]. /// /// Extension, only available if it is [`crate::response::Extensions::limits`] /// is returned by [`crate::response::ServerVersion`]. Limits, /// Same response as [`RequestInner::Realpath`]. /// /// Extension, only available if it is [`crate::response::Extensions::expand_path`] /// is returned by [`crate::response::ServerVersion`]. /// /// This supports canonicalisation of relative paths and those that need /// tilde-expansion, i.e. "~", "~/..." and "~user/...". /// /// These paths are expanded using shell-lilke rules and the resultant path /// is canonicalised similarly to [`RequestInner::Realpath`]. ExpandPath(Cow<'a, Path>), /// Same response as [`RequestInner::Setstat`]. /// /// Extension, only available if it is [`crate::response::Extensions::lsetstat`] /// is returned by [`crate::response::ServerVersion`]. Lsetstat(Cow<'a, Path>, FileAttrs), /// Responds with a [`crate::response::ResponseInner::Status`] message. /// /// Extension, only available if it is [`crate::response::Extensions::fsync`] /// is returned by [`crate::response::ServerVersion`]. Fsync(Cow<'a, Handle>), /// Responds with a [`crate::response::ResponseInner::Status`] message. /// /// Extension, only available if it is [`crate::response::Extensions::hardlink`] /// is returned by [`crate::response::ServerVersion`]. HardLink { oldpath: Cow<'a, Path>, newpath: Cow<'a, Path>, }, /// Responds with a [`crate::response::ResponseInner::Status`] message. /// /// Extension, only available if it is [`crate::response::Extensions::posix_rename`] /// is returned by [`crate::response::ServerVersion`]. PosixRename { oldpath: Cow<'a, Path>, newpath: Cow<'a, Path>, }, /// Responds with a [`crate::response::ResponseInner::Status`] message. /// /// Extension, only available if it is [`crate::response::Extensions::posix_rename`] /// is returned by [`crate::response::ServerVersion`]. /// /// For [openssh-portable], this is available from V_9_0_P1. /// /// 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. /// /// [openssh-portable]: https://github.com/openssh/openssh-portable Cp { read_from_handle: Cow<'a, Handle>, read_from_offset: u64, read_data_length: u64, write_to_handle: Cow<'a, Handle>, write_to_offset: u64, }, /// The 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. /// /// Responds with a [`crate::response::ResponseInner::Status`] message. /// /// The Write also includes any amount of custom data and its size is /// included in the size of the entire packet sent. Write { handle: Cow<'a, Handle>, offset: u64, data: Cow<'a, [u8]>, }, } #[derive(Debug)] pub struct Request<'a> { pub request_id: u32, pub inner: RequestInner<'a>, } impl Serialize for Request<'_> { fn serialize(&self, serializer: S) -> Result { use RequestInner::*; let request_id = self.request_id; match &self.inner { Open(params) => (constants::SSH_FXP_OPEN, request_id, params).serialize(serializer), Close(handle) => (constants::SSH_FXP_CLOSE, request_id, handle).serialize(serializer), Read { handle, offset, len, } => (constants::SSH_FXP_READ, request_id, handle, *offset, *len).serialize(serializer), Remove(filename) => { (constants::SSH_FXP_REMOVE, request_id, filename).serialize(serializer) } Rename { oldpath, newpath } => { (constants::SSH_FXP_RENAME, request_id, oldpath, newpath).serialize(serializer) } Mkdir { path, attrs } => { (constants::SSH_FXP_MKDIR, request_id, path, attrs).serialize(serializer) } Rmdir(path) => (constants::SSH_FXP_RMDIR, request_id, path).serialize(serializer), Opendir(path) => (constants::SSH_FXP_OPENDIR, request_id, path).serialize(serializer), Readdir(handle) => { (constants::SSH_FXP_READDIR, request_id, handle).serialize(serializer) } Stat(path) => (constants::SSH_FXP_STAT, request_id, path).serialize(serializer), Lstat(path) => (constants::SSH_FXP_LSTAT, request_id, path).serialize(serializer), Fstat(handle) => (constants::SSH_FXP_FSTAT, request_id, handle).serialize(serializer), Setstat { path, attrs } => { (constants::SSH_FXP_SETSTAT, request_id, path, attrs).serialize(serializer) } Fsetstat { handle, attrs } => { (constants::SSH_FXP_FSETSTAT, request_id, handle, attrs).serialize(serializer) } Readlink(path) => (constants::SSH_FXP_READLINK, request_id, path).serialize(serializer), Symlink { linkpath, targetpath, } => { (constants::SSH_FXP_SYMLINK, request_id, targetpath, linkpath).serialize(serializer) } Realpath(path) => (constants::SSH_FXP_REALPATH, request_id, path).serialize(serializer), Limits => ( constants::SSH_FXP_EXTENDED, request_id, constants::EXT_NAME_LIMITS.0, ) .serialize(serializer), ExpandPath(path) => ( constants::SSH_FXP_EXTENDED, request_id, constants::EXT_NAME_EXPAND_PATH.0, path, ) .serialize(serializer), Lsetstat(path, attrs) => ( constants::SSH_FXP_EXTENDED, request_id, constants::EXT_NAME_LSETSTAT.0, path, attrs, ) .serialize(serializer), Fsync(handle) => ( constants::SSH_FXP_EXTENDED, request_id, constants::EXT_NAME_FSYNC.0, handle, ) .serialize(serializer), HardLink { oldpath, newpath } => ( constants::SSH_FXP_EXTENDED, request_id, constants::EXT_NAME_HARDLINK.0, oldpath, newpath, ) .serialize(serializer), PosixRename { oldpath, newpath } => ( constants::SSH_FXP_EXTENDED, request_id, constants::EXT_NAME_POSIX_RENAME.0, oldpath, newpath, ) .serialize(serializer), Cp { read_from_handle, read_from_offset, read_data_length, write_to_handle, write_to_offset, } => ( constants::SSH_FXP_EXTENDED, request_id, constants::EXT_NAME_COPY_DATA.0, read_from_handle, read_from_offset, read_data_length, write_to_handle, write_to_offset, ) .serialize(serializer), Write { handle, offset, data, } => (constants::SSH_FXP_WRITE, request_id, handle, offset, data).serialize(serializer), } } } impl Request<'_> { /// The 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. /// /// Responds with a [`crate::response::ResponseInner::Status`] message. /// /// The Write also includes any amount of custom data and its size is /// included in the size of the entire packet sent. /// /// Return the serialized header (including the 4-byte size). /// /// * `serializer` - must be empty pub fn serialize_write_request( serializer: &mut ssh_format::Serializer, request_id: u32, handle: Cow<'_, Handle>, offset: u64, data_len: u32, ) -> ssh_format::Result<[u8; 4]> { ( constants::SSH_FXP_WRITE, request_id, handle, offset, data_len, ) .serialize(&mut *serializer)?; serializer.create_header(data_len) } } #[derive(Clone, Debug, Serialize)] pub struct OpenFileRequest<'a> { pub(crate) filename: Cow<'a, Path>, pub(crate) flags: u32, pub(crate) attrs: FileAttrs, } impl<'a> OpenFileRequest<'a> { /// Open file in read only mode pub const fn open(filename: Cow<'a, Path>) -> Self { OpenOptions::new().read(true).open(filename) } } openssh-sftp-protocol-0.24.0/src/response.rs000064400000000000000000000173551046102023000172050ustar 00000000000000#![forbid(unsafe_code)] use super::{ file_attrs::FileAttrs, {constants, seq_iter::SeqIter, visitor::impl_visitor, HandleOwned}, }; use std::{borrow::Cow, iter::FusedIterator, path::Path, str::from_utf8}; use bitflags::bitflags; use openssh_sftp_protocol_error::{ErrMsg, ErrorCode}; use serde::{ de::{Deserializer, Error, Unexpected}, Deserialize, }; bitflags! { /// The extension that the sftp-server supports. #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Extensions: u16 { const POSIX_RENAME = 1 << 0; const STATVFS = 1 << 1; const FSTATVFS= 1<< 2; const HARDLINK= 1<< 3; const FSYNC= 1<< 4; const LSETSTAT= 1<< 5; const LIMITS= 1<< 6; const EXPAND_PATH= 1<< 7; const COPY_DATA= 1<< 8; } } #[derive(Debug, Clone, Copy)] pub struct ServerVersion { pub version: u32, pub extensions: Extensions, } impl ServerVersion { /// * `bytes` - should not include the initial 4-byte which server /// as the length of the whole packet. pub fn deserialize<'de, It>( de: &mut ssh_format::Deserializer<'de, It>, ) -> ssh_format::Result where It: FusedIterator + Iterator, { let packet_type = u8::deserialize(&mut *de)?; if packet_type != constants::SSH_FXP_VERSION { return Err(ssh_format::Error::custom("Unexpected response")); } let version = u32::deserialize(&mut *de)?; let mut extensions = Extensions::default(); while de.has_remaining_data() { // sftp v3 does not specify the encoding of extension names and revisions. // // Read both name and revision before continue parsing them // so that if the current iteration is skipped by 'continue', // the next iteration can continue read in extensions without error. let name = Cow::<'_, [u8]>::deserialize(&mut *de)?; let revision = Cow::<'_, [u8]>::deserialize(&mut *de)?; let optional_extension_pair = (|| { let name = from_utf8(&name).ok()?; let revision = from_utf8(&revision).ok()?; let revision: u64 = revision.parse().ok()?; Some((name, revision)) })(); let extension_pair = if let Some(extension_pair) = optional_extension_pair { extension_pair } else { continue; }; match extension_pair { constants::EXT_NAME_POSIX_RENAME => { extensions |= Extensions::POSIX_RENAME; } constants::EXT_NAME_STATVFS => { extensions |= Extensions::STATVFS; } constants::EXT_NAME_FSTATVFS => { extensions |= Extensions::FSTATVFS; } constants::EXT_NAME_HARDLINK => { extensions |= Extensions::HARDLINK; } constants::EXT_NAME_FSYNC => { extensions |= Extensions::FSYNC; } constants::EXT_NAME_LSETSTAT => { extensions |= Extensions::LSETSTAT; } constants::EXT_NAME_LIMITS => { extensions |= Extensions::LIMITS; } constants::EXT_NAME_EXPAND_PATH => { extensions |= Extensions::EXPAND_PATH; } constants::EXT_NAME_COPY_DATA => { extensions |= Extensions::COPY_DATA; } _ => (), } } Ok(Self { version, extensions, }) } } /// Payload of extended reply response when [`crate::request::RequestInner::Limits`] /// is sent. #[derive(Debug, Copy, Clone, Eq, PartialEq, Deserialize)] pub struct Limits { pub packet_len: u64, pub read_len: u64, pub write_len: u64, pub open_handles: u64, } #[derive(Debug)] pub enum ResponseInner { Status { status_code: StatusCode, err_msg: ErrMsg, }, Handle(HandleOwned), Name(Box<[NameEntry]>), Attrs(FileAttrs), } #[derive(Debug)] pub struct Response { pub response_id: u32, pub response_inner: ResponseInner, } impl Response { /// Return true if the response is a data response. pub fn is_data(packet_type: u8) -> bool { packet_type == constants::SSH_FXP_DATA } /// Return true if the response is a extended reply response. pub fn is_extended_reply(packet_type: u8) -> bool { packet_type == constants::SSH_FXP_EXTENDED_REPLY } } impl_visitor!( Response, ResponseVisitor, "Expects a u8 type and payload", seq, { use constants::*; use ResponseInner::*; let mut iter = SeqIter::new(seq); let discriminant: u8 = iter.get_next()?; let response_id: u32 = iter.get_next()?; let response_inner = match discriminant { SSH_FXP_STATUS => Status { status_code: iter.get_next()?, err_msg: iter.get_next()?, }, SSH_FXP_HANDLE => Handle(HandleOwned(iter.get_next()?)), SSH_FXP_NAME => { let len: u32 = iter.get_next()?; let len = len as usize; let mut entries = Vec::::with_capacity(len); for _ in 0..len { let filename: Box = iter.get_next()?; let _longname: &[u8] = iter.get_next()?; let attrs: FileAttrs = iter.get_next()?; entries.push(NameEntry { filename, attrs }); } Name(entries.into_boxed_slice()) } SSH_FXP_ATTRS => Attrs(iter.get_next()?), _ => { return Err(Error::invalid_value( Unexpected::Unsigned(discriminant as u64), &"Invalid packet type", )) } }; Ok(Response { response_id, response_inner, }) } ); #[derive(Debug, Copy, Clone)] pub enum StatusCode { Success, Failure(ErrorCode), /// Indicates end-of-file condition. /// /// For RequestInner::Read it means that no more data is available in the file, /// and for RequestInner::Readdir it indicates that no more files are contained /// in the directory. Eof, } impl<'de> Deserialize<'de> for StatusCode { fn deserialize>(deserializer: D) -> Result { use constants::*; use ErrorCode::*; let discriminant = ::deserialize(deserializer)?; match discriminant { SSH_FX_OK => Ok(StatusCode::Success), SSH_FX_EOF => Ok(StatusCode::Eof), SSH_FX_NO_SUCH_FILE => Ok(StatusCode::Failure(NoSuchFile)), SSH_FX_PERMISSION_DENIED => Ok(StatusCode::Failure(PermDenied)), SSH_FX_FAILURE => Ok(StatusCode::Failure(Failure)), SSH_FX_BAD_MESSAGE => Ok(StatusCode::Failure(BadMessage)), SSH_FX_OP_UNSUPPORTED => Ok(StatusCode::Failure(OpUnsupported)), SSH_FX_NO_CONNECTION | SSH_FX_CONNECTION_LOST => Err(Error::invalid_value( Unexpected::Unsigned(discriminant as u64), &"Server MUST NOT return SSH_FX_NO_CONNECTION or SSH_FX_CONNECTION_LOST \ for they are pseudo-error that can only be generated locally.", )), _ => Ok(StatusCode::Failure(Unknown)), } } } /// Entry in [`ResponseInner::Name`] #[derive(Debug, Clone)] pub struct NameEntry { pub filename: Box, pub attrs: FileAttrs, } openssh-sftp-protocol-0.24.0/src/seq_iter.rs000064400000000000000000000011311046102023000171430ustar 00000000000000#![forbid(unsafe_code)] use std::marker::PhantomData; use serde::{ de::{Error, SeqAccess}, Deserialize, }; pub(crate) struct SeqIter<'de, V: SeqAccess<'de>>(usize, V, PhantomData<&'de ()>); impl<'de, V: SeqAccess<'de>> SeqIter<'de, V> { pub(crate) fn new(seq: V) -> Self { Self(0, seq, PhantomData) } pub(crate) fn get_next>(&mut self) -> Result { let res = self .1 .next_element()? .ok_or_else(|| Error::invalid_length(self.0, &"Not long enough")); self.0 += 1; res } } openssh-sftp-protocol-0.24.0/src/visitor.rs000064400000000000000000000025441046102023000170400ustar 00000000000000#![forbid(unsafe_code)] pub(crate) use std::fmt; pub(crate) use serde::{ de::{Deserializer, SeqAccess, Visitor}, Deserialize, }; macro_rules! impl_visitor { ($type:ident, $visitor_name: ident, $expecting_msg:expr, $seq_name: ident, $impl:block) => { impl<'de> crate::visitor::Deserialize<'de> for $type { fn deserialize>( deserializer: D, ) -> Result { struct $visitor_name; impl<'de> crate::visitor::Visitor<'de> for $visitor_name { type Value = $type; fn expecting( &self, formatter: &mut crate::visitor::fmt::Formatter, ) -> crate::visitor::fmt::Result { write!(formatter, $expecting_msg) } fn visit_seq(self, $seq_name: V) -> Result where V: crate::visitor::SeqAccess<'de>, { $impl } } // Pass a dummy size here since ssh_format doesn't care deserializer.deserialize_tuple(u32::MAX as usize, $visitor_name) } } }; } pub(crate) use impl_visitor;