feat: new yazi-sftp crate (#3166)

This commit is contained in:
三咲雅 misaki masa 2025-09-14 02:50:28 +08:00 committed by GitHub
parent f0502a46b7
commit 4a3710b1c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 3963 additions and 94 deletions

1262
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -32,9 +32,9 @@ foldhash = "0.2.0"
futures = "0.3.31" futures = "0.3.31"
globset = "0.4.16" globset = "0.4.16"
hashbrown = { version = "0.16.0", features = [ "serde" ] } hashbrown = { version = "0.16.0", features = [ "serde" ] }
indexmap = { version = "2.11.0", features = [ "serde" ] } indexmap = { version = "2.11.1", features = [ "serde" ] }
libc = "0.2.175" libc = "0.2.175"
lru = "0.16.0" lru = "0.16.1"
mlua = { version = "0.11.3", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serde" ] } mlua = { version = "0.11.3", features = [ "anyhow", "async", "error-send", "lua54", "macros", "serde" ] }
objc = "0.2.7" objc = "0.2.7"
ordered-float = { version = "5.0.0", features = [ "serde" ] } ordered-float = { version = "5.0.0", features = [ "serde" ] }
@ -42,6 +42,7 @@ parking_lot = "0.12.4"
paste = "1.0.15" paste = "1.0.15"
ratatui = { version = "0.29.0", features = [ "unstable-rendered-line-info", "unstable-widget-ref" ] } ratatui = { version = "0.29.0", features = [ "unstable-rendered-line-info", "unstable-widget-ref" ] }
regex = "1.11.2" regex = "1.11.2"
russh = { version = "0.54.3", default-features = false, features = [ "ring", "rsa" ] }
scopeguard = "1.2.0" scopeguard = "1.2.0"
serde = { version = "1.0.219", features = [ "derive" ] } serde = { version = "1.0.219", features = [ "derive" ] }
serde_json = "1.0.143" serde_json = "1.0.143"

View file

@ -33,7 +33,7 @@ libc = { workspace = true }
uzers = { workspace = true } uzers = { workspace = true }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.60.2", features = [ "Win32_Storage_FileSystem" ] } windows-sys = { version = "0.61.0", features = [ "Win32_Storage_FileSystem" ] }
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
core-foundation-sys = { workspace = true } core-foundation-sys = { workspace = true }

View file

@ -49,7 +49,7 @@ libc = { workspace = true }
uzers = { workspace = true } uzers = { workspace = true }
[target."cfg(windows)".dependencies] [target."cfg(windows)".dependencies]
windows-sys = { version = "0.60.2", features = [ "Win32_System_JobObjects" ] } windows-sys = { version = "0.61.0", features = [ "Win32_System_JobObjects" ] }
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }

19
yazi-sftp/Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
name = "yazi-sftp"
version = "0.1.0"
edition = "2024"
license = "MIT"
authors = [ "AspectUnk", "sxyazi <sxyazi@gmail.com>" ]
description = "Yazi SFTP client"
homepage = "https://yazi-rs.github.io"
repository = "https://github.com/sxyazi/yazi"
[dependencies]
bitflags = { workspace = true }
parking_lot = { workspace = true }
russh = { workspace = true }
serde = { workspace = true }
tokio = { workspace = true }
[target."cfg(windows)".dependencies]
os_str_bytes = { version = "7.1.1", default-features = false, features = [ "conversions" ] }

201
yazi-sftp/LICENSE Normal file
View file

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

15
yazi-sftp/README.md Normal file
View file

@ -0,0 +1,15 @@
## yazi-sftp
A fork of [`russh-sftp`](https://github.com/AspectUnk/russh-sftp) used by Yazi, with some changes:
- Supports paths containing invalid UTF-8
- Supports retrieving file nlink, username, and group
- Uses generic return parameters for a more idiomatic API, e.g.:
```rust
let attrs: responses::Attrs = session.send(requests::Stat::new(path)).await?;
```
- Reduced dependencies
- Performance optimizations:
- Copy-on-write for all packets to avoid unnecessary memory allocation
- Packet lengths are precomputed to avoid secondary allocations
- Avoids cloning buffers in `AsyncRead` and `AsyncWrite` implementations

100
yazi-sftp/src/byte_str.rs Normal file
View file

@ -0,0 +1,100 @@
use std::{borrow::Cow, ffi::{OsStr, OsString}, ops::Deref, path::{Path, PathBuf}};
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct ByteStr<'a>(Cow<'a, [u8]>);
impl Deref for ByteStr<'_> {
type Target = [u8];
fn deref(&self) -> &Self::Target { &self.0 }
}
impl<'a> From<&'a str> for ByteStr<'a> {
fn from(value: &'a str) -> Self { ByteStr(Cow::Borrowed(value.as_bytes())) }
}
impl<'a> From<&'a ByteStr<'a>> for ByteStr<'a> {
fn from(value: &'a ByteStr) -> Self { ByteStr(Cow::Borrowed(&value.0)) }
}
impl<'a> From<&'a OsStr> for ByteStr<'a> {
fn from(value: &'a OsStr) -> Self {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
ByteStr(Cow::Borrowed(value.as_bytes()))
}
#[cfg(windows)]
{
use os_str_bytes::OsStrBytes;
ByteStr(value.to_raw_bytes())
}
}
}
impl<'a> From<&'a Path> for ByteStr<'a> {
fn from(value: &'a Path) -> Self { ByteStr::from(value.as_os_str()) }
}
impl PartialEq<&str> for ByteStr<'_> {
fn eq(&self, other: &&str) -> bool { self.0 == other.as_bytes() }
}
impl<'a> ByteStr<'a> {
pub fn to_os_str(&self) -> Cow<'_, OsStr> {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
OsStr::from_bytes(&self.0).into()
}
#[cfg(windows)]
{
use os_str_bytes::OsStrBytes;
OsStr::assert_from_raw_bytes(self.0.as_ref())
}
}
pub fn into_os_string(self) -> OsString {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStringExt;
OsString::from_vec(self.0.into_owned())
}
#[cfg(windows)]
{
use os_str_bytes::OsStrBytes;
OsStr::assert_from_raw_bytes(self.0).into_owned()
}
}
pub fn to_path(&self) -> Cow<'_, Path> {
match self.to_os_str() {
Cow::Borrowed(s) => Path::new(s).into(),
Cow::Owned(s) => PathBuf::from(s).into(),
}
}
pub fn into_path(self) -> PathBuf {
match self.0 {
Cow::Borrowed(_) => self.to_path().into_owned(),
Cow::Owned(_) => self.into_os_string().into(),
}
}
pub fn join(&self, other: impl Into<ByteStr<'a>>) -> PathBuf {
let other = other.into();
match self.to_path() {
Cow::Borrowed(p) => p.join(other.to_path()),
Cow::Owned(mut p) => {
p.push(other.to_path());
p
}
}
}
pub(super) unsafe fn from_str_bytes_unchecked(bytes: &'a [u8]) -> Self {
Self(Cow::Borrowed(bytes))
}
}

375
yazi-sftp/src/de.rs Normal file
View file

@ -0,0 +1,375 @@
use serde::{Deserializer as _, de::{EnumAccess, MapAccess, SeqAccess, VariantAccess, value::U32Deserializer}};
use crate::Error;
pub(super) struct Deserializer<'a> {
input: &'a [u8],
}
impl<'a> Deserializer<'a> {
pub(super) fn once<'de, T>(input: &'de [u8]) -> Result<T, Error>
where
T: serde::Deserialize<'de>,
{
let mut de = Deserializer { input };
let t = T::deserialize(&mut de)?;
if !de.input.is_empty() {
return Err(Error::serde("trailing bytes"));
}
Ok(t)
}
}
impl<'de> serde::Deserializer<'de> for &mut Deserializer<'de> {
type Error = Error;
fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
let len = self.input.len();
visitor.visit_seq(SeqDeserializer { de: self, remaining: len })
}
fn deserialize_bool<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(Error::serde("bool not supported"))
}
fn deserialize_i8<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(Error::serde("i8 not supported"))
}
fn deserialize_i16<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(Error::serde("i16 not supported"))
}
fn deserialize_i32<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(Error::serde("i32 not supported"))
}
fn deserialize_i64<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(Error::serde("i64 not supported"))
}
fn deserialize_u8<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
let b = *self.input.first().ok_or(Error::serde("u8 not enough"))?;
self.input = &self.input[1..];
visitor.visit_u8(b)
}
fn deserialize_u16<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(Error::serde("u16 not supported"))
}
fn deserialize_u32<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
let b: [u8; 4] = self.input.get(..4).ok_or(Error::serde("u32 not enough"))?.try_into().unwrap();
self.input = &self.input[4..];
visitor.visit_u32(u32::from_be_bytes(b))
}
fn deserialize_u64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
let b: [u8; 8] = self.input.get(..8).ok_or(Error::serde("u64 not enough"))?.try_into().unwrap();
self.input = &self.input[8..];
visitor.visit_u64(u64::from_be_bytes(b))
}
fn deserialize_f32<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(Error::serde("f32 not supported"))
}
fn deserialize_f64<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(Error::serde("f64 not supported"))
}
fn deserialize_char<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(Error::serde("char not supported"))
}
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
let len: [u8; 4] =
self.input.get(..4).ok_or(Error::serde("invalid string length"))?.try_into().unwrap();
let len = u32::from_be_bytes(len) as usize;
self.input = &self.input[4..];
let b = self.input.get(..len).ok_or(Error::serde("string not enough"))?;
self.input = &self.input[len..];
visitor.visit_str(str::from_utf8(b).map_err(|e| Error::serde(e.to_string()))?)
}
fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
self.deserialize_str(visitor)
}
fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
let len: [u8; 4] =
self.input.get(..4).ok_or(Error::serde("invalid bytes length"))?.try_into().unwrap();
let len = u32::from_be_bytes(len) as usize;
let b = self.input.get(4..4 + len).ok_or(Error::serde("bytes not enough"))?;
self.input = &self.input[4 + len..];
visitor.visit_bytes(b)
}
fn deserialize_byte_buf<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
self.deserialize_bytes(visitor)
}
fn deserialize_option<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(Error::serde("option not supported"))
}
fn deserialize_unit<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_unit()
}
fn deserialize_unit_struct<V>(
self,
_name: &'static str,
visitor: V,
) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_unit()
}
fn deserialize_newtype_struct<V>(
self,
_name: &'static str,
visitor: V,
) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_newtype_struct(self)
}
fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
let len: [u8; 4] =
self.input.get(..4).ok_or(Error::serde("invalid seq length"))?.try_into().unwrap();
self.input = &self.input[4..];
visitor.visit_seq(SeqDeserializer { de: self, remaining: u32::from_be_bytes(len) as _ })
}
fn deserialize_tuple<V>(self, len: usize, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_seq(SeqDeserializer { de: self, remaining: len })
}
fn deserialize_tuple_struct<V>(
self,
_name: &'static str,
len: usize,
visitor: V,
) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
self.deserialize_tuple(len, visitor)
}
fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_map(MapDeserializer { de: self })
}
fn deserialize_struct<V>(
self,
_name: &'static str,
fields: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
self.deserialize_tuple(fields.len(), visitor)
}
fn deserialize_enum<V>(
self,
_name: &'static str,
_variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_enum(self)
}
fn deserialize_identifier<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(Error::serde("identifier not supported"))
}
fn deserialize_ignored_any<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
Err(Error::serde("ignored any not supported"))
}
fn is_human_readable(&self) -> bool { false }
}
struct SeqDeserializer<'a, 'de: 'a> {
de: &'a mut Deserializer<'de>,
remaining: usize,
}
impl<'de> SeqAccess<'de> for SeqDeserializer<'_, 'de> {
type Error = Error;
fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Self::Error>
where
T: serde::de::DeserializeSeed<'de>,
{
if self.remaining == 0 {
Ok(None)
} else {
self.remaining -= 1;
seed.deserialize(&mut *self.de).map(Some)
}
}
fn size_hint(&self) -> Option<usize> { Some(self.remaining) }
}
struct MapDeserializer<'a, 'de: 'a> {
de: &'a mut Deserializer<'de>,
}
impl<'de> MapAccess<'de> for MapDeserializer<'_, 'de> {
type Error = Error;
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
where
K: serde::de::DeserializeSeed<'de>,
{
if self.de.input.is_empty() { Ok(None) } else { seed.deserialize(&mut *self.de).map(Some) }
}
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
where
V: serde::de::DeserializeSeed<'de>,
{
seed.deserialize(&mut *self.de)
}
}
impl<'de> VariantAccess<'de> for &mut Deserializer<'de> {
type Error = Error;
fn unit_variant(self) -> Result<(), Self::Error> { Ok(()) }
fn newtype_variant_seed<T>(self, seed: T) -> Result<T::Value, Self::Error>
where
T: serde::de::DeserializeSeed<'de>,
{
seed.deserialize(self)
}
fn tuple_variant<V>(self, len: usize, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
use serde::Deserializer;
self.deserialize_tuple(len, visitor)
}
fn struct_variant<V>(
self,
fields: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
self.deserialize_tuple(fields.len(), visitor)
}
}
impl<'de> EnumAccess<'de> for &mut Deserializer<'de> {
type Error = Error;
type Variant = Self;
fn variant_seed<V>(self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error>
where
V: serde::de::DeserializeSeed<'de>,
{
let b: [u8; 4] =
self.input.get(..4).ok_or(Error::serde("enum not enough"))?.try_into().unwrap();
self.input = &self.input[4..];
let de = U32Deserializer::<Self::Error>::new(u32::from_be_bytes(b));
Ok((seed.deserialize(de)?, self))
}
}

66
yazi-sftp/src/error.rs Normal file
View file

@ -0,0 +1,66 @@
use std::borrow::Cow;
use crate::responses;
#[derive(Debug)]
pub enum Error {
IO(std::io::Error),
Serde(Cow<'static, str>),
Status(responses::Status),
Packet(&'static str),
Timeout,
Unsupported,
Custom(Cow<'static, str>),
}
impl Error {
pub(super) fn serde(s: impl Into<Cow<'static, str>>) -> Self { Self::Serde(s.into()) }
pub(super) fn custom(s: impl Into<Cow<'static, str>>) -> Self { Self::Custom(s.into()) }
}
impl serde::ser::Error for Error {
fn custom<T: std::fmt::Display>(msg: T) -> Self { Self::serde(msg.to_string()) }
}
impl serde::de::Error for Error {
fn custom<T: std::fmt::Display>(msg: T) -> Self { Self::serde(msg.to_string()) }
}
impl From<Error> for std::io::Error {
fn from(err: Error) -> Self {
match err {
Error::IO(e) => e,
Error::Serde(_) => Self::new(std::io::ErrorKind::InvalidData, err),
Error::Status(_) => Self::other(err),
Error::Packet(_) => Self::new(std::io::ErrorKind::InvalidData, err),
Error::Timeout => Self::new(std::io::ErrorKind::TimedOut, err),
Error::Unsupported => Self::new(std::io::ErrorKind::Unsupported, err),
Error::Custom(_) => Self::other(err),
}
}
}
impl<T> From<tokio::sync::mpsc::error::SendError<T>> for Error {
fn from(_: tokio::sync::mpsc::error::SendError<T>) -> Self { Self::custom("channel closed") }
}
impl From<tokio::sync::oneshot::error::RecvError> for Error {
fn from(_: tokio::sync::oneshot::error::RecvError) -> Self { Self::custom("channel closed") }
}
impl std::error::Error for Error {}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::IO(e) => write!(f, "IO error: {e}"),
Self::Serde(s) => write!(f, "Serde error: {s}"),
Self::Status(s) => write!(f, "Status error: {s:?}"),
Self::Packet(s) => write!(f, "Unexpected packet: {s}"),
Self::Timeout => write!(f, "Operation timed out"),
Self::Unsupported => write!(f, "Operation not supported"),
Self::Custom(s) => write!(f, "{s}"),
}
}
}

146
yazi-sftp/src/fs/attrs.rs Normal file
View file

@ -0,0 +1,146 @@
use std::{collections::HashMap, fmt};
use serde::{Deserialize, Deserializer, Serialize, de::Visitor, ser::SerializeStruct};
#[derive(Debug, Default)]
pub struct Attrs {
pub size: Option<u64>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub perm: Option<u32>,
pub atime: Option<u32>,
pub mtime: Option<u32>,
pub extended: HashMap<String, String>,
}
impl Attrs {
pub fn len(&self) -> usize {
let mut len = 4;
if let Some(size) = self.size {
len += size_of_val(&size);
}
if self.uid.is_some() || self.gid.is_some() {
len += 4 + 4;
}
if let Some(perm) = self.perm {
len += size_of_val(&perm);
}
if self.atime.is_some() || self.mtime.is_some() {
len += 4 + 4;
}
if !self.extended.is_empty() {
len += 4 + self.extended.iter().map(|(k, v)| 4 + k.len() + 4 + v.len()).sum::<usize>();
}
len
}
}
impl Serialize for Attrs {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut flags: u32 = 0;
let mut len = 1;
if self.size.is_some() {
flags |= 0x1;
len += 1;
}
if self.uid.is_some() || self.gid.is_some() {
flags |= 0x2;
len += 2;
}
if self.perm.is_some() {
flags |= 0x4;
len += 1;
}
if self.atime.is_some() || self.mtime.is_some() {
flags |= 0x8;
len += 2;
}
if !self.extended.is_empty() {
flags |= 0x80000000;
len += 1 + self.extended.len() * 2;
}
let mut seq = serializer.serialize_struct("Attrs", len)?;
seq.serialize_field("flags", &flags)?;
if let Some(size) = self.size {
seq.serialize_field("size", &size)?;
}
if self.uid.is_some() || self.gid.is_some() {
seq.serialize_field("uid", &self.uid.unwrap_or(0))?;
seq.serialize_field("gid", &self.gid.unwrap_or(0))?;
}
if let Some(perm) = self.perm {
seq.serialize_field("perm", &perm)?;
}
if self.atime.is_some() || self.mtime.is_some() {
seq.serialize_field("atime", &self.atime.unwrap_or(0))?;
seq.serialize_field("mtime", &self.mtime.unwrap_or(0))?;
}
if !self.extended.is_empty() {
let count = self.extended.len() as u32;
seq.serialize_field("extended_count", &count)?;
for (k, v) in self.extended.iter().take(count as usize) {
seq.serialize_field("extended_key", k)?;
seq.serialize_field("extended_value", v)?;
}
}
seq.end()
}
}
impl<'de> Deserialize<'de> for Attrs {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct AttrsVisitor;
impl<'de> Visitor<'de> for AttrsVisitor {
type Value = Attrs;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("attributes")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let b = seq.next_element::<u32>()?.unwrap_or(0);
let mut attrs = Attrs::default();
if b & 0x1 != 0 {
attrs.size = seq.next_element()?;
}
if b & 0x2 != 0 {
attrs.uid = seq.next_element()?;
attrs.gid = seq.next_element()?;
}
if b & 0x4 != 0 {
attrs.perm = seq.next_element()?;
}
if b & 0x8 != 0 {
attrs.atime = seq.next_element()?;
attrs.mtime = seq.next_element()?;
}
if b & 0x80000000 != 0 {
let count: u32 = seq.next_element()?.unwrap_or(0);
for _ in 0..count {
attrs.extended.insert(
seq.next_element()?.unwrap_or_default(),
seq.next_element()?.unwrap_or_default(),
);
}
}
Ok(attrs)
}
}
deserializer.deserialize_any(AttrsVisitor)
}
}

View file

@ -0,0 +1,46 @@
use std::{borrow::Cow, ffi::OsStr, path::PathBuf};
use crate::{ByteStr, fs::Attrs};
pub struct DirEntry<'a> {
pub(super) dir: ByteStr<'a>,
pub(super) name: ByteStr<'a>,
pub(super) long_name: ByteStr<'a>,
pub(super) attrs: Attrs,
}
impl<'a> DirEntry<'a> {
pub fn path(&self) -> PathBuf { self.dir.join(&self.name) }
pub fn name(&self) -> Cow<'_, OsStr> { self.name.to_os_str() }
pub fn long_name(&self) -> Cow<'_, OsStr> { self.long_name.to_os_str() }
pub fn attrs(&self) -> &Attrs { &self.attrs }
pub fn nlink(&self) -> Option<u64> { str::from_utf8(self.long_name_field(1)?).ok()?.parse().ok() }
pub fn user(&self) -> Option<Cow<'_, OsStr>> {
let b = self.long_name_field(2)?;
Some(unsafe {
match ByteStr::from_str_bytes_unchecked(b).to_os_str() {
Cow::Borrowed(_) => OsStr::from_encoded_bytes_unchecked(b).into(),
Cow::Owned(s) => s.into(),
}
})
}
pub fn group(&self) -> Option<Cow<'_, OsStr>> {
let b = self.long_name_field(3)?;
Some(unsafe {
match ByteStr::from_str_bytes_unchecked(b).to_os_str() {
Cow::Borrowed(_) => OsStr::from_encoded_bytes_unchecked(b).into(),
Cow::Owned(s) => s.into(),
}
})
}
fn long_name_field(&self, n: usize) -> Option<&[u8]> {
self.long_name.split(|b| b.is_ascii_whitespace()).filter(|s| !s.is_empty()).nth(n)
}
}

165
yazi-sftp/src/fs/file.rs Normal file
View file

@ -0,0 +1,165 @@
use std::{io, pin::Pin, task::{Context, Poll, ready}, time::Duration};
use tokio::{io::{AsyncRead, AsyncWrite, ReadBuf}, sync::oneshot, time::{Timeout, timeout}};
use crate::{Error, Packet, Session, fs::Attrs};
pub struct File<'a> {
session: &'a Session,
handle: String,
closed: bool,
cursor: u64,
close_rx: Option<Timeout<oneshot::Receiver<Packet<'static>>>>,
read_rx: Option<oneshot::Receiver<Packet<'static>>>,
write_rx: Option<(oneshot::Receiver<Packet<'static>>, usize)>,
flush_rx: Option<Timeout<oneshot::Receiver<Packet<'static>>>>,
}
impl Unpin for File<'_> {}
impl Drop for File<'_> {
fn drop(&mut self) {
if !self.closed {
self.session.close(&self.handle).ok();
}
}
}
impl<'a> File<'a> {
pub(crate) fn new(session: &'a Session, handle: impl Into<String>) -> Self {
Self {
session,
handle: handle.into(),
closed: false,
cursor: 0,
close_rx: None,
read_rx: None,
write_rx: None,
flush_rx: None,
}
}
pub async fn fstat(&self) -> Result<Attrs, Error> { self.session.fstat(&self.handle).await }
pub async fn fsetstat(&self, attrs: Attrs) -> Result<(), Error> {
self.session.fsetstat(&self.handle, attrs).await
}
}
impl AsyncRead for File<'_> {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let me = unsafe { self.get_unchecked_mut() };
if me.read_rx.is_none() {
let max = buf.remaining().min(261120) as u32;
me.read_rx = Some(me.session.read(&me.handle, me.cursor, max)?);
}
let result = ready!(Pin::new(me.read_rx.as_mut().unwrap()).poll(cx));
me.read_rx = None;
Poll::Ready(match result {
Ok(Packet::Data(data)) => {
let len = buf.remaining().min(data.data.len());
me.cursor += len as u64;
buf.put_slice(&data.data[..len]);
Ok(())
}
Ok(Packet::Status(status)) if status.is_eof() => Ok(()),
Ok(Packet::Status(status)) => Err(Error::Status(status).into()),
Ok(_) => Err(Error::Packet("not a Data or Status").into()),
Err(e) => Err(Error::from(e).into()),
})
}
}
impl AsyncWrite for File<'_> {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, io::Error>> {
let me = unsafe { self.get_unchecked_mut() };
let (rx, len) = match &mut me.write_rx {
Some((rx, len)) => (rx, *len),
None => {
let max = buf.len().min(261120);
let rx = me.session.write(&me.handle, me.cursor, &buf[..max])?;
(&mut me.write_rx.get_or_insert((rx, max)).0, max)
}
};
let result = ready!(Pin::new(rx).poll(cx));
me.write_rx = None;
Poll::Ready(match result {
Ok(Packet::Status(status)) if status.is_ok() => {
me.cursor += len as u64;
Ok(len)
}
Ok(Packet::Status(status)) => Err(Error::Status(status).into()),
Ok(_) => Err(Error::Packet("not a Status").into()),
Err(e) => Err(Error::from(e).into()),
})
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
let me = unsafe { self.get_unchecked_mut() };
if me.flush_rx.is_none() {
match me.session.fsync(&me.handle) {
Ok(rx) => me.flush_rx = Some(timeout(Duration::from_secs(10), rx)),
Err(Error::Unsupported) => return Poll::Ready(Ok(())),
Err(e) => Err(e)?,
}
}
let rx = unsafe { Pin::new_unchecked(me.flush_rx.as_mut().unwrap()) };
let result = ready!(rx.poll(cx));
me.flush_rx = None;
let Ok(result) = result else {
return Poll::Ready(Err(Error::Timeout.into()));
};
Poll::Ready(match result {
Ok(Packet::Status(status)) if status.is_ok() => Ok(()),
Ok(Packet::Status(status)) => Err(Error::Status(status).into()),
Ok(_) => Err(Error::Packet("not a Status").into()),
Err(e) => Err(Error::from(e).into()),
})
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
let me = unsafe { self.get_unchecked_mut() };
if me.close_rx.is_none() {
me.close_rx = Some(timeout(Duration::from_secs(10), me.session.close(&me.handle)?));
}
let rx = unsafe { Pin::new_unchecked(me.close_rx.as_mut().unwrap()) };
let result = ready!(rx.poll(cx));
me.close_rx = None;
let Ok(result) = result else {
return Poll::Ready(Err(Error::Timeout.into()));
};
Poll::Ready(match result {
Ok(Packet::Status(status)) if status.is_ok() => {
me.closed = true;
Ok(())
}
Ok(Packet::Status(status)) => Err(Error::Status(status).into()),
Ok(_) => Err(Error::Packet("not a Status").into()),
Err(e) => Err(Error::from(e).into()),
})
}
}

16
yazi-sftp/src/fs/flags.rs Normal file
View file

@ -0,0 +1,16 @@
use bitflags::bitflags;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
pub struct Flags(u32);
bitflags! {
impl Flags: u32 {
const READ = 0b000001;
const WRITE = 0b000010;
const APPEND = 0b000100;
const CREATE = 0b001000;
const TRUNCATE = 0b010000;
const EXCLUDE = 0b100000;
}
}

11
yazi-sftp/src/fs/mod.rs Normal file
View file

@ -0,0 +1,11 @@
mod attrs;
mod dir_entry;
mod file;
mod flags;
mod read_dir;
pub use attrs::*;
pub use dir_entry::*;
pub use file::*;
pub use flags::*;
pub use read_dir::*;

View file

@ -0,0 +1,56 @@
use std::mem;
use crate::{ByteStr, Error, Session, fs::DirEntry, requests, responses};
pub struct ReadDir<'a> {
dir: ByteStr<'a>,
handle: String,
session: &'a Session,
name: responses::Name<'a>,
cursor: usize,
done: bool,
}
impl<'a> ReadDir<'a> {
pub(crate) fn new(session: &'a Session, dir: ByteStr<'a>, handle: String) -> Self {
Self { dir, handle, session, name: Default::default(), cursor: 0, done: false }
}
pub async fn next(&mut self) -> Result<Option<DirEntry<'_>>, Error> {
loop {
self.fetch().await?;
let Some(item) = self.name.items.get_mut(self.cursor).map(mem::take) else {
return Ok(None);
};
self.cursor += 1;
if item.name != "." && item.name != ".." {
return Ok(Some(DirEntry {
dir: ByteStr::from(&self.dir),
name: item.name,
long_name: item.long_name,
attrs: item.attrs,
}));
}
}
}
async fn fetch(&mut self) -> Result<(), Error> {
if self.cursor < self.name.items.len() || self.done {
return Ok(());
}
self.name = match self.session.send(requests::ReadDir::new(&self.handle)).await {
Ok(resp) => resp,
Err(Error::Status(status)) if status.is_eof() => {
self.done = true;
return Ok(());
}
Err(e) => return Err(e),
};
self.cursor = 0;
Ok(())
}
}

18
yazi-sftp/src/id.rs Normal file
View file

@ -0,0 +1,18 @@
use std::sync::atomic::{AtomicU32, Ordering};
pub(super) struct Id(AtomicU32);
impl Default for Id {
fn default() -> Self { Self(AtomicU32::new(1)) }
}
impl Id {
pub(super) fn next(&self) -> u32 {
loop {
let old = self.0.fetch_add(1, Ordering::Relaxed);
if old != 0 {
return old;
}
}
}
}

22
yazi-sftp/src/lib.rs Normal file
View file

@ -0,0 +1,22 @@
#![allow(clippy::len_without_is_empty)]
pub mod fs;
pub mod requests;
pub mod responses;
mod byte_str;
mod de;
mod error;
mod id;
mod macros;
mod packet;
mod ser;
mod session;
pub use byte_str::*;
pub(crate) use de::*;
pub use error::*;
pub(crate) use id::*;
pub use packet::*;
pub(crate) use ser::*;
pub use session::*;

30
yazi-sftp/src/macros.rs Normal file
View file

@ -0,0 +1,30 @@
#[macro_export]
macro_rules! impl_from_packet {
($($variant:ident($type:ty)),* $(,)?) => {
$(
impl<'a> From<$type> for $crate::Packet<'a> {
fn from(value: $type) -> Self {
Self::$variant(value)
}
}
)*
};
}
#[macro_export]
macro_rules! impl_try_from_packet {
($($variant:ident($type:ty)),* $(,)?) => {
$(
impl<'a> TryFrom<$crate::Packet<'a>> for $type {
type Error = $crate::Error;
fn try_from(value: $crate::Packet<'a>) -> Result<Self, Self::Error> {
match value {
$crate::Packet::$variant(v) => Ok(v),
_ => Err($crate::Error::Packet(concat!("not a ", stringify!($variant)))),
}
}
}
)*
};
}

265
yazi-sftp/src/packet.rs Normal file
View file

@ -0,0 +1,265 @@
use serde::{Deserialize, Serialize};
use super::de::Deserializer;
use crate::{Error, impl_from_packet, impl_try_from_packet, requests, responses};
#[derive(Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum Packet<'a> {
Init(requests::Init),
Open(requests::Open<'a>),
Close(requests::Close<'a>),
Read(requests::Read<'a>),
Write(requests::Write<'a>),
Lstat(requests::Lstat<'a>),
Fstat(requests::Fstat<'a>),
SetStat(requests::SetStat<'a>),
FSetStat(requests::FSetStat<'a>),
OpenDir(requests::OpenDir<'a>),
ReadDir(requests::ReadDir<'a>),
Remove(requests::Remove<'a>),
Mkdir(requests::Mkdir<'a>),
Rmdir(requests::Rmdir<'a>),
Realpath(requests::Realpath<'a>),
Stat(requests::Stat<'a>),
Rename(requests::Rename<'a>),
Readlink(requests::Readlink<'a>),
Symlink(requests::Symlink<'a>),
ExtendedFsync(requests::Extended<'a, requests::ExtendedFsync<'a>>),
ExtendedHardlink(requests::Extended<'a, requests::ExtendedHardlink<'a>>),
ExtendedLimits(requests::Extended<'a, requests::ExtendedLimits>),
// Responses
Version(responses::Version),
Status(responses::Status),
Handle(responses::Handle),
Data(responses::Data),
Name(responses::Name<'a>),
Attrs(responses::Attrs),
ExtendedReply(responses::Extended<'a>),
}
impl_from_packet! {
Init(requests::Init),
Open(requests::Open<'a>),
Close(requests::Close<'a>),
Read(requests::Read<'a>),
Write(requests::Write<'a>),
Lstat(requests::Lstat<'a>),
Fstat(requests::Fstat<'a>),
SetStat(requests::SetStat<'a>),
FSetStat(requests::FSetStat<'a>),
OpenDir(requests::OpenDir<'a>),
ReadDir(requests::ReadDir<'a>),
Remove(requests::Remove<'a>),
Mkdir(requests::Mkdir<'a>),
Rmdir(requests::Rmdir<'a>),
Realpath(requests::Realpath<'a>),
Stat(requests::Stat<'a>),
Rename(requests::Rename<'a>),
Readlink(requests::Readlink<'a>),
Symlink(requests::Symlink<'a>),
ExtendedFsync(requests::Extended<'a, requests::ExtendedFsync<'a>>),
ExtendedHardlink(requests::Extended<'a, requests::ExtendedHardlink<'a>>),
ExtendedLimits(requests::Extended<'a, requests::ExtendedLimits>),
// Responses
Version(responses::Version),
Status(responses::Status),
Handle(responses::Handle),
Data(responses::Data),
Name(responses::Name<'a>),
Attrs(responses::Attrs),
ExtendedReply(responses::Extended<'a>),
}
impl_try_from_packet! {
Version(responses::Version),
Status(responses::Status),
Handle(responses::Handle),
Data(responses::Data),
Name(responses::Name<'a>),
Attrs(responses::Attrs),
ExtendedReply(responses::Extended<'a>),
}
impl Packet<'_> {
fn kind(&self) -> u8 {
match self {
Self::Init(_) => 1,
Self::Open(_) => 3,
Self::Close(_) => 4,
Self::Read(_) => 5,
Self::Write(_) => 6,
Self::Lstat(_) => 7,
Self::Fstat(_) => 8,
Self::SetStat(_) => 9,
Self::FSetStat(_) => 10,
Self::OpenDir(_) => 11,
Self::ReadDir(_) => 12,
Self::Remove(_) => 13,
Self::Mkdir(_) => 14,
Self::Rmdir(_) => 15,
Self::Realpath(_) => 16,
Self::Stat(_) => 17,
Self::Rename(_) => 18,
Self::Readlink(_) => 19,
Self::Symlink(_) => 20,
Self::ExtendedFsync(_) => 200,
Self::ExtendedHardlink(_) => 200,
Self::ExtendedLimits(_) => 200,
// Responses
Self::Version(_) => 2,
Self::Status(_) => 101,
Self::Handle(_) => 102,
Self::Data(_) => 103,
Self::Name(_) => 104,
Self::Attrs(_) => 105,
Self::ExtendedReply(_) => 201,
}
}
pub fn id(&self) -> u32 {
match self {
Self::Init(_) => 0,
Self::Open(v) => v.id,
Self::Close(v) => v.id,
Self::Read(v) => v.id,
Self::Write(v) => v.id,
Self::Lstat(v) => v.id,
Self::Fstat(v) => v.id,
Self::SetStat(v) => v.id,
Self::FSetStat(v) => v.id,
Self::OpenDir(v) => v.id,
Self::ReadDir(v) => v.id,
Self::Remove(v) => v.id,
Self::Mkdir(v) => v.id,
Self::Rmdir(v) => v.id,
Self::Realpath(v) => v.id,
Self::Stat(v) => v.id,
Self::Rename(v) => v.id,
Self::Readlink(v) => v.id,
Self::Symlink(v) => v.id,
Self::ExtendedFsync(v) => v.id,
Self::ExtendedHardlink(v) => v.id,
Self::ExtendedLimits(v) => v.id,
// Responses
Self::Version(_) => 0,
Self::Status(v) => v.id,
Self::Handle(v) => v.id,
Self::Data(v) => v.id,
Self::Name(v) => v.id,
Self::Attrs(v) => v.id,
Self::ExtendedReply(v) => v.id,
}
}
pub fn with_id(mut self, id: u32) -> Self {
match &mut self {
Self::Init(_) => {}
Self::Open(v) => v.id = id,
Self::Close(v) => v.id = id,
Self::Read(v) => v.id = id,
Self::Write(v) => v.id = id,
Self::Lstat(v) => v.id = id,
Self::Fstat(v) => v.id = id,
Self::SetStat(v) => v.id = id,
Self::FSetStat(v) => v.id = id,
Self::OpenDir(v) => v.id = id,
Self::ReadDir(v) => v.id = id,
Self::Remove(v) => v.id = id,
Self::Mkdir(v) => v.id = id,
Self::Rmdir(v) => v.id = id,
Self::Realpath(v) => v.id = id,
Self::Stat(v) => v.id = id,
Self::Rename(v) => v.id = id,
Self::Readlink(v) => v.id = id,
Self::Symlink(v) => v.id = id,
Self::ExtendedFsync(v) => v.id = id,
Self::ExtendedHardlink(v) => v.id = id,
Self::ExtendedLimits(v) => v.id = id,
// Responses
Self::Version(_) => {}
Self::Status(v) => v.id = id,
Self::Handle(v) => v.id = id,
Self::Data(v) => v.id = id,
Self::Name(v) => v.id = id,
Self::Attrs(v) => v.id = id,
Self::ExtendedReply(v) => v.id = id,
}
self
}
fn len(&self) -> usize {
let type_len = 1;
match self {
Self::Init(v) => type_len + v.len(),
Self::Open(v) => type_len + v.len(),
Self::Close(v) => type_len + v.len(),
Self::Read(v) => type_len + v.len(),
Self::Write(v) => type_len + v.len(),
Self::Lstat(v) => type_len + v.len(),
Self::Fstat(v) => type_len + v.len(),
Self::SetStat(v) => type_len + v.len(),
Self::FSetStat(v) => type_len + v.len(),
Self::OpenDir(v) => type_len + v.len(),
Self::ReadDir(v) => type_len + v.len(),
Self::Remove(v) => type_len + v.len(),
Self::Mkdir(v) => type_len + v.len(),
Self::Rmdir(v) => type_len + v.len(),
Self::Realpath(v) => type_len + v.len(),
Self::Stat(v) => type_len + v.len(),
Self::Rename(v) => type_len + v.len(),
Self::Readlink(v) => type_len + v.len(),
Self::Symlink(v) => type_len + v.len(),
Self::ExtendedFsync(v) => type_len + v.len(),
Self::ExtendedHardlink(v) => type_len + v.len(),
Self::ExtendedLimits(v) => type_len + v.len(),
// Responses
Self::Version(v) => type_len + v.len(),
Self::Status(v) => type_len + v.len(),
Self::Handle(v) => type_len + v.len(),
Self::Data(v) => type_len + v.len(),
Self::Name(v) => type_len + v.len(),
Self::Attrs(v) => type_len + v.len(),
Self::ExtendedReply(v) => type_len + v.len(),
}
}
}
pub fn to_bytes<'a, T>(value: T) -> Result<Vec<u8>, Error>
where
T: Into<Packet<'a>> + Serialize,
{
let packet: Packet = value.into();
let len = u32::try_from(packet.len()).map_err(|_| Error::serde("packet too large"))?;
let mut output = Vec::with_capacity(4 + len as usize);
output.extend_from_slice(&len.to_be_bytes());
output.push(packet.kind());
let mut serializer = crate::Serializer { output };
packet.serialize(&mut serializer)?;
Ok(serializer.output)
}
// TODO: use Vec<u8>
pub fn from_bytes(mut bytes: &[u8]) -> Result<Packet<'static>, Error> {
let kind = *bytes.first().ok_or(Error::serde("empty packet"))?;
bytes = &bytes[1..];
Ok(match kind {
2 => Packet::Version(Deserializer::once(bytes)?),
101 => Packet::Status(Deserializer::once(bytes)?),
102 => Packet::Handle(Deserializer::once(bytes)?),
103 => Packet::Data(Deserializer::once(bytes)?),
104 => Packet::Name(Deserializer::once(bytes)?),
201 => Packet::ExtendedReply(Deserializer::once(bytes)?),
_ => return Err(Error::Packet("unknown packet kind")),
})
}

View file

@ -0,0 +1,15 @@
use std::borrow::Cow;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct Close<'a> {
pub id: u32,
pub handle: Cow<'a, str>,
}
impl<'a> Close<'a> {
pub fn new(handle: impl Into<Cow<'a, str>>) -> Self { Self { id: 0, handle: handle.into() } }
pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.handle.len() }
}

View file

@ -0,0 +1,71 @@
use std::{borrow::Cow, fmt::Debug};
use serde::{Deserialize, Serialize};
use crate::ByteStr;
#[derive(Debug, Deserialize, Serialize)]
pub struct Extended<'a, D> {
pub id: u32,
pub request: Cow<'a, str>,
pub data: D,
}
impl<D: ExtendedData> Extended<'_, D> {
pub fn new<'a, R>(request: R, data: D) -> Extended<'a, D>
where
R: Into<Cow<'a, str>>,
{
Extended { id: 0, request: request.into(), data }
}
pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.request.len() + self.data.len() }
}
// --- Data
pub trait ExtendedData: Debug + Serialize + for<'de> Deserialize<'de> {
fn len(&self) -> usize;
}
// --- Fsync
#[derive(Debug, Deserialize, Serialize)]
pub struct ExtendedFsync<'a> {
pub handle: Cow<'a, str>,
}
impl<'a> ExtendedFsync<'a> {
pub fn new(handle: impl Into<Cow<'a, str>>) -> Self { Self { handle: handle.into() } }
}
impl ExtendedData for ExtendedFsync<'_> {
fn len(&self) -> usize { 4 + self.handle.len() }
}
// --- Hardlink
#[derive(Debug, Serialize, Deserialize)]
pub struct ExtendedHardlink<'a> {
pub original: ByteStr<'a>,
pub link: ByteStr<'a>,
}
impl<'a> ExtendedHardlink<'a> {
pub fn new<O, L>(original: O, link: L) -> Self
where
O: Into<ByteStr<'a>>,
L: Into<ByteStr<'a>>,
{
Self { original: original.into(), link: link.into() }
}
}
impl ExtendedData for ExtendedHardlink<'_> {
fn len(&self) -> usize { 4 + self.original.len() + 4 + self.link.len() }
}
// --- Limits
#[derive(Debug, Deserialize, Serialize)]
pub struct ExtendedLimits;
impl ExtendedData for ExtendedLimits {
fn len(&self) -> usize { 0 }
}

View file

@ -0,0 +1,15 @@
use std::borrow::Cow;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct Fstat<'a> {
pub id: u32,
pub handle: Cow<'a, str>,
}
impl<'a> Fstat<'a> {
pub fn new(handle: impl Into<Cow<'a, str>>) -> Self { Self { id: 0, handle: handle.into() } }
pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.handle.len() }
}

View file

@ -0,0 +1,22 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct Init {
pub version: u32,
pub extensions: HashMap<String, String>,
}
impl Init {
pub fn new(extensions: HashMap<String, String>) -> Self { Self { version: 3, extensions } }
pub fn len(&self) -> usize {
size_of_val(&self.version)
+ self.extensions.iter().map(|(k, v)| 4 + k.len() + 4 + v.len()).sum::<usize>()
}
}
impl Default for Init {
fn default() -> Self { Self::new(HashMap::new()) }
}

View file

@ -0,0 +1,15 @@
use serde::{Deserialize, Serialize};
use crate::ByteStr;
#[derive(Debug, Deserialize, Serialize)]
pub struct Lstat<'a> {
pub id: u32,
pub path: ByteStr<'a>,
}
impl Lstat<'_> {
pub fn new<'a>(path: impl Into<ByteStr<'a>>) -> Lstat<'a> { Lstat { id: 0, path: path.into() } }
pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() }
}

View file

@ -0,0 +1,21 @@
use serde::{Deserialize, Serialize};
use crate::{ByteStr, fs::Attrs};
#[derive(Debug, Deserialize, Serialize)]
pub struct Mkdir<'a> {
pub id: u32,
pub path: ByteStr<'a>,
pub attrs: Attrs,
}
impl<'a> Mkdir<'a> {
pub fn new<P>(path: P, attrs: Attrs) -> Self
where
P: Into<ByteStr<'a>>,
{
Self { id: 0, path: path.into(), attrs }
}
pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() + self.attrs.len() }
}

View file

@ -0,0 +1,39 @@
mod close;
mod extended;
mod fstat;
mod init;
mod lstat;
mod mkdir;
mod open;
mod open_dir;
mod read;
mod read_dir;
mod readlink;
mod realpath;
mod remove;
mod rename;
mod rmdir;
mod set_stat;
mod stat;
mod symlink;
mod write;
pub use close::*;
pub use extended::*;
pub use fstat::*;
pub use init::*;
pub use lstat::*;
pub use mkdir::*;
pub use open::*;
pub use open_dir::*;
pub use read::*;
pub use read_dir::*;
pub use readlink::*;
pub use realpath::*;
pub use remove::*;
pub use rename::*;
pub use rmdir::*;
pub use set_stat::*;
pub use stat::*;
pub use symlink::*;
pub use write::*;

View file

@ -0,0 +1,24 @@
use serde::{Deserialize, Serialize};
use crate::{ByteStr, fs::{Attrs, Flags}};
#[derive(Debug, Deserialize, Serialize)]
pub struct Open<'a> {
pub id: u32,
pub path: ByteStr<'a>,
pub flags: Flags,
pub attrs: Attrs,
}
impl<'a> Open<'a> {
pub fn new<P>(path: P, flags: Flags, attrs: Attrs) -> Self
where
P: Into<ByteStr<'a>>,
{
Self { id: 0, path: path.into(), flags, attrs }
}
pub fn len(&self) -> usize {
size_of_val(&self.id) + 4 + self.path.len() + size_of_val(&self.flags) + self.attrs.len()
}
}

View file

@ -0,0 +1,15 @@
use serde::{Deserialize, Serialize};
use crate::ByteStr;
#[derive(Debug, Deserialize, Serialize)]
pub struct OpenDir<'a> {
pub id: u32,
pub path: ByteStr<'a>,
}
impl<'a> OpenDir<'a> {
pub fn new(path: impl Into<ByteStr<'a>>) -> Self { Self { id: 0, path: path.into() } }
pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() }
}

View file

@ -0,0 +1,27 @@
use std::borrow::Cow;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct Read<'a> {
pub id: u32,
pub handle: Cow<'a, str>,
pub offset: u64,
pub len: u32,
}
impl<'a> Read<'a> {
pub fn new<H>(handle: H, offset: u64, len: u32) -> Self
where
H: Into<Cow<'a, str>>,
{
Self { id: 0, handle: handle.into(), offset, len }
}
pub fn len(&self) -> usize {
size_of_val(&self.id)
+ 4 + self.handle.len()
+ size_of_val(&self.offset)
+ size_of_val(&self.len)
}
}

View file

@ -0,0 +1,15 @@
use std::borrow::Cow;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct ReadDir<'a> {
pub id: u32,
pub handle: Cow<'a, str>,
}
impl<'a> ReadDir<'a> {
pub fn new(handle: &'a str) -> Self { Self { id: 0, handle: handle.into() } }
pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.handle.len() }
}

View file

@ -0,0 +1,15 @@
use serde::{Deserialize, Serialize};
use crate::ByteStr;
#[derive(Debug, Deserialize, Serialize)]
pub struct Readlink<'a> {
pub id: u32,
pub path: ByteStr<'a>,
}
impl<'a> Readlink<'a> {
pub fn new(path: impl Into<ByteStr<'a>>) -> Self { Self { id: 0, path: path.into() } }
pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() }
}

View file

@ -0,0 +1,15 @@
use serde::{Deserialize, Serialize};
use crate::ByteStr;
#[derive(Debug, Deserialize, Serialize)]
pub struct Realpath<'a> {
pub id: u32,
pub path: ByteStr<'a>,
}
impl<'a> Realpath<'a> {
pub fn new(path: impl Into<ByteStr<'a>>) -> Self { Self { id: 0, path: path.into() } }
pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() }
}

View file

@ -0,0 +1,15 @@
use serde::{Deserialize, Serialize};
use crate::ByteStr;
#[derive(Debug, Deserialize, Serialize)]
pub struct Remove<'a> {
pub id: u32,
pub path: ByteStr<'a>,
}
impl<'a> Remove<'a> {
pub fn new(path: impl Into<ByteStr<'a>>) -> Self { Self { id: 0, path: path.into() } }
pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() }
}

View file

@ -0,0 +1,22 @@
use serde::{Deserialize, Serialize};
use crate::ByteStr;
#[derive(Debug, Deserialize, Serialize)]
pub struct Rename<'a> {
pub id: u32,
pub from: ByteStr<'a>,
pub to: ByteStr<'a>,
}
impl<'a> Rename<'a> {
pub fn new<F, T>(from: F, to: T) -> Self
where
F: Into<ByteStr<'a>>,
T: Into<ByteStr<'a>>,
{
Self { id: 0, from: from.into(), to: to.into() }
}
pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.from.len() + 4 + self.to.len() }
}

View file

@ -0,0 +1,15 @@
use serde::{Deserialize, Serialize};
use crate::ByteStr;
#[derive(Debug, Deserialize, Serialize)]
pub struct Rmdir<'a> {
pub id: u32,
pub path: ByteStr<'a>,
}
impl<'a> Rmdir<'a> {
pub fn new(path: impl Into<ByteStr<'a>>) -> Self { Self { id: 0, path: path.into() } }
pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() }
}

View file

@ -0,0 +1,35 @@
use std::borrow::Cow;
use serde::{Deserialize, Serialize};
use crate::{ByteStr, fs::Attrs};
#[derive(Debug, Deserialize, Serialize)]
pub struct SetStat<'a> {
pub id: u32,
pub path: ByteStr<'a>,
pub attrs: Attrs,
}
impl<'a> SetStat<'a> {
pub fn new(path: impl Into<ByteStr<'a>>, attrs: Attrs) -> Self {
Self { id: 0, path: path.into(), attrs }
}
pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() + self.attrs.len() }
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FSetStat<'a> {
pub id: u32,
pub handle: Cow<'a, str>,
pub attrs: Attrs,
}
impl<'a> FSetStat<'a> {
pub fn new(handle: impl Into<Cow<'a, str>>, attrs: Attrs) -> Self {
Self { id: 0, handle: handle.into(), attrs }
}
pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.handle.len() + self.attrs.len() }
}

View file

@ -0,0 +1,15 @@
use serde::{Deserialize, Serialize};
use crate::ByteStr;
#[derive(Debug, Deserialize, Serialize)]
pub struct Stat<'a> {
pub id: u32,
pub path: ByteStr<'a>,
}
impl<'a> Stat<'a> {
pub fn new(path: impl Into<ByteStr<'a>>) -> Self { Self { id: 0, path: path.into() } }
pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.path.len() }
}

View file

@ -0,0 +1,24 @@
use serde::{Deserialize, Serialize};
use crate::ByteStr;
#[derive(Debug, Deserialize, Serialize)]
pub struct Symlink<'a> {
pub id: u32,
pub link: ByteStr<'a>,
pub original: ByteStr<'a>,
}
impl<'a> Symlink<'a> {
pub fn new<L, O>(link: L, original: O) -> Self
where
L: Into<ByteStr<'a>>,
O: Into<ByteStr<'a>>,
{
Self { id: 0, link: link.into(), original: original.into() }
}
pub fn len(&self) -> usize {
size_of_val(&self.id) + 4 + self.link.len() + 4 + self.original.len()
}
}

View file

@ -0,0 +1,25 @@
use std::borrow::Cow;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct Write<'a> {
pub id: u32,
pub handle: Cow<'a, str>,
pub offset: u64,
pub data: Cow<'a, [u8]>,
}
impl Write<'_> {
pub fn new<'a, H, D>(handle: H, offset: u64, data: D) -> Write<'a>
where
H: Into<Cow<'a, str>>,
D: Into<Cow<'a, [u8]>>,
{
Write { id: 0, handle: handle.into(), offset, data: data.into() }
}
pub fn len(&self) -> usize {
size_of_val(&self.id) + 4 + self.handle.len() + size_of_val(&self.offset) + 4 + self.data.len()
}
}

View file

@ -0,0 +1,13 @@
use serde::{Deserialize, Serialize};
use crate::fs;
#[derive(Debug, Deserialize, Serialize)]
pub struct Attrs {
pub id: u32,
pub attrs: fs::Attrs,
}
impl Attrs {
pub fn len(&self) -> usize { size_of_val(&self.id) + self.attrs.len() }
}

View file

@ -0,0 +1,11 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct Data {
pub id: u32,
pub data: Vec<u8>,
}
impl Data {
pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.data.len() }
}

View file

@ -0,0 +1,85 @@
use std::{borrow::Cow, fmt, ops::Deref};
use serde::{Deserialize, Serialize, de::Visitor, ser::SerializeSeq};
use crate::Error;
#[derive(Debug, Deserialize, Serialize)]
pub struct Extended<'a> {
pub id: u32,
pub data: ExtendedData<'a>,
}
impl<'a> Extended<'a> {
pub fn len(&self) -> usize { size_of_val(&self.id) + self.data.len() }
}
// --- Data
#[derive(Debug)]
pub struct ExtendedData<'a>(Cow<'a, [u8]>);
impl Deref for ExtendedData<'_> {
type Target = [u8];
fn deref(&self) -> &Self::Target { &self.0 }
}
impl Serialize for ExtendedData<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut seq = serializer.serialize_seq(None)?;
for b in self.0.as_ref() {
seq.serialize_element(b)?;
}
seq.end()
}
}
impl<'de> Deserialize<'de> for ExtendedData<'_> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct ExtendedDataVisitor;
impl<'de> Visitor<'de> for ExtendedDataVisitor {
type Value = Vec<u8>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("extended data")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut bytes = Vec::with_capacity(seq.size_hint().unwrap_or(0));
while let Some(b) = seq.next_element()? {
bytes.push(b);
}
Ok(bytes)
}
}
deserializer.deserialize_any(ExtendedDataVisitor).map(Cow::Owned).map(ExtendedData)
}
}
// --- Limits
#[derive(Debug, Deserialize, Serialize)]
pub struct ExtendedLimits {
pub packet_len: u64,
pub read_len: u64,
pub write_len: u64,
pub open_handles: u64,
}
impl TryFrom<Extended<'_>> for ExtendedLimits {
type Error = Error;
fn try_from(value: Extended<'_>) -> Result<Self, Self::Error> {
crate::Deserializer::once(&value.data)
}
}

View file

@ -0,0 +1,11 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct Handle {
pub id: u32,
pub handle: String,
}
impl Handle {
pub fn len(&self) -> usize { size_of_val(&self.id) + 4 + self.handle.len() }
}

View file

@ -0,0 +1,15 @@
mod attrs;
mod data;
mod extended;
mod handle;
mod name;
mod status;
mod version;
pub use attrs::*;
pub use data::*;
pub use extended::*;
pub use handle::*;
pub use name::*;
pub use status::*;
pub use version::*;

View file

@ -0,0 +1,26 @@
use serde::{Deserialize, Serialize};
use crate::{ByteStr, fs::Attrs};
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct Name<'a> {
pub id: u32,
pub items: Vec<NameItem<'a>>,
}
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct NameItem<'a> {
pub name: ByteStr<'a>,
pub long_name: ByteStr<'a>,
pub attrs: Attrs,
}
impl Name<'_> {
pub fn len(&self) -> usize {
size_of_val(&self.id) + 4 + self.items.iter().map(|v| v.len()).sum::<usize>()
}
}
impl NameItem<'_> {
pub fn len(&self) -> usize { 4 + self.name.len() + 4 + self.long_name.len() + self.attrs.len() }
}

View file

@ -0,0 +1,43 @@
use serde::{Deserialize, Serialize};
use crate::Error;
#[derive(Debug, Deserialize, Serialize)]
pub struct Status {
pub id: u32,
pub code: StatusCode,
pub message: String,
pub language: String,
}
impl From<Status> for Result<(), Error> {
fn from(status: Status) -> Self {
if status.is_ok() { Ok(()) } else { Err(Error::Status(status)) }
}
}
impl Status {
pub fn len(&self) -> usize {
size_of_val(&self.id)
+ size_of_val(&(self.code as u32))
+ 4 + self.message.len()
+ 4 + self.language.len()
}
pub fn is_ok(&self) -> bool { self.code == StatusCode::Ok }
pub fn is_eof(&self) -> bool { self.code == StatusCode::Eof }
}
#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
pub enum StatusCode {
Ok = 0,
Eof = 1,
NoSuchFile = 2,
PermissionDenied = 3,
Failure = 4,
BadMessage = 5,
NoConnection = 6,
ConnectionLost = 7,
OpUnsupported = 8,
}

View file

@ -0,0 +1,16 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
pub struct Version {
pub version: u32,
pub extensions: HashMap<String, String>,
}
impl Version {
pub fn len(&self) -> usize {
size_of_val(&self.version)
+ self.extensions.iter().map(|(k, v)| 4 + k.len() + 4 + v.len()).sum::<usize>()
}
}

286
yazi-sftp/src/ser.rs Normal file
View file

@ -0,0 +1,286 @@
use serde::ser::{SerializeMap, SerializeSeq, SerializeStruct, SerializeStructVariant, SerializeTuple, SerializeTupleStruct, SerializeTupleVariant};
use crate::Error;
pub(super) struct Serializer {
pub(super) output: Vec<u8>,
}
impl<'a> serde::Serializer for &'a mut Serializer {
type Error = crate::Error;
type Ok = ();
type SerializeMap = &'a mut Serializer;
type SerializeSeq = &'a mut Serializer;
type SerializeStruct = &'a mut Serializer;
type SerializeStructVariant = &'a mut Serializer;
type SerializeTuple = &'a mut Serializer;
type SerializeTupleStruct = &'a mut Serializer;
type SerializeTupleVariant = &'a mut Serializer;
fn serialize_bool(self, _v: bool) -> Result<Self::Ok, Self::Error> {
Err(Error::serde("bool not supported"))
}
fn serialize_i8(self, _v: i8) -> Result<Self::Ok, Self::Error> {
Err(Error::serde("i8 not supported"))
}
fn serialize_i16(self, _v: i16) -> Result<Self::Ok, Self::Error> {
Err(Error::serde("i16 not supported"))
}
fn serialize_i32(self, _v: i32) -> Result<Self::Ok, Self::Error> {
Err(Error::serde("i32 not supported"))
}
fn serialize_i64(self, _v: i64) -> Result<Self::Ok, Self::Error> {
Err(Error::serde("i64 not supported"))
}
fn serialize_u8(self, v: u8) -> Result<Self::Ok, Self::Error> {
self.output.push(v);
Ok(())
}
fn serialize_u16(self, _v: u16) -> Result<Self::Ok, Self::Error> {
Err(Error::serde("u16 not supported"))
}
fn serialize_u32(self, v: u32) -> Result<Self::Ok, Self::Error> {
self.output.extend_from_slice(&v.to_be_bytes());
Ok(())
}
fn serialize_u64(self, v: u64) -> Result<Self::Ok, Self::Error> {
self.output.extend_from_slice(&v.to_be_bytes());
Ok(())
}
fn serialize_f32(self, _v: f32) -> Result<Self::Ok, Self::Error> {
Err(Error::serde("f32 not supported"))
}
fn serialize_f64(self, _v: f64) -> Result<Self::Ok, Self::Error> {
Err(Error::serde("f64 not supported"))
}
fn serialize_char(self, _v: char) -> Result<Self::Ok, Self::Error> {
Err(Error::serde("char not supported"))
}
fn serialize_str(self, v: &str) -> Result<Self::Ok, Self::Error> {
self.serialize_bytes(v.as_bytes())
}
fn serialize_bytes(self, v: &[u8]) -> Result<Self::Ok, Self::Error> {
let len = u32::try_from(v.len()).map_err(|_| Error::serde("bytes too long"))?;
self.output.extend_from_slice(&len.to_be_bytes());
self.output.extend_from_slice(v);
Ok(())
}
fn serialize_none(self) -> Result<Self::Ok, Self::Error> { Ok(()) }
fn serialize_some<T>(self, value: &T) -> Result<Self::Ok, Self::Error>
where
T: serde::Serialize + ?Sized,
{
value.serialize(self)
}
fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
Err(Error::serde("unit not supported"))
}
fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> { Ok(()) }
fn serialize_unit_variant(
self,
_name: &'static str,
variant_index: u32,
_variant: &'static str,
) -> Result<Self::Ok, Self::Error> {
self.serialize_u32(variant_index)
}
fn serialize_newtype_struct<T>(
self,
_name: &'static str,
value: &T,
) -> Result<Self::Ok, Self::Error>
where
T: serde::Serialize + ?Sized,
{
value.serialize(self)
}
fn serialize_newtype_variant<T>(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
value: &T,
) -> Result<Self::Ok, Self::Error>
where
T: serde::Serialize + ?Sized,
{
value.serialize(self)
}
fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
if let Some(len) = len {
self.serialize_u32(len.try_into().map_err(|_| Error::serde("sequence too long"))?)?;
}
Ok(self)
}
fn serialize_tuple_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleVariant, Self::Error> {
Ok(self)
}
fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> { Ok(self) }
fn serialize_tuple_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleStruct, Self::Error> {
Ok(self)
}
fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
Ok(self)
}
fn serialize_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeStruct, Self::Error> {
Ok(self)
}
fn serialize_struct_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeStructVariant, Self::Error> {
Err(Error::serde("struct variant not supported"))
}
fn is_human_readable(&self) -> bool { false }
}
impl SerializeMap for &mut Serializer {
type Error = Error;
type Ok = ();
fn serialize_key<T>(&mut self, key: &T) -> Result<(), Self::Error>
where
T: serde::Serialize + ?Sized,
{
key.serialize(&mut **self)
}
fn serialize_value<T>(&mut self, value: &T) -> Result<(), Self::Error>
where
T: serde::Serialize + ?Sized,
{
value.serialize(&mut **self)
}
fn end(self) -> Result<Self::Ok, Self::Error> { Ok(()) }
}
impl SerializeSeq for &mut Serializer {
type Error = Error;
type Ok = ();
fn serialize_element<T>(&mut self, value: &T) -> Result<(), Self::Error>
where
T: serde::Serialize + ?Sized,
{
value.serialize(&mut **self)
}
fn end(self) -> Result<Self::Ok, Self::Error> { Ok(()) }
}
impl SerializeStruct for &mut Serializer {
type Error = Error;
type Ok = ();
fn serialize_field<T>(&mut self, _key: &'static str, value: &T) -> Result<(), Self::Error>
where
T: serde::Serialize + ?Sized,
{
value.serialize(&mut **self)
}
fn end(self) -> Result<Self::Ok, Self::Error> { Ok(()) }
}
impl SerializeStructVariant for &mut Serializer {
type Error = Error;
type Ok = ();
fn serialize_field<T>(&mut self, _key: &'static str, value: &T) -> Result<(), Self::Error>
where
T: serde::Serialize + ?Sized,
{
value.serialize(&mut **self)
}
fn end(self) -> Result<Self::Ok, Self::Error> { Ok(()) }
}
impl SerializeTuple for &mut Serializer {
type Error = Error;
type Ok = ();
fn serialize_element<T>(&mut self, value: &T) -> Result<(), Self::Error>
where
T: serde::Serialize + ?Sized,
{
value.serialize(&mut **self)
}
fn end(self) -> Result<Self::Ok, Self::Error> { Ok(()) }
}
impl SerializeTupleStruct for &mut Serializer {
type Error = Error;
type Ok = ();
fn serialize_field<T>(&mut self, value: &T) -> Result<(), Self::Error>
where
T: serde::Serialize + ?Sized,
{
value.serialize(&mut **self)
}
fn end(self) -> Result<Self::Ok, Self::Error> { Ok(()) }
}
impl SerializeTupleVariant for &mut Serializer {
type Error = Error;
type Ok = ();
fn serialize_field<T>(&mut self, value: &T) -> Result<(), Self::Error>
where
T: serde::Serialize + ?Sized,
{
value.serialize(&mut **self)
}
fn end(self) -> Result<Self::Ok, Self::Error> { Ok(()) }
}

265
yazi-sftp/src/session.rs Normal file
View file

@ -0,0 +1,265 @@
use std::{any::TypeId, collections::HashMap, io::ErrorKind, path::PathBuf, sync::Arc};
use parking_lot::Mutex;
use russh::{ChannelStream, client::Msg};
use serde::Serialize;
use tokio::{io::{AsyncReadExt, AsyncWriteExt}, select, sync::{mpsc, oneshot}};
use crate::{ByteStr, Error, Id, Packet, fs::{Attrs, File, Flags, ReadDir}, requests, responses};
pub struct Session {
tx: mpsc::UnboundedSender<Vec<u8>>,
pub(crate) id: Id,
callback: Arc<Mutex<HashMap<u32, oneshot::Sender<Packet<'static>>>>>,
extensions: HashMap<String, String>,
}
impl Session {
pub fn make(stream: ChannelStream<Msg>) -> Self {
let (tx, mut rx) = mpsc::unbounded_channel::<Vec<u8>>();
let (mut reader, mut writer) = tokio::io::split(stream);
let callback = Arc::new(Mutex::new(HashMap::<_, oneshot::Sender<_>>::new()));
tokio::spawn(async move {
while let Some(data) = rx.recv().await {
if data.is_empty() {
rx.close();
writer.shutdown().await.ok();
break;
} else {
writer.write_all(&data).await.ok();
}
}
});
let (tx_, callback_) = (tx.clone(), callback.clone());
tokio::spawn(async move {
loop {
select! {
Ok(len) = reader.read_u32() => {
let mut buf = vec![0; len as usize];
if let Err(e) = reader.read_exact(&mut buf).await && e.kind() == ErrorKind::UnexpectedEof {
tx_.send(vec![]).ok();
break;
}
if let Ok(packet) = crate::from_bytes(&buf)
&& let Some(cb) = callback_.lock().remove(&packet.id())
{
cb.send(packet).ok();
}
}
_ = tx_.closed() => break,
}
}
});
Self { tx, id: Id::default(), callback, extensions: HashMap::new() }
}
pub async fn init(&mut self) -> Result<(), Error> {
let version: responses::Version = self.send(requests::Init::default()).await?;
self.extensions = version.extensions;
Ok(())
}
pub async fn open<'a, P>(&self, path: P, flags: Flags, attrs: Attrs) -> Result<File<'_>, Error>
where
P: Into<ByteStr<'a>>,
{
let handle: responses::Handle = self.send(requests::Open::new(path, flags, attrs)).await?;
Ok(File::new(self, handle.handle))
}
pub fn close(&self, handle: &str) -> Result<oneshot::Receiver<Packet<'static>>, Error> {
self.send_sync(requests::Close::new(handle))
}
pub fn read(
&self,
handle: &str,
offset: u64,
len: u32,
) -> Result<oneshot::Receiver<Packet<'static>>, Error> {
self.send_sync(requests::Read::new(handle, offset, len))
}
pub fn write(
&self,
handle: &str,
offset: u64,
data: &[u8],
) -> Result<oneshot::Receiver<Packet<'static>>, Error> {
self.send_sync(requests::Write::new(handle, offset, data))
}
pub async fn lstat<'a, P>(&self, path: P) -> Result<Attrs, Error>
where
P: Into<ByteStr<'a>>,
{
let attrs: responses::Attrs = self.send(requests::Lstat::new(path)).await?;
Ok(attrs.attrs)
}
pub async fn fstat(&self, handle: &str) -> Result<Attrs, Error> {
let attrs: responses::Attrs = self.send(requests::Fstat::new(handle)).await?;
Ok(attrs.attrs)
}
pub async fn setstat<'a, P>(&self, path: P, attrs: Attrs) -> Result<(), Error>
where
P: Into<ByteStr<'a>>,
{
let status: responses::Status = self.send(requests::SetStat::new(path, attrs)).await?;
status.into()
}
pub async fn fsetstat(&self, handle: &str, attrs: Attrs) -> Result<(), Error> {
let status: responses::Status = self.send(requests::FSetStat::new(handle, attrs)).await?;
status.into()
}
pub async fn read_dir<'a>(&'a self, dir: impl Into<ByteStr<'a>>) -> Result<ReadDir<'a>, Error> {
let dir: ByteStr = dir.into();
let handle: responses::Handle = self.send(requests::OpenDir::new(&dir)).await?;
Ok(ReadDir::new(self, dir, handle.handle))
}
pub async fn remove<'a, P>(&self, path: P) -> Result<(), Error>
where
P: Into<ByteStr<'a>>,
{
let status: responses::Status = self.send(requests::Remove::new(path)).await?;
status.into()
}
pub async fn mkdir<'a, P>(&self, path: P, attrs: Attrs) -> Result<(), Error>
where
P: Into<ByteStr<'a>>,
{
let status: responses::Status = self.send(requests::Mkdir::new(path, attrs)).await?;
status.into()
}
pub async fn rmdir<'a, P>(&self, path: P) -> Result<(), Error>
where
P: Into<ByteStr<'a>>,
{
let status: responses::Status = self.send(requests::Rmdir::new(path)).await?;
status.into()
}
pub async fn realpath<'a, P>(&self, path: P) -> Result<PathBuf, Error>
where
P: Into<ByteStr<'a>>,
{
let mut name: responses::Name = self.send(requests::Realpath::new(path)).await?;
if name.items.is_empty() {
Err(Error::custom("realpath returned no names"))
} else {
Ok(name.items.swap_remove(0).name.into_path())
}
}
pub async fn stat<'a, P>(&self, path: P) -> Result<Attrs, Error>
where
P: Into<ByteStr<'a>>,
{
let attrs: responses::Attrs = self.send(requests::Stat::new(path)).await?;
Ok(attrs.attrs)
}
pub async fn rename<'a, F, T>(&self, from: F, to: T) -> Result<(), Error>
where
F: Into<ByteStr<'a>>,
T: Into<ByteStr<'a>>,
{
let status: responses::Status = self.send(requests::Rename::new(from, to)).await?;
status.into()
}
pub async fn readlink<'a, P>(&self, path: P) -> Result<PathBuf, Error>
where
P: Into<ByteStr<'a>>,
{
let mut name: responses::Name = self.send(requests::Readlink::new(path)).await?;
if name.items.is_empty() {
Err(Error::custom("readlink returned no names"))
} else {
Ok(name.items.swap_remove(0).name.into_path())
}
}
pub async fn symlink<'a, L, O>(&self, link: L, original: O) -> Result<(), Error>
where
L: Into<ByteStr<'a>>,
O: Into<ByteStr<'a>>,
{
let status: responses::Status = self.send(requests::Symlink::new(link, original)).await?;
status.into()
}
pub fn fsync(&self, handle: &str) -> Result<oneshot::Receiver<Packet<'static>>, Error> {
if self.extensions.get("fsync@openssh.com").is_none_or(|s| s != "1") {
return Err(Error::Unsupported);
}
let data = requests::ExtendedFsync::new(handle);
self.send_sync(requests::Extended::new("fsync@openssh.com", data))
}
pub async fn hardlink<'a, O, L>(&self, original: O, link: L) -> Result<(), Error>
where
O: Into<ByteStr<'a>>,
L: Into<ByteStr<'a>>,
{
if self.extensions.get("hardlink@openssh.com").is_none_or(|s| s != "1") {
return Err(Error::Unsupported);
}
let data = requests::ExtendedHardlink::new(original, link);
let status: responses::Status =
self.send(requests::Extended::new("hardlink@openssh.com", data)).await?;
status.into()
}
pub async fn limits(&self) -> Result<responses::ExtendedLimits, Error> {
if self.extensions.get("limits@openssh.com").is_none_or(|s| s != "1") {
return Err(Error::Unsupported);
}
let extended: responses::Extended =
self.send(requests::Extended::new("limits@openssh.com", requests::ExtendedLimits)).await?;
extended.try_into()
}
pub async fn send<'a, I, O>(&self, input: I) -> Result<O, Error>
where
I: Into<Packet<'a>> + Serialize,
O: TryFrom<Packet<'static>, Error = Error> + 'static,
{
match self.send_sync(input)?.await? {
Packet::Status(status) if TypeId::of::<O>() != TypeId::of::<responses::Status>() => {
Err(Error::Status(status))
}
response => response.try_into(),
}
}
pub fn send_sync<'a, I>(&self, input: I) -> Result<oneshot::Receiver<Packet<'static>>, Error>
where
I: Into<Packet<'a>> + Serialize,
{
let mut request: Packet = input.into();
if request.id() == 0 {
request = request.with_id(self.id.next());
}
let (tx, rx) = oneshot::channel();
self.callback.lock().insert(request.id(), tx);
self.tx.send(crate::to_bytes(request)?)?;
Ok(rx)
}
}

View file

@ -30,7 +30,7 @@ libc = { workspace = true }
uzers = { workspace = true } uzers = { workspace = true }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.60.2", features = [ "Win32_UI_Shell" ] } windows-sys = { version = "0.61.0", features = [ "Win32_UI_Shell" ] }
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }

View file

@ -26,4 +26,4 @@ libc = { workspace = true }
crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.60.2", features = [ "Win32_Globalization", "Win32_Storage_FileSystem", "Win32_System_IO", "Win32_System_Console", "Win32_System_Threading", "Win32_Security" ] } windows-sys = { version = "0.61.0", features = [ "Win32_Globalization", "Win32_Storage_FileSystem", "Win32_System_IO", "Win32_System_Console", "Win32_System_Threading", "Win32_Security" ] }