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