use core::ffi::{c_char, c_int};
use core::ptr;
use std::slice;
use super::client::PcSshClient;
use super::common::{
catch, cstr_to_str, PCSSH_ERR_BUFFER_TOO_SMALL, PCSSH_ERR_GENERIC, PCSSH_ERR_INVALID_ARGUMENT,
PCSSH_ERR_IO, PCSSH_ERR_PARSE, PCSSH_ERR_PROTOCOL, PCSSH_OK,
};
use crate::sftp::{Attrs, NameEntry, SftpError};
use crate::shared::SftpSession;
pub const PCSSH_SFTP_READ: u32 = 0x0000_0001;
pub const PCSSH_SFTP_WRITE: u32 = 0x0000_0002;
pub const PCSSH_SFTP_APPEND: u32 = 0x0000_0004;
pub const PCSSH_SFTP_CREAT: u32 = 0x0000_0008;
pub const PCSSH_SFTP_TRUNC: u32 = 0x0000_0010;
pub const PCSSH_SFTP_EXCL: u32 = 0x0000_0020;
pub const PCSSH_ATTR_SIZE: u32 = 0x0000_0001;
pub const PCSSH_ATTR_UIDGID: u32 = 0x0000_0002;
pub const PCSSH_ATTR_PERMISSIONS: u32 = 0x0000_0004;
pub const PCSSH_ATTR_ACMODTIME: u32 = 0x0000_0008;
pub struct PcSshSftp {
inner: SftpSession,
}
pub struct PcSshSftpFile {
sftp: *mut PcSshSftp,
handle: Vec<u8>,
offset: u64,
closed: bool,
}
pub struct PcSshSftpDir {
sftp: *mut PcSshSftp,
handle: Vec<u8>,
closed: bool,
}
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct PcSshSftpAttrs {
pub flags: u32,
pub size: u64,
pub uid: u32,
pub gid: u32,
pub permissions: u32,
pub atime: u32,
pub mtime: u32,
}
fn map_sftp_err(e: &SftpError) -> c_int {
match e {
SftpError::Io(_) => PCSSH_ERR_IO,
SftpError::Format(_) => PCSSH_ERR_PARSE,
SftpError::Protocol(_) => PCSSH_ERR_PROTOCOL,
SftpError::Status { .. } => PCSSH_ERR_GENERIC,
}
}
fn attrs_to_c(a: &Attrs) -> PcSshSftpAttrs {
let mut out = PcSshSftpAttrs::default();
if let Some(s) = a.size {
out.flags |= PCSSH_ATTR_SIZE;
out.size = s;
}
if let Some((u, g)) = a.uid_gid {
out.flags |= PCSSH_ATTR_UIDGID;
out.uid = u;
out.gid = g;
}
if let Some(p) = a.permissions {
out.flags |= PCSSH_ATTR_PERMISSIONS;
out.permissions = p;
}
if let Some((at, mt)) = a.atime_mtime {
out.flags |= PCSSH_ATTR_ACMODTIME;
out.atime = at;
out.mtime = mt;
}
out
}
fn attrs_from_c(a: &PcSshSftpAttrs) -> Attrs {
let mut out = Attrs::default();
if a.flags & PCSSH_ATTR_SIZE != 0 {
out.size = Some(a.size);
}
if a.flags & PCSSH_ATTR_UIDGID != 0 {
out.uid_gid = Some((a.uid, a.gid));
}
if a.flags & PCSSH_ATTR_PERMISSIONS != 0 {
out.permissions = Some(a.permissions);
}
if a.flags & PCSSH_ATTR_ACMODTIME != 0 {
out.atime_mtime = Some((a.atime, a.mtime));
}
out
}
unsafe fn copy_to_caller_buf(src: &[u8], buf: *mut u8, cap: usize, out_len: *mut usize) -> c_int {
if out_len.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let need = src.len();
unsafe { *out_len = need };
if need > cap {
return PCSSH_ERR_BUFFER_TOO_SMALL;
}
if need == 0 {
return PCSSH_OK;
}
if buf.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
unsafe { ptr::copy_nonoverlapping(src.as_ptr(), buf, need) };
PCSSH_OK
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_open(
client: *mut PcSshClient,
out_sftp: *mut *mut PcSshSftp,
) -> c_int {
catch(|| {
if client.is_null() || out_sftp.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
unsafe { *out_sftp = ptr::null_mut() };
let c = unsafe { &*client };
let session = match c.inner.sftp() {
Ok(s) => s,
Err(e) => return super::common::map_error(&e),
};
let boxed = Box::new(PcSshSftp { inner: session });
unsafe { *out_sftp = Box::into_raw(boxed) };
PCSSH_OK
})
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_free(sftp: *mut PcSshSftp) {
if sftp.is_null() {
return;
}
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let boxed = unsafe { Box::from_raw(sftp) };
drop(boxed);
}));
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_open_file(
sftp: *mut PcSshSftp,
path: *const c_char,
flags: u32,
mode: u32,
out_file: *mut *mut PcSshSftpFile,
) -> c_int {
catch(|| {
if sftp.is_null() || out_file.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
unsafe { *out_file = ptr::null_mut() };
let path_s = match unsafe { cstr_to_str(path) } {
Some(s) => s,
None => return PCSSH_ERR_INVALID_ARGUMENT,
};
let attrs = if flags & PCSSH_SFTP_CREAT != 0 {
Attrs {
permissions: Some(mode),
..Default::default()
}
} else {
Attrs::default()
};
let s = unsafe { &mut *sftp };
let handle = match s.inner.open(path_s.as_bytes(), flags, attrs) {
Ok(h) => h,
Err(e) => return map_sftp_err(&e),
};
let boxed = Box::new(PcSshSftpFile {
sftp,
handle,
offset: 0,
closed: false,
});
unsafe { *out_file = Box::into_raw(boxed) };
PCSSH_OK
})
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_read(
file: *mut PcSshSftpFile,
buf: *mut u8,
cap: usize,
out_len: *mut usize,
) -> c_int {
catch(|| {
if file.is_null() || out_len.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
if buf.is_null() && cap != 0 {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let f = unsafe { &mut *file };
if f.closed || f.sftp.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
if cap == 0 {
unsafe { *out_len = 0 };
return PCSSH_OK;
}
let want = cap.min(u32::MAX as usize) as u32;
let s = unsafe { &mut *f.sftp };
let chunk = match s.inner.read(&f.handle, f.offset, want) {
Ok(c) => c,
Err(e) => return map_sftp_err(&e),
};
let got = chunk.len();
if got > 0 {
unsafe { ptr::copy_nonoverlapping(chunk.as_ptr(), buf, got) };
f.offset = f.offset.saturating_add(got as u64);
}
unsafe { *out_len = got };
PCSSH_OK
})
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_write(
file: *mut PcSshSftpFile,
buf: *const u8,
len: usize,
) -> c_int {
catch(|| {
if file.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
if buf.is_null() && len != 0 {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let f = unsafe { &mut *file };
if f.closed || f.sftp.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
if len == 0 {
return PCSSH_OK;
}
let data = unsafe { slice::from_raw_parts(buf, len) };
let s = unsafe { &mut *f.sftp };
if let Err(e) = s.inner.write(&f.handle, f.offset, data) {
return map_sftp_err(&e);
}
f.offset = f.offset.saturating_add(len as u64);
PCSSH_OK
})
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_seek(file: *mut PcSshSftpFile, offset: u64) -> c_int {
catch(|| {
if file.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let f = unsafe { &mut *file };
f.offset = offset;
PCSSH_OK
})
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_tell(file: *mut PcSshSftpFile, out_offset: *mut u64) -> c_int {
catch(|| {
if file.is_null() || out_offset.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let f = unsafe { &*file };
unsafe { *out_offset = f.offset };
PCSSH_OK
})
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_close_file(file: *mut PcSshSftpFile) -> c_int {
catch(|| {
if file.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let f = unsafe { &mut *file };
if f.closed {
return PCSSH_OK;
}
if f.sftp.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let s = unsafe { &mut *f.sftp };
let rc = match s.inner.close(&f.handle) {
Ok(()) => PCSSH_OK,
Err(e) => map_sftp_err(&e),
};
f.closed = true;
rc
})
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_file_free(file: *mut PcSshSftpFile) {
if file.is_null() {
return;
}
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut boxed = unsafe { Box::from_raw(file) };
if !boxed.closed && !boxed.sftp.is_null() {
let s = unsafe { &mut *boxed.sftp };
let _ = s.inner.close(&boxed.handle);
boxed.closed = true;
}
drop(boxed);
}));
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_opendir(
sftp: *mut PcSshSftp,
path: *const c_char,
out_dir: *mut *mut PcSshSftpDir,
) -> c_int {
catch(|| {
if sftp.is_null() || out_dir.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
unsafe { *out_dir = ptr::null_mut() };
let path_s = match unsafe { cstr_to_str(path) } {
Some(s) => s,
None => return PCSSH_ERR_INVALID_ARGUMENT,
};
let s = unsafe { &mut *sftp };
let handle = match s.inner.opendir(path_s.as_bytes()) {
Ok(h) => h,
Err(e) => return map_sftp_err(&e),
};
let boxed = Box::new(PcSshSftpDir {
sftp,
handle,
closed: false,
});
unsafe { *out_dir = Box::into_raw(boxed) };
PCSSH_OK
})
}
#[allow(clippy::too_many_arguments)]
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_readdir(
dir: *mut PcSshSftpDir,
name_buf: *mut u8,
name_cap: usize,
name_len: *mut usize,
longname_buf: *mut u8,
longname_cap: usize,
longname_len: *mut usize,
out_attrs: *mut PcSshSftpAttrs,
) -> c_int {
catch(|| {
if dir.is_null() || out_attrs.is_null() || name_len.is_null() || longname_len.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let d = unsafe { &mut *dir };
if d.closed || d.sftp.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let s = unsafe { &mut *d.sftp };
let chunk = match s.inner.readdir(&d.handle) {
Ok(c) => c,
Err(e) => return map_sftp_err(&e),
};
let entry: Option<NameEntry> = chunk.and_then(|mut v| v.drain(..1).next());
let Some(e) = entry else {
unsafe { *out_attrs = PcSshSftpAttrs::default() };
unsafe {
*name_len = 0;
*longname_len = 0;
}
return PCSSH_OK;
};
unsafe { *out_attrs = attrs_to_c(&e.attrs) };
let rc = unsafe { copy_to_caller_buf(&e.filename, name_buf, name_cap, name_len) };
if rc != PCSSH_OK {
return rc;
}
unsafe { copy_to_caller_buf(&e.longname, longname_buf, longname_cap, longname_len) }
})
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_closedir(dir: *mut PcSshSftpDir) -> c_int {
catch(|| {
if dir.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let d = unsafe { &mut *dir };
if d.closed {
return PCSSH_OK;
}
if d.sftp.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let s = unsafe { &mut *d.sftp };
let rc = match s.inner.close(&d.handle) {
Ok(()) => PCSSH_OK,
Err(e) => map_sftp_err(&e),
};
d.closed = true;
rc
})
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_dir_free(dir: *mut PcSshSftpDir) {
if dir.is_null() {
return;
}
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut boxed = unsafe { Box::from_raw(dir) };
if !boxed.closed && !boxed.sftp.is_null() {
let s = unsafe { &mut *boxed.sftp };
let _ = s.inner.close(&boxed.handle);
boxed.closed = true;
}
drop(boxed);
}));
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_stat(
sftp: *mut PcSshSftp,
path: *const c_char,
out_attrs: *mut PcSshSftpAttrs,
) -> c_int {
catch(|| {
if sftp.is_null() || out_attrs.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let path_s = match unsafe { cstr_to_str(path) } {
Some(s) => s,
None => return PCSSH_ERR_INVALID_ARGUMENT,
};
let s = unsafe { &mut *sftp };
let attrs = match s.inner.stat(path_s.as_bytes()) {
Ok(a) => a,
Err(e) => return map_sftp_err(&e),
};
unsafe { *out_attrs = attrs_to_c(&attrs) };
PCSSH_OK
})
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_lstat(
sftp: *mut PcSshSftp,
path: *const c_char,
out_attrs: *mut PcSshSftpAttrs,
) -> c_int {
catch(|| {
if sftp.is_null() || out_attrs.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let path_s = match unsafe { cstr_to_str(path) } {
Some(s) => s,
None => return PCSSH_ERR_INVALID_ARGUMENT,
};
let s = unsafe { &mut *sftp };
let attrs = match s.inner.lstat(path_s.as_bytes()) {
Ok(a) => a,
Err(e) => return map_sftp_err(&e),
};
unsafe { *out_attrs = attrs_to_c(&attrs) };
PCSSH_OK
})
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_fstat(
file: *mut PcSshSftpFile,
out_attrs: *mut PcSshSftpAttrs,
) -> c_int {
catch(|| {
if file.is_null() || out_attrs.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let f = unsafe { &mut *file };
if f.closed || f.sftp.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let s = unsafe { &mut *f.sftp };
let attrs = match s.inner.fstat(&f.handle) {
Ok(a) => a,
Err(e) => return map_sftp_err(&e),
};
unsafe { *out_attrs = attrs_to_c(&attrs) };
PCSSH_OK
})
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_setstat(
sftp: *mut PcSshSftp,
path: *const c_char,
attrs: *const PcSshSftpAttrs,
) -> c_int {
catch(|| {
if sftp.is_null() || attrs.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let path_s = match unsafe { cstr_to_str(path) } {
Some(s) => s,
None => return PCSSH_ERR_INVALID_ARGUMENT,
};
let a = unsafe { &*attrs };
let s = unsafe { &mut *sftp };
match s.inner.setstat(path_s.as_bytes(), attrs_from_c(a)) {
Ok(()) => PCSSH_OK,
Err(e) => map_sftp_err(&e),
}
})
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_fsetstat(
file: *mut PcSshSftpFile,
attrs: *const PcSshSftpAttrs,
) -> c_int {
catch(|| {
if file.is_null() || attrs.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let f = unsafe { &mut *file };
if f.closed || f.sftp.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let a = unsafe { &*attrs };
let s = unsafe { &mut *f.sftp };
match s.inner.fsetstat(&f.handle, attrs_from_c(a)) {
Ok(()) => PCSSH_OK,
Err(e) => map_sftp_err(&e),
}
})
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_mkdir(
sftp: *mut PcSshSftp,
path: *const c_char,
mode: u32,
) -> c_int {
catch(|| {
if sftp.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let path_s = match unsafe { cstr_to_str(path) } {
Some(s) => s,
None => return PCSSH_ERR_INVALID_ARGUMENT,
};
let attrs = Attrs {
permissions: Some(mode),
..Default::default()
};
let s = unsafe { &mut *sftp };
match s.inner.mkdir(path_s.as_bytes(), attrs) {
Ok(()) => PCSSH_OK,
Err(e) => map_sftp_err(&e),
}
})
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_rmdir(sftp: *mut PcSshSftp, path: *const c_char) -> c_int {
catch(|| {
if sftp.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let path_s = match unsafe { cstr_to_str(path) } {
Some(s) => s,
None => return PCSSH_ERR_INVALID_ARGUMENT,
};
let s = unsafe { &mut *sftp };
match s.inner.rmdir(path_s.as_bytes()) {
Ok(()) => PCSSH_OK,
Err(e) => map_sftp_err(&e),
}
})
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_remove(sftp: *mut PcSshSftp, path: *const c_char) -> c_int {
catch(|| {
if sftp.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let path_s = match unsafe { cstr_to_str(path) } {
Some(s) => s,
None => return PCSSH_ERR_INVALID_ARGUMENT,
};
let s = unsafe { &mut *sftp };
match s.inner.remove(path_s.as_bytes()) {
Ok(()) => PCSSH_OK,
Err(e) => map_sftp_err(&e),
}
})
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_rename(
sftp: *mut PcSshSftp,
old_path: *const c_char,
new_path: *const c_char,
) -> c_int {
catch(|| {
if sftp.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let old_s = match unsafe { cstr_to_str(old_path) } {
Some(s) => s,
None => return PCSSH_ERR_INVALID_ARGUMENT,
};
let new_s = match unsafe { cstr_to_str(new_path) } {
Some(s) => s,
None => return PCSSH_ERR_INVALID_ARGUMENT,
};
let s = unsafe { &mut *sftp };
match s.inner.rename(old_s.as_bytes(), new_s.as_bytes()) {
Ok(()) => PCSSH_OK,
Err(e) => map_sftp_err(&e),
}
})
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_symlink(
sftp: *mut PcSshSftp,
target: *const c_char,
link_path: *const c_char,
) -> c_int {
catch(|| {
if sftp.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let tgt = match unsafe { cstr_to_str(target) } {
Some(s) => s,
None => return PCSSH_ERR_INVALID_ARGUMENT,
};
let lnk = match unsafe { cstr_to_str(link_path) } {
Some(s) => s,
None => return PCSSH_ERR_INVALID_ARGUMENT,
};
let s = unsafe { &mut *sftp };
match s.inner.symlink(tgt.as_bytes(), lnk.as_bytes()) {
Ok(()) => PCSSH_OK,
Err(e) => map_sftp_err(&e),
}
})
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_readlink(
sftp: *mut PcSshSftp,
path: *const c_char,
buf: *mut u8,
cap: usize,
out_len: *mut usize,
) -> c_int {
catch(|| {
if sftp.is_null() || out_len.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let path_s = match unsafe { cstr_to_str(path) } {
Some(s) => s,
None => return PCSSH_ERR_INVALID_ARGUMENT,
};
let s = unsafe { &mut *sftp };
let target = match s.inner.readlink(path_s.as_bytes()) {
Ok(t) => t,
Err(e) => return map_sftp_err(&e),
};
unsafe { copy_to_caller_buf(&target, buf, cap, out_len) }
})
}
#[no_mangle]
pub unsafe extern "C" fn pcssh_sftp_realpath(
sftp: *mut PcSshSftp,
path: *const c_char,
buf: *mut u8,
cap: usize,
out_len: *mut usize,
) -> c_int {
catch(|| {
if sftp.is_null() || out_len.is_null() {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let path_s = match unsafe { cstr_to_str(path) } {
Some(s) => s,
None => return PCSSH_ERR_INVALID_ARGUMENT,
};
let s = unsafe { &mut *sftp };
let canon = match s.inner.realpath(path_s.as_bytes()) {
Ok(t) => t,
Err(e) => return map_sftp_err(&e),
};
unsafe { copy_to_caller_buf(&canon, buf, cap, out_len) }
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn attrs_round_trip_through_c_form() {
let a = Attrs {
size: Some(123),
uid_gid: Some((1000, 100)),
permissions: Some(0o644),
atime_mtime: Some((1, 2)),
extended: vec![],
};
let c = attrs_to_c(&a);
assert_eq!(
c.flags,
PCSSH_ATTR_SIZE | PCSSH_ATTR_UIDGID | PCSSH_ATTR_PERMISSIONS | PCSSH_ATTR_ACMODTIME
);
assert_eq!(c.size, 123);
assert_eq!(c.uid, 1000);
assert_eq!(c.gid, 100);
assert_eq!(c.permissions, 0o644);
assert_eq!(c.atime, 1);
assert_eq!(c.mtime, 2);
let back = attrs_from_c(&c);
assert_eq!(back.size, Some(123));
assert_eq!(back.uid_gid, Some((1000, 100)));
assert_eq!(back.permissions, Some(0o644));
assert_eq!(back.atime_mtime, Some((1, 2)));
}
#[test]
fn attrs_empty_round_trips() {
let a = Attrs::default();
let c = attrs_to_c(&a);
assert_eq!(c.flags, 0);
let back = attrs_from_c(&c);
assert_eq!(back, Attrs::default());
}
#[test]
fn copy_to_caller_buf_exact_fit() {
let src = b"hello";
let mut buf = [0u8; 5];
let mut len = 0usize;
let rc = unsafe { copy_to_caller_buf(src, buf.as_mut_ptr(), 5, &mut len) };
assert_eq!(rc, PCSSH_OK);
assert_eq!(len, 5);
assert_eq!(&buf, src);
}
#[test]
fn copy_to_caller_buf_too_small_reports_required() {
let src = b"hello";
let mut buf = [0u8; 2];
let mut len = 0usize;
let rc = unsafe { copy_to_caller_buf(src, buf.as_mut_ptr(), 2, &mut len) };
assert_eq!(rc, PCSSH_ERR_BUFFER_TOO_SMALL);
assert_eq!(len, 5);
}
#[test]
fn copy_to_caller_buf_empty_src_ok_with_zero_cap() {
let src: &[u8] = &[];
let mut len = 0usize;
let rc = unsafe { copy_to_caller_buf(src, std::ptr::null_mut(), 0, &mut len) };
assert_eq!(rc, PCSSH_OK);
assert_eq!(len, 0);
}
#[test]
fn free_null_safe() {
unsafe {
pcssh_sftp_free(std::ptr::null_mut());
pcssh_sftp_file_free(std::ptr::null_mut());
pcssh_sftp_dir_free(std::ptr::null_mut());
}
}
}