Skip to main content

watch_path/
url.rs

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}