use core::ffi::{c_char, c_int};
use core::ptr;
use std::slice;
use std::sync::{Arc, Mutex};
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_INVALID_HANDLE, PCSSH_ERR_IO, PCSSH_ERR_PARSE, PCSSH_ERR_PROTOCOL, PCSSH_OK,
};
use crate::sftp::{Attrs, NameEntry, SftpError};
use crate::shared::SftpSession;
type SftpCell = Mutex<Option<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: Arc<SftpCell>,
}
pub struct PcSshSftpFile {
sftp: Arc<SftpCell>,
handle: Vec<u8>,
offset: u64,
closed: bool,
}
pub struct PcSshSftpDir {
sftp: Arc<SftpCell>,
handle: Vec<u8>,
closed: bool,
}
fn with_parent<F>(cell: &Arc<SftpCell>, f: F) -> c_int
where
F: FnOnce(&mut SftpSession) -> c_int,
{
let mut g = match cell.lock() {
Ok(g) => g,
Err(_) => return PCSSH_ERR_GENERIC,
};
match g.as_mut() {
Some(s) => f(s),
None => PCSSH_ERR_INVALID_HANDLE,
}
}
#[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: Arc::new(Mutex::new(Some(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) };
if let Ok(mut g) = boxed.inner.lock() {
*g = None;
}
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 { &*sftp };
let cell = s.inner.clone();
let mut handle_out: Option<Vec<u8>> = None;
let rc = with_parent(&cell, |sess| {
match sess.open(path_s.as_bytes(), flags, attrs) {
Ok(h) => {
handle_out = Some(h);
PCSSH_OK
}
Err(e) => map_sftp_err(&e),
}
});
if rc != PCSSH_OK {
return rc;
}
let handle = match handle_out {
Some(h) => h,
None => return PCSSH_ERR_GENERIC,
};
let boxed = Box::new(PcSshSftpFile {
sftp: cell,
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 {
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 handle = f.handle.clone();
let offset = f.offset;
let mut got_buf: Option<Vec<u8>> = None;
let rc = with_parent(&f.sftp, |sess| match sess.read(&handle, offset, want) {
Ok(c) => {
got_buf = Some(c);
PCSSH_OK
}
Err(e) => map_sftp_err(&e),
});
if rc != PCSSH_OK {
return rc;
}
let chunk = got_buf.unwrap_or_default();
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 {
return PCSSH_ERR_INVALID_ARGUMENT;
}
if len == 0 {
return PCSSH_OK;
}
let data = unsafe { slice::from_raw_parts(buf, len) };
let handle = f.handle.clone();
let offset = f.offset;
let rc = with_parent(&f.sftp, |sess| match sess.write(&handle, offset, data) {
Ok(()) => PCSSH_OK,
Err(e) => map_sftp_err(&e),
});
if rc != PCSSH_OK {
return rc;
}
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;
}
let handle = f.handle.clone();
let rc = with_parent(&f.sftp, |sess| match sess.close(&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 {
let handle = boxed.handle.clone();
let _ = with_parent(&boxed.sftp, |sess| match sess.close(&handle) {
Ok(()) => PCSSH_OK,
Err(e) => map_sftp_err(&e),
});
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 { &*sftp };
let cell = s.inner.clone();
let mut handle_out: Option<Vec<u8>> = None;
let rc = with_parent(&cell, |sess| match sess.opendir(path_s.as_bytes()) {
Ok(h) => {
handle_out = Some(h);
PCSSH_OK
}
Err(e) => map_sftp_err(&e),
});
if rc != PCSSH_OK {
return rc;
}
let handle = match handle_out {
Some(h) => h,
None => return PCSSH_ERR_GENERIC,
};
let boxed = Box::new(PcSshSftpDir {
sftp: cell,
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 {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let handle = d.handle.clone();
let mut chunk_out: Option<Option<Vec<NameEntry>>> = None;
let rc = with_parent(&d.sftp, |sess| match sess.readdir(&handle) {
Ok(c) => {
chunk_out = Some(c);
PCSSH_OK
}
Err(e) => map_sftp_err(&e),
});
if rc != PCSSH_OK {
return rc;
}
let chunk = chunk_out.unwrap_or(None);
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;
}
let handle = d.handle.clone();
let rc = with_parent(&d.sftp, |sess| match sess.close(&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 {
let handle = boxed.handle.clone();
let _ = with_parent(&boxed.sftp, |sess| match sess.close(&handle) {
Ok(()) => PCSSH_OK,
Err(e) => map_sftp_err(&e),
});
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 { &*sftp };
let mut attrs_out: Option<Attrs> = None;
let rc = with_parent(&s.inner, |sess| match sess.stat(path_s.as_bytes()) {
Ok(a) => {
attrs_out = Some(a);
PCSSH_OK
}
Err(e) => map_sftp_err(&e),
});
if rc != PCSSH_OK {
return rc;
}
unsafe { *out_attrs = attrs_to_c(&attrs_out.unwrap_or_default()) };
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 { &*sftp };
let mut attrs_out: Option<Attrs> = None;
let rc = with_parent(&s.inner, |sess| match sess.lstat(path_s.as_bytes()) {
Ok(a) => {
attrs_out = Some(a);
PCSSH_OK
}
Err(e) => map_sftp_err(&e),
});
if rc != PCSSH_OK {
return rc;
}
unsafe { *out_attrs = attrs_to_c(&attrs_out.unwrap_or_default()) };
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 {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let handle = f.handle.clone();
let mut attrs_out: Option<Attrs> = None;
let rc = with_parent(&f.sftp, |sess| match sess.fstat(&handle) {
Ok(a) => {
attrs_out = Some(a);
PCSSH_OK
}
Err(e) => map_sftp_err(&e),
});
if rc != PCSSH_OK {
return rc;
}
unsafe { *out_attrs = attrs_to_c(&attrs_out.unwrap_or_default()) };
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 a_owned = attrs_from_c(a);
let s = unsafe { &*sftp };
with_parent(&s.inner, |sess| {
match sess.setstat(path_s.as_bytes(), a_owned) {
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 {
return PCSSH_ERR_INVALID_ARGUMENT;
}
let a = unsafe { &*attrs };
let a_owned = attrs_from_c(a);
let handle = f.handle.clone();
with_parent(&f.sftp, |sess| match sess.fsetstat(&handle, a_owned) {
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 { &*sftp };
with_parent(&s.inner, |sess| {
match sess.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 { &*sftp };
with_parent(&s.inner, |sess| match sess.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 { &*sftp };
with_parent(&s.inner, |sess| match sess.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 { &*sftp };
with_parent(&s.inner, |sess| {
match sess.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 { &*sftp };
with_parent(&s.inner, |sess| {
match sess.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 { &*sftp };
let mut tgt_out: Option<Vec<u8>> = None;
let rc = with_parent(&s.inner, |sess| match sess.readlink(path_s.as_bytes()) {
Ok(t) => {
tgt_out = Some(t);
PCSSH_OK
}
Err(e) => map_sftp_err(&e),
});
if rc != PCSSH_OK {
return rc;
}
let target = tgt_out.unwrap_or_default();
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 { &*sftp };
let mut canon_out: Option<Vec<u8>> = None;
let rc = with_parent(&s.inner, |sess| match sess.realpath(path_s.as_bytes()) {
Ok(t) => {
canon_out = Some(t);
PCSSH_OK
}
Err(e) => map_sftp_err(&e),
});
if rc != PCSSH_OK {
return rc;
}
let canon = canon_out.unwrap_or_default();
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());
}
}
#[test]
fn with_parent_returns_invalid_handle_after_wipe() {
let cell: Arc<SftpCell> = Arc::new(Mutex::new(None));
let mut called = false;
let rc = with_parent(&cell, |_sess| {
called = true;
PCSSH_OK
});
assert_eq!(rc, PCSSH_ERR_INVALID_HANDLE);
assert!(!called, "callback must not run for a wiped cell");
}
}