arcbox_helper/validate/
socket_target.rs1use std::path::Component;
2use std::str::FromStr;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct SocketTarget(String);
12
13impl SocketTarget {
14 pub fn as_str(&self) -> &str {
15 &self.0
16 }
17}
18
19impl FromStr for SocketTarget {
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 let rest = s
26 .strip_prefix("/Users/")
27 .ok_or_else(|| format!("socket target '{s}' must be under /Users/"))?;
28
29 let (username, after_user) = rest
30 .split_once('/')
31 .ok_or_else(|| format!("socket target '{s}' has no path after username"))?;
32
33 if username.is_empty() {
34 return Err(format!("socket target '{s}' has empty username"));
35 }
36
37 if !after_user.starts_with(".arcbox/") && !after_user.starts_with(".arcbox-dev/") {
38 return Err(format!(
39 "socket target '{s}' must be under ~/.arcbox/ or ~/.arcbox-dev/"
40 ));
41 }
42
43 #[allow(clippy::case_sensitive_file_extension_comparisons)]
44 if !s.ends_with(".sock") {
45 return Err(format!("socket target '{s}' must end with .sock"));
46 }
47
48 if path.components().any(|c| matches!(c, Component::ParentDir)) {
49 return Err(format!(
50 "socket target '{s}' contains '..' path traversal component"
51 ));
52 }
53
54 Ok(Self(s.to_owned()))
55 }
56}
57
58impl std::fmt::Display for SocketTarget {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 f.write_str(&self.0)
61 }
62}