arcbox_helper/validate/
cli_target.rs1use std::path::Component;
2use std::str::FromStr;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct CliTarget(String);
12
13impl CliTarget {
14 pub fn as_str(&self) -> &str {
15 &self.0
16 }
17}
18
19impl FromStr for CliTarget {
20 type Err = String;
21
22 fn from_str(s: &str) -> Result<Self, Self::Err> {
23 let path = std::path::Path::new(s);
24
25 if !path.is_absolute() {
26 return Err(format!("CLI target '{s}' must be an absolute path"));
27 }
28
29 if path.components().any(|c| matches!(c, Component::ParentDir)) {
30 return Err(format!("CLI target '{s}' contains '..' path traversal"));
31 }
32
33 let components: Vec<_> = path.components().collect();
34 let has_valid_app_structure = components.windows(4).any(|w| {
35 matches!(&w[0], Component::Normal(name) if name.to_string_lossy().ends_with(".app"))
36 && w[1] == Component::Normal("Contents".as_ref())
37 && w[2] == Component::Normal("MacOS".as_ref())
38 && w[3] == Component::Normal("xbin".as_ref())
39 });
40
41 if !has_valid_app_structure {
42 return Err(format!(
43 "CLI target '{s}' must be inside an .app bundle's Contents/MacOS/xbin/"
44 ));
45 }
46
47 if !s.starts_with("/Applications/") && !s.starts_with("/Users/") {
48 return Err(format!(
49 "CLI target '{s}' must be under /Applications/ or /Users/"
50 ));
51 }
52
53 Ok(Self(s.to_owned()))
54 }
55}
56
57impl std::fmt::Display for CliTarget {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 f.write_str(&self.0)
60 }
61}