1use fidl::endpoints::create_proxy;
6
7use crate::io::{Directory, DirentKind, LocalDirectory, RemoteDirectory};
8use crate::path::{
9 add_source_filename_to_path_if_absent, open_parent_subdir_readable, LocalOrRemoteDirectoryPath,
10};
11use anyhow::{bail, Result};
12use fidl::endpoints::ServerEnd;
13use regex::Regex;
14use std::path::PathBuf;
15use thiserror::Error;
16use {fidl_fuchsia_io as fio, fidl_fuchsia_sys2 as fsys};
17
18#[derive(Error, Debug)]
19pub enum CopyError {
20 #[error("Destination can not have a wildcard.")]
21 DestinationContainWildcard,
22
23 #[error("At least two paths (local or remote) must be provided.")]
24 NotEnoughPaths,
25
26 #[error("File name was unexpectedly empty.")]
27 EmptyFileName,
28
29 #[error("Path does not contain a parent folder.")]
30 NoParentFolder { path: String },
31
32 #[error("No files found matching: {pattern}.")]
33 NoWildCardMatches { pattern: String },
34
35 #[error(
36 "Could not find an instance with the moniker: {moniker}\n\
37 Use `ffx component list` or `ffx component show` to find the correct moniker of your instance."
38 )]
39 InstanceNotFound { moniker: String },
40
41 #[error("Encountered an unexpected error when attempting to open a directory with the provider moniker: {moniker}. {error:?}.")]
42 UnexpectedErrorFromMoniker { moniker: String, error: fsys::OpenError },
43
44 #[error("No file found at {file} in remote component directory.")]
45 NamespaceFileNotFound { file: String },
46}
47
48pub async fn copy_cmd<W: std::io::Write>(
55 realm_query: &fsys::RealmQueryProxy,
56 mut paths: Vec<String>,
57 verbose: bool,
58 mut writer: W,
59) -> Result<()> {
60 validate_paths(&paths)?;
61
62 let destination_path = paths.pop().unwrap();
64
65 for source_path in paths {
66 let result: Result<()> = match (
67 LocalOrRemoteDirectoryPath::parse(&source_path),
68 LocalOrRemoteDirectoryPath::parse(&destination_path),
69 ) {
70 (
71 LocalOrRemoteDirectoryPath::Remote(source),
72 LocalOrRemoteDirectoryPath::Local(destination_path),
73 ) => {
74 let source_dir = RemoteDirectory::from_proxy(
75 open_component_dir_for_moniker(&realm_query, &source.moniker, &source.dir_type)
76 .await?,
77 );
78 let destination_dir = LocalDirectory::new();
79
80 do_copy(
81 &source_dir,
82 &source.relative_path,
83 &destination_dir,
84 &destination_path,
85 verbose,
86 &mut writer,
87 )
88 .await
89 }
90
91 (
92 LocalOrRemoteDirectoryPath::Local(source_path),
93 LocalOrRemoteDirectoryPath::Remote(destination),
94 ) => {
95 let source_dir = LocalDirectory::new();
96 let destination_dir = RemoteDirectory::from_proxy(
97 open_component_dir_for_moniker(
98 &realm_query,
99 &destination.moniker,
100 &destination.dir_type,
101 )
102 .await?,
103 );
104
105 do_copy(
106 &source_dir,
107 &source_path,
108 &destination_dir,
109 &destination.relative_path,
110 verbose,
111 &mut writer,
112 )
113 .await
114 }
115
116 (
117 LocalOrRemoteDirectoryPath::Remote(source),
118 LocalOrRemoteDirectoryPath::Remote(destination),
119 ) => {
120 let source_dir = RemoteDirectory::from_proxy(
121 open_component_dir_for_moniker(&realm_query, &source.moniker, &source.dir_type)
122 .await?,
123 );
124
125 let destination_dir = RemoteDirectory::from_proxy(
126 open_component_dir_for_moniker(
127 &realm_query,
128 &destination.moniker,
129 &destination.dir_type,
130 )
131 .await?,
132 );
133
134 do_copy(
135 &source_dir,
136 &source.relative_path,
137 &destination_dir,
138 &destination.relative_path,
139 verbose,
140 &mut writer,
141 )
142 .await
143 }
144
145 (
146 LocalOrRemoteDirectoryPath::Local(source_path),
147 LocalOrRemoteDirectoryPath::Local(destination_path),
148 ) => {
149 let source_dir = LocalDirectory::new();
150 let destination_dir = LocalDirectory::new();
151 do_copy(
152 &source_dir,
153 &source_path,
154 &destination_dir,
155 &destination_path,
156 verbose,
157 &mut writer,
158 )
159 .await
160 }
161 };
162
163 match result {
164 Ok(_) => continue,
165 Err(e) => bail!("Copy from {} to {} failed: {}", &source_path, &destination_path, e),
166 };
167 }
168
169 Ok(())
170}
171
172async fn do_copy<S: Directory, D: Directory, W: std::io::Write>(
173 source_dir: &S,
174 source_path: &PathBuf,
175 destination_dir: &D,
176 destination_path: &PathBuf,
177 verbose: bool,
178 writer: &mut W,
179) -> Result<()> {
180 let source_paths = maybe_expand_wildcards(source_path, source_dir).await?;
181 for path in source_paths {
182 if is_file(source_dir, &path).await? {
183 let destination_path_path =
184 add_source_filename_to_path_if_absent(destination_dir, &path, &destination_path)
185 .await?;
186
187 let data = source_dir.read_file_bytes(path).await?;
188 destination_dir.write_file(destination_path_path.clone(), &data).await?;
189
190 if verbose {
191 writeln!(
192 writer,
193 "Copied {} -> {}",
194 source_path.display(),
195 destination_path_path.display()
196 )?;
197 }
198 } else {
199 writeln!(
201 writer,
202 "Directory \"{}\" ignored as recursive copying is unsupported. (See https://fxbug.dev/42067334)",
203 path.display()
204 )?;
205 }
206 }
207
208 Ok(())
209}
210
211async fn is_file<D: Directory>(dir: &D, path: &PathBuf) -> Result<bool> {
212 let parent_dir = open_parent_subdir_readable(path, dir)?;
213 let source_file = path.file_name().map_or_else(
214 || Err(CopyError::EmptyFileName),
215 |file| Ok(file.to_string_lossy().to_string()),
216 )?;
217
218 let remote_type = parent_dir.entry_type(&source_file).await?;
219 match remote_type {
220 Some(kind) => match kind {
221 DirentKind::File => Ok(true),
222 _ => Ok(false),
223 },
224 None => Err(CopyError::NamespaceFileNotFound { file: source_file }.into()),
225 }
226}
227
228async fn maybe_expand_wildcards<D: Directory>(path: &PathBuf, dir: &D) -> Result<Vec<PathBuf>> {
236 if !&path.to_string_lossy().contains("*") {
237 return Ok(vec![path.clone()]);
238 }
239 let parent_dir = open_parent_subdir_readable(path, dir)?;
240
241 let file_pattern = &path
242 .file_name()
243 .map_or_else(
244 || Err(CopyError::EmptyFileName),
245 |file| Ok(file.to_string_lossy().to_string()),
246 )?
247 .replace("*", ".*"); let entries = get_dirents_matching_pattern(&parent_dir, file_pattern.clone()).await?;
250
251 if entries.len() == 0 {
252 return Err(CopyError::NoWildCardMatches { pattern: file_pattern.to_string() }.into());
253 }
254
255 let parent_dir_path = match path.parent() {
256 Some(parent) => PathBuf::from(parent),
257 None => {
258 return Err(
259 CopyError::NoParentFolder { path: path.to_string_lossy().to_string() }.into()
260 )
261 }
262 };
263 Ok(entries.iter().map(|file| parent_dir_path.join(file)).collect::<Vec<_>>())
264}
265
266fn validate_paths(paths: &Vec<String>) -> Result<()> {
275 if paths.len() < 2 {
276 Err(CopyError::NotEnoughPaths.into())
277 } else if paths.last().unwrap().contains("*") {
278 Err(CopyError::DestinationContainWildcard.into())
279 } else {
280 Ok(())
281 }
282}
283
284async fn open_component_dir_for_moniker(
290 realm_query: &fsys::RealmQueryProxy,
291 moniker: &str,
292 dir_type: &fsys::OpenDirType,
293) -> Result<fio::DirectoryProxy> {
294 let (dir, server_end) = create_proxy::<fio::DirectoryMarker>();
295 let server_end = ServerEnd::new(server_end.into_channel());
296 let flags = match dir_type {
297 fsys::OpenDirType::PackageDir => fio::OpenFlags::RIGHT_READABLE,
298 _ => fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::RIGHT_WRITABLE,
299 };
300 match realm_query
301 .deprecated_open(&moniker, dir_type.clone(), flags, fio::ModeType::empty(), ".", server_end)
302 .await?
303 {
304 Ok(()) => Ok(dir),
305 Err(fsys::OpenError::InstanceNotFound) => {
306 Err(CopyError::InstanceNotFound { moniker: moniker.to_string() }.into())
307 }
308 Err(e) => {
309 Err(CopyError::UnexpectedErrorFromMoniker { moniker: moniker.to_string(), error: e }
310 .into())
311 }
312 }
313}
314
315async fn get_dirents_matching_pattern<D: Directory>(
321 dir: &D,
322 file_pattern: String,
323) -> Result<Vec<String>> {
324 let mut entries = dir.entry_names().await?;
325
326 let file_pattern = Regex::new(format!(r"^{}$", file_pattern).as_str())?;
327
328 entries.retain(|file_name| file_pattern.is_match(file_name.as_str()));
329
330 Ok(entries)
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use crate::test_utils::{
337 create_tmp_dir, generate_directory_paths, generate_file_paths, serve_realm_query, File,
338 SeedPath,
339 };
340 use std::collections::HashMap;
341 use std::fs::{read, write};
342 use std::iter::zip;
343 use test_case::test_case;
344
345 const CHANNEL_SIZE_LIMIT: u64 = 64 * 1024;
346 const LARGE_FILE_ARRAY: [u8; CHANNEL_SIZE_LIMIT as usize] = [b'a'; CHANNEL_SIZE_LIMIT as usize];
347 const OVER_LIMIT_FILE_ARRAY: [u8; (CHANNEL_SIZE_LIMIT + 1) as usize] =
348 [b'a'; (CHANNEL_SIZE_LIMIT + 1) as usize];
349
350 const LARGE_FILE_DATA: &str = unsafe { std::str::from_utf8_unchecked(&LARGE_FILE_ARRAY) };
352 const OVER_LIMIT_FILE_DATA: &str =
353 unsafe { std::str::from_utf8_unchecked(&OVER_LIMIT_FILE_ARRAY) };
354
355 #[derive(Clone)]
356 struct Input {
357 source: &'static str,
358 destination: &'static str,
359 }
360
361 #[derive(Clone)]
362 struct Inputs {
363 sources: Vec<&'static str>,
364 destination: &'static str,
365 }
366
367 #[derive(Clone)]
368 struct Expectation {
369 path: &'static str,
370 data: &'static str,
371 }
372
373 fn create_realm_query(
374 foo_dir_type: fsys::OpenDirType,
375 foo_files: Vec<SeedPath>,
376 bar_dir_type: fsys::OpenDirType,
377 bar_files: Vec<SeedPath>,
378 ) -> (fsys::RealmQueryProxy, PathBuf, PathBuf) {
379 let foo_ns_dir = create_tmp_dir(foo_files).unwrap();
380 let bar_ns_dir = create_tmp_dir(bar_files).unwrap();
381 let foo_path = foo_ns_dir.path().to_path_buf();
382 let bar_path = bar_ns_dir.path().to_path_buf();
383 let realm_query = serve_realm_query(
384 vec![],
385 HashMap::new(),
386 HashMap::new(),
387 HashMap::from([
388 (("./foo/bar".to_string(), foo_dir_type), foo_ns_dir),
389 (("./bar/foo".to_string(), bar_dir_type), bar_ns_dir),
390 ]),
391 );
392 (realm_query, foo_path, bar_path)
393 }
394
395 fn create_realm_query_simple(
396 foo_files: Vec<SeedPath>,
397 bar_files: Vec<SeedPath>,
398 ) -> (fsys::RealmQueryProxy, PathBuf, PathBuf) {
399 create_realm_query(
400 fsys::OpenDirType::NamespaceDir,
401 foo_files,
402 fsys::OpenDirType::NamespaceDir,
403 bar_files,
404 )
405 }
406
407 #[test_case(Input{source: "/foo/bar::out::/data/foo.txt", destination: "foo.txt"},
408 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
409 Expectation{path: "foo.txt", data: "Hello"}; "single_file")]
410 #[fuchsia::test]
411 async fn copy_from_outgoing_dir(
412 input: Input,
413 foo_files: Vec<SeedPath>,
414 expectation: Expectation,
415 ) {
416 let local_dir = create_tmp_dir(vec![]).unwrap();
419 let local_path = local_dir.path();
420
421 let (realm_query, _, _) = create_realm_query(
422 fsys::OpenDirType::OutgoingDir,
423 foo_files,
424 fsys::OpenDirType::OutgoingDir,
425 vec![],
426 );
427 let destination_path = local_path.join(input.destination).display().to_string();
428
429 copy_cmd(
430 &realm_query,
431 vec![input.source.to_string(), destination_path],
432 false,
433 std::io::stdout(),
434 )
435 .await
436 .unwrap();
437
438 let expected_data = expectation.data.to_owned().into_bytes();
439 let actual_data_path = local_path.join(expectation.path);
440 let actual_data = read(actual_data_path).unwrap();
441 assert_eq!(actual_data, expected_data);
442 }
443
444 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "foo.txt"},
445 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
446 Expectation{path: "foo.txt", data: "Hello"}; "single_file")]
447 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "foo.txt"},
448 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
449 Expectation{path: "foo.txt", data: "Hello"}; "overwrite_file")]
450 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "bar.txt"},
451 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
452 Expectation{path: "bar.txt", data: "Hello"}; "different_file_name")]
453 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: ""},
454 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
455 Expectation{path: "foo.txt", data: "Hello"}; "infer_path")]
456 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "./"},
457 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
458 Expectation{path: "foo.txt", data: "Hello"}; "infer_path_slash")]
459 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "foo.txt"},
460 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/bar.txt", data: "World"}]),
461 Expectation{path: "foo.txt", data: "Hello"}; "populated_directory")]
462 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "foo.txt"},
463 generate_file_paths(vec![File{ name: "data/foo.txt", data: LARGE_FILE_DATA}]),
464 Expectation{path: "foo.txt", data: LARGE_FILE_DATA}; "large_file")]
465 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "foo.txt"},
466 generate_file_paths(vec![File{ name: "data/foo.txt", data: OVER_LIMIT_FILE_DATA}]),
467 Expectation{path: "foo.txt", data: OVER_LIMIT_FILE_DATA}; "over_limit_file")]
468 #[fuchsia::test]
469 async fn copy_device_to_local(
470 input: Input,
471 foo_files: Vec<SeedPath>,
472 expectation: Expectation,
473 ) {
474 let local_dir = create_tmp_dir(vec![]).unwrap();
475 let local_path = local_dir.path();
476
477 let (realm_query, _, _) = create_realm_query_simple(foo_files, vec![]);
478 let destination_path = local_path.join(input.destination).display().to_string();
479
480 eprintln!("Destination path: {:?}", destination_path);
481
482 copy_cmd(
483 &realm_query,
484 vec![input.source.to_string(), destination_path],
485 false,
486 std::io::stdout(),
487 )
488 .await
489 .unwrap();
490
491 let expected_data = expectation.data.to_owned().into_bytes();
492 let actual_data_path = local_path.join(expectation.path);
493 let actual_data = read(actual_data_path).unwrap();
494 assert_eq!(actual_data, expected_data);
495 }
496
497 #[test_case(Input{source: "/foo/bar::/data/*", destination: "foo.txt"},
498 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
499 vec![Expectation{path: "foo.txt", data: "Hello"}]; "all_matches")]
500 #[test_case(Input{source: "/foo/bar::/data/*", destination: "foo.txt"},
501 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "foo.txt", data: "World"}]),
502 vec![Expectation{path: "foo.txt", data: "Hello"}]; "all_matches_overwrite")]
503 #[test_case(Input{source: "/foo/bar::/data/*", destination: "foo.txt"},
504 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/nested/foo.txt", data: "World"}]),
505 vec![Expectation{path: "foo.txt", data: "Hello"}]; "all_matches_nested")]
506 #[test_case(Input{source: "/foo/bar::/data/*.txt", destination: "foo.txt"},
507 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
508 vec![Expectation{path: "foo.txt", data: "Hello"}]; "file_extension")]
509 #[test_case(Input{source: "/foo/bar::/data/foo.*", destination: "foo.txt"},
510 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
511 vec![Expectation{path: "foo.txt", data: "Hello"}]; "file_extension_2")]
512 #[test_case(Input{source: "/foo/bar::/data/fo*.txt", destination: "foo.txt"},
513 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
514 vec![Expectation{path: "foo.txt", data: "Hello"}]; "file_substring_match")]
515 #[test_case(Input{source: "/foo/bar::/data/*", destination: "./"},
516 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/bar.txt", data: "World"}]),
517 vec![Expectation{path: "foo.txt", data: "Hello"}, Expectation{path: "bar.txt", data: "World"}]; "multi_file")]
518 #[test_case(Input{source: "/foo/bar::/data/*fo*.txt", destination: "./"},
519 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/foobar.txt", data: "World"}]),
520 vec![Expectation{path: "foo.txt", data: "Hello"}, Expectation{path: "foobar.txt", data: "World"}]; "multi_wildcard")]
521 #[test_case(Input{source: "/foo/bar::/data/*", destination: "./"},
522 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/foobar.txt", data: "World"},
523 File{ name: "foo.txt", data: "World"}, File{ name: "foobar.txt", data: "Hello"}]),
524 vec![Expectation{path: "foo.txt", data: "Hello"}, Expectation{path: "foobar.txt", data: "World"}]; "multi_file_overwrite")]
525 #[fuchsia::test]
526 async fn copy_device_to_local_wildcard(
527 input: Input,
528 foo_files: Vec<SeedPath>,
529 expectation: Vec<Expectation>,
530 ) {
531 let local_dir = create_tmp_dir(vec![]).unwrap();
532 let local_path = local_dir.path();
533
534 let (realm_query, _, _) = create_realm_query_simple(foo_files, vec![]);
535 let destination_path = local_path.join(input.destination);
536
537 copy_cmd(
538 &realm_query,
539 vec![input.source.to_string(), destination_path.display().to_string()],
540 true,
541 std::io::stdout(),
542 )
543 .await
544 .unwrap();
545
546 for expected in expectation {
547 let expected_data = expected.data.to_owned().into_bytes();
548 let actual_data_path = local_path.join(expected.path);
549
550 eprintln!("reading file '{}'", actual_data_path.display());
551
552 let actual_data = read(actual_data_path).unwrap();
553 assert_eq!(actual_data, expected_data);
554 }
555 }
556
557 #[test_case(Input{source: "/wrong_moniker/foo/bar::/data/foo.txt", destination: "foo.txt"},
558 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]); "bad_moniker")]
559 #[test_case(Input{source: "/foo/bar::/data/bar.txt", destination: "foo.txt"},
560 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]); "bad_file")]
561 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "bar/foo.txt"},
562 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]); "bad_directory")]
563 #[fuchsia::test]
564 async fn copy_device_to_local_fails(input: Input, foo_files: Vec<SeedPath>) {
565 let local_dir = create_tmp_dir(vec![]).unwrap();
566 let local_path = local_dir.path();
567
568 let (realm_query, _, _) = create_realm_query_simple(foo_files, vec![]);
569 let destination_path = local_path.join(input.destination).display().to_string();
570 let result = copy_cmd(
571 &realm_query,
572 vec![input.source.to_string(), destination_path],
573 true,
574 std::io::stdout(),
575 )
576 .await;
577
578 assert!(result.is_err());
579 }
580
581 #[test_case(Input{source: "foo.txt", destination: "/foo/bar::/data/foo.txt"},
582 generate_directory_paths(vec!["data"]),
583 Expectation{path: "data/foo.txt", data: "Hello"}; "single_file")]
584 #[test_case(Input{source: "foo.txt", destination: "/foo/bar::/data/bar.txt"},
585 generate_directory_paths(vec!["data"]),
586 Expectation{path: "data/bar.txt", data: "Hello"}; "different_file_name")]
587 #[test_case(Input{source: "foo.txt", destination: "/foo/bar::/data/foo.txt"},
588 generate_file_paths(vec![File{ name: "data/foo.txt", data: "World"}]),
589 Expectation{path: "data/foo.txt", data: "Hello"}; "overwrite_file")]
590 #[test_case(Input{source: "foo.txt", destination: "/foo/bar::/data"},
591 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
592 Expectation{path: "data/foo.txt", data: "Hello"}; "infer_path")]
593 #[test_case(Input{source: "foo.txt", destination: "/foo/bar::/data/"},
594 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
595 Expectation{path: "data/foo.txt", data: "Hello"}; "infer_slash_path")]
596 #[test_case(Input{source: "foo.txt", destination: "/foo/bar::/data/nested/foo.txt"},
597 generate_directory_paths(vec!["data", "data/nested"]),
598 Expectation{path: "data/nested/foo.txt", data: "Hello"}; "nested_path")]
599 #[test_case(Input{source: "foo.txt", destination: "/foo/bar::/data/nested"},
600 generate_directory_paths(vec!["data", "data/nested"]),
601 Expectation{path: "data/nested/foo.txt", data: "Hello"}; "infer_nested_path")]
602 #[test_case(Input{source: "foo.txt", destination: "/foo/bar::/data/"},
603 generate_directory_paths(vec!["data"]),
604 Expectation{path: "data/foo.txt", data: LARGE_FILE_DATA}; "large_file")]
605 #[test_case(Input{source: "foo.txt", destination: "/foo/bar::/data/"},
606 generate_directory_paths(vec!["data"]),
607 Expectation{path: "data/foo.txt", data: OVER_LIMIT_FILE_DATA}; "over_channel_limit_file")]
608 #[fuchsia::test]
609 async fn copy_local_to_device(
610 input: Input,
611 foo_files: Vec<SeedPath>,
612 expectation: Expectation,
613 ) {
614 let local_dir = create_tmp_dir(vec![]).unwrap();
615 let local_path = local_dir.path();
616
617 let source_path = local_path.join(&input.source);
618 write(&source_path, expectation.data.to_owned().into_bytes()).unwrap();
619 let (realm_query, foo_path, _) = create_realm_query_simple(foo_files, vec![]);
620
621 copy_cmd(
622 &realm_query,
623 vec![source_path.display().to_string(), input.destination.to_string()],
624 false,
625 std::io::stdout(),
626 )
627 .await
628 .unwrap();
629
630 let actual_path = foo_path.join(expectation.path);
631 let actual_data = read(actual_path).unwrap();
632 let expected_data = expectation.data.to_owned().into_bytes();
633 assert_eq!(actual_data, expected_data);
634 }
635
636 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "/bar/foo::/data/foo.txt"},
637 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/bar.txt", data: "World"}]),
638 generate_directory_paths(vec!["data"]),
639 vec![Expectation{path: "data/foo.txt", data: "Hello"}]; "single_file")]
640 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "/bar/foo::/data/nested/foo.txt"},
641 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/bar.txt", data: "World"}]),
642 generate_directory_paths(vec!["data", "data/nested"]),
643 vec![Expectation{path: "data/nested/foo.txt", data: "Hello"}]; "nested")]
644 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "/bar/foo::/data/bar.txt"},
645 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/bar.txt", data: "World"}]),
646 generate_directory_paths(vec!["data"]),
647 vec![Expectation{path: "data/bar.txt", data: "Hello"}]; "different_file_name")]
648 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "/bar/foo::/data/foo.txt"},
649 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/bar.txt", data: "World"}]),
650 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
651 vec![Expectation{path: "data/foo.txt", data: "Hello"}]; "overwrite_file")]
652 #[test_case(Input{source: "/foo/bar::/data/*", destination: "/bar/foo::/data"},
653 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/bar.txt", data: "World"}]),
654 generate_directory_paths(vec!["data"]),
655 vec![Expectation{path: "data/foo.txt", data: "Hello"}, Expectation{path: "data/bar.txt", data: "World"}]; "wildcard_match_all_multi_file")]
656 #[test_case(Input{source: "/foo/bar::/data/*.txt", destination: "/bar/foo::/data"},
657 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/bar.txt", data: "World"}]),
658 generate_directory_paths(vec!["data"]),
659 vec![Expectation{path: "data/foo.txt", data: "Hello"}, Expectation{path: "data/bar.txt", data: "World"}]; "wildcard_match_files_extensions_multi_file")]
660 #[test_case(Input{source: "/foo/bar::/data/*", destination: "/bar/foo::/data"},
661 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/bar.txt", data: "World"}]),
662 generate_file_paths(vec![File{ name: "data/foo.txt", data: "World"}, File{ name: "data/bar.txt", data: "Hello"}]),
663 vec![Expectation{path: "data/foo.txt", data: "Hello"}, Expectation{path: "data/bar.txt", data: "World"}]; "wildcard_match_all_multi_file_overwrite")]
664 #[fuchsia::test]
665 async fn copy_device_to_device(
666 input: Input,
667 foo_files: Vec<SeedPath>,
668 bar_files: Vec<SeedPath>,
669 expectation: Vec<Expectation>,
670 ) {
671 let (realm_query, _, bar_path) = create_realm_query_simple(foo_files, bar_files);
672
673 copy_cmd(
674 &realm_query,
675 vec![input.source.to_string(), input.destination.to_string()],
676 false,
677 std::io::stdout(),
678 )
679 .await
680 .unwrap();
681
682 for expected in expectation {
683 let destination_path = bar_path.join(expected.path);
684 let actual_data = read(destination_path).unwrap();
685 let expected_data = expected.data.to_owned().into_bytes();
686 assert_eq!(actual_data, expected_data);
687 }
688 }
689
690 #[test_case(Input{source: "/foo/bar::/data/cat.txt", destination: "/bar/foo::/data/foo.txt"}; "bad_file")]
691 #[test_case(Input{source: "/foo/bar::/foo.txt", destination: "/bar/foo::/data/foo.txt"}; "bad_source_folder")]
692 #[test_case(Input{source: "/hello/world::/data/foo.txt", destination: "/bar/foo::/data/file.txt"}; "bad_source_moniker")]
693 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "/hello/world::/data/file.txt"}; "bad_destination_moniker")]
694 #[fuchsia::test]
695 async fn copy_device_to_device_fails(input: Input) {
696 let (realm_query, _, _) = create_realm_query_simple(
697 generate_file_paths(vec![
698 File { name: "data/foo.txt", data: "Hello" },
699 File { name: "data/bar.txt", data: "World" },
700 ]),
701 generate_directory_paths(vec!["data"]),
702 );
703
704 let result = copy_cmd(
705 &realm_query,
706 vec![input.source.to_string(), input.destination.to_string()],
707 false,
708 std::io::stdout(),
709 )
710 .await;
711
712 assert!(result.is_err());
713 }
714
715 #[test_case(Inputs{sources: vec!["foo.txt"], destination: "/foo/bar::/data/"},
716 generate_directory_paths(vec!["data"]),
717 vec![Expectation{path: "data/foo.txt", data: "Hello"}]; "single_file_wildcard")]
718 #[test_case(Inputs{sources: vec!["foo.txt"], destination: "/foo/bar::/data/"},
719 generate_file_paths(vec![File{ name: "data/foo.txt", data: "World"}]),
720 vec![Expectation{path: "data/foo.txt", data: "Hello"}]; "single_file_wildcard_overwrite")]
721 #[test_case(Inputs{sources: vec!["foo.txt", "bar.txt"], destination: "/foo/bar::/data/"},
722 generate_directory_paths(vec!["data"]),
723 vec![Expectation{path: "data/foo.txt", data: "Hello"}, Expectation{path: "data/bar.txt", data: "World"}]; "multi_file_wildcard")]
724 #[test_case(Inputs{sources: vec!["foo.txt", "bar.txt"], destination: "/foo/bar::/data/"},
725 generate_file_paths(vec![File{ name: "data/foo.txt", data: "World"}, File{ name: "data/bar.txt", data: "World"}]),
726 vec![Expectation{path: "data/foo.txt", data: "Hello"}, Expectation{path: "data/bar.txt", data: "World"}]; "multi_wildcard_file_overwrite")]
727 #[fuchsia::test]
728 async fn copy_local_to_device_wildcard(
729 input: Inputs,
730 foo_files: Vec<SeedPath>,
731 expectation: Vec<Expectation>,
732 ) {
733 let local_dir = create_tmp_dir(vec![]).unwrap();
734 let local_path = local_dir.path();
735
736 for (path, expected) in zip(input.sources.clone(), expectation.clone()) {
737 let source_path = local_path.join(path);
738 write(&source_path, expected.data).unwrap();
739 }
740
741 let (realm_query, foo_path, _) = create_realm_query_simple(foo_files, vec![]);
742 let mut paths: Vec<String> = input
743 .sources
744 .into_iter()
745 .map(|path| local_path.join(path).display().to_string())
746 .collect();
747 paths.push(input.destination.to_string());
748
749 copy_cmd(&realm_query, paths, false, std::io::stdout()).await.unwrap();
750
751 for expected in expectation {
752 let actual_path = foo_path.join(expected.path);
753 let actual_data = read(actual_path).unwrap();
754 let expected_data = expected.data.to_owned().into_bytes();
755 assert_eq!(actual_data, expected_data);
756 }
757 }
758
759 #[test_case(Input{source: "foo.txt", destination: "wrong_moniker/foo/bar::/data/foo.txt"}; "bad_moniker")]
760 #[test_case(Input{source: "foo.txt", destination: "/foo/bar:://bar/foo.txt"}; "bad_directory")]
761 #[fuchsia::test]
762 async fn copy_local_to_device_fails(input: Input) {
763 let local_dir = create_tmp_dir(vec![]).unwrap();
764 let local_path = local_dir.path();
765
766 let source_path = local_path.join(input.source);
767 write(&source_path, "Hello".to_owned().into_bytes()).unwrap();
768
769 let (realm_query, _, _) =
770 create_realm_query_simple(generate_directory_paths(vec!["data"]), vec![]);
771
772 let result = copy_cmd(
773 &realm_query,
774 vec![source_path.display().to_string(), input.destination.to_string()],
775 false,
776 std::io::stdout(),
777 )
778 .await;
779
780 assert!(result.is_err());
781 }
782
783 #[test_case(vec![]; "no_wildcard_matches")]
784 #[test_case(vec!["foo.txt"]; "not_enough_args")]
785 #[test_case(vec!["/foo/bar::/data/*", "/foo/bar::/data/*"]; "remote_wildcard_destination")]
786 #[test_case(vec!["/foo/bar::/data/*", "/foo/bar::/data/*", "/"]; "multi_wildcards_remote")]
787 #[test_case(vec!["*", "*"]; "local_wildcard_destination")]
788 #[fuchsia::test]
789 async fn copy_wildcard_fails(paths: Vec<&str>) {
790 let (realm_query, _, _) = create_realm_query_simple(
791 generate_file_paths(vec![File { name: "data/foo.txt", data: "Hello" }]),
792 vec![],
793 );
794 let paths = paths.into_iter().map(|s| s.to_string()).collect();
795 let result = copy_cmd(&realm_query, paths, false, std::io::stdout()).await;
796
797 assert!(result.is_err());
798 }
799
800 #[test_case(Inputs{sources: vec!["/foo/bar::/data/foo.txt", "bar.txt"], destination: "/bar/foo::/data/"},
801 generate_file_paths(vec![File{ name: "bar.txt", data: "World"}]),
802 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/bar.txt", data: "World"}]),
803 generate_directory_paths(vec!["data"]),
804 vec![Expectation{path: "data/foo.txt", data: "Hello"}, Expectation{path: "data/bar.txt", data: "World"}]; "no_wildcard_mix")]
805 #[test_case(Inputs{sources: vec!["/foo/bar::/data/foo.txt", "/foo/bar::/data/*", "foobar.txt"], destination: "/bar/foo::/data/"},
806 generate_file_paths(vec![File{ name: "foobar.txt", data: "World"}]),
807 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/bar.txt", data: "World"}]),
808 generate_directory_paths(vec!["data"]),
809 vec![Expectation{path: "data/foo.txt", data: "Hello"}, Expectation{path: "data/bar.txt", data: "World"}, Expectation{path: "data/foobar.txt", data: "World"}]; "wildcard_mix")]
810 #[test_case(Inputs{sources: vec!["/foo/bar::/data/*", "/foo/bar::/data/*", "foobar.txt"], destination: "/bar/foo::/data/"},
811 generate_file_paths(vec![File{ name: "foobar.txt", data: "World"}]),
812 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/bar.txt", data: "World"}]),
813 generate_directory_paths(vec!["data"]),
814 vec![Expectation{path: "data/foo.txt", data: "Hello"}, Expectation{path: "data/bar.txt", data: "World"}, Expectation{path: "data/foobar.txt", data: "World"}]; "double_wildcard")]
815 #[fuchsia::test]
816 async fn copy_mixed_tests_remote_destination(
817 input: Inputs,
818 local_files: Vec<SeedPath>,
819 foo_files: Vec<SeedPath>,
820 bar_files: Vec<SeedPath>,
821 expectation: Vec<Expectation>,
822 ) {
823 let local_dir = create_tmp_dir(local_files).unwrap();
824 let local_path = local_dir.path();
825
826 let (realm_query, _, bar_path) = create_realm_query_simple(foo_files, bar_files);
827 let mut paths: Vec<String> = input
828 .sources
829 .clone()
830 .into_iter()
831 .map(|path| match LocalOrRemoteDirectoryPath::parse(&path) {
832 LocalOrRemoteDirectoryPath::Remote(_) => path.to_string(),
833 LocalOrRemoteDirectoryPath::Local(_) => local_path.join(path).display().to_string(),
834 })
835 .collect();
836 paths.push(input.destination.to_owned());
837
838 copy_cmd(&realm_query, paths, false, std::io::stdout()).await.unwrap();
839
840 for expected in expectation {
841 let actual_path = bar_path.join(expected.path);
842 let actual_data = read(actual_path).unwrap();
843 let expected_data = expected.data.to_owned().into_bytes();
844 assert_eq!(actual_data, expected_data);
845 }
846 }
847}