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    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        // Show that the copy command will respect an input that specifies
414        // a directory other than the namespace.
415        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            /*verbose=*/ 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            /*verbose=*/ 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            /*verbose=*/ 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            /*verbose=*/ 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            /*verbose=*/ 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            /*verbose=*/ 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            /*verbose=*/ 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, /*verbose=*/ 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            /*verbose=*/ 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, /*verbose=*/ 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, /*verbose=*/ 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}