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>> {
137 if r.kind != HANDLE {
138 let why = match Dec::new(r.payload()).u32() {
139 Some(status) => anyhow::Error::new(Refused { status }),
140 None => anyhow!("unreadable reply (type {})", r.kind),
143 };
144 return Err(why.context(format!("{what} refused")));
145 }
146 Ok(Dec::new(r.payload()).str().context("handle")?.to_vec())
147}
148
149impl RemoteFs for SftpFs {
150 async fn read_batch(&self, paths: &[String]) -> Vec<Result<Vec<u8>>> {
151 let mut opens = Vec::with_capacity(paths.len());
155 for p in paths {
156 opens.push(
157 self.issue(
158 OPEN,
159 Enc::new().str(p.as_bytes()).u32(FXF_READ).u32(0).done(),
160 )
161 .await,
162 );
163 }
164
165 let mut handles: Vec<Option<Vec<u8>>> = Vec::with_capacity(paths.len());
166 let mut out: Vec<Result<Vec<u8>>> = Vec::with_capacity(paths.len());
167 for (rx, path) in opens.into_iter().zip(paths) {
168 let opened = match rx {
169 Ok(rx) => await_reply(rx).await.and_then(|r| handle_from(&r, path)),
170 Err(e) => Err(e),
171 };
172 match opened {
173 Ok(h) => {
174 handles.push(Some(h));
175 out.push(Ok(Vec::new()));
176 }
177 Err(e) => {
178 handles.push(None);
179 out.push(Err(e));
180 }
181 }
182 }
183
184 let mut live: Vec<usize> = (0..paths.len()).filter(|&i| handles[i].is_some()).collect();
187 while !live.is_empty() {
188 let mut rxs = Vec::with_capacity(live.len());
189 for &i in &live {
190 let handle = handles[i].as_ref().expect("live implies a handle");
191 let offset = out[i].as_ref().map_or(0, Vec::len) as u64;
192 rxs.push(
193 self.issue(
194 READ,
195 Enc::new().str(handle).u64(offset).u32(READ_CHUNK).done(),
196 )
197 .await,
198 );
199 }
200
201 let mut still_live = Vec::new();
202 for (&i, rx) in live.iter().zip(rxs) {
203 let chunk = match rx {
204 Ok(rx) => await_reply(rx).await,
205 Err(e) => Err(e),
206 };
207 match chunk {
208 Ok(r) if r.kind == DATA => {
209 let data = Dec::new(r.payload()).str().unwrap_or(&[]).to_vec();
210 let full = data.len() as u32 == READ_CHUNK;
211 if let Ok(buf) = &mut out[i] {
212 buf.extend_from_slice(&data);
213 }
214 if full {
215 still_live.push(i);
216 }
217 }
218 Ok(r) if r.kind == STATUS => {
223 let code = Dec::new(r.payload()).u32().unwrap_or(u32::MAX);
224 if code != STATUS_EOF {
225 out[i] = Err(anyhow!("read failed with sftp status {code}"));
226 }
227 }
228 Ok(r) => out[i] = Err(anyhow!("read gave reply type {}", r.kind)),
229 Err(e) => out[i] = Err(e),
230 }
231 }
232 live = still_live;
233 }
234
235 for handle in handles.iter().flatten() {
236 let _ = self.issue(CLOSE, Enc::new().str(handle).done()).await;
237 }
238 out
239 }
240
241 async fn read_ranges(&self, reqs: &[RangeReq]) -> Vec<Result<Vec<u8>>> {
242 let mut opens = Vec::with_capacity(reqs.len());
243 for r in reqs {
244 opens.push(
245 self.issue(
246 OPEN,
247 Enc::new()
248 .str(r.path.as_bytes())
249 .u32(FXF_READ)
250 .u32(0)
251 .done(),
252 )
253 .await,
254 );
255 }
256
257 let mut handles: Vec<Option<Vec<u8>>> = Vec::with_capacity(reqs.len());
258 let mut out: Vec<Result<Vec<u8>>> = Vec::with_capacity(reqs.len());
259 for (rx, r) in opens.into_iter().zip(reqs) {
260 let opened = match rx {
261 Ok(rx) => await_reply(rx)
262 .await
263 .and_then(|reply| handle_from(&reply, &r.path)),
264 Err(e) => Err(e),
265 };
266 match opened {
267 Ok(h) => {
268 handles.push(Some(h));
269 out.push(Ok(Vec::new()));
270 }
271 Err(e) => {
272 handles.push(None);
273 out.push(Err(e));
274 }
275 }
276 }
277
278 struct Piece {
282 req: usize,
283 offset: u64,
284 len: u32,
285 }
286 let mut pieces = Vec::new();
287 for (i, r) in reqs.iter().enumerate() {
288 if handles[i].is_none() {
289 continue;
290 }
291 let mut at = r.offset;
292 let end = r.offset.saturating_add(r.len);
293 while at < end {
294 let len =
295 u32::try_from((end - at).min(u64::from(READ_CHUNK))).unwrap_or(READ_CHUNK);
296 pieces.push(Piece {
297 req: i,
298 offset: at,
299 len,
300 });
301 at += u64::from(len);
302 }
303 }
304
305 let mut rxs = Vec::with_capacity(pieces.len());
306 for p in &pieces {
307 let handle = handles[p.req].as_ref().expect("pieces skip failed opens");
308 rxs.push(
309 self.issue(READ, Enc::new().str(handle).u64(p.offset).u32(p.len).done())
310 .await,
311 );
312 }
313
314 for (p, rx) in pieces.iter().zip(rxs) {
317 let reply = match rx {
318 Ok(rx) => await_reply(rx).await,
319 Err(e) => Err(e),
320 };
321 match reply {
322 Ok(r) if r.kind == DATA => {
323 let data = Dec::new(r.payload()).str().unwrap_or(&[]).to_vec();
324 if let Ok(buf) = &mut out[p.req] {
325 buf.extend_from_slice(&data);
326 }
327 }
328 Ok(r) if r.kind == STATUS => {
332 let code = Dec::new(r.payload()).u32().unwrap_or(u32::MAX);
333 if code != STATUS_EOF {
334 out[p.req] = Err(anyhow!("read failed with sftp status {code}"));
335 }
336 }
337 Ok(r) => out[p.req] = Err(anyhow!("read gave reply type {}", r.kind)),
338 Err(e) => out[p.req] = Err(e),
339 }
340 }
341
342 for handle in handles.iter().flatten() {
343 let _ = self.issue(CLOSE, Enc::new().str(handle).done()).await;
344 }
345 out
346 }
347
348 async fn home(&self) -> Result<String> {
349 let rx = self.issue(REALPATH, Enc::new().str(b".").done()).await?;
355 let reply = await_reply(rx).await?;
356 ensure!(
357 reply.kind == NAME,
358 "realpath answered {} rather than a name",
359 reply.kind
360 );
361 let mut d = Dec::new(reply.payload());
362 let count = d.u32().context("realpath count")?;
366 ensure!(count >= 1, "realpath answered with no name");
367 let path = String::from_utf8(d.str().context("realpath name")?.to_vec())
368 .context("home directory path is not utf-8")?;
369 ensure!(
373 path.starts_with('/'),
374 "realpath answered {path:?}, which is not an absolute path"
375 );
376 Ok(path)
377 }
378
379 async fn list_dirs(&self, paths: &[String]) -> Vec<Result<Vec<Entry>>> {
380 let mut opens = Vec::with_capacity(paths.len());
385 for p in paths {
386 opens.push(
387 self.issue(OPENDIR, Enc::new().str(p.as_bytes()).done())
388 .await,
389 );
390 }
391
392 let mut handles: Vec<Option<Vec<u8>>> = Vec::with_capacity(paths.len());
393 let mut out: Vec<Result<Vec<Entry>>> = Vec::with_capacity(paths.len());
394 for (rx, path) in opens.into_iter().zip(paths) {
395 let opened = match rx {
396 Ok(rx) => await_reply(rx).await.and_then(|r| handle_from(&r, path)),
397 Err(e) => Err(e),
398 };
399 match opened {
400 Ok(h) => {
401 handles.push(Some(h));
402 out.push(Ok(Vec::new()));
403 }
404 Err(e) => {
405 handles.push(None);
406 out.push(Err(e));
407 }
408 }
409 }
410
411 let mut live: Vec<usize> = (0..paths.len()).filter(|&i| handles[i].is_some()).collect();
415 while !live.is_empty() {
416 let mut rxs = Vec::with_capacity(live.len());
417 for &i in &live {
418 let handle = handles[i].as_ref().expect("live implies a handle");
419 rxs.push(self.issue(READDIR, Enc::new().str(handle).done()).await);
420 }
421
422 let mut still_live = Vec::new();
423 for (&i, rx) in live.iter().zip(rxs) {
424 let page = match rx {
425 Ok(rx) => await_reply(rx).await,
426 Err(e) => Err(e),
427 };
428 match page {
429 Ok(r) if r.kind == NAME => match decode_names(r.payload()) {
430 Ok(entries) => {
431 if let Ok(acc) = &mut out[i] {
432 acc.extend(entries);
433 }
434 still_live.push(i);
435 }
436 Err(e) => out[i] = Err(e),
437 },
438 Ok(r) if r.kind == STATUS => {
443 let code = Dec::new(r.payload()).u32().unwrap_or(u32::MAX);
444 if code != STATUS_EOF {
445 out[i] = Err(anyhow!("readdir failed with sftp status {code}"));
446 }
447 }
448 Ok(r) => out[i] = Err(anyhow!("readdir gave reply type {}", r.kind)),
449 Err(e) => out[i] = Err(e),
450 }
451 }
452 live = still_live;
453 }
454
455 for handle in handles.iter().flatten() {
456 let _ = self.issue(CLOSE, Enc::new().str(handle).done()).await;
457 }
458 out
459 }
460
461 fn round_trips(&self) -> u64 {
462 self.round_trips.load(Ordering::Relaxed)
463 }
464}
465
466async fn writer<W: AsyncWrite + Unpin>(
467 mut tx: Tx<W>,
468 mut jobs: mpsc::Receiver<Job>,
469 pending: Pending,
470 round_trips: Arc<AtomicU64>,
471) {
472 let mut batch: Vec<Job> = Vec::with_capacity(MAX_BATCH);
473 loop {
474 if jobs.recv_many(&mut batch, MAX_BATCH).await == 0 {
475 return;
476 }
477 if batch.len() < MAX_BATCH {
478 tokio::time::sleep(COALESCE).await;
479 while batch.len() < MAX_BATCH {
480 match jobs.try_recv() {
481 Ok(job) => batch.push(job),
482 Err(_) => break,
483 }
484 }
485 }
486
487 for job in batch.drain(..) {
488 let id = tx.alloc_id();
489 let mut payload = Vec::with_capacity(4 + job.body.len());
490 payload.extend_from_slice(&id.to_be_bytes());
491 payload.extend_from_slice(&job.body);
492 pending
495 .lock()
496 .expect("pending map poisoned")
497 .insert(id, job.reply);
498 if tx.queue(job.kind, &payload).await.is_err() {
499 return;
500 }
501 }
502
503 if tx.flush().await.is_err() {
504 return;
505 }
506 round_trips.fetch_add(1, Ordering::Relaxed);
507 }
508}
509
510async fn reader<R: AsyncRead + Unpin>(mut rx: Rx<R>, pending: Pending) {
511 while let Ok(reply) = rx.recv().await {
512 let waiter = pending
513 .lock()
514 .expect("pending map poisoned")
515 .remove(&reply.id);
516 if let Some(waiter) = waiter {
517 let _ = waiter.send(reply);
518 }
519 }
520 pending.lock().expect("pending map poisoned").clear();
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528 use crate::sftp::wire::{INIT, VERSION};
529 use crate::sftp::{read_frame, write_frame};
530 use tokio::io::AsyncWriteExt;
531
532 async fn fake_server<R, W>(mut r: R, mut w: W, body: Vec<u8>, fail_read: bool)
535 where
536 R: AsyncRead + Unpin,
537 W: AsyncWrite + Unpin,
538 {
539 let (kind, _) = read_frame(&mut r).await.expect("init frame");
540 assert_eq!(kind, INIT);
541 write_frame(&mut w, VERSION, &Enc::new().u32(3).done())
542 .await
543 .expect("version");
544 w.flush().await.expect("flush version");
545
546 while let Ok((kind, payload)) = read_frame(&mut r).await {
547 let mut d = Dec::new(&payload);
548 let id = d.u32().expect("request id");
549 let (out_kind, out) = match kind {
550 OPEN => (HANDLE, Enc::new().u32(id).str(b"h").done()),
551 READ => {
552 d.str().expect("handle");
553 let offset = d.u64().expect("offset") as usize;
554 if fail_read {
555 (
557 STATUS,
558 Enc::new()
559 .u32(id)
560 .u32(4)
561 .str(b"is a directory")
562 .str(b"")
563 .done(),
564 )
565 } else if offset >= body.len() {
566 (
567 STATUS,
568 Enc::new().u32(id).u32(1).str(b"eof").str(b"").done(),
569 )
570 } else {
571 (DATA, Enc::new().u32(id).str(&body[offset..]).done())
572 }
573 }
574 CLOSE => (STATUS, Enc::new().u32(id).u32(0).str(b"ok").str(b"").done()),
575 other => panic!("fake server got unexpected request type {other}"),
576 };
577 write_frame(&mut w, out_kind, &out).await.expect("reply");
578 w.flush().await.expect("flush reply");
579 }
580 }
581
582 #[tokio::test]
586 async fn forty_reads_cost_a_constant_number_of_round_trips() {
587 let (client, server) = tokio::io::duplex(1 << 20);
588 let (cr, cw) = tokio::io::split(client);
589 let (sr, sw) = tokio::io::split(server);
590 tokio::spawn(fake_server(sr, sw, b"hello".to_vec(), false));
591
592 let fs = SftpFs::over(cw, cr).await.expect("handshake");
593 let paths: Vec<String> = (0..40).map(|i| format!("/f{i}")).collect();
594 let out = tokio::time::timeout(Duration::from_secs(10), fs.read_batch(&paths))
595 .await
596 .expect("read_batch should not hang");
597
598 assert_eq!(out.len(), 40);
599 for r in &out {
600 assert_eq!(r.as_ref().expect("read succeeded").as_slice(), b"hello");
601 }
602
603 let trips = fs.round_trips();
606 assert!(trips <= 6, "forty files cost {trips} round trips");
607 }
608
609 #[tokio::test]
613 async fn a_failed_read_is_not_reported_as_an_empty_success() {
614 let (client, server) = tokio::io::duplex(1 << 16);
615 let (cr, cw) = tokio::io::split(client);
616 let (sr, sw) = tokio::io::split(server);
617 tokio::spawn(fake_server(sr, sw, b"unused".to_vec(), true));
618
619 let fs = SftpFs::over(cw, cr).await.expect("handshake");
620 let out = tokio::time::timeout(
621 Duration::from_secs(10),
622 fs.read_batch(&["/a-directory".to_string()]),
623 )
624 .await
625 .expect("read_batch should not hang");
626
627 assert!(
628 out[0].is_err(),
629 "a read that failed must not look like an empty file"
630 );
631 }
632
633 #[tokio::test]
636 async fn a_dead_session_fails_callers_instead_of_hanging() {
637 let (client, server) = tokio::io::duplex(1 << 16);
638 let (cr, cw) = tokio::io::split(client);
639 let (mut sr, mut sw) = tokio::io::split(server);
640 tokio::spawn(async move {
641 read_frame(&mut sr).await.expect("init frame");
642 write_frame(&mut sw, VERSION, &Enc::new().u32(3).done())
643 .await
644 .expect("version");
645 sw.flush().await.expect("flush version");
646 });
648
649 let fs = SftpFs::over(cw, cr).await.expect("handshake");
650 let out = tokio::time::timeout(Duration::from_secs(10), fs.read_batch(&["/a".to_string()]))
651 .await
652 .expect("a dead session must not hang the caller");
653
654 assert!(out[0].is_err(), "a closed session must surface as an error");
655 }
656
657 #[tokio::test]
663 async fn the_home_directory_is_whatever_the_remote_says_it_is() {
664 let fs = crate::testing::FakeRemote::new()
665 .home("/export/scratch/u42")
666 .spawn()
667 .await;
668 assert_eq!(fs.home().await.expect("a home"), "/export/scratch/u42");
669 }
670
671 #[tokio::test]
674 async fn a_remote_that_will_not_say_where_home_is_fails_rather_than_guessing() {
675 let fs = crate::testing::FakeRemote::new().spawn().await;
676 assert!(
677 fs.home().await.is_err(),
678 "a refusal must not turn into a default path"
679 );
680 }
681}