1use crate::watcher::WatchError;
2
3#[derive(Debug, Clone)]
4pub struct WatchTarget {
5 pub protocol: Protocol,
6 pub host: Option<String>,
7 pub port: Option<u16>,
8 pub user: Option<String>,
9 pub path: String,
10}
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum Protocol {
14 File,
15 Ssh,
16 Sftp,
17 Scp,
18 Ftp,
19 Http,
20 Https,
21}
22
23pub fn parse(url: &str) -> Result<WatchTarget, WatchError> {
24 if let Some(rest) = url.strip_prefix("file://") {
25 return Ok(WatchTarget {
26 protocol: Protocol::File,
27 host: None,
28 port: None,
29 user: None,
30 path: rest.to_string(),
31 });
32 }
33
34 if let Some((scheme, rest)) = url.split_once("://") {
35 let protocol = match scheme {
36 "ssh" => Protocol::Ssh,
37 "sftp" => Protocol::Sftp,
38 "scp" => Protocol::Scp,
39 "ftp" => Protocol::Ftp,
40 "http" => Protocol::Http,
41 "https" => Protocol::Https,
42 other => return Err(WatchError::UnsupportedProtocol(other.to_string())),
43 };
44
45 let (authority, path) = match rest.find('/') {
46 Some(idx) => (&rest[..idx], &rest[idx..]),
47 None => (rest, "/"),
48 };
49
50 let (user_part, host_part) = match authority.find('@') {
51 Some(idx) => (Some(&authority[..idx]), &authority[idx + 1..]),
52 None => (None, authority),
53 };
54
55 let (host, port) = match host_part.find(':') {
56 Some(idx) => {
57 let port_str = &host_part[idx + 1..];
58 match port_str.parse::<u16>() {
59 Ok(p) => (Some(host_part[..idx].to_string()), Some(p)),
60 Err(_) => (Some(host_part.to_string()), None),
61 }
62 }
63 None => (Some(host_part.to_string()), None),
64 };
65
66 return Ok(WatchTarget {
67 protocol,
68 host,
69 port,
70 user: user_part.map(|s| s.to_string()),
71 path: path.to_string(),
72 });
73 }
74
75 Ok(WatchTarget {
76 protocol: Protocol::File,
77 host: None,
78 port: None,
79 user: None,
80 path: url.to_string(),
81 })
82}
83
84impl std::fmt::Display for WatchTarget {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 match self.protocol {
87 Protocol::File => write!(f, "file://{}", self.path),
88 _ => {
89 let scheme = match self.protocol {
90 Protocol::Ssh => "ssh",
91 Protocol::Sftp => "sftp",
92 Protocol::Scp => "scp",
93 Protocol::Ftp => "ftp",
94 Protocol::Http => "http",
95 Protocol::Https => "https",
96 Protocol::File => unreachable!(),
97 };
98 write!(f, "{scheme}://")?;
99 if let Some(ref user) = self.user {
100 write!(f, "{user}@")?;
101 }
102 if let Some(ref host) = self.host {
103 write!(f, "{host}")?;
104 }
105 if let Some(port) = self.port {
106 write!(f, ":{port}")?;
107 }
108 write!(f, "{}", self.path)
109 }
110 }
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn parse_bare_path() {
120 let target = parse("./saves").unwrap();
121 assert_eq!(target.protocol, Protocol::File);
122 assert_eq!(target.path, "./saves");
123 assert!(target.host.is_none());
124 }
125
126 #[test]
127 fn parse_file_url() {
128 let target = parse("file:///home/user/saves").unwrap();
129 assert_eq!(target.protocol, Protocol::File);
130 assert_eq!(target.path, "/home/user/saves");
131 }
132
133 #[test]
134 fn parse_ssh_url() {
135 let target = parse("ssh://user@switch:22/saves").unwrap();
136 assert_eq!(target.protocol, Protocol::Ssh);
137 assert_eq!(target.user.as_deref(), Some("user"));
138 assert_eq!(target.host.as_deref(), Some("switch"));
139 assert_eq!(target.port, Some(22));
140 assert_eq!(target.path, "/saves");
141 }
142
143 #[test]
144 fn parse_sftp_url_no_port() {
145 let target = parse("sftp://admin@mydevice/data/saves").unwrap();
146 assert_eq!(target.protocol, Protocol::Sftp);
147 assert_eq!(target.user.as_deref(), Some("admin"));
148 assert_eq!(target.host.as_deref(), Some("mydevice"));
149 assert!(target.port.is_none());
150 assert_eq!(target.path, "/data/saves");
151 }
152
153 #[test]
154 fn parse_ftp_url() {
155 let target = parse("ftp://anbernic/roms/saves").unwrap();
156 assert_eq!(target.protocol, Protocol::Ftp);
157 assert!(target.user.is_none());
158 assert_eq!(target.host.as_deref(), Some("anbernic"));
159 assert_eq!(target.path, "/roms/saves");
160 }
161
162 #[test]
163 fn parse_http_url() {
164 let target = parse("http://device:8080/saves").unwrap();
165 assert_eq!(target.protocol, Protocol::Http);
166 assert_eq!(target.host.as_deref(), Some("device"));
167 assert_eq!(target.port, Some(8080));
168 }
169
170 #[test]
171 fn roundtrip_display() {
172 let target = parse("ssh://user@switch:22/saves").unwrap();
173 assert_eq!(target.to_string(), "ssh://user@switch:22/saves");
174 }
175
176 #[test]
177 fn unsupported_protocol() {
178 let result = parse("gopher://host/path");
179 assert!(result.is_err());
180 }
181}