use std::fs::{self, OpenOptions};
use std::io::{Read, Write};
use std::path::{Component, Path, PathBuf};
use super::protocol::{read_header, read_payload_term, write_fatal, write_ok, Header, ScpError};
#[derive(Default, Clone, Copy)]
pub struct ScpRecvOptions {
pub recursive: bool,
pub preserve_times: bool,
pub target_is_file: bool,
}
pub struct Receiver<S: Read + Write> {
stream: S,
base: PathBuf,
stack: Vec<PathBuf>,
pending_times: Option<(i64, i64)>,
opts: ScpRecvOptions,
}
impl<S: Read + Write> Receiver<S> {
pub fn new(mut stream: S, base_path: &Path, opts: ScpRecvOptions) -> Result<Self, ScpError> {
let canon_base = fs::canonicalize(base_path).map_err(ScpError::Io)?;
write_ok(&mut stream)?;
Ok(Self {
stream,
base: canon_base,
stack: Vec::new(),
pending_times: None,
opts,
})
}
pub fn run(&mut self) -> Result<(), ScpError> {
loop {
let h = match read_header(&mut self.stream) {
Ok(Some(h)) => h,
Ok(None) => return Ok(()),
Err(e) => {
let _ = write_fatal(&mut self.stream, &e.to_string());
return Err(e);
}
};
match h {
Header::Times { mtime, atime } => {
self.pending_times = Some((mtime, atime));
write_ok(&mut self.stream)?;
}
Header::Dir { mode, name } => {
if !self.opts.recursive {
let msg = "directory entry but -r not set";
let _ = write_fatal(&mut self.stream, msg);
return Err(ScpError::Unexpected("directory entry but -r not set"));
}
self.recv_dir(mode, &name)?;
}
Header::EndDir => {
if self.stack.pop().is_none() {
let _ = write_fatal(&mut self.stream, "E at top level");
return Err(ScpError::Unexpected("E at top level"));
}
self.pending_times = None;
write_ok(&mut self.stream)?;
}
Header::File { mode, size, name } => {
self.recv_file(mode, size, &name)?;
}
}
}
}
fn recv_dir(&mut self, mode: u32, name: &str) -> Result<(), ScpError> {
let parent = self.current_dir();
let target = parent.join(name);
self.guard_path(&target)?;
match fs::symlink_metadata(&target) {
Ok(md) => {
if md.file_type().is_symlink() {
let _ =
write_fatal(&mut self.stream, "directory target is an existing symlink");
return Err(ScpError::PathEscape);
}
if !md.is_dir() {
let _ = write_fatal(
&mut self.stream,
"directory target collides with a non-directory",
);
return Err(ScpError::Unexpected(
"directory target collides with non-directory",
));
}
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
if let Err(e) = fs::create_dir(&target) {
let _ = write_fatal(&mut self.stream, &e.to_string());
return Err(ScpError::Io(e));
}
}
Err(e) => {
let _ = write_fatal(&mut self.stream, &e.to_string());
return Err(ScpError::Io(e));
}
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(&target, fs::Permissions::from_mode(mode & 0o7777));
}
#[cfg(not(unix))]
let _ = mode;
if let Some((mtime, atime)) = self.pending_times.take() {
if self.opts.preserve_times {
let _ = set_times(&target, mtime, atime);
}
}
self.stack.push(target);
write_ok(&mut self.stream)?;
Ok(())
}
fn recv_file(&mut self, mode: u32, size: u64, name: &str) -> Result<(), ScpError> {
let target = self.resolve_file_target(name);
self.guard_path(&target)?;
match fs::symlink_metadata(&target) {
Ok(md) if md.file_type().is_symlink() => {
let _ = write_fatal(&mut self.stream, "refusing to overwrite a symlink");
return Err(ScpError::PathEscape);
}
_ => {}
}
write_ok(&mut self.stream)?;
if let Some(parent) = target.parent() {
let _ = fs::create_dir_all(parent);
}
let mut open_opts = OpenOptions::new();
open_opts.create(true).write(true).truncate(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
open_opts.custom_flags(nix::libc::O_NOFOLLOW);
}
let f = match open_opts.open(&target) {
Ok(f) => f,
Err(e) => {
let _ = write_fatal(&mut self.stream, &e.to_string());
return Err(ScpError::Io(e));
}
};
let mut f = f;
if let Err(e) = read_payload_term(&mut self.stream, &mut f, size) {
let _ = write_fatal(&mut self.stream, &e.to_string());
return Err(e);
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = fs::set_permissions(&target, fs::Permissions::from_mode(mode & 0o7777));
}
#[cfg(not(unix))]
let _ = mode;
if let Some((mtime, atime)) = self.pending_times.take() {
if self.opts.preserve_times {
let _ = set_times(&target, mtime, atime);
}
}
write_ok(&mut self.stream)?;
Ok(())
}
fn current_dir(&self) -> PathBuf {
match self.stack.last() {
Some(d) => d.clone(),
None => self.base.clone(),
}
}
fn resolve_file_target(&self, name: &str) -> PathBuf {
if self.stack.is_empty() && self.opts.target_is_file {
self.base.clone()
} else {
self.current_dir().join(name)
}
}
fn guard_path(&mut self, target: &Path) -> Result<(), ScpError> {
let norm = lexical_normalize(target);
if norm.components().any(|c| matches!(c, Component::ParentDir)) {
let _ = write_fatal(&mut self.stream, "path escapes base directory");
return Err(ScpError::PathEscape);
}
let base_norm = lexical_normalize(&self.base);
if !norm.starts_with(&base_norm) && norm != base_norm {
let _ = write_fatal(&mut self.stream, "path escapes base directory");
return Err(ScpError::PathEscape);
}
Ok(())
}
}
fn lexical_normalize(p: &Path) -> PathBuf {
let mut out: Vec<std::path::Component<'_>> = Vec::new();
for comp in p.components() {
match comp {
std::path::Component::ParentDir => {
if let Some(std::path::Component::Normal(_)) = out.last() {
out.pop();
} else {
out.push(comp);
}
}
std::path::Component::CurDir => {}
other => out.push(other),
}
}
let mut buf = PathBuf::new();
for c in out {
buf.push(c.as_os_str());
}
buf
}
#[cfg(unix)]
fn set_times(path: &Path, mtime: i64, atime: i64) -> std::io::Result<()> {
use std::os::unix::fs::OpenOptionsExt;
use std::time::{Duration, SystemTime};
let m = SystemTime::UNIX_EPOCH + Duration::from_secs(mtime.max(0) as u64);
let a = SystemTime::UNIX_EPOCH + Duration::from_secs(atime.max(0) as u64);
let f = std::fs::File::options()
.write(true)
.custom_flags(nix::libc::O_NOFOLLOW)
.open(path)?;
f.set_modified(m)?;
let _ = a;
let _ = f;
Ok(())
}
#[cfg(not(unix))]
fn set_times(_path: &Path, _mtime: i64, _atime: i64) -> std::io::Result<()> {
Ok(())
}