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