1use std::collections::BTreeMap;
34use std::ffi::{OsStr, OsString};
35use std::fmt;
36use std::io::{self, ErrorKind, Write as _};
37use std::path::{Component, Path, PathBuf};
38use std::process::Stdio;
39use std::sync::{Arc, Condvar, Mutex, PoisonError};
40use std::thread;
41
42use ignore::overrides::OverrideBuilder;
43
44use rig_core::tool::{ContentHash, content_hash};
45
46use crate::process::{Launcher, Process, Spawn};
47use crate::workspace::{
48 Contents, Excerpt, Listing, Stat, Workspace, excerpt_of, not_text, read_file, too_large,
49};
50use crate::{MAX_BYTES, MAX_FILE_BYTES};
51
52pub const TRANSPORT_FAILED: i32 = 255;
54const FILES_PER_CALL: usize = 100;
56pub const CALLS_PER_HOST: usize = 2;
61
62const NOT_FOUND: i32 = 2;
64const NOT_A_FILE: i32 = 3;
65const READ_ONLY: i32 = 4;
66const A_DIRECTORY: i32 = 5;
67const ROOT_IS_A_FILE: i32 = 6;
68
69#[derive(Clone)]
75pub struct Remote {
76 name: String,
77 transport: Vec<OsString>,
78 launcher: Launcher,
81 running: Arc<(Mutex<usize>, Condvar)>,
83}
84
85#[derive(Clone, Copy, PartialEq, Eq)]
88enum Again {
89 Once,
90 Never,
91}
92
93struct Turn<'a>(&'a (Mutex<usize>, Condvar));
95
96impl<'a> Turn<'a> {
97 fn take(running: &'a (Mutex<usize>, Condvar)) -> Self {
99 let (count, freed) = running;
100 let mut count = count.lock().unwrap_or_else(PoisonError::into_inner);
101 while *count >= CALLS_PER_HOST {
102 count = freed.wait(count).unwrap_or_else(PoisonError::into_inner);
103 }
104 *count += 1;
105 Self(running)
106 }
107}
108
109impl Drop for Turn<'_> {
110 fn drop(&mut self) {
111 let (count, freed) = self.0;
112 let mut count = count.lock().unwrap_or_else(PoisonError::into_inner);
113 *count = count.saturating_sub(1);
114 freed.notify_one();
115 }
116}
117
118struct Ran {
120 code: Option<i32>,
121 out: Vec<u8>,
122 err: String,
123}
124
125impl fmt::Debug for Remote {
126 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
127 f.debug_struct("Remote")
128 .field("name", &self.name)
129 .field("transport", &self.transport)
130 .finish()
131 }
132}
133
134impl Remote {
135 pub fn new(
144 name: impl Into<String>,
145 transport: impl IntoIterator<Item = impl Into<OsString>>,
146 mut launcher: Launcher,
147 ) -> io::Result<Self> {
148 let name = name.into();
149 let transport: Vec<OsString> = transport.into_iter().map(Into::into).collect();
150 if name.is_empty() || name.contains([':', '/']) || name.contains(char::is_whitespace) {
151 return Err(io::Error::new(
152 ErrorKind::InvalidInput,
153 format!("`{name}` is no host name: it is empty or holds a `:`, `/` or space"),
154 ));
155 }
156 if transport.is_empty() {
157 return Err(io::Error::new(
158 ErrorKind::InvalidInput,
159 format!("the transport of {name} is empty"),
160 ));
161 }
162 launcher.cwd = None;
163 Ok(Self {
164 name,
165 transport,
166 launcher,
167 running: Arc::default(),
168 })
169 }
170
171 pub fn name(&self) -> &str {
173 &self.name
174 }
175
176 fn command(&self, script: &str) -> io::Result<(OsString, Vec<OsString>)> {
179 let Some((program, args)) = self.transport.split_first() else {
180 return Err(io::Error::other(format!(
181 "the transport of {} is empty",
182 self.name
183 )));
184 };
185 let mut args = args.to_vec();
186 args.push(format!("sh -c {}", quote(script)).into());
187 Ok((program.clone(), args))
188 }
189
190 fn once(&self, script: &str, stdin: Option<&[u8]>) -> io::Result<Ran> {
193 let (program, args) = self.command(script)?;
194 let _turn = Turn::take(&self.running);
195 let mut command = self.launcher.command(&program);
196 command
197 .args(args)
198 .stdin(match stdin {
199 Some(_) => Stdio::piped(),
200 None => Stdio::null(),
201 })
202 .stdout(Stdio::piped())
203 .stderr(Stdio::piped());
204 let mut child = command
205 .spawn()
206 .map_err(|error| self.unstarted(&program, error))?;
207 let pipe = child.stdin.take();
208 let output = thread::scope(|scope| {
209 if let (Some(mut pipe), Some(bytes)) = (pipe, stdin) {
210 scope.spawn(move || pipe.write_all(bytes).ok());
211 }
212 child.wait_with_output()
213 })?;
214 Ok(Ran {
215 code: output.status.code(),
216 out: output.stdout,
217 err: String::from_utf8_lossy(&output.stderr).trim().to_owned(),
218 })
219 }
220
221 fn run(&self, script: &str, stdin: Option<&[u8]>, again: Again) -> io::Result<Ran> {
224 let mut ran = self.once(script, stdin)?;
225 if ran.code == Some(TRANSPORT_FAILED) && again == Again::Once {
226 ran = self.once(script, stdin)?;
227 }
228 match ran.code {
229 Some(TRANSPORT_FAILED) => Err(self.failed(&ran, again)),
230 _ => Ok(ran),
231 }
232 }
233
234 fn checked(&self, script: &str, stdin: Option<&[u8]>, again: Again) -> io::Result<Vec<u8>> {
237 let ran = self.run(script, stdin, again)?;
238 self.outcome(ran, again)
239 }
240
241 fn outcome(&self, ran: Ran, again: Again) -> io::Result<Vec<u8>> {
244 match ran.code {
245 Some(0) => Ok(ran.out),
246 Some(NOT_FOUND) => Err(ErrorKind::NotFound.into()),
247 Some(NOT_A_FILE) => Err(io::Error::new(
248 ErrorKind::InvalidInput,
249 "not a regular file",
250 )),
251 Some(READ_ONLY) => Err(io::Error::new(
252 ErrorKind::PermissionDenied,
253 "the file is read-only; leave it unchanged or ask the user",
254 )),
255 Some(A_DIRECTORY) => Err(ErrorKind::IsADirectory.into()),
256 _ => Err(self.failed(&ran, again)),
257 }
258 }
259
260 fn failed(&self, ran: &Ran, again: Again) -> io::Error {
266 let (kind, ended) = self.ended(ran);
267 let after = match again {
268 Again::Once if ran.code == Some(TRANSPORT_FAILED) => " (tried twice)",
269 Again::Once => "",
270 Again::Never => {
271 "; the change may have been made or not, so read the file before trying again"
272 }
273 };
274 io::Error::new(kind, format!("{ended}{after}"))
275 }
276
277 fn ended(&self, ran: &Ran) -> (ErrorKind, String) {
279 let name = &self.name;
280 let (kind, how) = match ran.code {
281 Some(TRANSPORT_FAILED) => (
282 ErrorKind::NotConnected,
283 format!("the transport to {name} failed (exit status {TRANSPORT_FAILED})"),
284 ),
285 Some(code) => (
286 ErrorKind::Other,
287 format!("the operation on {name} ended with exit status {code}"),
288 ),
289 None => (
290 ErrorKind::Other,
291 format!("the transport to {name} was killed by a signal"),
292 ),
293 };
294 let said = match ran.err.as_str() {
295 "" => " and said nothing".to_owned(),
296 err => format!(": {err}"),
297 };
298 (kind, format!("{how}{said}"))
299 }
300
301 fn written(&self, path: &Path, script: &str, bytes: &[u8]) -> io::Result<Vec<u8>> {
307 let ran = self.once(script, Some(bytes))?;
308 if matches!(
309 ran.code,
310 Some(0 | NOT_FOUND | NOT_A_FILE | READ_ONLY | A_DIRECTORY)
311 ) {
312 return self.outcome(ran, Again::Never);
313 }
314 let (kind, ended) = self.ended(&ran);
315 let check = format!(
316 "p={}; [ -f \"$p\" ] || exit {NOT_FOUND}; \
317 h=$({{ sha256sum || shasum -a 256; }} < \"$p\" 2>/dev/null); echo \"${{h%% *}}\"",
318 quote_path(path)
319 );
320 let checked = self.checked(&check, None, Again::Once);
321 let sent = content_hash(bytes).0;
322 let found = match checked.as_deref().map(|out| String::from_utf8_lossy(out)) {
323 Ok(hash) if hash.trim() == sent => {
324 "it holds what this write sent, so the write happened".to_owned()
325 }
326 Ok(hash) if !hash.trim().is_empty() => "it does not hold what this write sent, so \
327 the write did not happen; make the change again"
328 .to_owned(),
329 Err(error) if error.kind() == ErrorKind::NotFound => {
330 "it is not there, so the write did not happen; make the change again".to_owned()
331 }
332 Ok(_) => "it could not be hashed there, so the write may or may not have \
333 happened; read the file before going on"
334 .to_owned(),
335 Err(error) => format!(
336 "that failed too ({error}), so the write may or may not have happened; read \
337 the file before going on"
338 ),
339 };
340 Err(io::Error::new(
341 kind,
342 format!("the write failed: {ended}. The file was checked again: {found}"),
343 ))
344 }
345
346 fn unstarted(&self, program: &OsStr, error: io::Error) -> io::Error {
348 io::Error::other(format!(
349 "could not start `{}`, the transport to {}: {error}",
350 program.to_string_lossy(),
351 self.name
352 ))
353 }
354}
355
356impl Workspace for Remote {
357 fn host(&self) -> Option<&str> {
359 Some(&self.name)
360 }
361
362 fn stat(&self, path: &Path) -> io::Result<Stat> {
363 let script = format!(
364 "p={}; if [ -f \"$p\" ]; then wc -c < \"$p\" || exit 1; \
365 if [ -w \"$p\" ]; then echo w; else echo r; fi; \
366 elif [ -d \"$p\" ]; then echo d; elif [ -e \"$p\" ]; then echo o; else echo m; fi",
367 quote_path(path)
368 );
369 let out = String::from_utf8_lossy(&self.checked(&script, None, Again::Once)?).into_owned();
370 let mut words = out.split_whitespace();
371 Ok(match (words.next(), words.next()) {
372 (Some("d"), _) => Stat::Dir,
373 (Some("o"), _) => Stat::Other,
374 (Some("m"), _) => Stat::Missing,
375 (Some(size), Some(access)) => Stat::File {
376 size: size.parse().map_err(io::Error::other)?,
377 read_only: access == "r",
378 modified: None,
379 },
380 _ => return Err(io::Error::other(format!("stat said `{}`", out.trim()))),
381 })
382 }
383
384 fn read(&self, path: &Path, limit: u64) -> io::Result<Contents> {
385 let script = format!(
386 "p={}; [ -f \"$p\" ] || {{ [ -e \"$p\" ] && exit {NOT_A_FILE}; exit {NOT_FOUND}; }}; \
387 wc -c < \"$p\" && head -c {limit} -- \"$p\"",
388 quote_path(path)
389 );
390 let out = self.checked(&script, None, Again::Once)?;
391 let Some((size, bytes)) = split_line(&out) else {
392 return Err(io::Error::other("the remote read said nothing"));
393 };
394 Ok(Contents {
395 bytes: bytes.to_vec(),
396 size: size.trim().parse().map_err(io::Error::other)?,
397 })
398 }
399
400 fn excerpt(&self, path: &Path, first: usize, count: usize) -> io::Result<Excerpt> {
406 let first = first.max(1);
407 let last = first.saturating_add(count.max(1) - 1);
408 let script = format!(
409 "p={}; [ -f \"$p\" ] || {{ [ -e \"$p\" ] && exit {NOT_A_FILE}; exit {NOT_FOUND}; }}; \
410 s=$(wc -c < \"$p\") || exit 1; [ $s -le {MAX_FILE_BYTES} ] || {{ echo $s; exit 0; }}; \
411 t=$(mktemp) || exit 1; trap 'rm -f \"$t\"' EXIT; cat -- \"$p\" > \"$t\" || exit 1; \
412 s=$(wc -c < \"$t\") && echo $s && wc -l < \"$t\" && tail -c 1 \"$t\" | wc -l && \
413 {{ sha256sum || shasum -a 256 || echo -; }} < \"$t\" 2>/dev/null && \
414 if [ $s -le {MAX_BYTES} ]; then cat \"$t\"; else sed -n '{first},{last}p;{last}q' \"$t\"; fi",
415 quote_path(path)
416 );
417 let out = self.checked(&script, None, Again::Once)?;
418 let mut rest = out.as_slice();
419 let mut line = || {
420 let (line, after) = split_line(rest)?;
421 rest = after;
422 Some(line.trim().to_owned())
423 };
424 let size: u64 = line()
425 .and_then(|size| size.parse().ok())
426 .ok_or_else(|| io::Error::other("the remote read said no size"))?;
427 if size > MAX_FILE_BYTES {
428 return Err(too_large(size));
429 }
430 let (Some(newlines), Some(ends_in_newline), Some(hash)) = (line(), line(), line()) else {
431 return Err(io::Error::other("the remote read stopped short"));
432 };
433 let text = String::from_utf8(rest.to_vec()).map_err(|_| not_text());
434 if size <= MAX_BYTES as u64 {
435 return Ok(Excerpt::of(&text?, first, count));
436 }
437 let hash = hash.split_whitespace().next().unwrap_or_default();
438 if hash.len() != 64 {
439 return excerpt_of(self, path, first, count);
440 }
441 let newlines: usize = newlines.parse().map_err(io::Error::other)?;
442 let unended = size > 0 && ends_in_newline == "0";
443 Ok(Excerpt {
444 text: text?,
445 total: newlines + usize::from(unended),
446 hash: ContentHash(hash.to_ascii_lowercase()),
447 whole: None,
448 })
449 }
450
451 fn read_files(&self, paths: &[PathBuf], limit: u64) -> Vec<Option<Vec<u8>>> {
455 let mut read = Vec::with_capacity(paths.len());
456 for chunk in paths.chunks(FILES_PER_CALL) {
457 let quoted: Vec<String> = chunk.iter().map(|path| quote_path(path)).collect();
458 let script = format!(
459 "t=$(mktemp) || exit 1; trap 'rm -f \"$t\"' EXIT; for p in {}; do \
460 if [ -f \"$p\" ] && n=$(wc -c < \"$p\") && [ $((n)) -le {limit} ] && \
461 cat -- \"$p\" > \"$t\" 2>/dev/null; then wc -c < \"$t\"; cat \"$t\"; \
462 else echo -; fi; done",
463 quoted.join(" ")
464 );
465 let out = match self.run(&script, None, Again::Once) {
466 Ok(ran) if ran.code == Some(0) => ran.out,
467 _ => Vec::new(),
468 };
469 let mut rest = out.as_slice();
470 for _ in chunk {
471 let file = split_line(rest).and_then(|(size, after)| {
472 let size: usize = size.trim().parse().ok()?;
473 let (bytes, after) = after.split_at_checked(size)?;
474 Some((bytes, after))
475 });
476 match file {
477 Some((bytes, after)) => {
478 read.push(Some(bytes.to_vec()));
479 rest = after;
480 }
481 None => {
482 read.push(None);
483 rest = split_line(rest).map_or(&[][..], |(_, after)| after);
484 }
485 }
486 }
487 }
488 read
489 }
490
491 fn write(&self, path: &Path, bytes: &[u8]) -> io::Result<()> {
494 self.replace(path, bytes, 0).map(drop)
495 }
496
497 fn replace(&self, path: &Path, bytes: &[u8], limit: u64) -> io::Result<io::Result<Contents>> {
502 let script = format!(
503 "p={}; if [ -e \"$p\" ]; then [ -f \"$p\" ] || exit {NOT_A_FILE}; \
504 [ -w \"$p\" ] || exit {READ_ONLY}; fi; d=$(dirname -- \"$p\"); \
505 mkdir -p -- \"$d\" && t=$(mktemp \"$d/.rig-write.XXXXXX\") || exit 1; \
506 if [ -e \"$p\" ]; then cp -p -- \"$p\" \"$t\" && wc -c < \"$t\" && \
507 head -c {limit} -- \"$t\"; \
508 else chmod \"$(printf %o $((0666 & ~$(umask))))\" \"$t\"; fi && \
509 cat > \"$t\" && mv -f -- \"$t\" \"$p\" || {{ rm -f -- \"$t\"; exit 1; }}",
510 quote_path(path)
511 );
512 let out = self.written(path, &script, bytes)?;
513 Ok(match split_line(&out) {
514 None => Err(ErrorKind::NotFound.into()),
515 Some((size, bytes)) => size
516 .trim()
517 .parse()
518 .map(|size| Contents {
519 bytes: bytes.to_vec(),
520 size,
521 })
522 .map_err(io::Error::other),
523 })
524 }
525
526 fn write_if(
531 &self,
532 path: &Path,
533 bytes: &[u8],
534 held: &ContentHash,
535 ) -> io::Result<Option<Contents>> {
536 let script = format!(
537 "p={}; [ -f \"$p\" ] || {{ [ -e \"$p\" ] && exit {NOT_A_FILE}; exit {NOT_FOUND}; }}; \
538 [ -w \"$p\" ] || exit {READ_ONLY}; \
539 t=$(mktemp \"$(dirname -- \"$p\")/.rig-write.XXXXXX\") || exit 1; \
540 cp -p -- \"$p\" \"$t\" || {{ rm -f -- \"$t\"; exit 1; }}; \
541 h=$({{ sha256sum || shasum -a 256; }} < \"$t\" 2>/dev/null); \
542 if [ \"${{h%% *}}\" = {} ]; then \
543 cat > \"$t\" && mv -f -- \"$t\" \"$p\" && echo = || {{ rm -f -- \"$t\"; exit 1; }}; \
544 else wc -c < \"$t\" && head -c {MAX_FILE_BYTES} -- \"$t\"; s=$?; rm -f -- \"$t\"; exit $s; fi",
545 quote_path(path),
546 quote(&held.0)
547 );
548 let out = self.written(path, &script, bytes)?;
549 let Some((first, rest)) = split_line(&out) else {
550 return Err(io::Error::other("the remote write said nothing"));
551 };
552 if first == "=" {
553 return Ok(None);
554 }
555 Ok(Some(Contents {
556 bytes: rest.to_vec(),
557 size: first.trim().parse().map_err(io::Error::other)?,
558 }))
559 }
560
561 fn remove(&self, path: &Path) -> io::Result<()> {
562 let script = format!(
563 "p={}; [ -d \"$p\" ] && exit {A_DIRECTORY}; \
564 [ -e \"$p\" ] || [ -L \"$p\" ] || exit {NOT_FOUND}; rm -f -- \"$p\"",
565 quote_path(path)
566 );
567 self.checked(&script, None, Again::Never).map(drop)
568 }
569
570 fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
571 let script = format!(
572 "f={}; t={}; [ -e \"$f\" ] || exit {NOT_FOUND}; \
573 mkdir -p -- \"$(dirname -- \"$t\")\" && mv -f -- \"$f\" \"$t\"",
574 quote_path(from),
575 quote_path(to)
576 );
577 self.checked(&script, None, Again::Never).map(drop)
578 }
579
580 fn canonical(&self, path: &Path) -> PathBuf {
583 let joined = match path.is_absolute() || path.starts_with("~") {
584 true => path.to_path_buf(),
585 false => Path::new("~").join(path),
586 };
587 let mut out = PathBuf::new();
588 for component in joined.components() {
589 match component {
590 Component::CurDir => {}
591 Component::ParentDir => match out.components().next_back() {
592 Some(Component::Normal(last)) if last != "~" => {
593 out.pop();
594 }
595 Some(Component::RootDir) => {}
596 _ => out.push(".."),
597 },
598 other => out.push(other),
599 }
600 }
601 out
602 }
603
604 fn files(&self, root: &Path, glob: Option<&str>, listing: Listing) -> io::Result<Vec<PathBuf>> {
608 let globs = glob
609 .map(|glob| {
610 OverrideBuilder::new(root)
611 .add(glob)
612 .and_then(|builder| builder.build())
613 .map_err(|error| {
614 io::Error::new(ErrorKind::InvalidInput, format!("bad glob: {error}"))
615 })
616 })
617 .transpose()?;
618 let git = match listing {
619 Listing::All => "false",
620 Listing::Visible | Listing::Hidden => {
621 "git rev-parse --is-inside-work-tree >/dev/null 2>&1"
622 }
623 };
624 let script = format!(
625 "r={}; [ -f \"$r\" ] && exit {ROOT_IS_A_FILE}; cd \"$r\" || exit {NOT_FOUND}; \
626 if {git}; then git ls-files -z -co --exclude-standard; \
627 else find . -type f -print0; fi",
628 quote_path(root)
629 );
630 let ran = self.run(&script, None, Again::Once)?;
631 match ran.code {
632 Some(ROOT_IS_A_FILE) => return Ok(vec![root.to_path_buf()]),
633 Some(0) => {}
634 Some(NOT_FOUND) => return Err(ErrorKind::NotFound.into()),
635 _ => return Err(self.failed(&ran, Again::Once)),
636 }
637 let mut files: Vec<PathBuf> = String::from_utf8_lossy(&ran.out)
638 .split('\0')
639 .map(|name| Path::new(name.trim_start_matches("./")))
640 .filter(|name| !name.as_os_str().is_empty() && listing.keeps(name))
641 .map(|name| root.join(name))
642 .filter(|path| {
643 globs
644 .as_ref()
645 .is_none_or(|globs| globs.matched(path, false).is_whitelist())
646 })
647 .collect();
648 files.sort();
649 Ok(files)
650 }
651
652 fn spawn(
661 &self,
662 launcher: &Launcher,
663 dir: Option<&Path>,
664 program: &OsStr,
665 args: &[OsString],
666 how: Spawn,
667 ) -> io::Result<Process> {
668 let dir = dir.unwrap_or(Path::new("~"));
669 let mut script = format!("cd {} && exec env", quote_path(&self.canonical(dir)));
670 for (name, value) in &launcher.env {
671 let set = format!("{}={}", name.to_string_lossy(), value.to_string_lossy());
672 script.push_str(&format!(" {}", quote(&set)));
673 }
674 script.push_str(" sh -lc 'exec \"$0\" \"$@\"'");
675 for word in std::iter::once(program).chain(args.iter().map(OsString::as_os_str)) {
676 script.push_str(&format!(" {}", quote(&word.to_string_lossy())));
677 }
678 let (transport, line) = self.command(&script)?;
679 let mut launcher = launcher.clone();
680 launcher.cwd = None;
681 launcher
682 .spawn(&transport, line, how)
683 .map(|process| process.through(&self.name))
684 .map_err(|error| self.unstarted(&transport, error))
685 }
686}
687
688#[derive(Clone, Debug)]
701pub struct Hosts {
702 inner: Arc<dyn Workspace>,
703 hosts: BTreeMap<String, Remote>,
704}
705
706impl Hosts {
707 pub fn new(inner: Arc<dyn Workspace>, remotes: impl IntoIterator<Item = Remote>) -> Self {
710 let hosts = remotes
711 .into_iter()
712 .map(|remote| (remote.name.clone(), remote))
713 .collect();
714 Self { inner, hosts }
715 }
716
717 pub fn route(&self, path: &Path) -> Option<(&Remote, PathBuf)> {
719 let (host, rest) = path.to_str()?.split_once(':')?;
720 let remote = self.hosts.get(host)?;
721 Some((
722 remote,
723 PathBuf::from(if rest.is_empty() { "~" } else { rest }),
724 ))
725 }
726
727 fn named(remote: &Remote, path: &Path) -> PathBuf {
729 let mut named = OsString::from(&remote.name);
730 named.push(":");
731 named.push(path);
732 PathBuf::from(named)
733 }
734}
735
736impl Workspace for Hosts {
737 fn host(&self) -> Option<&str> {
738 self.inner.host()
739 }
740
741 fn join(&self, base: &Path, path: &Path) -> PathBuf {
742 match self.route(path) {
743 Some(_) => path.to_path_buf(),
744 None => self.inner.join(base, path),
745 }
746 }
747
748 fn stat(&self, path: &Path) -> io::Result<Stat> {
749 match self.route(path) {
750 Some((remote, path)) => remote.stat(&path),
751 None => self.inner.stat(path),
752 }
753 }
754
755 fn read(&self, path: &Path, limit: u64) -> io::Result<Contents> {
756 match self.route(path) {
757 Some((remote, path)) => remote.read(&path, limit),
758 None => self.inner.read(path, limit),
759 }
760 }
761
762 fn read_files(&self, paths: &[PathBuf], limit: u64) -> Vec<Option<Vec<u8>>> {
765 let first = paths.first().and_then(|path| self.route(path));
766 let there = first.and_then(|(remote, _)| {
767 let there: Option<Vec<PathBuf>> = paths
768 .iter()
769 .map(|path| {
770 let (other, there) = self.route(path)?;
771 (other.name == remote.name).then_some(there)
772 })
773 .collect();
774 Some((remote, there?))
775 });
776 match there {
777 Some((remote, there)) => remote.read_files(&there, limit),
778 None => paths
779 .iter()
780 .map(|path| read_file(self, path, limit))
781 .collect(),
782 }
783 }
784
785 fn excerpt(&self, path: &Path, first: usize, count: usize) -> io::Result<Excerpt> {
786 match self.route(path) {
787 Some((remote, path)) => remote.excerpt(&path, first, count),
788 None => self.inner.excerpt(path, first, count),
789 }
790 }
791
792 fn write(&self, path: &Path, bytes: &[u8]) -> io::Result<()> {
793 match self.route(path) {
794 Some((remote, path)) => remote.write(&path, bytes),
795 None => self.inner.write(path, bytes),
796 }
797 }
798
799 fn replace(&self, path: &Path, bytes: &[u8], limit: u64) -> io::Result<io::Result<Contents>> {
800 match self.route(path) {
801 Some((remote, path)) => remote.replace(&path, bytes, limit),
802 None => self.inner.replace(path, bytes, limit),
803 }
804 }
805
806 fn write_if(
807 &self,
808 path: &Path,
809 bytes: &[u8],
810 held: &ContentHash,
811 ) -> io::Result<Option<Contents>> {
812 match self.route(path) {
813 Some((remote, path)) => remote.write_if(&path, bytes, held),
814 None => self.inner.write_if(path, bytes, held),
815 }
816 }
817
818 fn remove(&self, path: &Path) -> io::Result<()> {
819 match self.route(path) {
820 Some((remote, path)) => remote.remove(&path),
821 None => self.inner.remove(path),
822 }
823 }
824
825 fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
828 match (self.route(from), self.route(to)) {
829 (None, None) => self.inner.rename(from, to),
830 (Some((one, from)), Some((other, to))) if one.name == other.name => {
831 one.rename(&from, &to)
832 }
833 _ => Err(io::Error::new(
834 ErrorKind::Unsupported,
835 "a file is not moved between machines; read it and write it there instead",
836 )),
837 }
838 }
839
840 fn canonical(&self, path: &Path) -> PathBuf {
841 match self.route(path) {
842 Some((remote, path)) => Self::named(remote, &remote.canonical(&path)),
843 None => self.inner.canonical(path),
844 }
845 }
846
847 fn files(&self, root: &Path, glob: Option<&str>, listing: Listing) -> io::Result<Vec<PathBuf>> {
848 match self.route(root) {
849 Some((remote, root)) => Ok(remote
850 .files(&root, glob, listing)?
851 .iter()
852 .map(|path| Self::named(remote, path))
853 .collect()),
854 None => self.inner.files(root, glob, listing),
855 }
856 }
857
858 fn spawn(
860 &self,
861 launcher: &Launcher,
862 dir: Option<&Path>,
863 program: &OsStr,
864 args: &[OsString],
865 how: Spawn,
866 ) -> io::Result<Process> {
867 match dir.and_then(|dir| self.route(dir)) {
868 Some((remote, dir)) => remote.spawn(launcher, Some(&dir), program, args, how),
869 None => self.inner.spawn(launcher, dir, program, args, how),
870 }
871 }
872
873 fn watch(
875 self: Arc<Self>,
876 root: &Path,
877 recursive: bool,
878 listing: Listing,
879 ) -> io::Result<crate::watch::Watch> {
880 match self.route(root) {
881 Some(_) => crate::watch::poll(self, root, recursive, listing),
882 None => self.inner.clone().watch(root, recursive, listing),
883 }
884 }
885}
886
887fn split_line(bytes: &[u8]) -> Option<(String, &[u8])> {
890 let newline = bytes.iter().position(|&byte| byte == b'\n')?;
891 let (line, rest) = bytes.split_at(newline);
892 Some((
893 String::from_utf8_lossy(line).into_owned(),
894 rest.get(1..).unwrap_or_default(),
895 ))
896}
897
898fn quote(text: &str) -> String {
900 format!("'{}'", text.replace('\'', "'\\''"))
901}
902
903fn quote_path(path: &Path) -> String {
906 let path = path.to_string_lossy();
907 match path.strip_prefix("~/") {
908 Some(rest) => format!("\"$HOME\"/{}", quote(rest)),
909 None if path == "~" => "\"$HOME\"".to_owned(),
910 None => quote(&path),
911 }
912}
913
914#[cfg(test)]
915mod tests;