component_debug/
path.rs

1// Copyright 2023 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};
6use anyhow::{anyhow, bail, Result};
7use std::path::{Component, PathBuf};
8use std::str::FromStr;
9use thiserror::Error;
10
11use flex_fuchsia_sys2 as fsys;
12
13/// Separator for user input for parsing command line arguments to structs
14/// in this crate.
15const REMOTE_PATH_SEPARATOR: &'static str = "::";
16
17pub const REMOTE_COMPONENT_STORAGE_PATH_HELP: &'static str = r#"Remote storage paths allow the following formats:
181)  [instance ID]::[path relative to storage]
19    Example: "c1a6d0aebbf7c092c53e8e696636af8ec0629ff39b7f2e548430b0034d809da4::/path/to/file"
20
21    `..` is not valid anywhere in the remote path.
22
23    To learn about component instance IDs, see https://fuchsia.dev/go/components/instance-id"#;
24
25pub const REMOTE_DIRECTORY_PATH_HELP: &'static str = r#"Remote directory paths must be:
261)  [moniker]::[path in namespace]
27    Example: /foo/bar::/config/data/sample.json
28
29    To learn more about monikers, see https://fuchsia.dev/go/components/moniker#absolute
30
312)  [moniker]::[dir type]::[path] where [dir type] is one of "in", "out", or "pkg", specifying
32    the component's namespace directory, outgoing directory, or package directory (if packaged).
33"#;
34
35#[derive(Clone, Debug, PartialEq)]
36pub struct RemoteDirectoryPath {
37    pub moniker: String,
38    pub dir_type: fsys::OpenDirType,
39    pub relative_path: PathBuf,
40}
41
42#[derive(Clone, Debug, PartialEq)]
43pub struct RemoteComponentStoragePath {
44    pub instance_id: String,
45    pub relative_path: PathBuf,
46}
47
48#[derive(Error, Debug, PartialEq)]
49pub enum ParsePathError {
50    #[error("Unsupported directory type: {dir_type}. {}", REMOTE_DIRECTORY_PATH_HELP)]
51    UnsupportedDirectory { dir_type: String },
52
53    #[error("Disallowed path component: {component}. {}", REMOTE_DIRECTORY_PATH_HELP)]
54    DisallowedPathComponent { component: String },
55
56    #[error("Malformatted remote directory path. {}", REMOTE_DIRECTORY_PATH_HELP)]
57    InvalidFormat,
58}
59
60impl FromStr for RemoteDirectoryPath {
61    type Err = ParsePathError;
62
63    fn from_str(input: &str) -> Result<Self, Self::Err> {
64        let parts: Vec<&str> = input.split(REMOTE_PATH_SEPARATOR).collect();
65        if parts.len() < 2 || parts.len() > 3 {
66            return Err(ParsePathError::InvalidFormat);
67        }
68
69        // TODO(https://fxbug.dev/42077346): Use common Moniker parsing logic instead of String.
70        let moniker = parts.first().unwrap().to_string();
71        let (dir_type, path_str) = if parts.len() == 3 {
72            let parsed = parse_dir_type_from_str(parts[1])?;
73            (parsed, parts[2])
74        } else {
75            (fsys::OpenDirType::NamespaceDir, parts[1])
76        };
77        let path = PathBuf::from(path_str);
78
79        // Perform checks on path that ignore `.`  and disallow `..`, `/` or Windows path prefixes such as C: or \\
80        let mut normalized_path = PathBuf::new();
81        for component in path.components() {
82            match component {
83                Component::Normal(c) => normalized_path.push(c),
84                Component::RootDir => continue,
85                Component::CurDir => continue,
86                c => {
87                    return Err(ParsePathError::DisallowedPathComponent {
88                        component: format!("{:?}", c),
89                    })
90                }
91            }
92        }
93
94        Ok(Self { moniker, dir_type, relative_path: normalized_path })
95    }
96}
97
98fn parse_dir_type_from_str(s: &str) -> Result<fsys::OpenDirType, ParsePathError> {
99    // Only match on either the namespace (in), outgoing (out) or package (pkg) directories.
100    // The parser could be expected to support others, if the need arises.
101    match s {
102        "in" | "namespace" => Ok(fsys::OpenDirType::NamespaceDir),
103        "out" => Ok(fsys::OpenDirType::OutgoingDir),
104        "pkg" => Ok(fsys::OpenDirType::PackageDir),
105        _ => Err(ParsePathError::UnsupportedDirectory { dir_type: s.into() }),
106    }
107}
108
109fn dir_type_to_str(dir_type: &fsys::OpenDirType) -> Result<&str> {
110    // Only match on either the namespace (in), outgoing (out) or package (pkg) directories.
111    // The parser could be expected to support others, if the need arises.
112    match dir_type {
113        fsys::OpenDirType::NamespaceDir => Ok("in"),
114        fsys::OpenDirType::OutgoingDir => Ok("out"),
115        fsys::OpenDirType::PackageDir => Ok("pkg"),
116        _ => Err(anyhow!("Unsupported OpenDirType: {:?}", dir_type)),
117    }
118}
119
120/// Represents a path to a file/directory within a component's storage.
121impl RemoteComponentStoragePath {
122    pub fn parse(input: &str) -> Result<Self> {
123        match input.split_once(REMOTE_PATH_SEPARATOR) {
124            Some((first, second)) => {
125                if second.contains(REMOTE_PATH_SEPARATOR) {
126                    bail!(
127                        "Remote storage path must contain exactly one `{}` separator. {}",
128                        REMOTE_PATH_SEPARATOR,
129                        REMOTE_COMPONENT_STORAGE_PATH_HELP
130                    )
131                }
132
133                let instance_id = first.to_string();
134                let relative_path = PathBuf::from(second);
135
136                // Perform checks on path that ignore `.`  and disallow `..`, `/` or Windows path prefixes such as C: or \\
137                let mut normalized_relative_path = PathBuf::new();
138                for component in relative_path.components() {
139                    match component {
140                        Component::Normal(c) => normalized_relative_path.push(c),
141                        Component::RootDir => continue,
142                        Component::CurDir => continue,
143                        c => bail!(
144                            "Unsupported path component: {:?}. {}",
145                            c,
146                            REMOTE_COMPONENT_STORAGE_PATH_HELP
147                        ),
148                    }
149                }
150
151                Ok(Self { instance_id, relative_path: normalized_relative_path })
152            }
153            None => {
154                bail!(
155                    "Remote storage path must contain exactly one `{}` separator. {}",
156                    REMOTE_PATH_SEPARATOR,
157                    REMOTE_COMPONENT_STORAGE_PATH_HELP
158                )
159            }
160        }
161    }
162
163    pub fn contains_wildcard(&self) -> bool {
164        return self.to_string().contains("*");
165    }
166
167    pub fn relative_path_string(&self) -> String {
168        return self.relative_path.to_string_lossy().to_string();
169    }
170}
171
172impl ToString for RemoteComponentStoragePath {
173    fn to_string(&self) -> String {
174        format!(
175            "{}{sep}/{}",
176            self.instance_id,
177            self.relative_path.to_string_lossy(),
178            sep = REMOTE_PATH_SEPARATOR
179        )
180    }
181}
182
183impl ToString for RemoteDirectoryPath {
184    fn to_string(&self) -> String {
185        format!(
186            "{}{sep}{}{sep}/{}",
187            self.moniker,
188            dir_type_to_str(&self.dir_type).unwrap(),
189            self.relative_path.to_string_lossy(),
190            sep = REMOTE_PATH_SEPARATOR
191        )
192    }
193}
194
195#[derive(Clone)]
196/// Represents either a local path to a file or directory, or the path to a file/directory
197/// in a directory associated with a remote component.
198pub enum LocalOrRemoteDirectoryPath {
199    Local(PathBuf),
200    Remote(RemoteDirectoryPath),
201}
202
203impl LocalOrRemoteDirectoryPath {
204    pub fn parse(path: &str) -> LocalOrRemoteDirectoryPath {
205        match RemoteDirectoryPath::from_str(path) {
206            Ok(path) => LocalOrRemoteDirectoryPath::Remote(path),
207            // If we can't parse a remote path, then it is a host path.
208            Err(_) => LocalOrRemoteDirectoryPath::Local(PathBuf::from(path)),
209        }
210    }
211}
212
213#[derive(Clone)]
214/// Represents either a local path to a file or directory, or the path to a file/directory
215/// in a directory associated with a remote component.
216pub enum LocalOrRemoteComponentStoragePath {
217    Local(PathBuf),
218    Remote(RemoteComponentStoragePath),
219}
220
221impl LocalOrRemoteComponentStoragePath {
222    pub fn parse(path: &str) -> LocalOrRemoteComponentStoragePath {
223        match RemoteComponentStoragePath::parse(path) {
224            Ok(path) => LocalOrRemoteComponentStoragePath::Remote(path),
225            // If we can't parse a remote path, then it is a host path.
226            Err(_) => LocalOrRemoteComponentStoragePath::Local(PathBuf::from(path)),
227        }
228    }
229}
230
231/// Returns a readable `Directory` by opening the parent dir of `path`.
232///
233/// * `path`: The path from which to derive the parent
234/// * `dir`: RemoteDirectory to on which to open a subdir.
235pub fn open_parent_subdir_readable<D: Directory>(path: &PathBuf, dir: &D) -> Result<D> {
236    if path.components().count() < 2 {
237        // The path is something like "/foo" which, as a relative path from `dir`, would make `dir`
238        // the parent.
239        return dir.clone();
240    }
241
242    dir.open_dir_readonly(path.parent().unwrap())
243}
244
245/// If `destination_path` in `destination_dir` is itself a directory, returns
246/// a path with the filename portion of `source_path` appended. Otherwise, returns
247/// a copy of the input `destination_path`.
248///
249/// The purpose of this function is to help infer a path in cases which an ending file name for the destination path is not provided.
250/// For example, the command "ffx component storage copy ~/alarm.wav [instance-id]::/" does not know what name to give the new file copied.
251/// [instance-id]::/. Thus it is necessary to infer this new file name and generate the new path "[instance-id]::/alarm.wav".
252///
253/// # Arguments
254///
255/// * `destination_dir`: Directory to query for the type of `destination_path`
256/// * `source_path`: path from which to read a filename, if needed
257/// * `destination_path`: destination path
258///
259/// # Error Conditions:
260///
261/// * File name for `source_path` is empty
262/// * Communications error talking to remote endpoint
263pub async fn add_source_filename_to_path_if_absent<D: Directory>(
264    destination_dir: &D,
265    source_path: &PathBuf,
266    destination_path: &PathBuf,
267) -> Result<PathBuf> {
268    let source_file = source_path
269        .file_name()
270        .map_or_else(|| Err(anyhow!("Source path is empty")), |file| Ok(PathBuf::from(file)))?;
271    let source_file_str = source_file.display().to_string();
272
273    // If the destination is a directory, append `source_file_str`.
274    if let Some(destination_file) = destination_path.file_name() {
275        let parent_dir = open_parent_subdir_readable(destination_path, destination_dir)?;
276        match parent_dir.entry_type(destination_file.to_string_lossy().as_ref()).await? {
277            Some(DirentKind::File) | None => Ok(destination_path.clone()),
278            Some(DirentKind::Directory) => Ok(destination_path.join(source_file_str)),
279        }
280    } else {
281        Ok(destination_path.join(source_file_str))
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use crate::path::{dir_type_to_str, parse_dir_type_from_str, RemoteDirectoryPath};
288    use flex_fuchsia_sys2 as fsys;
289    use std::str::FromStr;
290
291    #[test]
292    fn test_parse_dir_type_from_str() {
293        assert_eq!(parse_dir_type_from_str("in"), Ok(fsys::OpenDirType::NamespaceDir));
294        assert_eq!(parse_dir_type_from_str("namespace"), Ok(fsys::OpenDirType::NamespaceDir));
295        assert_eq!(parse_dir_type_from_str("out"), Ok(fsys::OpenDirType::OutgoingDir));
296        assert_eq!(parse_dir_type_from_str("pkg"), Ok(fsys::OpenDirType::PackageDir));
297        assert!(parse_dir_type_from_str("nonexistent").is_err());
298    }
299
300    #[test]
301    fn test_dir_type_to_str() {
302        assert_eq!(dir_type_to_str(&fsys::OpenDirType::NamespaceDir).unwrap(), "in");
303        assert_eq!(dir_type_to_str(&fsys::OpenDirType::OutgoingDir).unwrap(), "out");
304        assert_eq!(dir_type_to_str(&fsys::OpenDirType::PackageDir).unwrap(), "pkg");
305        assert!(dir_type_to_str(&fsys::OpenDirType::RuntimeDir).is_err());
306        assert!(dir_type_to_str(&fsys::OpenDirType::ExposedDir).is_err());
307    }
308
309    #[test]
310    fn test_remote_directory_path_from_str() {
311        assert_eq!(
312            RemoteDirectoryPath::from_str("/foo/bar::/path"),
313            Ok(RemoteDirectoryPath {
314                moniker: "/foo/bar".into(),
315                dir_type: fsys::OpenDirType::NamespaceDir,
316                relative_path: "path".into(),
317            })
318        );
319
320        assert_eq!(
321            RemoteDirectoryPath::from_str("/foo/bar::out::/path"),
322            Ok(RemoteDirectoryPath {
323                moniker: "/foo/bar".into(),
324                dir_type: fsys::OpenDirType::OutgoingDir,
325                relative_path: "path".into(),
326            })
327        );
328
329        assert_eq!(
330            RemoteDirectoryPath::from_str("/foo/bar::pkg::/path"),
331            Ok(RemoteDirectoryPath {
332                moniker: "/foo/bar".into(),
333                dir_type: fsys::OpenDirType::PackageDir,
334                relative_path: "path".into(),
335            })
336        );
337
338        assert!(RemoteDirectoryPath::from_str("/foo/bar").is_err());
339        assert!(RemoteDirectoryPath::from_str("/foo/bar::one::two::three").is_err());
340        assert!(RemoteDirectoryPath::from_str("/foo/bar::not_a_dir::three").is_err());
341    }
342}