1use std::collections::HashMap;
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::sync::{Arc, Mutex};
12use std::time::Duration;
13
14use anyhow::{Context, Result, anyhow, ensure};
15use tokio::io::{AsyncRead, AsyncWrite};
16use tokio::sync::{mpsc, oneshot};
17
18use super::{Entry, RangeReq, Refused, RemoteFs};
19use crate::sftp::transport::{self, SshChild};
20use crate::sftp::wire::{
21 Attrs, CLOSE, DATA, Dec, Enc, FXF_READ, HANDLE, NAME, OPEN, OPENDIR, READ, READDIR, REALPATH,
22 STATUS, STATUS_EOF,
23};
24use crate::sftp::{Reply, Rx, Sftp, Tx};
25
26const QUEUE_DEPTH: usize = 1024;
27const MAX_BATCH: usize = 256;
28const READ_CHUNK: u32 = 32 * 1024;
29
30const COALESCE: Duration = Duration::from_micros(200);
35
36struct Job {
37 kind: u8,
38 body: Vec<u8>,
40 reply: oneshot::Sender<Reply>,
41}
42
43type Pending = Arc<Mutex<HashMap<u32, oneshot::Sender<Reply>>>>;
44
45pub struct SftpFs {
46 jobs: mpsc::Sender<Job>,
47 round_trips: Arc<AtomicU64>,
48 _child: Option<SshChild>,
50}
51
52impl SftpFs {
53 pub async fn connect(host: &str) -> Result<Self> {
54 let (child, w, r) = transport::open(host)?;
55 let sftp = Sftp::handshake(w, r).await?;
56 Ok(Self::drive(sftp, Some(child)))
57 }
58
59 pub async fn over<W, R>(w: W, r: R) -> Result<Self>
62 where
63 W: AsyncWrite + Unpin + Send + 'static,
64 R: AsyncRead + Unpin + Send + 'static,
65 {
66 let sftp = Sftp::handshake(w, r).await?;
67 Ok(Self::drive(sftp, None))
68 }
69
70 fn drive<W, R>(sftp: Sftp<W, R>, child: Option<SshChild>) -> Self
71 where
72 W: AsyncWrite + Unpin + Send + 'static,
73 R: AsyncRead + Unpin + Send + 'static,
74 {
75 let (tx, rx) = sftp.into_halves();
76 let (jobs, job_rx) = mpsc::channel(QUEUE_DEPTH);
77 let pending: Pending = Arc::new(Mutex::new(HashMap::new()));
78 let round_trips = Arc::new(AtomicU64::new(0));
79
80 tokio::spawn(writer(
81 tx,
82 job_rx,
83 Arc::clone(&pending),
84 Arc::clone(&round_trips),
85 ));
86 tokio::spawn(reader(rx, pending));
87
88 Self {
89 jobs,
90 round_trips,
91 _child: child,
92 }
93 }
94
95 async fn issue(&self, kind: u8, body: Vec<u8>) -> Result<oneshot::Receiver<Reply>> {
97 let (reply, rx) = oneshot::channel();
98 self.jobs
99 .send(Job { kind, body, reply })
100 .await
101 .map_err(|_| anyhow!("sftp session is gone"))?;
102 Ok(rx)
103 }
104}
105
106async fn await_reply(rx: oneshot::Receiver<Reply>) -> Result<Reply> {
107 rx.await
108 .map_err(|_| anyhow!("sftp session closed before replying"))
109}
110
111fn decode_names(payload: &[u8]) -> Result<Vec<Entry>> {
113 let mut d = Dec::new(payload);
114 let count = d.u32().context("readdir count")?;
115 ensure!(count <= 1 << 16, "implausible readdir count {count}");
118 let mut out = Vec::with_capacity(count as usize);
119 for _ in 0..count {
120 let name = String::from_utf8_lossy(d.str().context("filename")?).into_owned();
121 d.str().context("longname")?;
124 let attrs = Attrs::decode(&mut d).context("attrs")?;
125 out.push(Entry { name, attrs });
126 }
127 Ok(out)
128}
129
130fn handle_from(r: &Reply, what: &str) -> Result<Vec<u8>> {
138 if r.kind != HANDLE {
139 let why = match Dec::new(r.payload()).u32() {
140 Some(status) => anyhow::Error::new(Refused { status }),
141 None => anyhow!("unreadable reply (type {})", r.kind),
144 };
145 return Err(why.context(format!("{what} refused")));
146 }
147 Ok(Dec::new(r.payload()).str().context("handle")?.to_vec())
148}
149
150impl RemoteFs for SftpFs {
151 async fn read_batch(&self, paths: &[String]) -> Vec<Result<Vec<u8>>> {
152 let mut opens = Vec::with_capacity(paths.len());
156 for p in paths {
157 opens.push(
158 self.issue(
159 OPEN,
160 Enc::new().str(p.as_bytes()).u32(FXF_READ).u32(0).done(),
161 )
162 .await,
163 );
164 }
165
166 let mut handles: Vec<Option<Vec<u8>>> = Vec::with_capacity(paths.len());
167 let mut out: Vec<Result<Vec<u8>>> = Vec::with_capacity(paths.len());
168 for (rx, path) in opens.into_iter().zip(paths) {
169 let opened = match rx {
170 Ok(rx) => await_reply(rx).await.and_then(|r| handle_from(&r, path)),
171 Err(e) => Err(e),
172 };
173 match opened {
174 Ok(h) => {
175 handles.push(Some(h));
176 out.push(Ok(Vec::new()));
177 }
178 Err(e) => {
179 handles.push(None);
180 out.push(Err(e));
181 }
182 }
183 }
184
185 let mut live: Vec<usize> = (0..paths.len()).filter(|&i| handles[i].is_some()).collect();
188 while !live.is_empty() {
189 let mut rxs = Vec::with_capacity(live.len());
190 for &i in &live {
191 let handle = handles[i].as_ref().expect("live implies a handle");
192 let offset = out[i].as_ref().map_or(0, Vec::len) as u64;
193 rxs.push(
194 self.issue(
195 READ,
196 Enc::new().str(handle).u64(offset).u32(READ_CHUNK).done(),
197 )
198 .await,
199 );
200 }
201
202 let mut still_live = Vec::new();
203 for (&i, rx) in live.iter().zip(rxs) {
204 let chunk = match rx {
205 Ok(rx) => await_reply(rx).await,
206 Err(e) => Err(e),
207 };
208 match chunk {
209 Ok(r) if r.kind == DATA => {
210 let data = Dec::new(r.payload()).str().unwrap_or(&[]).to_vec();
211 let full = data.len() as u32 == READ_CHUNK;
212 if let Ok(buf) = &mut out[i] {
213 buf.extend_from_slice(&data);
214 }
215 if full {
216 still_live.push(i);
217 }
218 }
219 Ok(r) if r.kind == STATUS => {
224 let code = Dec::new(r.payload()).u32().unwrap_or(u32::MAX);
225 if code != STATUS_EOF {
226 out[i] = Err(anyhow!("read failed with sftp status {code}"));
227 }
228 }
229 Ok(r) => out[i] = Err(anyhow!("read gave reply type {}", r.kind)),
230 Err(e) => out[i] = Err(e),
231 }
232 }
233 live = still_live;
234 }
235
236 for handle in handles.iter().flatten() {
237 let _ = self.issue(CLOSE, Enc::new().str(handle).done()).await;
238 }
239 out
240 }
241
242 async fn read_ranges(&self, reqs: &[RangeReq]) -> Vec<Result<Vec<u8>>> {
243 let mut opens = Vec::with_capacity(reqs.len());
244 for r in reqs {
245 opens.push(
246 self.issue(
247 OPEN,
248 Enc::new()
249 .str(r.path.as_bytes())
250 .u32(FXF_READ)
251 .u32(0)
252 .done(),
253 )
254 .await,
255 );
256 }
257
258 let mut handles: Vec<Option<Vec<u8>>> = Vec::with_capacity(reqs.len());
259 let mut out: Vec<Result<Vec<u8>>> = Vec::with_capacity(reqs.len());
260 for (rx, r) in opens.into_iter().zip(reqs) {
261 let opened = match rx {
262 Ok(rx) => await_reply(rx)
263 .await
264 .and_then(|reply| handle_from(&reply, &r.path)),
265 Err(e) => Err(e),
266 };
267 match opened {
268 Ok(h) => {
269 handles.push(Some(h));
270 out.push(Ok(Vec::new()));
271 }
272 Err(e) => {
273 handles.push(None);
274 out.push(Err(e));
275 }
276 }
277 }
278
279 struct Piece {
283 req: usize,
284 offset: u64,
285 len: u32,
286 }
287 let mut pieces = Vec::new();
288 for (i, r) in reqs.iter().enumerate() {
289 if handles[i].is_none() {
290 continue;
291 }
292 let mut at = r.offset;
293 let end = r.offset.saturating_add(r.len);
294 while at < end {
295 let len =
296 u32::try_from((end - at).min(u64::from(READ_CHUNK))).unwrap_or(READ_CHUNK);
297 pieces.push(Piece {
298 req: i,
299 offset: at,
300 len,
301 });
302 at += u64::from(len);
303 }
304 }
305
306 let mut rxs = Vec::with_capacity(pieces.len());
307 for p in &pieces {
308 let handle = handles[p.req].as_ref().expect("pieces skip failed opens");
309 rxs.push(
310 self.issue(READ, Enc::new().str(handle).u64(p.offset).u32(p.len).done())
311 .await,
312 );
313 }
314
315 for (p, rx) in pieces.iter().zip(rxs) {
318 let reply = match rx {
319 Ok(rx) => await_reply(rx).await,
320 Err(e) => Err(e),
321 };
322 match reply {
323 Ok(r) if r.kind == DATA => {
324 let data = Dec::new(r.payload()).str().unwrap_or(&[]).to_vec();
325 if let Ok(buf) = &mut out[p.req] {
326 buf.extend_from_slice(&data);
327 }
328 }
329 Ok(r) if r.kind == STATUS => {
333 let code = Dec::new(r.payload()).u32().unwrap_or(u32::MAX);
334 if code != STATUS_EOF {
335 out[p.req] = Err(anyhow!("read failed with sftp status {code}"));
336 }
337 }
338 Ok(r) => out[p.req] = Err(anyhow!("read gave reply type {}", r.kind)),
339 Err(e) => out[p.req] = Err(e),
340 }
341 }
342
343 for handle in handles.iter().flatten() {
344 let _ = self.issue(CLOSE, Enc::new().str(handle).done()).await;
345 }
346 out
347 }
348
349 async fn home(&self) -> Result<String> {
350 let rx = self.issue(REALPATH, Enc::new().str(b".").done()).await?;
356 let reply = await_reply(rx).await?;
357 ensure!(
358 reply.kind == NAME,
359 "realpath answered {} rather than a name",
360 reply.kind
361 );
362 let mut d = Dec::new(reply.payload());
363 let count = d.u32().context("realpath count")?;
367 ensure!(count >= 1, "realpath answered with no name");
368 let path = String::from_utf8(d.str().context("realpath name")?.to_vec())
369 .context("home directory path is not utf-8")?;
370 ensure!(
374 path.starts_with('/'),
375 "realpath answered {path:?}, which is not an absolute path"
376 );
377 Ok(path)
378 }
379
380 async fn list_dirs(&self, paths: &[String]) -> Vec<Result<Vec<Entry>>> {
381 let mut opens = Vec::with_capacity(paths.len());
386 for p in paths {
387 opens.push(
388 self.issue(OPENDIR, Enc::new().str(p.as_bytes()).done())
389 .await,
390 );
391 }
392
393 let mut handles: Vec<Option<Vec<u8>>> = Vec::with_capacity(paths.len());
394 let mut out: Vec<Result<Vec<Entry>>> = Vec::with_capacity(paths.len());
395 for (rx, path) in opens.into_iter().zip(paths) {
396 let opened = match rx {
397 Ok(rx) => await_reply(rx).await.and_then(|r| handle_from(&r, path)),
398 Err(e) => Err(e),
399 };
400 match opened {
401 Ok(h) => {
402 handles.push(Some(h));
403 out.push(Ok(Vec::new()));
404 }
405 Err(e) => {
406 handles.push(None);
407 out.push(Err(e));
408 }
409 }
410 }
411
412 let mut live: Vec<usize> = (0..paths.len()).filter(|&i| handles[i].is_some()).collect();
416 while !live.is_empty() {
417 let mut rxs = Vec::with_capacity(live.len());
418 for &i in &live {
419 let handle = handles[i].as_ref().expect("live implies a handle");
420 rxs.push(self.issue(READDIR, Enc::new().str(handle).done()).await);
421 }
422
423 let mut still_live = Vec::new();
424 for (&i, rx) in live.iter().zip(rxs) {
425 let page = match rx {
426 Ok(rx) => await_reply(rx).await,
427 Err(e) => Err(e),
428 };
429 match page {
430 Ok(r) if r.kind == NAME => match decode_names(r.payload()) {
431 Ok(entries) => {
432 if let Ok(acc) = &mut out[i] {
433 acc.extend(entries);
434 }
435 still_live.push(i);
436 }
437 Err(e) => out[i] = Err(e),
438 },
439 Ok(r) if r.kind == STATUS => {
444 let code = Dec::new(r.payload()).u32().unwrap_or(u32::MAX);
445 if code != STATUS_EOF {
446 out[i] = Err(anyhow!("readdir failed with sftp status {code}"));
447 }
448 }
449 Ok(r) => out[i] = Err(anyhow!("readdir gave reply type {}", r.kind)),
450 Err(e) => out[i] = Err(e),
451 }
452 }
453 live = still_live;
454 }
455
456 for handle in handles.iter().flatten() {
457 let _ = self.issue(CLOSE, Enc::new().str(handle).done()).await;
458 }
459 out
460 }
461
462 fn round_trips(&self) -> u64 {
463 self.round_trips.load(Ordering::Relaxed)
464 }
465}
466
467async fn writer<W: AsyncWrite + Unpin>(
468 mut tx: Tx<W>,
469 mut jobs: mpsc::Receiver<Job>,
470 pending: Pending,
471 round_trips: Arc<AtomicU64>,
472) {
473 let mut batch: Vec<Job> = Vec::with_capacity(MAX_BATCH);
474 loop {
475 if jobs.recv_many(&mut batch, MAX_BATCH).await == 0 {
476 return;
477 }
478 if batch.len() < MAX_BATCH {
479 tokio::time::sleep(COALESCE).await;
480 while batch.len() < MAX_BATCH {
481 match jobs.try_recv() {
482 Ok(job) => batch.push(job),
483 Err(_) => break,
484 }
485 }
486 }
487
488 for job in batch.drain(..) {
489 let id = tx.alloc_id();
490 let mut payload = Vec::with_capacity(4 + job.body.len());
491 payload.extend_from_slice(&id.to_be_bytes());
492 payload.extend_from_slice(&job.body);
493 pending
496 .lock()
497 .expect("pending map poisoned")
498 .insert(id, job.reply);
499 if tx.queue(job.kind, &payload).await.is_err() {
500 return;
501 }
502 }
503
504 if tx.flush().await.is_err() {
505 return;
506 }
507 round_trips.fetch_add(1, Ordering::Relaxed);
508 }
509}
510
511async fn reader<R: AsyncRead + Unpin>(mut rx: Rx<R>, pending: Pending) {
512 while let Ok(reply) = rx.recv().await {
513 let waiter = pending
514 .lock()
515 .expect("pending map poisoned")
516 .remove(&reply.id);
517 if let Some(waiter) = waiter {
518 let _ = waiter.send(reply);
519 }
520 }
521 pending.lock().expect("pending map poisoned").clear();
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529 use crate::sftp::wire::{INIT, VERSION};
530 use crate::sftp::{read_frame, write_frame};
531 use tokio::io::AsyncWriteExt;
532
533 async fn fake_server<R, W>(mut r: R, mut w: W, body: Vec<u8>, fail_read: bool)
536 where
537 R: AsyncRead + Unpin,
538 W: AsyncWrite + Unpin,
539 {
540 let (kind, _) = read_frame(&mut r).await.expect("init frame");
541 assert_eq!(kind, INIT);
542 write_frame(&mut w, VERSION, &Enc::new().u32(3).done())
543 .await
544 .expect("version");
545 w.flush().await.expect("flush version");
546
547 while let Ok((kind, payload)) = read_frame(&mut r).await {
548 let mut d = Dec::new(&payload);
549 let id = d.u32().expect("request id");
550 let (out_kind, out) = match kind {
551 OPEN => (HANDLE, Enc::new().u32(id).str(b"h").done()),
552 READ => {
553 d.str().expect("handle");
554 let offset = d.u64().expect("offset") as usize;
555 if fail_read {
556 (
558 STATUS,
559 Enc::new()
560 .u32(id)
561 .u32(4)
562 .str(b"is a directory")
563 .str(b"")
564 .done(),
565 )
566 } else if offset >= body.len() {
567 (
568 STATUS,
569 Enc::new().u32(id).u32(1).str(b"eof").str(b"").done(),
570 )
571 } else {
572 (DATA, Enc::new().u32(id).str(&body[offset..]).done())
573 }
574 }
575 CLOSE => (STATUS, Enc::new().u32(id).u32(0).str(b"ok").str(b"").done()),
576 other => panic!("fake server got unexpected request type {other}"),
577 };
578 write_frame(&mut w, out_kind, &out).await.expect("reply");
579 w.flush().await.expect("flush reply");
580 }
581 }
582
583 #[tokio::test]
587 async fn forty_reads_cost_a_constant_number_of_round_trips() {
588 let (client, server) = tokio::io::duplex(1 << 20);
589 let (cr, cw) = tokio::io::split(client);
590 let (sr, sw) = tokio::io::split(server);
591 tokio::spawn(fake_server(sr, sw, b"hello".to_vec(), false));
592
593 let fs = SftpFs::over(cw, cr).await.expect("handshake");
594 let paths: Vec<String> = (0..40).map(|i| format!("/f{i}")).collect();
595 let out = tokio::time::timeout(Duration::from_secs(10), fs.read_batch(&paths))
596 .await
597 .expect("read_batch should not hang");
598
599 assert_eq!(out.len(), 40);
600 for r in &out {
601 assert_eq!(r.as_ref().expect("read succeeded").as_slice(), b"hello");
602 }
603
604 let trips = fs.round_trips();
607 assert!(trips <= 6, "forty files cost {trips} round trips");
608 }
609
610 #[tokio::test]
614 async fn a_failed_read_is_not_reported_as_an_empty_success() {
615 let (client, server) = tokio::io::duplex(1 << 16);
616 let (cr, cw) = tokio::io::split(client);
617 let (sr, sw) = tokio::io::split(server);
618 tokio::spawn(fake_server(sr, sw, b"unused".to_vec(), true));
619
620 let fs = SftpFs::over(cw, cr).await.expect("handshake");
621 let out = tokio::time::timeout(
622 Duration::from_secs(10),
623 fs.read_batch(&["/a-directory".to_string()]),
624 )
625 .await
626 .expect("read_batch should not hang");
627
628 assert!(
629 out[0].is_err(),
630 "a read that failed must not look like an empty file"
631 );
632 }
633
634 #[tokio::test]
637 async fn a_dead_session_fails_callers_instead_of_hanging() {
638 let (client, server) = tokio::io::duplex(1 << 16);
639 let (cr, cw) = tokio::io::split(client);
640 let (mut sr, mut sw) = tokio::io::split(server);
641 tokio::spawn(async move {
642 read_frame(&mut sr).await.expect("init frame");
643 write_frame(&mut sw, VERSION, &Enc::new().u32(3).done())
644 .await
645 .expect("version");
646 sw.flush().await.expect("flush version");
647 });
649
650 let fs = SftpFs::over(cw, cr).await.expect("handshake");
651 let out = tokio::time::timeout(Duration::from_secs(10), fs.read_batch(&["/a".to_string()]))
652 .await
653 .expect("a dead session must not hang the caller");
654
655 assert!(out[0].is_err(), "a closed session must surface as an error");
656 }
657
658 #[tokio::test]
664 async fn the_home_directory_is_whatever_the_remote_says_it_is() {
665 let fs = crate::testing::FakeRemote::new()
666 .home("/export/scratch/u42")
667 .spawn()
668 .await;
669 assert_eq!(fs.home().await.expect("a home"), "/export/scratch/u42");
670 }
671
672 #[tokio::test]
675 async fn a_remote_that_will_not_say_where_home_is_fails_rather_than_guessing() {
676 let fs = crate::testing::FakeRemote::new().spawn().await;
677 assert!(
678 fs.home().await.is_err(),
679 "a refusal must not turn into a default path"
680 );
681 }
682}