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