use std::fmt;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use crate::PickedFile;
pub trait Opener {
fn open_link(&self, link: &str) -> OpenFuture;
fn open_file(&self, path: &Path) -> OpenFuture;
fn reveal_file(&self, path: &Path) -> OpenFuture;
fn can_open_link(&self, link: &str) -> Option<bool>;
}
pub type OpenFuture = Pin<Box<dyn Future<Output = Result<(), OpenError>>>>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum OpenError {
NoApp,
Failed(String),
}
impl fmt::Display for OpenError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
OpenError::NoApp => f.write_str("no app opens it"),
OpenError::Failed(reason) => write!(f, "it did not open: {reason}"),
}
}
}
impl std::error::Error for OpenError {}
#[derive(Debug, Default)]
pub struct OpenRequest {
pub files: Vec<PickedFile>,
pub links: Vec<String>,
}
impl OpenRequest {
pub fn is_empty(&self) -> bool {
self.files.is_empty() && self.links.is_empty()
}
}
pub(crate) fn file_uri(path: &Path) -> String {
let path = std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf());
let written = uri_path_of(&path);
let encoded = percent_encoded(&written);
if let Some(share) = encoded.strip_prefix("//") {
format!("file://{share}")
} else if encoded.starts_with('/') {
format!("file://{encoded}")
} else {
format!("file:///{encoded}")
}
}
#[cfg(unix)]
fn uri_path_of(path: &Path) -> Vec<u8> {
use std::os::unix::ffi::OsStrExt;
path.as_os_str().as_bytes().to_vec()
}
#[cfg(not(unix))]
fn uri_path_of(path: &Path) -> Vec<u8> {
path.to_string_lossy().replace('\\', "/").into_bytes()
}
fn percent_encoded(bytes: &[u8]) -> String {
let mut encoded = String::with_capacity(bytes.len());
for &byte in bytes {
if byte.is_ascii_alphanumeric() || b"-._~!$&'()*+,;=:@/".contains(&byte) {
encoded.push(char::from(byte));
} else {
encoded.push_str(&format!("%{byte:02X}"));
}
}
encoded
}
pub fn file_uri_path(uri: &str) -> Option<PathBuf> {
let rest = uri.strip_prefix("file://")?;
let path = match rest.find('/')? {
0 => rest,
host_end if &rest[..host_end] == "localhost" => &rest[host_end..],
_ => return None,
};
let mut bytes = Vec::with_capacity(path.len());
let mut encoded = path.bytes();
while let Some(byte) = encoded.next() {
if byte == b'%' {
let high = encoded.next().and_then(hex_digit)?;
let low = encoded.next().and_then(hex_digit)?;
bytes.push(high << 4 | low);
} else {
bytes.push(byte);
}
}
path_from_bytes(bytes)
}
fn hex_digit(digit: u8) -> Option<u8> {
char::from(digit).to_digit(16).map(|value| value as u8)
}
#[cfg(unix)]
fn path_from_bytes(bytes: Vec<u8>) -> Option<PathBuf> {
use std::ffi::OsString;
use std::os::unix::ffi::OsStringExt;
Some(PathBuf::from(OsString::from_vec(bytes)))
}
#[cfg(not(unix))]
fn path_from_bytes(bytes: Vec<u8>) -> Option<PathBuf> {
let text = String::from_utf8(bytes).ok()?;
let has_drive = text.len() > 2
&& text.as_bytes()[0] == b'/'
&& text.as_bytes()[1].is_ascii_alphabetic()
&& text.as_bytes()[2] == b':';
let text = if has_drive { &text[1..] } else { &text[..] };
Some(PathBuf::from(text.replace('/', "\\")))
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
#[cfg(unix)]
#[test]
fn a_path_is_a_file_uri_with_what_a_uri_cannot_hold_encoded_and_reads_back() {
let path = Path::new("/Movies/a b/été #1?.mp4");
let uri = file_uri(path);
assert_eq!(uri, "file:///Movies/a%20b/%C3%A9t%C3%A9%20%231%3F.mp4");
assert_eq!(file_uri_path(&uri).as_deref(), Some(path));
let relative = file_uri(Path::new("clip.mov"));
assert!(relative.starts_with("file:///") && relative.ends_with("/clip.mov"));
}
#[cfg(windows)]
#[test]
fn a_windows_path_is_a_file_uri_with_its_drive_or_its_share() {
assert_eq!(
file_uri(Path::new(r"C:\Movies\a b.mp4")),
"file:///C:/Movies/a%20b.mp4"
);
assert_eq!(
file_uri(Path::new(r"\\server\share\a.mp4")),
"file://server/share/a.mp4"
);
}
#[cfg(unix)]
#[test]
fn a_file_uri_names_its_path_decoded() {
assert_eq!(
file_uri_path("file:///home/me/My%20Notes/caf%C3%A9.txt"),
Some(PathBuf::from("/home/me/My Notes/café.txt"))
);
assert_eq!(
file_uri_path("file://localhost/tmp/a"),
Some(PathBuf::from("/tmp/a"))
);
assert_eq!(file_uri_path("file://elsewhere/tmp/a"), None);
assert_eq!(file_uri_path("https://example.org/a"), None);
assert_eq!(file_uri_path("file:///bad%2"), None);
}
}