Skip to main content

component_debug/
copy.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use 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
49/// Transfer files between a directories associated with a component to/from the local filesystem.
50///
51/// # Arguments
52/// * `realm_query`: |RealmQueryProxy| to open the component directories.
53/// * `paths`: The local and remote paths to copy. The last entry is the destination.
54/// * `verbose`: Flag used to indicate whether or not to print output to console.
55pub 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    // paths is safe to unwrap as validate_paths ensures that it is non-empty.
64    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            // TODO(https://fxbug.dev/42067334): add recursive copy support.
201            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
229/// If `path` contains a wildcard, returns the expanded list of files. Otherwise,
230/// returns a list with a single entry.
231///
232/// # Arguments
233///
234/// * `path`: A path that may contain a wildcard.
235/// * `dir`: Directory proxy to query to expand wildcards.
236async 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("*", ".*"); // Regex syntax requires a . before wildcard.
249
250    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
267/// Checks that the paths meet the following conditions:
268///
269/// * Destination path does not contain a wildcard.
270/// * At least two path arguments are provided.
271///
272/// # Arguments
273///
274/// *`paths`: list of filepaths to be processed.
275fn 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
285/// Retrieves the directory proxy for one of a component's associated directories.
286/// # Arguments
287/// * `realm_query`: |RealmQueryProxy| to retrieve a component instance.
288/// * `moniker`: Absolute moniker of a component instance.
289/// * `dir_type`: The type of directory (namespace, outgoing, ...)
290async 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
308// Retrieves all entries within a remote directory containing a file pattern.
309///
310/// # Arguments
311/// * `dir`: A directory.
312/// * `file_pattern`: A file pattern to match.
313async 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    // We can call from_utf8_unchecked as the file arrays only contain the character 'a' which is safe to unwrap.
344    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        // Show that the copy command will respect an input that specifies
410        // a directory other than the namespace.
411        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            /*verbose=*/ 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            /*verbose=*/ 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            /*verbose=*/ 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            /*verbose=*/ 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            /*verbose=*/ 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            /*verbose=*/ 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            /*verbose=*/ 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, /*verbose=*/ 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            /*verbose=*/ 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, /*verbose=*/ 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, /*verbose=*/ 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}