tower_lsp_server/
uri_ext.rs1use percent_encoding::AsciiSet;
2use std::borrow::Cow;
3use std::path::{Path, PathBuf};
4use std::str::FromStr;
5
6#[cfg(not(windows))]
7pub use std::fs::canonicalize as strict_canonicalize;
8
9#[inline]
12#[cfg(windows)]
13fn strict_canonicalize<P: AsRef<Path>>(path: P) -> std::io::Result<PathBuf> {
14 use std::io;
15
16 fn impl_(path: PathBuf) -> std::io::Result<PathBuf> {
17 let head = path
18 .components()
19 .next()
20 .ok_or(io::Error::other("empty path"))?;
21 let disk_;
22 let head = if let std::path::Component::Prefix(prefix) = head {
23 if let std::path::Prefix::VerbatimDisk(disk) = prefix.kind() {
24 disk_ = format!("{}:", disk as char);
25 Path::new(&disk_)
26 .components()
27 .next()
28 .ok_or(io::Error::other("failed to parse disk component"))?
29 } else {
30 head
31 }
32 } else {
33 head
34 };
35 Ok(std::iter::once(head)
36 .chain(path.components().skip(1))
37 .collect())
38 }
39
40 let canon = std::fs::canonicalize(path)?;
41 impl_(canon)
42}
43
44#[cfg(windows)]
45fn capitalize_drive_letter(path: &str) -> String {
46 if path.len() >= 2 && path.chars().nth(1) == Some(':') {
48 let mut chars = path.chars();
49 let drive_letter = chars.next().unwrap().to_ascii_uppercase();
50 let rest: String = chars.collect();
51 format!("{}{}", drive_letter, rest)
52 } else {
53 path.to_string()
54 }
55}
56
57mod sealed {
58 pub trait Sealed {}
59}
60
61pub trait UriExt: Sized + sealed::Sealed {
64 fn to_file_path(&self) -> Option<Cow<Path>>;
73
74 fn from_file_path<A: AsRef<Path>>(path: A) -> Option<Self>;
80}
81
82impl sealed::Sealed for lsp_types::Uri {}
83
84const ASCII_SET: AsciiSet =
85 percent_encoding::NON_ALPHANUMERIC
87 .remove(b'-')
88 .remove(b'.')
89 .remove(b'_')
90 .remove(b'~')
91 .remove(b'/');
93
94impl UriExt for lsp_types::Uri {
95 fn to_file_path(&self) -> Option<Cow<Path>> {
96 let path = match self.path().as_estr().decode().into_string_lossy() {
97 Cow::Borrowed(ref_) => Cow::Borrowed(Path::new(ref_)),
98 Cow::Owned(owned) => Cow::Owned(PathBuf::from(owned)),
99 };
100
101 if cfg!(windows) {
102 let auth_host = self
103 .authority()
104 .map(|auth| auth.host().as_str())
105 .unwrap_or_default();
106
107 if auth_host.is_empty() {
108 let host = path.to_string_lossy();
112 let host = &host[1..];
113 return Some(Cow::Owned(PathBuf::from(host)));
114 }
115
116 Some(Cow::Owned(
117 Path::new(&format!("{auth_host}:"))
119 .components()
120 .chain(path.components())
121 .collect(),
122 ))
123 } else {
124 Some(path)
125 }
126 }
127
128 fn from_file_path<A: AsRef<Path>>(path: A) -> Option<Self> {
129 let path = path.as_ref();
130
131 let fragment = if path.is_absolute() {
132 Cow::Borrowed(path)
133 } else {
134 match strict_canonicalize(path) {
135 Ok(path) => Cow::Owned(path),
136 Err(_) => return None,
137 }
138 };
139
140 #[cfg(windows)]
141 let raw_uri = {
142 format!(
147 "file:///{}",
148 percent_encoding::utf8_percent_encode(
149 &capitalize_drive_letter(&fragment.to_string_lossy().replace('\\', "/")),
150 &ASCII_SET
151 )
152 )
153 };
154
155 #[cfg(not(windows))]
156 let raw_uri = {
157 format!(
158 "file://{}",
159 percent_encoding::utf8_percent_encode(&fragment.to_string_lossy(), &ASCII_SET)
160 )
161 };
162
163 Self::from_str(&raw_uri).ok()
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::strict_canonicalize;
170 use crate::UriExt;
171 use lsp_types::Uri;
172 use std::path::{Path, PathBuf};
173
174 fn with_schema(path: &str) -> String {
175 const EXPECTED_SCHEMA: &str = if cfg!(windows) { "file:///" } else { "file://" };
176 format!("{EXPECTED_SCHEMA}{path}")
177 }
178
179 #[test]
180 #[cfg(windows)]
181 fn test_idempotent_canonicalization() {
182 let lhs = strict_canonicalize(Path::new(".")).unwrap();
183 let rhs = strict_canonicalize(&lhs).unwrap();
184 assert_eq!(lhs, rhs);
185 }
186
187 #[test]
188 #[cfg(unix)]
189 fn test_path_roundtrip_conversion() {
190 let sources = [
191 strict_canonicalize(Path::new(".")).unwrap(),
192 PathBuf::from("/some/path/to/file.txt"),
193 PathBuf::from("/some/path/to/file with spaces.txt"),
194 PathBuf::from("/some/path/[[...rest]]/file.txt"),
195 PathBuf::from("/some/path/to/файл.txt"),
196 PathBuf::from("/some/path/to/文件.txt"),
197 ];
198
199 for source in sources {
200 let conv = Uri::from_file_path(&source).unwrap();
201 let roundtrip = conv.to_file_path().unwrap();
202 assert_eq!(source, roundtrip, "conv={conv:?}");
203 }
204 }
205
206 #[test]
207 #[cfg(windows)]
208 fn test_path_roundtrip_conversion() {
209 let sources = [
210 strict_canonicalize(Path::new(".")).unwrap(),
211 PathBuf::from("C:\\some\\path\\to\\file.txt"),
212 PathBuf::from("C:\\some\\path\\to\\file with spaces.txt"),
213 PathBuf::from("C:\\some\\path\\[[...rest]]\\file.txt"),
214 PathBuf::from("C:\\some\\path\\to\\файл.txt"),
215 PathBuf::from("C:\\some\\path\\to\\文件.txt"),
216 ];
217
218 for source in sources {
219 let conv = Uri::from_file_path(&source).unwrap();
220 let roundtrip = conv.to_file_path().unwrap();
221 assert_eq!(source, roundtrip, "conv={conv:?}");
222 }
223 }
224
225 #[test]
226 #[cfg(windows)]
227 fn test_windows_uri_roundtrip_conversion() {
228 use std::str::FromStr;
229
230 let uris = [
231 Uri::from_str("file:///C:/some/path/to/file.txt").unwrap(),
232 Uri::from_str("file:///c:/some/path/to/file.txt").unwrap(),
233 Uri::from_str("file:///c%3A/some/path/to/file.txt").unwrap(),
234 ];
235
236 let final_uri = Uri::from_str("file:///C%3A/some/path/to/file.txt").unwrap();
237
238 for uri in uris {
239 let path = uri.to_file_path().unwrap();
240 assert_eq!(
241 &path,
242 Path::new("C:\\some\\path\\to\\file.txt"),
243 "uri={uri:?}"
244 );
245
246 let conv = Uri::from_file_path(&path).unwrap();
247
248 assert_eq!(
249 final_uri,
250 conv,
251 "path={path:?} left={} right={}",
252 final_uri.as_str(),
253 conv.as_str()
254 );
255 }
256 }
257
258 #[test]
259 #[cfg(unix)]
260 fn test_path_to_uri() {
261 let paths = [
262 PathBuf::from("/some/path/to/file.txt"),
263 PathBuf::from("/some/path/to/file with spaces.txt"),
264 PathBuf::from("/some/path/[[...rest]]/file.txt"),
265 PathBuf::from("/some/path/to/файл.txt"),
266 PathBuf::from("/some/path/to/文件.txt"),
267 ];
268
269 let expected = [
270 with_schema("/some/path/to/file.txt"),
271 with_schema("/some/path/to/file%20with%20spaces.txt"),
272 with_schema("/some/path/%5B%5B...rest%5D%5D/file.txt"),
273 with_schema("/some/path/to/%D1%84%D0%B0%D0%B9%D0%BB.txt"),
274 with_schema("/some/path/to/%E6%96%87%E4%BB%B6.txt"),
275 ];
276
277 for (path, expected) in paths.iter().zip(expected) {
278 let uri = Uri::from_file_path(path).unwrap();
279 assert_eq!(uri.to_string(), expected);
280 }
281 }
282
283 #[test]
284 #[cfg(windows)]
285 fn test_path_to_uri_windows() {
286 let paths = [
287 PathBuf::from("C:\\some\\path\\to\\file.txt"),
288 PathBuf::from("C:\\some\\path\\to\\file with spaces.txt"),
289 PathBuf::from("C:\\some\\path\\[[...rest]]\\file.txt"),
290 PathBuf::from("C:\\some\\path\\to\\файл.txt"),
291 PathBuf::from("C:\\some\\path\\to\\文件.txt"),
292 ];
293
294 let expected = [
297 with_schema("C%3A/some/path/to/file.txt"),
298 with_schema("C%3A/some/path/to/file%20with%20spaces.txt"),
299 with_schema("C%3A/some/path/%5B%5B...rest%5D%5D/file.txt"),
300 with_schema("C%3A/some/path/to/%D1%84%D0%B0%D0%B9%D0%BB.txt"),
301 with_schema("C%3A/some/path/to/%E6%96%87%E4%BB%B6.txt"),
302 ];
303
304 for (path, expected) in paths.iter().zip(expected) {
305 let uri = Uri::from_file_path(path).unwrap();
306 assert_eq!(uri.to_string(), expected);
307 }
308 }
309}