eska_presentation/
path_output.rs1use std::{fmt::Write as _, path::Path};
4
5#[must_use]
7pub fn render(path: &Path, hyperlink: bool) -> String {
8 render_link(path, path, hyperlink)
9}
10
11#[must_use]
13pub fn render_link(label: &Path, target: &Path, hyperlink: bool) -> String {
14 let label = display_path(label);
15 let Some(target) = hyperlink.then(|| file_uri(target)).flatten() else {
16 return label;
17 };
18 format!("\x1b]8;;{target}\x1b\\{label}\x1b]8;;\x1b\\")
19}
20
21#[must_use]
23pub fn display_path(path: &Path) -> String {
24 let mut display = String::new();
25 for character in path.to_string_lossy().chars() {
26 if character.is_control() {
27 display.extend(character.escape_default());
28 } else {
29 display.push(character);
30 }
31 }
32 display
33}
34
35fn file_uri(path: &Path) -> Option<String> {
37 let path = std::path::absolute(path).ok()?;
38 #[cfg(unix)]
39 let normalized = {
40 use std::os::unix::ffi::OsStrExt;
41 path.as_os_str().as_bytes().to_vec()
42 };
43 #[cfg(not(unix))]
44 let normalized = {
45 let path = path.to_str()?.replace('\\', "/");
46 let path = path.strip_prefix("//?/UNC/").map_or_else(
48 || path.strip_prefix("//?/").unwrap_or(&path).to_owned(),
49 |unc| format!("//{unc}"),
50 );
51 path.into_bytes()
52 };
53 let prefix = if normalized.starts_with(b"//") {
54 "file:"
55 } else if normalized.starts_with(b"/") {
56 "file://"
57 } else {
58 "file:///"
59 };
60 let mut uri = prefix.to_owned();
61 for byte in normalized {
62 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~' | b'/' | b':') {
63 uri.push(char::from(byte));
64 } else {
65 write!(uri, "%{byte:02X}").expect("writing to String cannot fail");
66 }
67 }
68 Some(uri)
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
77 fn links_paths_without_confusing_uri_delimiters() {
78 let path = std::env::current_dir().unwrap().join("Проект #1%?.cf");
79 let linked = render(&path, true);
80 assert!(linked.starts_with("\x1b]8;;file:"));
81 assert!(linked.contains("%D0%9F%D1%80%D0%BE%D0%B5%D0%BA%D1%82%20%231%25%3F.cf"));
82 assert!(linked.ends_with(&format!("\x1b\\{}\x1b]8;;\x1b\\", path.display())));
83 assert_eq!(render(&path, false), path.to_string_lossy());
84 }
85
86 #[test]
88 fn resolves_relative_missing_targets() {
89 let path = Path::new("missing project/src");
90 assert_eq!(
91 file_uri(path),
92 file_uri(&std::env::current_dir().unwrap().join(path))
93 );
94 assert!(render(path, true).ends_with("\x1b\\missing project/src\x1b]8;;\x1b\\"));
95 }
96
97 #[cfg(unix)]
99 #[test]
100 fn encodes_non_unicode_and_control_bytes() {
101 use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
102 let path = Path::new(OsStr::from_bytes(b"/tmp/name\\\xff\x1b\n.cf"));
103 let linked = render(path, true);
104 assert!(linked.starts_with("\x1b]8;;file:///tmp/name%5C%FF%1B%0A.cf\x1b\\"));
105 let label = display_path(path);
106 assert!(!label.contains('\x1b'));
107 assert!(!label.contains('\n'));
108 assert!(label.contains("\\u{1b}\\n"));
109 }
110}