1use std::fmt;
24use std::future::Future;
25use std::path::{Path, PathBuf};
26use std::pin::Pin;
27
28use crate::PickedFile;
29
30pub trait Opener {
32 fn open_link(&self, link: &str) -> OpenFuture;
35
36 fn open_file(&self, path: &Path) -> OpenFuture;
38
39 fn reveal_file(&self, path: &Path) -> OpenFuture;
41
42 fn can_open_link(&self, link: &str) -> Option<bool>;
45}
46
47pub type OpenFuture = Pin<Box<dyn Future<Output = Result<(), OpenError>>>>;
49
50#[derive(Clone, Debug, PartialEq, Eq)]
52pub enum OpenError {
53 NoApp,
55 Failed(String),
57}
58
59impl fmt::Display for OpenError {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 match self {
62 OpenError::NoApp => f.write_str("no app opens it"),
63 OpenError::Failed(reason) => write!(f, "it did not open: {reason}"),
64 }
65 }
66}
67
68impl std::error::Error for OpenError {}
69
70#[derive(Debug, Default)]
72pub struct OpenRequest {
73 pub files: Vec<PickedFile>,
74 pub links: Vec<String>,
75}
76
77impl OpenRequest {
78 pub fn is_empty(&self) -> bool {
79 self.files.is_empty() && self.links.is_empty()
80 }
81}
82
83pub(crate) fn file_uri(path: &Path) -> String {
87 let path = std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf());
88 let written = uri_path_of(&path);
89 let encoded = percent_encoded(&written);
90 if let Some(share) = encoded.strip_prefix("//") {
91 format!("file://{share}")
92 } else if encoded.starts_with('/') {
93 format!("file://{encoded}")
94 } else {
95 format!("file:///{encoded}")
96 }
97}
98
99#[cfg(unix)]
101fn uri_path_of(path: &Path) -> Vec<u8> {
102 use std::os::unix::ffi::OsStrExt;
103 path.as_os_str().as_bytes().to_vec()
104}
105
106#[cfg(not(unix))]
108fn uri_path_of(path: &Path) -> Vec<u8> {
109 path.to_string_lossy().replace('\\', "/").into_bytes()
110}
111
112fn percent_encoded(bytes: &[u8]) -> String {
115 let mut encoded = String::with_capacity(bytes.len());
116 for &byte in bytes {
117 if byte.is_ascii_alphanumeric() || b"-._~!$&'()*+,;=:@/".contains(&byte) {
118 encoded.push(char::from(byte));
119 } else {
120 encoded.push_str(&format!("%{byte:02X}"));
121 }
122 }
123 encoded
124}
125
126pub fn file_uri_path(uri: &str) -> Option<PathBuf> {
129 let rest = uri.strip_prefix("file://")?;
130 let path = match rest.find('/')? {
131 0 => rest,
132 host_end if &rest[..host_end] == "localhost" => &rest[host_end..],
133 _ => return None,
134 };
135 let mut bytes = Vec::with_capacity(path.len());
136 let mut encoded = path.bytes();
137 while let Some(byte) = encoded.next() {
138 if byte == b'%' {
139 let high = encoded.next().and_then(hex_digit)?;
140 let low = encoded.next().and_then(hex_digit)?;
141 bytes.push(high << 4 | low);
142 } else {
143 bytes.push(byte);
144 }
145 }
146 path_from_bytes(bytes)
147}
148
149fn hex_digit(digit: u8) -> Option<u8> {
150 char::from(digit).to_digit(16).map(|value| value as u8)
151}
152
153#[cfg(unix)]
156fn path_from_bytes(bytes: Vec<u8>) -> Option<PathBuf> {
157 use std::ffi::OsString;
158 use std::os::unix::ffi::OsStringExt;
159 Some(PathBuf::from(OsString::from_vec(bytes)))
160}
161
162#[cfg(not(unix))]
163fn path_from_bytes(bytes: Vec<u8>) -> Option<PathBuf> {
164 let text = String::from_utf8(bytes).ok()?;
165 let has_drive = text.len() > 2
166 && text.as_bytes()[0] == b'/'
167 && text.as_bytes()[1].is_ascii_alphabetic()
168 && text.as_bytes()[2] == b':';
169 let text = if has_drive { &text[1..] } else { &text[..] };
170 Some(PathBuf::from(text.replace('/', "\\")))
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 #[cfg(unix)]
178 #[cfg(unix)]
179 #[test]
180 fn a_path_is_a_file_uri_with_what_a_uri_cannot_hold_encoded_and_reads_back() {
181 let path = Path::new("/Movies/a b/été #1?.mp4");
182 let uri = file_uri(path);
183 assert_eq!(uri, "file:///Movies/a%20b/%C3%A9t%C3%A9%20%231%3F.mp4");
184 assert_eq!(file_uri_path(&uri).as_deref(), Some(path));
185 let relative = file_uri(Path::new("clip.mov"));
186 assert!(relative.starts_with("file:///") && relative.ends_with("/clip.mov"));
187 }
188
189 #[cfg(windows)]
190 #[test]
191 fn a_windows_path_is_a_file_uri_with_its_drive_or_its_share() {
192 assert_eq!(
193 file_uri(Path::new(r"C:\Movies\a b.mp4")),
194 "file:///C:/Movies/a%20b.mp4"
195 );
196 assert_eq!(
197 file_uri(Path::new(r"\\server\share\a.mp4")),
198 "file://server/share/a.mp4"
199 );
200 }
201
202 #[cfg(unix)]
203 #[test]
204 fn a_file_uri_names_its_path_decoded() {
205 assert_eq!(
206 file_uri_path("file:///home/me/My%20Notes/caf%C3%A9.txt"),
207 Some(PathBuf::from("/home/me/My Notes/café.txt"))
208 );
209 assert_eq!(
210 file_uri_path("file://localhost/tmp/a"),
211 Some(PathBuf::from("/tmp/a"))
212 );
213 assert_eq!(file_uri_path("file://elsewhere/tmp/a"), None);
214 assert_eq!(file_uri_path("https://example.org/a"), None);
215 assert_eq!(file_uri_path("file:///bad%2"), None);
216 }
217}