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 = match std::str::from_utf8(&LARGE_FILE_ARRAY) {
344 Ok(data) => data,
345 Err(_) => panic!("test data must be UTF-8"),
346 };
347 const OVER_LIMIT_FILE_DATA: &str = match std::str::from_utf8(&OVER_LIMIT_FILE_ARRAY) {
348 Ok(data) => data,
349 Err(_) => panic!("test data must be UTF-8"),
350 };
351
352 #[derive(Clone)]
353 struct Input {
354 source: &'static str,
355 destination: &'static str,
356 }
357
358 #[derive(Clone)]
359 struct Inputs {
360 sources: Vec<&'static str>,
361 destination: &'static str,
362 }
363
364 #[derive(Clone)]
365 struct Expectation {
366 path: &'static str,
367 data: &'static str,
368 }
369
370 fn create_realm_query(
371 foo_dir_type: fsys::OpenDirType,
372 foo_files: Vec<SeedPath>,
373 bar_dir_type: fsys::OpenDirType,
374 bar_files: Vec<SeedPath>,
375 ) -> (fsys::RealmQueryProxy, PathBuf, PathBuf) {
376 let foo_ns_dir = create_tmp_dir(foo_files).unwrap();
377 let bar_ns_dir = create_tmp_dir(bar_files).unwrap();
378 let foo_path = foo_ns_dir.path().to_path_buf();
379 let bar_path = bar_ns_dir.path().to_path_buf();
380 let realm_query = serve_realm_query(
381 vec![],
382 HashMap::new(),
383 HashMap::new(),
384 HashMap::from([
385 (("./foo/bar".to_string(), foo_dir_type), foo_ns_dir),
386 (("./bar/foo".to_string(), bar_dir_type), bar_ns_dir),
387 ]),
388 );
389 (realm_query, foo_path, bar_path)
390 }
391
392 fn create_realm_query_simple(
393 foo_files: Vec<SeedPath>,
394 bar_files: Vec<SeedPath>,
395 ) -> (fsys::RealmQueryProxy, PathBuf, PathBuf) {
396 create_realm_query(
397 fsys::OpenDirType::NamespaceDir,
398 foo_files,
399 fsys::OpenDirType::NamespaceDir,
400 bar_files,
401 )
402 }
403
404 #[test_case(Input{source: "/foo/bar::out::/data/foo.txt", destination: "foo.txt"},
405 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
406 Expectation{path: "foo.txt", data: "Hello"}; "single_file")]
407 #[fuchsia::test]
408 async fn copy_from_outgoing_dir(
409 input: Input,
410 foo_files: Vec<SeedPath>,
411 expectation: Expectation,
412 ) {
413 let local_dir = create_tmp_dir(vec![]).unwrap();
416 let local_path = local_dir.path();
417
418 let (realm_query, _, _) = create_realm_query(
419 fsys::OpenDirType::OutgoingDir,
420 foo_files,
421 fsys::OpenDirType::OutgoingDir,
422 vec![],
423 );
424 let destination_path = local_path.join(input.destination).display().to_string();
425
426 copy_cmd(
427 &realm_query,
428 vec![input.source.to_string(), destination_path],
429 false,
430 std::io::stdout(),
431 )
432 .await
433 .unwrap();
434
435 let expected_data = expectation.data.to_owned().into_bytes();
436 let actual_data_path = local_path.join(expectation.path);
437 let actual_data = read(actual_data_path).unwrap();
438 assert_eq!(actual_data, expected_data);
439 }
440
441 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "foo.txt"},
442 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
443 Expectation{path: "foo.txt", data: "Hello"}; "single_file")]
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"}; "overwrite_file")]
447 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "bar.txt"},
448 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
449 Expectation{path: "bar.txt", data: "Hello"}; "different_file_name")]
450 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: ""},
451 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
452 Expectation{path: "foo.txt", data: "Hello"}; "infer_path")]
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_slash")]
456 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "foo.txt"},
457 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/bar.txt", data: "World"}]),
458 Expectation{path: "foo.txt", data: "Hello"}; "populated_directory")]
459 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "foo.txt"},
460 generate_file_paths(vec![File{ name: "data/foo.txt", data: LARGE_FILE_DATA}]),
461 Expectation{path: "foo.txt", data: LARGE_FILE_DATA}; "large_file")]
462 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "foo.txt"},
463 generate_file_paths(vec![File{ name: "data/foo.txt", data: OVER_LIMIT_FILE_DATA}]),
464 Expectation{path: "foo.txt", data: OVER_LIMIT_FILE_DATA}; "over_limit_file")]
465 #[fuchsia::test]
466 async fn copy_device_to_local(
467 input: Input,
468 foo_files: Vec<SeedPath>,
469 expectation: Expectation,
470 ) {
471 let local_dir = create_tmp_dir(vec![]).unwrap();
472 let local_path = local_dir.path();
473
474 let (realm_query, _, _) = create_realm_query_simple(foo_files, vec![]);
475 let destination_path = local_path.join(input.destination).display().to_string();
476
477 eprintln!("Destination path: {:?}", destination_path);
478
479 copy_cmd(
480 &realm_query,
481 vec![input.source.to_string(), destination_path],
482 false,
483 std::io::stdout(),
484 )
485 .await
486 .unwrap();
487
488 let expected_data = expectation.data.to_owned().into_bytes();
489 let actual_data_path = local_path.join(expectation.path);
490 let actual_data = read(actual_data_path).unwrap();
491 assert_eq!(actual_data, expected_data);
492 }
493
494 #[test_case(Input{source: "/foo/bar::/data/*", destination: "foo.txt"},
495 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
496 vec![Expectation{path: "foo.txt", data: "Hello"}]; "all_matches")]
497 #[test_case(Input{source: "/foo/bar::/data/*", destination: "foo.txt"},
498 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "foo.txt", data: "World"}]),
499 vec![Expectation{path: "foo.txt", data: "Hello"}]; "all_matches_overwrite")]
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: "data/nested/foo.txt", data: "World"}]),
502 vec![Expectation{path: "foo.txt", data: "Hello"}]; "all_matches_nested")]
503 #[test_case(Input{source: "/foo/bar::/data/*.txt", destination: "foo.txt"},
504 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
505 vec![Expectation{path: "foo.txt", data: "Hello"}]; "file_extension")]
506 #[test_case(Input{source: "/foo/bar::/data/foo.*", 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_2")]
509 #[test_case(Input{source: "/foo/bar::/data/fo*.txt", destination: "foo.txt"},
510 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
511 vec![Expectation{path: "foo.txt", data: "Hello"}]; "file_substring_match")]
512 #[test_case(Input{source: "/foo/bar::/data/*", destination: "./"},
513 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/bar.txt", data: "World"}]),
514 vec![Expectation{path: "foo.txt", data: "Hello"}, Expectation{path: "bar.txt", data: "World"}]; "multi_file")]
515 #[test_case(Input{source: "/foo/bar::/data/*fo*.txt", destination: "./"},
516 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/foobar.txt", data: "World"}]),
517 vec![Expectation{path: "foo.txt", data: "Hello"}, Expectation{path: "foobar.txt", data: "World"}]; "multi_wildcard")]
518 #[test_case(Input{source: "/foo/bar::/data/*", destination: "./"},
519 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/foobar.txt", data: "World"},
520 File{ name: "foo.txt", data: "World"}, File{ name: "foobar.txt", data: "Hello"}]),
521 vec![Expectation{path: "foo.txt", data: "Hello"}, Expectation{path: "foobar.txt", data: "World"}]; "multi_file_overwrite")]
522 #[fuchsia::test]
523 async fn copy_device_to_local_wildcard(
524 input: Input,
525 foo_files: Vec<SeedPath>,
526 expectation: Vec<Expectation>,
527 ) {
528 let local_dir = create_tmp_dir(vec![]).unwrap();
529 let local_path = local_dir.path();
530
531 let (realm_query, _, _) = create_realm_query_simple(foo_files, vec![]);
532 let destination_path = local_path.join(input.destination);
533
534 copy_cmd(
535 &realm_query,
536 vec![input.source.to_string(), destination_path.display().to_string()],
537 true,
538 std::io::stdout(),
539 )
540 .await
541 .unwrap();
542
543 for expected in expectation {
544 let expected_data = expected.data.to_owned().into_bytes();
545 let actual_data_path = local_path.join(expected.path);
546
547 eprintln!("reading file '{}'", actual_data_path.display());
548
549 let actual_data = read(actual_data_path).unwrap();
550 assert_eq!(actual_data, expected_data);
551 }
552 }
553
554 #[test_case(Input{source: "/wrong_moniker/foo/bar::/data/foo.txt", destination: "foo.txt"},
555 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]); "bad_moniker")]
556 #[test_case(Input{source: "/foo/bar::/data/bar.txt", destination: "foo.txt"},
557 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]); "bad_file")]
558 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "bar/foo.txt"},
559 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]); "bad_directory")]
560 #[fuchsia::test]
561 async fn copy_device_to_local_fails(input: Input, foo_files: Vec<SeedPath>) {
562 let local_dir = create_tmp_dir(vec![]).unwrap();
563 let local_path = local_dir.path();
564
565 let (realm_query, _, _) = create_realm_query_simple(foo_files, vec![]);
566 let destination_path = local_path.join(input.destination).display().to_string();
567 let result = copy_cmd(
568 &realm_query,
569 vec![input.source.to_string(), destination_path],
570 true,
571 std::io::stdout(),
572 )
573 .await;
574
575 assert!(result.is_err());
576 }
577
578 #[test_case(Input{source: "foo.txt", destination: "/foo/bar::/data/foo.txt"},
579 generate_directory_paths(vec!["data"]),
580 Expectation{path: "data/foo.txt", data: "Hello"}; "single_file")]
581 #[test_case(Input{source: "foo.txt", destination: "/foo/bar::/data/bar.txt"},
582 generate_directory_paths(vec!["data"]),
583 Expectation{path: "data/bar.txt", data: "Hello"}; "different_file_name")]
584 #[test_case(Input{source: "foo.txt", destination: "/foo/bar::/data/foo.txt"},
585 generate_file_paths(vec![File{ name: "data/foo.txt", data: "World"}]),
586 Expectation{path: "data/foo.txt", data: "Hello"}; "overwrite_file")]
587 #[test_case(Input{source: "foo.txt", destination: "/foo/bar::/data"},
588 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
589 Expectation{path: "data/foo.txt", data: "Hello"}; "infer_path")]
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_slash_path")]
593 #[test_case(Input{source: "foo.txt", destination: "/foo/bar::/data/nested/foo.txt"},
594 generate_directory_paths(vec!["data", "data/nested"]),
595 Expectation{path: "data/nested/foo.txt", data: "Hello"}; "nested_path")]
596 #[test_case(Input{source: "foo.txt", destination: "/foo/bar::/data/nested"},
597 generate_directory_paths(vec!["data", "data/nested"]),
598 Expectation{path: "data/nested/foo.txt", data: "Hello"}; "infer_nested_path")]
599 #[test_case(Input{source: "foo.txt", destination: "/foo/bar::/data/"},
600 generate_directory_paths(vec!["data"]),
601 Expectation{path: "data/foo.txt", data: LARGE_FILE_DATA}; "large_file")]
602 #[test_case(Input{source: "foo.txt", destination: "/foo/bar::/data/"},
603 generate_directory_paths(vec!["data"]),
604 Expectation{path: "data/foo.txt", data: OVER_LIMIT_FILE_DATA}; "over_channel_limit_file")]
605 #[fuchsia::test]
606 async fn copy_local_to_device(
607 input: Input,
608 foo_files: Vec<SeedPath>,
609 expectation: Expectation,
610 ) {
611 let local_dir = create_tmp_dir(vec![]).unwrap();
612 let local_path = local_dir.path();
613
614 let source_path = local_path.join(&input.source);
615 write(&source_path, expectation.data.to_owned().into_bytes()).unwrap();
616 let (realm_query, foo_path, _) = create_realm_query_simple(foo_files, vec![]);
617
618 copy_cmd(
619 &realm_query,
620 vec![source_path.display().to_string(), input.destination.to_string()],
621 false,
622 std::io::stdout(),
623 )
624 .await
625 .unwrap();
626
627 let actual_path = foo_path.join(expectation.path);
628 let actual_data = read(actual_path).unwrap();
629 let expected_data = expectation.data.to_owned().into_bytes();
630 assert_eq!(actual_data, expected_data);
631 }
632
633 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "/bar/foo::/data/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"]),
636 vec![Expectation{path: "data/foo.txt", data: "Hello"}]; "single_file")]
637 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "/bar/foo::/data/nested/foo.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", "data/nested"]),
640 vec![Expectation{path: "data/nested/foo.txt", data: "Hello"}]; "nested")]
641 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "/bar/foo::/data/bar.txt"},
642 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/bar.txt", data: "World"}]),
643 generate_directory_paths(vec!["data"]),
644 vec![Expectation{path: "data/bar.txt", data: "Hello"}]; "different_file_name")]
645 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "/bar/foo::/data/foo.txt"},
646 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/bar.txt", data: "World"}]),
647 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}]),
648 vec![Expectation{path: "data/foo.txt", data: "Hello"}]; "overwrite_file")]
649 #[test_case(Input{source: "/foo/bar::/data/*", 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_all_multi_file")]
653 #[test_case(Input{source: "/foo/bar::/data/*.txt", 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_directory_paths(vec!["data"]),
656 vec![Expectation{path: "data/foo.txt", data: "Hello"}, Expectation{path: "data/bar.txt", data: "World"}]; "wildcard_match_files_extensions_multi_file")]
657 #[test_case(Input{source: "/foo/bar::/data/*", destination: "/bar/foo::/data"},
658 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/bar.txt", data: "World"}]),
659 generate_file_paths(vec![File{ name: "data/foo.txt", data: "World"}, File{ name: "data/bar.txt", data: "Hello"}]),
660 vec![Expectation{path: "data/foo.txt", data: "Hello"}, Expectation{path: "data/bar.txt", data: "World"}]; "wildcard_match_all_multi_file_overwrite")]
661 #[fuchsia::test]
662 async fn copy_device_to_device(
663 input: Input,
664 foo_files: Vec<SeedPath>,
665 bar_files: Vec<SeedPath>,
666 expectation: Vec<Expectation>,
667 ) {
668 let (realm_query, _, bar_path) = create_realm_query_simple(foo_files, bar_files);
669
670 copy_cmd(
671 &realm_query,
672 vec![input.source.to_string(), input.destination.to_string()],
673 false,
674 std::io::stdout(),
675 )
676 .await
677 .unwrap();
678
679 for expected in expectation {
680 let destination_path = bar_path.join(expected.path);
681 let actual_data = read(destination_path).unwrap();
682 let expected_data = expected.data.to_owned().into_bytes();
683 assert_eq!(actual_data, expected_data);
684 }
685 }
686
687 #[test_case(Input{source: "/foo/bar::/data/cat.txt", destination: "/bar/foo::/data/foo.txt"}; "bad_file")]
688 #[test_case(Input{source: "/foo/bar::/foo.txt", destination: "/bar/foo::/data/foo.txt"}; "bad_source_folder")]
689 #[test_case(Input{source: "/hello/world::/data/foo.txt", destination: "/bar/foo::/data/file.txt"}; "bad_source_moniker")]
690 #[test_case(Input{source: "/foo/bar::/data/foo.txt", destination: "/hello/world::/data/file.txt"}; "bad_destination_moniker")]
691 #[fuchsia::test]
692 async fn copy_device_to_device_fails(input: Input) {
693 let (realm_query, _, _) = create_realm_query_simple(
694 generate_file_paths(vec![
695 File { name: "data/foo.txt", data: "Hello" },
696 File { name: "data/bar.txt", data: "World" },
697 ]),
698 generate_directory_paths(vec!["data"]),
699 );
700
701 let result = copy_cmd(
702 &realm_query,
703 vec![input.source.to_string(), input.destination.to_string()],
704 false,
705 std::io::stdout(),
706 )
707 .await;
708
709 assert!(result.is_err());
710 }
711
712 #[test_case(Inputs{sources: vec!["foo.txt"], destination: "/foo/bar::/data/"},
713 generate_directory_paths(vec!["data"]),
714 vec![Expectation{path: "data/foo.txt", data: "Hello"}]; "single_file_wildcard")]
715 #[test_case(Inputs{sources: vec!["foo.txt"], destination: "/foo/bar::/data/"},
716 generate_file_paths(vec![File{ name: "data/foo.txt", data: "World"}]),
717 vec![Expectation{path: "data/foo.txt", data: "Hello"}]; "single_file_wildcard_overwrite")]
718 #[test_case(Inputs{sources: vec!["foo.txt", "bar.txt"], destination: "/foo/bar::/data/"},
719 generate_directory_paths(vec!["data"]),
720 vec![Expectation{path: "data/foo.txt", data: "Hello"}, Expectation{path: "data/bar.txt", data: "World"}]; "multi_file_wildcard")]
721 #[test_case(Inputs{sources: vec!["foo.txt", "bar.txt"], destination: "/foo/bar::/data/"},
722 generate_file_paths(vec![File{ name: "data/foo.txt", data: "World"}, File{ name: "data/bar.txt", data: "World"}]),
723 vec![Expectation{path: "data/foo.txt", data: "Hello"}, Expectation{path: "data/bar.txt", data: "World"}]; "multi_wildcard_file_overwrite")]
724 #[fuchsia::test]
725 async fn copy_local_to_device_wildcard(
726 input: Inputs,
727 foo_files: Vec<SeedPath>,
728 expectation: Vec<Expectation>,
729 ) {
730 let local_dir = create_tmp_dir(vec![]).unwrap();
731 let local_path = local_dir.path();
732
733 for (path, expected) in zip(input.sources.clone(), expectation.clone()) {
734 let source_path = local_path.join(path);
735 write(&source_path, expected.data).unwrap();
736 }
737
738 let (realm_query, foo_path, _) = create_realm_query_simple(foo_files, vec![]);
739 let mut paths: Vec<String> = input
740 .sources
741 .into_iter()
742 .map(|path| local_path.join(path).display().to_string())
743 .collect();
744 paths.push(input.destination.to_string());
745
746 copy_cmd(&realm_query, paths, false, std::io::stdout()).await.unwrap();
747
748 for expected in expectation {
749 let actual_path = foo_path.join(expected.path);
750 let actual_data = read(actual_path).unwrap();
751 let expected_data = expected.data.to_owned().into_bytes();
752 assert_eq!(actual_data, expected_data);
753 }
754 }
755
756 #[test_case(Input{source: "foo.txt", destination: "wrong_moniker/foo/bar::/data/foo.txt"}; "bad_moniker")]
757 #[test_case(Input{source: "foo.txt", destination: "/foo/bar:://bar/foo.txt"}; "bad_directory")]
758 #[fuchsia::test]
759 async fn copy_local_to_device_fails(input: Input) {
760 let local_dir = create_tmp_dir(vec![]).unwrap();
761 let local_path = local_dir.path();
762
763 let source_path = local_path.join(input.source);
764 write(&source_path, "Hello".to_owned().into_bytes()).unwrap();
765
766 let (realm_query, _, _) =
767 create_realm_query_simple(generate_directory_paths(vec!["data"]), vec![]);
768
769 let result = copy_cmd(
770 &realm_query,
771 vec![source_path.display().to_string(), input.destination.to_string()],
772 false,
773 std::io::stdout(),
774 )
775 .await;
776
777 assert!(result.is_err());
778 }
779
780 #[test_case(vec![]; "no_wildcard_matches")]
781 #[test_case(vec!["foo.txt"]; "not_enough_args")]
782 #[test_case(vec!["/foo/bar::/data/*", "/foo/bar::/data/*"]; "remote_wildcard_destination")]
783 #[test_case(vec!["/foo/bar::/data/*", "/foo/bar::/data/*", "/"]; "multi_wildcards_remote")]
784 #[test_case(vec!["*", "*"]; "local_wildcard_destination")]
785 #[fuchsia::test]
786 async fn copy_wildcard_fails(paths: Vec<&str>) {
787 let (realm_query, _, _) = create_realm_query_simple(
788 generate_file_paths(vec![File { name: "data/foo.txt", data: "Hello" }]),
789 vec![],
790 );
791 let paths = paths.into_iter().map(|s| s.to_string()).collect();
792 let result = copy_cmd(&realm_query, paths, false, std::io::stdout()).await;
793
794 assert!(result.is_err());
795 }
796
797 #[test_case(Inputs{sources: vec!["/foo/bar::/data/foo.txt", "bar.txt"], destination: "/bar/foo::/data/"},
798 generate_file_paths(vec![File{ name: "bar.txt", data: "World"}]),
799 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/bar.txt", data: "World"}]),
800 generate_directory_paths(vec!["data"]),
801 vec![Expectation{path: "data/foo.txt", data: "Hello"}, Expectation{path: "data/bar.txt", data: "World"}]; "no_wildcard_mix")]
802 #[test_case(Inputs{sources: vec!["/foo/bar::/data/foo.txt", "/foo/bar::/data/*", "foobar.txt"], destination: "/bar/foo::/data/"},
803 generate_file_paths(vec![File{ name: "foobar.txt", data: "World"}]),
804 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/bar.txt", data: "World"}]),
805 generate_directory_paths(vec!["data"]),
806 vec![Expectation{path: "data/foo.txt", data: "Hello"}, Expectation{path: "data/bar.txt", data: "World"}, Expectation{path: "data/foobar.txt", data: "World"}]; "wildcard_mix")]
807 #[test_case(Inputs{sources: vec!["/foo/bar::/data/*", "/foo/bar::/data/*", "foobar.txt"], destination: "/bar/foo::/data/"},
808 generate_file_paths(vec![File{ name: "foobar.txt", data: "World"}]),
809 generate_file_paths(vec![File{ name: "data/foo.txt", data: "Hello"}, File{ name: "data/bar.txt", data: "World"}]),
810 generate_directory_paths(vec!["data"]),
811 vec![Expectation{path: "data/foo.txt", data: "Hello"}, Expectation{path: "data/bar.txt", data: "World"}, Expectation{path: "data/foobar.txt", data: "World"}]; "double_wildcard")]
812 #[fuchsia::test]
813 async fn copy_mixed_tests_remote_destination(
814 input: Inputs,
815 local_files: Vec<SeedPath>,
816 foo_files: Vec<SeedPath>,
817 bar_files: Vec<SeedPath>,
818 expectation: Vec<Expectation>,
819 ) {
820 let local_dir = create_tmp_dir(local_files).unwrap();
821 let local_path = local_dir.path();
822
823 let (realm_query, _, bar_path) = create_realm_query_simple(foo_files, bar_files);
824 let mut paths: Vec<String> = input
825 .sources
826 .clone()
827 .into_iter()
828 .map(|path| match LocalOrRemoteDirectoryPath::parse(&path) {
829 LocalOrRemoteDirectoryPath::Remote(_) => path.to_string(),
830 LocalOrRemoteDirectoryPath::Local(_) => local_path.join(path).display().to_string(),
831 })
832 .collect();
833 paths.push(input.destination.to_owned());
834
835 copy_cmd(&realm_query, paths, false, std::io::stdout()).await.unwrap();
836
837 for expected in expectation {
838 let actual_path = bar_path.join(expected.path);
839 let actual_data = read(actual_path).unwrap();
840 let expected_data = expected.data.to_owned().into_bytes();
841 assert_eq!(actual_data, expected_data);
842 }
843 }
844}