use std::fs::{self, OpenOptions};
use std::io::{Read, Write};
use std::path::{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> {
write_ok(&mut stream)?;
Ok(Self {
stream,
base: base_path.to_path_buf(),
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)?;
if let Err(e) = fs::create_dir_all(&target) {
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)?;
write_ok(&mut self.stream)?;
if let Some(parent) = target.parent() {
let _ = fs::create_dir_all(parent);
}
let f = match OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.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);
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::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).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(())
}