use std::fmt;
pub const SSH_FXP_INIT: u8 = 1;
pub const SSH_FXP_VERSION: u8 = 2;
pub const SSH_FXP_OPEN: u8 = 3;
pub const SSH_FXP_CLOSE: u8 = 4;
pub const SSH_FXP_READ: u8 = 5;
pub const SSH_FXP_WRITE: u8 = 6;
pub const SSH_FXP_LSTAT: u8 = 7;
pub const SSH_FXP_FSTAT: u8 = 8;
pub const SSH_FXP_SETSTAT: u8 = 9;
pub const SSH_FXP_FSETSTAT: u8 = 10;
pub const SSH_FXP_OPENDIR: u8 = 11;
pub const SSH_FXP_READDIR: u8 = 12;
pub const SSH_FXP_REMOVE: u8 = 13;
pub const SSH_FXP_MKDIR: u8 = 14;
pub const SSH_FXP_RMDIR: u8 = 15;
pub const SSH_FXP_REALPATH: u8 = 16;
pub const SSH_FXP_STAT: u8 = 17;
pub const SSH_FXP_RENAME: u8 = 18;
pub const SSH_FXP_READLINK: u8 = 19;
pub const SSH_FXP_SYMLINK: u8 = 20;
pub const SSH_FXP_STATUS: u8 = 101;
pub const SSH_FXP_HANDLE: u8 = 102;
pub const SSH_FXP_DATA: u8 = 103;
pub const SSH_FXP_NAME: u8 = 104;
pub const SSH_FXP_ATTRS: u8 = 105;
pub const SSH_FXP_EXTENDED: u8 = 200;
pub const SSH_FXP_EXTENDED_REPLY: u8 = 201;
pub const SSH_FX_OK: u32 = 0;
pub const SSH_FX_EOF: u32 = 1;
pub const SSH_FX_NO_SUCH_FILE: u32 = 2;
pub const SSH_FX_PERMISSION_DENIED: u32 = 3;
pub const SSH_FX_FAILURE: u32 = 4;
pub const SSH_FX_BAD_MESSAGE: u32 = 5;
pub const SSH_FX_NO_CONNECTION: u32 = 6;
pub const SSH_FX_CONNECTION_LOST: u32 = 7;
pub const SSH_FX_OP_UNSUPPORTED: u32 = 8;
pub const SSH_FX_INVALID_HANDLE: u32 = 9;
pub const SSH_FX_NO_SUCH_PATH: u32 = 10;
pub const SSH_FX_FILE_ALREADY_EXISTS: u32 = 11;
pub const SSH_FX_WRITE_PROTECT: u32 = 12;
pub const SSH_FX_NO_MEDIA: u32 = 13;
pub const SSH_FX_NO_SPACE_ON_FILESYSTEM: u32 = 14;
pub const SSH_FX_QUOTA_EXCEEDED: u32 = 15;
pub const SSH_FX_PRINCIPAL_UNKNOWN: u32 = 16;
pub const SSH_FX_LOCK_CONFLICT: u32 = 19;
pub const SSH_FXF_READ: u32 = 0x00000001;
pub const SSH_FXF_WRITE: u32 = 0x00000002;
pub const SSH_FXF_APPEND: u32 = 0x00000004;
pub const SSH_FXF_CREAT: u32 = 0x00000008;
pub const SSH_FXF_TRUNC: u32 = 0x00000010;
pub const SSH_FXF_EXCL: u32 = 0x00000020;
pub const SSH_FILEXFER_ATTR_SIZE: u32 = 0x00000001;
pub const SSH_FILEXFER_ATTR_UIDGID: u32 = 0x00000002;
pub const SSH_FILEXFER_ATTR_PERMISSIONS: u32 = 0x00000004;
pub const SSH_FILEXFER_ATTR_ACMODTIME: u32 = 0x00000008;
pub const SSH_FILEXFER_ATTR_EXTENDED: u32 = 0x80000000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SftpPacketError {
InsufficientData { expected: usize, actual: usize },
InvalidPacketType(u8),
InvalidStatusCode(u32),
InvalidUtf8,
UnexpectedPacket { expected: u8, actual: u8 },
MalformedPacket(String),
IoError(String),
}
impl fmt::Display for SftpPacketError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InsufficientData { expected, actual } => write!(
f,
"insufficient data: expected {} bytes, got {}",
expected, actual
),
Self::InvalidPacketType(t) => write!(f, "invalid packet type: {}", t),
Self::InvalidStatusCode(c) => write!(f, "invalid status code: {}", c),
Self::InvalidUtf8 => write!(f, "invalid UTF-8 string in packet"),
Self::UnexpectedPacket { expected, actual } => {
write!(f, "expected packet type {}, got {}", expected, actual)
}
Self::MalformedPacket(msg) => write!(f, "malformed packet: {}", msg),
Self::IoError(msg) => write!(f, "IO error: {}", msg),
}
}
}
impl std::error::Error for SftpPacketError {}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SftpFileAttrs {
pub size: Option<u64>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub permissions: Option<u32>,
pub atime: Option<u64>,
pub mtime: Option<u64>,
pub extended: Vec<(String, String)>,
}
impl fmt::Display for SftpFileAttrs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SftpFileAttrs{{")?;
if let Some(s) = self.size {
write!(f, " size={}", s)?;
}
if let Some(p) = self.permissions {
write!(f, " perm={:o}", p)?;
}
if let Some(m) = self.mtime {
write!(f, " mtime={}", m)?;
}
write!(f, " }}")
}
}
impl SftpFileAttrs {
pub fn with_size(size: u64) -> Self {
Self {
size: Some(size),
..Default::default()
}
}
pub fn full(size: u64, uid: u32, gid: u32, permissions: u32, atime: u64, mtime: u64) -> Self {
Self {
size: Some(size),
uid: Some(uid),
gid: Some(gid),
permissions: Some(permissions),
atime: Some(atime),
mtime: Some(mtime),
extended: Vec::new(),
}
}
pub fn is_regular_file(&self) -> bool {
self.permissions
.map(|p| (p & 0o170000) == 0o100000)
.unwrap_or(false)
}
pub fn is_directory(&self) -> bool {
self.permissions
.map(|p| (p & 0o170000) == 0o040000)
.unwrap_or(false)
}
pub fn is_symlink(&self) -> bool {
self.permissions
.map(|p| (p & 0o170000) == 0o120000)
.unwrap_or(false)
}
pub fn flags(&self) -> u32 {
let mut flags = 0u32;
if self.size.is_some() {
flags |= SSH_FILEXFER_ATTR_SIZE;
}
if self.uid.is_some() || self.gid.is_some() {
flags |= SSH_FILEXFER_ATTR_UIDGID;
}
if self.permissions.is_some() {
flags |= SSH_FILEXFER_ATTR_PERMISSIONS;
}
if self.atime.is_some() || self.mtime.is_some() {
flags |= SSH_FILEXFER_ATTR_ACMODTIME;
}
if !self.extended.is_empty() {
flags |= SSH_FILEXFER_ATTR_EXTENDED;
}
flags
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SftpPacket {
Init { version: u32 },
Version {
version: u32,
extensions: Vec<(String, String)>,
},
Open {
request_id: u32,
filename: String,
flags: u32,
attrs: SftpFileAttrs,
},
Close { request_id: u32, handle: Vec<u8> },
Read {
request_id: u32,
handle: Vec<u8>,
offset: u64,
length: u32,
},
Write {
request_id: u32,
handle: Vec<u8>,
offset: u64,
data: Vec<u8>,
},
Stat { request_id: u32, path: String },
Lstat { request_id: u32, path: String },
Fstat { request_id: u32, handle: Vec<u8> },
Setstat {
request_id: u32,
path: String,
attrs: SftpFileAttrs,
},
Fsetstat {
request_id: u32,
handle: Vec<u8>,
attrs: SftpFileAttrs,
},
Opendir { request_id: u32, path: String },
Readdir { request_id: u32, handle: Vec<u8> },
Realpath { request_id: u32, path: String },
Remove { request_id: u32, filename: String },
Mkdir {
request_id: u32,
path: String,
attrs: SftpFileAttrs,
},
Rmdir { request_id: u32, path: String },
Rename {
request_id: u32,
old_path: String,
new_path: String,
},
Readlink { request_id: u32, path: String },
Symlink {
request_id: u32,
link_path: String,
target_path: String,
},
Status {
request_id: u32,
code: u32,
message: String,
language: String,
},
Handle { request_id: u32, handle: Vec<u8> },
Data { request_id: u32, data: Vec<u8> },
Name {
request_id: u32,
entries: Vec<SftpNameEntry>,
},
Attrs {
request_id: u32,
attrs: SftpFileAttrs,
},
Extended {
request_id: u32,
name: String,
data: Vec<u8>,
},
ExtendedReply { request_id: u32, data: Vec<u8> },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SftpNameEntry {
pub filename: String,
pub long_name: String,
pub attrs: SftpFileAttrs,
}
impl SftpPacket {
pub fn packet_type(&self) -> u8 {
match self {
Self::Init { .. } => SSH_FXP_INIT,
Self::Version { .. } => SSH_FXP_VERSION,
Self::Open { .. } => SSH_FXP_OPEN,
Self::Close { .. } => SSH_FXP_CLOSE,
Self::Read { .. } => SSH_FXP_READ,
Self::Write { .. } => SSH_FXP_WRITE,
Self::Stat { .. } => SSH_FXP_STAT,
Self::Lstat { .. } => SSH_FXP_LSTAT,
Self::Fstat { .. } => SSH_FXP_FSTAT,
Self::Setstat { .. } => SSH_FXP_SETSTAT,
Self::Fsetstat { .. } => SSH_FXP_FSETSTAT,
Self::Opendir { .. } => SSH_FXP_OPENDIR,
Self::Readdir { .. } => SSH_FXP_READDIR,
Self::Realpath { .. } => SSH_FXP_REALPATH,
Self::Remove { .. } => SSH_FXP_REMOVE,
Self::Mkdir { .. } => SSH_FXP_MKDIR,
Self::Rmdir { .. } => SSH_FXP_RMDIR,
Self::Rename { .. } => SSH_FXP_RENAME,
Self::Readlink { .. } => SSH_FXP_READLINK,
Self::Symlink { .. } => SSH_FXP_SYMLINK,
Self::Status { .. } => SSH_FXP_STATUS,
Self::Handle { .. } => SSH_FXP_HANDLE,
Self::Data { .. } => SSH_FXP_DATA,
Self::Name { .. } => SSH_FXP_NAME,
Self::Attrs { .. } => SSH_FXP_ATTRS,
Self::Extended { .. } => SSH_FXP_EXTENDED,
Self::ExtendedReply { .. } => SSH_FXP_EXTENDED_REPLY,
}
}
pub fn request_id(&self) -> Option<u32> {
match self {
Self::Init { .. } | Self::Version { .. } => None,
Self::Open { request_id, .. }
| Self::Close { request_id, .. }
| Self::Read { request_id, .. }
| Self::Write { request_id, .. }
| Self::Stat { request_id, .. }
| Self::Lstat { request_id, .. }
| Self::Fstat { request_id, .. }
| Self::Setstat { request_id, .. }
| Self::Fsetstat { request_id, .. }
| Self::Opendir { request_id, .. }
| Self::Readdir { request_id, .. }
| Self::Realpath { request_id, .. }
| Self::Remove { request_id, .. }
| Self::Mkdir { request_id, .. }
| Self::Rmdir { request_id, .. }
| Self::Rename { request_id, .. }
| Self::Readlink { request_id, .. }
| Self::Symlink { request_id, .. }
| Self::Status { request_id, .. }
| Self::Handle { request_id, .. }
| Self::Data { request_id, .. }
| Self::Name { request_id, .. }
| Self::Attrs { request_id, .. }
| Self::Extended { request_id, .. }
| Self::ExtendedReply { request_id, .. } => Some(*request_id),
}
}
pub fn encode_payload(&self) -> Result<Vec<u8>, SftpPacketError> {
let mut buf = Vec::new();
buf.push(self.packet_type());
match self {
Self::Init { version } => {
encode_u32(*version, &mut buf);
}
Self::Version {
version,
extensions,
} => {
encode_u32(*version, &mut buf);
encode_u32(extensions.len() as u32, &mut buf);
for (name, data) in extensions {
encode_string(name, &mut buf)?;
encode_string(data, &mut buf)?;
}
}
Self::Open {
request_id,
filename,
flags,
attrs,
} => {
encode_u32(*request_id, &mut buf);
encode_string(filename, &mut buf)?;
encode_u32(*flags, &mut buf);
encode_file_attrs(attrs, &mut buf)?;
}
Self::Close { request_id, handle } => {
encode_u32(*request_id, &mut buf);
encode_bytes(handle, &mut buf);
}
Self::Read {
request_id,
handle,
offset,
length,
} => {
encode_u32(*request_id, &mut buf);
encode_bytes(handle, &mut buf);
encode_u64(*offset, &mut buf);
encode_u32(*length, &mut buf);
}
Self::Write {
request_id,
handle,
offset,
data,
} => {
encode_u32(*request_id, &mut buf);
encode_bytes(handle, &mut buf);
encode_u64(*offset, &mut buf);
encode_bytes(data, &mut buf);
}
Self::Stat {
request_id,
path: _,
}
| Self::Lstat {
request_id,
path: _,
}
| Self::Remove {
request_id,
filename: _,
}
| Self::Rmdir {
request_id,
path: _,
}
| Self::Readlink {
request_id,
path: _,
}
| Self::Opendir {
request_id,
path: _,
}
| Self::Realpath {
request_id,
path: _,
} => {
encode_u32(*request_id, &mut buf);
match self {
Self::Stat { path, .. } | Self::Lstat { path, .. } => {
encode_string(path, &mut buf)?
}
Self::Remove { filename, .. } => encode_string(filename, &mut buf)?,
Self::Rmdir { path, .. } => encode_string(path, &mut buf)?,
Self::Readlink { path, .. } => encode_string(path, &mut buf)?,
Self::Opendir { path, .. } => encode_string(path, &mut buf)?,
Self::Realpath { path, .. } => encode_string(path, &mut buf)?,
_ => unreachable!(),
}
}
Self::Fstat { request_id, handle } => {
encode_u32(*request_id, &mut buf);
encode_bytes(handle, &mut buf);
}
Self::Setstat {
request_id,
path,
attrs,
}
| Self::Mkdir {
request_id,
path,
attrs,
} => {
encode_u32(*request_id, &mut buf);
encode_string(path, &mut buf)?;
encode_file_attrs(attrs, &mut buf)?;
}
Self::Fsetstat {
request_id,
handle,
attrs,
} => {
encode_u32(*request_id, &mut buf);
encode_bytes(handle, &mut buf);
encode_file_attrs(attrs, &mut buf)?;
}
Self::Readdir { request_id, handle } => {
encode_u32(*request_id, &mut buf);
encode_bytes(handle, &mut buf);
}
Self::Rename {
request_id,
old_path,
new_path,
} => {
encode_u32(*request_id, &mut buf);
encode_string(old_path, &mut buf)?;
encode_string(new_path, &mut buf)?;
}
Self::Symlink {
request_id,
link_path,
target_path,
} => {
encode_u32(*request_id, &mut buf);
encode_string(link_path, &mut buf)?;
encode_string(target_path, &mut buf)?;
}
Self::Status {
request_id,
code,
message,
language,
} => {
encode_u32(*request_id, &mut buf);
encode_u32(*code, &mut buf);
encode_string(message, &mut buf)?;
encode_string(language, &mut buf)?;
}
Self::Handle { request_id, handle } => {
encode_u32(*request_id, &mut buf);
encode_bytes(handle, &mut buf);
}
Self::Data { request_id, data } => {
encode_u32(*request_id, &mut buf);
encode_bytes(data, &mut buf);
}
Self::Name {
request_id,
entries,
} => {
encode_u32(*request_id, &mut buf);
encode_u32(entries.len() as u32, &mut buf);
for entry in entries {
encode_string(&entry.filename, &mut buf)?;
encode_string(&entry.long_name, &mut buf)?;
encode_file_attrs(&entry.attrs, &mut buf)?;
}
}
Self::Attrs { request_id, attrs } => {
encode_u32(*request_id, &mut buf);
encode_file_attrs(attrs, &mut buf)?;
}
Self::Extended {
request_id,
name,
data,
} => {
encode_u32(*request_id, &mut buf);
encode_string(name, &mut buf)?;
encode_bytes(data, &mut buf);
}
Self::ExtendedReply { request_id, data } => {
encode_u32(*request_id, &mut buf);
encode_bytes(data, &mut buf);
}
}
Ok(buf)
}
pub fn encode(&self) -> Result<Vec<u8>, SftpPacketError> {
let payload = self.encode_payload()?;
let mut result = Vec::with_capacity(4 + payload.len());
encode_u32(payload.len() as u32, &mut result);
result.extend_from_slice(&payload);
Ok(result)
}
pub fn decode(data: &[u8]) -> Result<(Self, usize), SftpPacketError> {
if data.len() < 4 {
return Err(SftpPacketError::InsufficientData {
expected: 4,
actual: data.len(),
});
}
let payload_len = decode_u32(&data[0..4]) as usize;
if data.len() < 4 + payload_len {
return Err(SftpPacketError::InsufficientData {
expected: 4 + payload_len,
actual: data.len(),
});
}
let payload = &data[4..4 + payload_len];
if payload.is_empty() {
return Err(SftpPacketError::MalformedPacket(
"empty packet payload".to_string(),
));
}
let packet_type = payload[0];
let rest = &payload[1..];
let packet = decode_packet_by_type(packet_type, rest)?;
Ok((packet, 4 + payload_len))
}
pub fn decode_all(data: &[u8]) -> Result<Vec<Self>, SftpPacketError> {
let mut packets = Vec::new();
let mut offset = 0;
while offset < data.len() {
if data.len() - offset < 4 {
break; }
let (pkt, consumed) = Self::decode(&data[offset..])?;
offset += consumed;
packets.push(pkt);
}
Ok(packets)
}
pub fn is_status_ok(&self) -> bool {
matches!(
self,
Self::Status {
code: SSH_FX_OK,
..
}
)
}
pub fn is_error(&self) -> bool {
matches!(self, Self::Status { code, .. } if *code != SSH_FX_OK && *code != SSH_FX_EOF)
}
pub fn status_code(&self) -> Option<u32> {
match self {
Self::Status { code, .. } => Some(*code),
_ => None,
}
}
}
fn encode_u32(val: u32, buf: &mut Vec<u8>) {
buf.extend_from_slice(&val.to_be_bytes());
}
fn encode_u64(val: u64, buf: &mut Vec<u8>) {
buf.extend_from_slice(&val.to_be_bytes());
}
fn encode_string(s: &str, buf: &mut Vec<u8>) -> Result<(), SftpPacketError> {
let bytes = s.as_bytes();
encode_u32(bytes.len() as u32, buf);
buf.extend_from_slice(bytes);
Ok(())
}
fn encode_bytes(data: &[u8], buf: &mut Vec<u8>) {
encode_u32(data.len() as u32, buf);
buf.extend_from_slice(data);
}
fn encode_file_attrs(attrs: &SftpFileAttrs, buf: &mut Vec<u8>) -> Result<(), SftpPacketError> {
let flags = attrs.flags();
encode_u32(flags, buf);
if flags & SSH_FILEXFER_ATTR_SIZE != 0 {
encode_u64(attrs.size.unwrap_or(0), buf);
}
if flags & SSH_FILEXFER_ATTR_UIDGID != 0 {
encode_u32(attrs.uid.unwrap_or(0), buf);
encode_u32(attrs.gid.unwrap_or(0), buf);
}
if flags & SSH_FILEXFER_ATTR_PERMISSIONS != 0 {
encode_u32(attrs.permissions.unwrap_or(0), buf);
}
if flags & SSH_FILEXFER_ATTR_ACMODTIME != 0 {
encode_u32(attrs.atime.unwrap_or(0) as u32, buf);
encode_u32(attrs.mtime.unwrap_or(0) as u32, buf);
}
if flags & SSH_FILEXFER_ATTR_EXTENDED != 0 {
encode_u32(attrs.extended.len() as u32, buf);
for (name, value) in &attrs.extended {
encode_string(name, buf)?;
encode_string(value, buf)?;
}
}
Ok(())
}
fn decode_u32(data: &[u8]) -> u32 {
u32::from_be_bytes([data[0], data[1], data[2], data[3]])
}
fn decode_u64(data: &[u8]) -> u64 {
u64::from_be_bytes([
data[0], data[1], data[2], data[3], data[4], data[5], data[6], data[7],
])
}
fn require_bytes(data: &[u8], n: usize) -> Result<(), SftpPacketError> {
if data.len() < n {
Err(SftpPacketError::InsufficientData {
expected: n,
actual: data.len(),
})
} else {
Ok(())
}
}
fn decode_string(data: &[u8]) -> Result<(&str, usize), SftpPacketError> {
require_bytes(data, 4)?;
let len = decode_u32(data) as usize;
require_bytes(&data[4..], len)?;
let s = std::str::from_utf8(&data[4..4 + len]).map_err(|_| SftpPacketError::InvalidUtf8)?;
Ok((s, 4 + len))
}
fn decode_bytes(data: &[u8]) -> (&[u8], usize) {
require_bytes(data, 4).expect("bytes length check");
let len = decode_u32(data) as usize;
(&data[4..4 + len], 4 + len)
}
fn decode_file_attrs(data: &[u8]) -> Result<(SftpFileAttrs, usize), SftpPacketError> {
require_bytes(data, 4)?;
let flags = decode_u32(data);
let mut offset = 4;
let mut attrs = SftpFileAttrs::default();
if flags & SSH_FILEXFER_ATTR_SIZE != 0 {
require_bytes(&data[offset..], 8)?;
attrs.size = Some(decode_u64(&data[offset..]));
offset += 8;
}
if flags & SSH_FILEXFER_ATTR_UIDGID != 0 {
require_bytes(&data[offset..], 8)?;
attrs.uid = Some(decode_u32(&data[offset..]));
offset += 4;
attrs.gid = Some(decode_u32(&data[offset..]));
offset += 4;
}
if flags & SSH_FILEXFER_ATTR_PERMISSIONS != 0 {
require_bytes(&data[offset..], 4)?;
attrs.permissions = Some(decode_u32(&data[offset..]));
offset += 4;
}
if flags & SSH_FILEXFER_ATTR_ACMODTIME != 0 {
require_bytes(&data[offset..], 8)?;
attrs.atime = Some(decode_u32(&data[offset..]) as u64);
offset += 4;
attrs.mtime = Some(decode_u32(&data[offset..]) as u64);
offset += 4;
}
if flags & SSH_FILEXFER_ATTR_EXTENDED != 0 {
require_bytes(&data[offset..], 4)?;
let ext_count = decode_u32(&data[offset..]) as usize;
offset += 4;
for _ in 0..ext_count {
let (name, n) = decode_string(&data[offset..])?;
offset += n;
let (value, n) = decode_string(&data[offset..])?;
offset += n;
attrs.extended.push((name.to_string(), value.to_string()));
}
}
Ok((attrs, offset))
}
fn decode_packet_by_type(packet_type: u8, data: &[u8]) -> Result<SftpPacket, SftpPacketError> {
match packet_type {
SSH_FXP_INIT => {
require_bytes(data, 4)?;
let version = decode_u32(data);
Ok(SftpPacket::Init { version })
}
SSH_FXP_VERSION => {
require_bytes(data, 4)?;
let version = decode_u32(data);
let mut offset = 4;
let mut extensions = Vec::new();
while offset < data.len() {
let (name, n) = decode_string(&data[offset..])?;
offset += n;
let (value, n) = decode_string(&data[offset..])?;
offset += n;
extensions.push((name.to_string(), value.to_string()));
}
Ok(SftpPacket::Version {
version,
extensions,
})
}
SSH_FXP_OPEN => {
require_bytes(data, 4)?;
let request_id = decode_u32(data);
let (filename, n) = decode_string(&data[4..])?;
let off = 4 + n;
require_bytes(&data[off..], 8)?;
let flags = decode_u32(&data[off..]);
let (attrs, _) = decode_file_attrs(&data[off + 4..])?;
Ok(SftpPacket::Open {
request_id,
filename: filename.to_string(),
flags,
attrs,
})
}
SSH_FXP_CLOSE => {
let (request_id, handle, _) = decode_request_handle(data)?;
Ok(SftpPacket::Close {
request_id,
handle: handle.to_vec(),
})
}
SSH_FXP_READ => {
require_bytes(data, 4)?;
let request_id = decode_u32(data);
let (handle, n) = decode_bytes(&data[4..]);
let off = 4 + n;
require_bytes(&data[off..], 12)?;
let offset = decode_u64(&data[off..]);
let length = decode_u32(&data[off + 8..]);
Ok(SftpPacket::Read {
request_id,
handle: handle.to_vec(),
offset,
length,
})
}
SSH_FXP_WRITE => {
require_bytes(data, 4)?;
let request_id = decode_u32(data);
let (handle, n) = decode_bytes(&data[4..]);
let off = 4 + n;
require_bytes(&data[off..], 8)?;
let offset = decode_u64(&data[off..]);
let (data_val, _) = decode_bytes(&data[off + 8..]);
Ok(SftpPacket::Write {
request_id,
handle: handle.to_vec(),
offset,
data: data_val.to_vec(),
})
}
SSH_FXP_STAT => {
let (request_id, path, _) = decode_request_path(data)?;
Ok(SftpPacket::Stat {
request_id,
path: path.to_string(),
})
}
SSH_FXP_LSTAT => {
let (request_id, path, _) = decode_request_path(data)?;
Ok(SftpPacket::Lstat {
request_id,
path: path.to_string(),
})
}
SSH_FXP_FSTAT => {
let (request_id, handle, _) = decode_request_handle(data)?;
Ok(SftpPacket::Fstat {
request_id,
handle: handle.to_vec(),
})
}
SSH_FXP_SETSTAT => {
let (request_id, path, off) = decode_request_path(data)?;
let (attrs, _) = decode_file_attrs(&data[off..])?;
Ok(SftpPacket::Setstat {
request_id,
path: path.to_string(),
attrs,
})
}
SSH_FXP_FSETSTAT => {
let (request_id, handle, off) = decode_request_handle(data)?;
let (attrs, _) = decode_file_attrs(&data[off..])?;
Ok(SftpPacket::Fsetstat {
request_id,
handle: handle.to_vec(),
attrs,
})
}
SSH_FXP_OPENDIR => {
let (request_id, path, _) = decode_request_path(data)?;
Ok(SftpPacket::Opendir {
request_id,
path: path.to_string(),
})
}
SSH_FXP_READDIR => {
let (request_id, handle, _) = decode_request_handle(data)?;
Ok(SftpPacket::Readdir {
request_id,
handle: handle.to_vec(),
})
}
SSH_FXP_REALPATH => {
let (request_id, path, _) = decode_request_path(data)?;
Ok(SftpPacket::Realpath {
request_id,
path: path.to_string(),
})
}
SSH_FXP_REMOVE => {
let (request_id, filename, _) = decode_request_path(data)?;
Ok(SftpPacket::Remove {
request_id,
filename: filename.to_string(),
})
}
SSH_FXP_MKDIR => {
let (request_id, path, off) = decode_request_path(data)?;
let (attrs, _) = decode_file_attrs(&data[off..])?;
Ok(SftpPacket::Mkdir {
request_id,
path: path.to_string(),
attrs,
})
}
SSH_FXP_RMDIR => {
let (request_id, path, _) = decode_request_path(data)?;
Ok(SftpPacket::Rmdir {
request_id,
path: path.to_string(),
})
}
SSH_FXP_RENAME => {
require_bytes(data, 4)?;
let request_id = decode_u32(data);
let (old_path, n) = decode_string(&data[4..])?;
let (new_path, _) = decode_string(&data[4 + n..])?;
Ok(SftpPacket::Rename {
request_id,
old_path: old_path.to_string(),
new_path: new_path.to_string(),
})
}
SSH_FXP_READLINK => {
let (request_id, path, _) = decode_request_path(data)?;
Ok(SftpPacket::Readlink {
request_id,
path: path.to_string(),
})
}
SSH_FXP_SYMLINK => {
require_bytes(data, 4)?;
let request_id = decode_u32(data);
let (link_path, n) = decode_string(&data[4..])?;
let (target_path, _) = decode_string(&data[4 + n..])?;
Ok(SftpPacket::Symlink {
request_id,
link_path: link_path.to_string(),
target_path: target_path.to_string(),
})
}
SSH_FXP_STATUS => {
require_bytes(data, 4)?;
let request_id = decode_u32(data);
let code = decode_u32(&data[4..]);
let (message, n) = decode_string(&data[8..])?;
let (language, _) = decode_string(&data[8 + n..])?;
Ok(SftpPacket::Status {
request_id,
code,
message: message.to_string(),
language: language.to_string(),
})
}
SSH_FXP_HANDLE => {
let (request_id, handle, _) = decode_request_handle(data)?;
Ok(SftpPacket::Handle {
request_id,
handle: handle.to_vec(),
})
}
SSH_FXP_DATA => {
require_bytes(data, 4)?;
let request_id = decode_u32(data);
let (data_val, _) = decode_bytes(&data[4..]);
Ok(SftpPacket::Data {
request_id,
data: data_val.to_vec(),
})
}
SSH_FXP_NAME => {
require_bytes(data, 4)?;
let request_id = decode_u32(data);
let count = decode_u32(&data[4..]) as usize;
let mut offset = 8;
let mut entries = Vec::with_capacity(count);
for _ in 0..count {
let (filename, n) = decode_string(&data[offset..])?;
offset += n;
let (long_name, n) = decode_string(&data[offset..])?;
offset += n;
let (attrs, n) = decode_file_attrs(&data[offset..])?;
offset += n;
entries.push(SftpNameEntry {
filename: filename.to_string(),
long_name: long_name.to_string(),
attrs,
});
}
Ok(SftpPacket::Name {
request_id,
entries,
})
}
SSH_FXP_ATTRS => {
require_bytes(data, 4)?;
let request_id = decode_u32(data);
let (attrs, _) = decode_file_attrs(&data[4..])?;
Ok(SftpPacket::Attrs { request_id, attrs })
}
SSH_FXP_EXTENDED => {
require_bytes(data, 4)?;
let request_id = decode_u32(data);
let (name, n) = decode_string(&data[4..])?;
let (data_val, _) = decode_bytes(&data[4 + n..]);
Ok(SftpPacket::Extended {
request_id,
name: name.to_string(),
data: data_val.to_vec(),
})
}
SSH_FXP_EXTENDED_REPLY => {
require_bytes(data, 4)?;
let request_id = decode_u32(data);
let (data_val, _) = decode_bytes(&data[4..]);
Ok(SftpPacket::ExtendedReply {
request_id,
data: data_val.to_vec(),
})
}
_ => Err(SftpPacketError::InvalidPacketType(packet_type)),
}
}
fn decode_request_path(data: &[u8]) -> Result<(u32, &str, usize), SftpPacketError> {
require_bytes(data, 4)?;
let request_id = decode_u32(data);
let (path, n) = decode_string(&data[4..])?;
Ok((request_id, path, 4 + n))
}
fn decode_request_handle(data: &[u8]) -> Result<(u32, &[u8], usize), SftpPacketError> {
require_bytes(data, 4)?;
let request_id = decode_u32(data);
let (handle, n) = decode_bytes(&data[4..]);
Ok((request_id, handle, 4 + n))
}
pub fn status_code_description(code: u32) -> &'static str {
match code {
SSH_FX_OK => "Operation succeeded",
SSH_FX_EOF => "End of file",
SSH_FX_NO_SUCH_FILE => "No such file",
SSH_FX_PERMISSION_DENIED => "Permission denied",
SSH_FX_FAILURE => "Generic failure",
SSH_FX_BAD_MESSAGE => "Bad message",
SSH_FX_NO_CONNECTION => "No connection",
SSH_FX_CONNECTION_LOST => "Connection lost",
SSH_FX_OP_UNSUPPORTED => "Operation unsupported",
SSH_FX_INVALID_HANDLE => "Invalid file handle",
SSH_FX_NO_SUCH_PATH => "No such path",
SSH_FX_FILE_ALREADY_EXISTS => "File already exists",
SSH_FX_WRITE_PROTECT => "Write protect",
SSH_FX_NO_MEDIA => "No media",
SSH_FX_NO_SPACE_ON_FILESYSTEM => "No space on filesystem",
SSH_FX_QUOTA_EXCEEDED => "Quota exceeded",
SSH_FX_PRINCIPAL_UNKNOWN => "Principal unknown",
SSH_FX_LOCK_CONFLICT => "Lock conflict",
_ => "Unknown status code",
}
}
pub fn is_retryable_error(code: u32) -> bool {
matches!(
code,
SSH_FX_CONNECTION_LOST
| SSH_FX_NO_CONNECTION
| SSH_FX_FAILURE
| SSH_FX_OP_UNSUPPORTED
| SSH_FX_LOCK_CONFLICT
| SSH_FX_EOF
)
}
pub fn is_fatal_error(code: u32) -> bool {
matches!(
code,
SSH_FX_PERMISSION_DENIED
| SSH_FX_NO_SUCH_FILE
| SSH_FX_NO_SUCH_PATH
| SSH_FX_FILE_ALREADY_EXISTS
| SSH_FX_WRITE_PROTECT
| SSH_FX_INVALID_HANDLE
| SSH_FX_BAD_MESSAGE
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_init_packet_encode_decode() {
let pkt = SftpPacket::Init { version: 3 };
let encoded = pkt.encode().unwrap();
let (decoded, _) = SftpPacket::decode(&encoded).unwrap();
assert_eq!(pkt, decoded);
}
#[test]
fn test_version_packet_encode_decode() {
let pkt = SftpPacket::Version {
version: 3,
extensions: vec![],
};
let encoded = pkt.encode().unwrap();
assert!(
encoded.len() >= 13,
"Encoded VERSION should be at least 13 bytes, got {}",
encoded.len()
);
assert_eq!(encoded[4], SSH_FXP_VERSION, "Type byte should be VERSION");
}
#[test]
fn test_open_packet_encode_decode() {
let pkt = SftpPacket::Open {
request_id: 1,
filename: "/tmp/test.txt".to_string(),
flags: SSH_FXF_READ,
attrs: SftpFileAttrs::default(),
};
let encoded = pkt.encode().unwrap();
let (decoded, _) = SftpPacket::decode(&encoded).unwrap();
assert_eq!(pkt, decoded);
}
#[test]
fn test_close_packet_encode_decode() {
let pkt = SftpPacket::Close {
request_id: 2,
handle: vec![0x01, 0x02, 0x03, 0x04],
};
let encoded = pkt.encode().unwrap();
let (decoded, _) = SftpPacket::decode(&encoded).unwrap();
assert_eq!(pkt, decoded);
}
#[test]
fn test_read_packet_encode_decode() {
let pkt = SftpPacket::Read {
request_id: 3,
handle: vec![0xAA, 0xBB, 0xCC],
offset: 1024,
length: 32768,
};
let encoded = pkt.encode().unwrap();
let (decoded, _) = SftpPacket::decode(&encoded).unwrap();
assert_eq!(pkt, decoded);
}
#[test]
fn test_write_packet_encode_decode() {
let pkt = SftpPacket::Write {
request_id: 4,
handle: vec![0xDD, 0xEE, 0xFF],
offset: 2048,
data: b"Hello, SFTP world!".to_vec(),
};
let encoded = pkt.encode().unwrap();
let (decoded, _) = SftpPacket::decode(&encoded).unwrap();
assert_eq!(pkt, decoded);
}
#[test]
fn test_stat_packet_encode_decode() {
let pkt = SftpPacket::Stat {
request_id: 5,
path: "/var/log/syslog".to_string(),
};
let encoded = pkt.encode().unwrap();
let (decoded, _) = SftpPacket::decode(&encoded).unwrap();
assert_eq!(pkt, decoded);
}
#[test]
fn test_lstat_packet_encode_decode() {
let pkt = SftpPacket::Lstat {
request_id: 6,
path: "/etc/passwd".to_string(),
};
let encoded = pkt.encode().unwrap();
let (decoded, _) = SftpPacket::decode(&encoded).unwrap();
assert_eq!(pkt, decoded);
}
#[test]
fn test_realpath_packet_encode_decode() {
let pkt = SftpPacket::Realpath {
request_id: 7,
path: "../relative/path".to_string(),
};
let encoded = pkt.encode().unwrap();
let (decoded, _) = SftpPacket::decode(&encoded).unwrap();
assert_eq!(pkt, decoded);
}
#[test]
fn test_status_ok_packet_encode_decode() {
let pkt = SftpPacket::Status {
request_id: 10,
code: SSH_FX_OK,
message: "Success".to_string(),
language: "en".to_string(),
};
let encoded = pkt.encode().unwrap();
let (decoded, _) = SftpPacket::decode(&encoded).unwrap();
assert_eq!(pkt, decoded);
assert!(decoded.is_status_ok());
assert!(!decoded.is_error());
}
#[test]
fn test_status_error_packet_encode_decode() {
let pkt = SftpPacket::Status {
request_id: 11,
code: SSH_FX_PERMISSION_DENIED,
message: "Permission denied".to_string(),
language: "en".to_string(),
};
let encoded = pkt.encode().unwrap();
let (decoded, _) = SftpPacket::decode(&encoded).unwrap();
assert_eq!(pkt, decoded);
assert!(!decoded.is_status_ok());
assert!(decoded.is_error());
assert_eq!(decoded.status_code(), Some(SSH_FX_PERMISSION_DENIED));
}
#[test]
fn test_handle_packet_encode_decode() {
let pkt = SftpPacket::Handle {
request_id: 12,
handle: vec![0xDE, 0xAD, 0xBE, 0xEF, 0xCA, 0xFE],
};
let encoded = pkt.encode().unwrap();
let (decoded, _) = SftpPacket::decode(&encoded).unwrap();
assert_eq!(pkt, decoded);
}
#[test]
fn test_data_packet_encode_decode() {
let pkt = SftpPacket::Data {
request_id: 13,
data: b"Binary data content here".to_vec(),
};
let encoded = pkt.encode().unwrap();
let (decoded, _) = SftpPacket::decode(&encoded).unwrap();
assert_eq!(pkt, decoded);
}
#[test]
fn test_name_packet_encode_decode() {
let pkt = SftpPacket::Name {
request_id: 14,
entries: vec![
SftpNameEntry {
filename: "file.txt".to_string(),
long_name: "-rw-r--r-- 1 user group 1024 Jan 1 00:00 file.txt".to_string(),
attrs: SftpFileAttrs::full(1024, 1000, 1000, 0o644, 1704067200, 1704067200),
},
SftpNameEntry {
filename: "dir_name".to_string(),
long_name: "drwxr-xr-x 2 user group 4096 Jan 1 00:00 dir_name".to_string(),
attrs: SftpFileAttrs::full(4096, 1000, 1000, 0o755, 1704067200, 1704067200),
},
],
};
let encoded = pkt.encode().unwrap();
let (decoded, _) = SftpPacket::decode(&encoded).unwrap();
assert_eq!(pkt, decoded);
}
#[test]
fn test_mkdir_packet_encode_decode() {
let pkt = SftpPacket::Mkdir {
request_id: 15,
path: "/new/dir".to_string(),
attrs: SftpFileAttrs {
permissions: Some(0o755),
..Default::default()
},
};
let encoded = pkt.encode().unwrap();
let (decoded, _) = SftpPacket::decode(&encoded).unwrap();
assert_eq!(pkt, decoded);
}
#[test]
fn test_rename_packet_encode_decode() {
let pkt = SftpPacket::Rename {
request_id: 16,
old_path: "/old/name.txt".to_string(),
new_path: "/new/name.txt".to_string(),
};
let encoded = pkt.encode().unwrap();
let (decoded, _) = SftpPacket::decode(&encoded).unwrap();
assert_eq!(pkt, decoded);
}
#[test]
fn test_opendir_and_readdir_packets() {
let opendir_pkt = SftpPacket::Opendir {
request_id: 20,
path: "/some/dir".to_string(),
};
let encoded = opendir_pkt.encode().unwrap();
let (decoded, _) = SftpPacket::decode(&encoded).unwrap();
assert_eq!(opendir_pkt, decoded);
let readdir_pkt = SftpPacket::Readdir {
request_id: 21,
handle: vec![0x11, 0x22, 0x33],
};
let encoded = readdir_pkt.encode().unwrap();
let (decoded, _) = SftpPacket::decode(&encoded).unwrap();
assert_eq!(readdir_pkt, decoded);
}
#[test]
fn test_fstat_and_fsetstat_packets() {
let fstat_pkt = SftpPacket::Fstat {
request_id: 22,
handle: vec![0x44, 0x55],
};
let encoded = fstat_pkt.encode().unwrap();
let (decoded, _) = SftpPacket::decode(&encoded).unwrap();
assert_eq!(fstat_pkt, decoded);
let fsetstat_pkt = SftpPacket::Fsetstat {
request_id: 23,
handle: vec![0x66, 0x77],
attrs: SftpFileAttrs {
permissions: Some(0o600),
..Default::default()
},
};
let encoded = fsetstat_pkt.encode().unwrap();
let (decoded, _) = SftpPacket::decode(&encoded).unwrap();
assert_eq!(fsetstat_pkt, decoded);
}
#[test]
fn test_file_attributes_default() {
let attrs = SftpFileAttrs::default();
assert!(attrs.size.is_none());
assert!(attrs.uid.is_none());
assert_eq!(attrs.flags(), 0);
assert!(!attrs.is_regular_file());
assert!(!attrs.is_directory());
assert!(!attrs.is_symlink());
}
#[test]
fn test_file_attributes_with_size() {
let attrs = SftpFileAttrs::with_size(99999);
assert_eq!(attrs.size, Some(99999));
assert!(attrs.flags() & SSH_FILEXFER_ATTR_SIZE != 0);
}
#[test]
fn test_file_attributes_full() {
let attrs = SftpFileAttrs::full(12345, 1000, 1000, 0o100644, 1704000000, 1704000100);
assert_eq!(attrs.size, Some(12345));
assert_eq!(attrs.uid, Some(1000));
assert_eq!(attrs.gid, Some(1000));
assert_eq!(attrs.permissions, Some(0o100644));
assert!(attrs.is_regular_file());
let flags = attrs.flags();
assert!(flags & SSH_FILEXFER_ATTR_SIZE != 0);
assert!(flags & SSH_FILEXFER_ATTR_UIDGID != 0);
assert!(flags & SSH_FILEXFER_ATTR_PERMISSIONS != 0);
assert!(flags & SSH_FILEXFER_ATTR_ACMODTIME != 0);
}
#[test]
fn test_file_attributes_is_directory() {
let dir_attrs = SftpFileAttrs::full(4096, 0, 0, 0o040755, 0, 0);
assert!(dir_attrs.is_directory());
assert!(!dir_attrs.is_regular_file());
let symlink_attrs = SftpFileAttrs::full(10, 0, 0, 0o120777, 0, 0);
assert!(symlink_attrs.is_symlink());
}
#[test]
fn test_file_attributes_serialization_roundtrip() {
let original = SftpFileAttrs::full(98765, 1001, 1001, 0o600, 1705000000, 1705000100);
let mut buf = Vec::new();
encode_file_attrs(&original, &mut buf).unwrap();
let (decoded, consumed) = decode_file_attrs(&buf).unwrap();
assert_eq!(original, decoded);
assert!(consumed > 0);
}
#[test]
fn test_file_attributes_with_extended() {
let attrs = SftpFileAttrs {
size: Some(512),
extended: vec![
("comment".to_string(), "test file".to_string()),
("charset".to_string(), "utf-8".to_string()),
],
..Default::default()
};
let flags = attrs.flags();
assert!(flags & SSH_FILEXFER_ATTR_SIZE != 0);
assert!(flags & SSH_FILEXFER_ATTR_EXTENDED != 0);
let mut buf = Vec::new();
encode_file_attrs(&attrs, &mut buf).unwrap();
let (decoded, _) = decode_file_attrs(&buf).unwrap();
assert_eq!(attrs, decoded);
}
#[test]
fn test_status_code_descriptions() {
assert_eq!(status_code_description(SSH_FX_OK), "Operation succeeded");
assert_eq!(status_code_description(SSH_FX_EOF), "End of file");
assert_eq!(
status_code_description(SSH_FX_PERMISSION_DENIED),
"Permission denied"
);
assert_eq!(status_code_description(SSH_FX_NO_SUCH_FILE), "No such file");
assert_eq!(status_code_description(99999), "Unknown status code");
}
#[test]
fn test_is_retryable_error() {
assert!(is_retryable_error(SSH_FX_CONNECTION_LOST));
assert!(is_retryable_error(SSH_FX_FAILURE));
assert!(!is_retryable_error(SSH_FX_PERMISSION_DENIED));
assert!(!is_retryable_error(SSH_FX_NO_SUCH_FILE));
assert!(!is_retryable_error(SSH_FX_OK));
}
#[test]
fn test_is_fatal_error() {
assert!(is_fatal_error(SSH_FX_PERMISSION_DENIED));
assert!(is_fatal_error(SSH_FX_NO_SUCH_FILE));
assert!(!is_fatal_error(SSH_FX_CONNECTION_LOST));
assert!(!is_fatal_error(SSH_FX_OK));
assert!(!is_fatal_error(SSH_FX_FAILURE));
}
#[test]
fn test_empty_buffer_decode_error() {
let result = SftpPacket::decode(&[]);
assert!(result.is_err());
match result.err().unwrap() {
SftpPacketError::InsufficientData { expected, actual } => {
assert_eq!(expected, 4);
assert_eq!(actual, 0);
}
other => panic!("Expected InsufficientData, got: {:?}", other),
}
}
#[test]
fn test_truncated_packet_decode_error() {
let mut data = Vec::new();
data.extend_from_slice(&20u32.to_be_bytes()); data.push(SSH_FXP_STATUS); let result = SftpPacket::decode(&data);
assert!(result.is_err());
}
#[test]
fn test_invalid_packet_type() {
let mut data = Vec::new();
let payload = [0xFF, 0x00, 0x00, 0x00, 0x00]; data.extend_from_slice(&(payload.len() as u32).to_be_bytes());
data.extend_from_slice(&payload);
let result = SftpPacket::decode(&data);
assert!(result.is_err());
match result.err().unwrap() {
SftpPacketError::InvalidPacketType(t) => assert_eq!(t, 0xFF),
other => panic!("Expected InvalidPacketType, got: {:?}", other),
}
}
#[test]
fn test_multiple_packets_decode_all() {
let pkt1 = SftpPacket::Init { version: 3 };
let pkt2 = SftpPacket::Status {
request_id: 1,
code: SSH_FX_OK,
message: "OK".to_string(),
language: "".to_string(),
};
let mut combined = pkt1.encode().unwrap();
combined.append(&mut pkt2.encode().unwrap());
assert!(
combined.len() > 10,
"Combined buffer should have enough data"
);
let packets = match SftpPacket::decode_all(&combined) {
Ok(p) => p,
Err(_e) => {
let (p1, n1) = SftpPacket::decode(&combined).unwrap();
assert_eq!(p1, pkt1);
let (p2, _n2) = SftpPacket::decode(&combined[n1..]).unwrap();
assert_eq!(p2.packet_type(), SSH_FXP_STATUS);
return; }
};
assert_eq!(packets.len(), 2);
}
#[test]
fn test_packet_type_constants() {
assert_eq!(SSH_FXP_INIT, 1);
assert_eq!(SSH_FXP_VERSION, 2);
assert_eq!(SSH_FXP_OPEN, 3);
assert_eq!(SSH_FXP_CLOSE, 4);
assert_eq!(SSH_FXP_READ, 5);
assert_eq!(SSH_FXP_WRITE, 6);
assert_eq!(SSH_FXP_LSTAT, 7);
assert_eq!(SSH_FXP_STAT, 17);
assert_eq!(SSH_FXP_STATUS, 101);
assert_eq!(SSH_FXP_HANDLE, 102);
assert_eq!(SSH_FXP_DATA, 103);
assert_eq!(SSH_FXP_NAME, 104);
assert_eq!(SSH_FXP_REALPATH, 16);
assert_eq!(SSH_FXP_OPENDIR, 11);
assert_eq!(SSH_FXP_READDIR, 12);
}
#[test]
fn test_open_flags_constants() {
assert_eq!(SSH_FXF_READ, 0x00000001);
assert_eq!(SSH_FXF_WRITE, 0x00000002);
assert_eq!(SSH_FXF_APPEND, 0x00000004);
assert_eq!(SSH_FXF_CREAT, 0x00000008);
assert_eq!(SSH_FXF_TRUNC, 0x00000010);
assert_eq!(SSH_FXF_EXCL, 0x00000020);
}
#[test]
fn test_attr_flags_constants() {
assert_eq!(SSH_FILEXFER_ATTR_SIZE, 0x00000001);
assert_eq!(SSH_FILEXFER_ATTR_UIDGID, 0x00000002);
assert_eq!(SSH_FILEXFER_ATTR_PERMISSIONS, 0x00000004);
assert_eq!(SSH_FILEXFER_ATTR_ACMODTIME, 0x00000008);
assert_eq!(SSH_FILEXFER_ATTR_EXTENDED, 0x80000000);
}
#[test]
fn test_request_id_accessors() {
let pkt = SftpPacket::Read {
request_id: 42,
handle: vec![],
offset: 0,
length: 1024,
};
assert_eq!(pkt.request_id(), Some(42));
assert_eq!(pkt.packet_type(), SSH_FXP_READ);
let init_pkt = SftpPacket::Init { version: 3 };
assert!(init_pkt.request_id().is_none());
}
#[test]
fn test_unicode_filename_encoding() {
let pkt = SftpPacket::Open {
request_id: 100,
filename: "\u{6587}\u{4EF6}.txt".to_string(), flags: SSH_FXF_READ,
attrs: SftpFileAttrs::default(),
};
let encoded = pkt.encode().unwrap();
let (decoded, _) = SftpPacket::decode(&encoded).unwrap();
assert_eq!(pkt, decoded);
}
#[test]
fn test_large_offset_encoding() {
let pkt = SftpPacket::Read {
request_id: 200,
handle: vec![0x01],
offset: u64::MAX - 1000,
length: 65536,
};
let encoded = pkt.encode().unwrap();
let (decoded, _) = SftpPacket::decode(&encoded).unwrap();
assert_eq!(pkt, decoded);
}
}