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