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