1use 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
13const 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 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 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 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 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
120impl 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 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)]
196pub 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 Err(_) => LocalOrRemoteDirectoryPath::Local(PathBuf::from(path)),
209 }
210 }
211}
212
213#[derive(Clone)]
214pub 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 Err(_) => LocalOrRemoteComponentStoragePath::Local(PathBuf::from(path)),
227 }
228 }
229}
230
231pub fn open_parent_subdir_readable<D: Directory>(path: &PathBuf, dir: &D) -> Result<D> {
236 if path.components().count() < 2 {
237 return dir.clone();
240 }
241
242 dir.open_dir_readonly(path.parent().unwrap())
243}
244
245pub 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 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}