Skip to main content

php_lsp/backend/
drive_letter.rs

1//! Windows clients disagree on how they spell a drive in a `file` URI
2//! (VS Code sends `file:///c%3A/...`, others `file:///C:/...`), while every
3//! URI the server builds from a path comes from `Uri::from_file_path`
4//! (`file:///C%3A/...`). Incoming URIs are rewritten to that form so one file
5//! never ends up under two keys, and outgoing ones go back to the client's
6//! spelling, which is the key its own document model uses.
7
8use std::collections::HashMap;
9use std::pin::Pin;
10use std::sync::{Arc, Mutex};
11
12use futures::{Stream, StreamExt};
13use serde_json::{Map, Value};
14use tower_lsp_server::jsonrpc::{Request, Response};
15use tower_lsp_server::{ClientSocket, Loopback};
16
17/// The client's spelling of each drive-letter URI it sent, by canonical form.
18#[derive(Clone, Default)]
19pub(super) struct ClientSpellings(Arc<Mutex<HashMap<String, String>>>);
20
21impl ClientSpellings {
22    /// `request` with every drive-letter `file` URI in its params rewritten to
23    /// the `file:///C%3A/` form.
24    pub(super) fn normalize_request(&self, request: Request) -> Request {
25        if request.params().is_none_or(|p| !contains_drive_uri(p)) {
26            return request;
27        }
28        let mut seen = Vec::new();
29        let request = map_params(request, |params| normalize_value(params, &mut seen));
30        let mut spellings = self.0.lock().unwrap_or_else(|e| e.into_inner());
31        for (canonical, original) in seen {
32            spellings.entry(canonical).or_insert(original);
33        }
34        request
35    }
36
37    /// `request` to the client with the URIs it spelled differently restored.
38    pub(super) fn restore_request(&self, request: Request) -> Request {
39        let spellings = self.0.lock().unwrap_or_else(|e| e.into_inner());
40        if spellings.is_empty() || request.params().is_none() {
41            return request;
42        }
43        map_params(request, |params| restore_value(params, &spellings))
44    }
45
46    /// Like [`Self::restore_request`], for a response to a client request.
47    pub(super) fn restore_response(&self, response: Response) -> Response {
48        let spellings = self.0.lock().unwrap_or_else(|e| e.into_inner());
49        if spellings.is_empty() {
50            return response;
51        }
52        let (id, body) = response.into_parts();
53        let body = body.map(|mut result| {
54            restore_value(&mut result, &spellings);
55            result
56        });
57        Response::from_parts(id, body)
58    }
59}
60
61/// The server-to-client half of the transport, sending URIs in the client's spelling.
62pub struct ClientLoopback {
63    pub(super) socket: ClientSocket,
64    pub(super) spellings: ClientSpellings,
65}
66
67impl Loopback for ClientLoopback {
68    type RequestStream = Pin<Box<dyn Stream<Item = Request> + Send>>;
69    type ResponseSink = <ClientSocket as Loopback>::ResponseSink;
70
71    fn split(self) -> (Self::RequestStream, Self::ResponseSink) {
72        let (requests, responses) = Loopback::split(self.socket);
73        let spellings = self.spellings;
74        (
75            Box::pin(requests.map(move |request| spellings.restore_request(request))),
76            responses,
77        )
78    }
79}
80
81fn map_params(request: Request, f: impl FnOnce(&mut Value)) -> Request {
82    let (method, id, params) = request.into_parts();
83    let mut builder = Request::build(method);
84    if let Some(mut params) = params {
85        f(&mut params);
86        builder = builder.params(params);
87    }
88    if let Some(id) = id {
89        builder = builder.id(id);
90    }
91    builder.finish()
92}
93
94fn is_uri_key(key: &str) -> bool {
95    key.len() >= 3 && key[key.len() - 3..].eq_ignore_ascii_case("uri")
96}
97
98/// Only values under a `uri`-suffixed key are touched, so document text that
99/// happens to hold a URI is left alone.
100fn contains_drive_uri(value: &Value) -> bool {
101    match value {
102        Value::Array(items) => items.iter().any(contains_drive_uri),
103        Value::Object(map) => map.iter().any(|(key, v)| match v {
104            Value::String(s) => is_uri_key(key) && normalized_drive_uri(s).is_some(),
105            _ => contains_drive_uri(v),
106        }),
107        _ => false,
108    }
109}
110
111/// Rewrites drive-letter URIs in place, noting each `(canonical, original)` in `seen`.
112fn normalize_value(value: &mut Value, seen: &mut Vec<(String, String)>) {
113    match value {
114        Value::Array(items) => items.iter_mut().for_each(|v| normalize_value(v, seen)),
115        Value::Object(map) => {
116            for (key, v) in map.iter_mut() {
117                match v {
118                    Value::String(s) if is_uri_key(key) => {
119                        if let Some(normalized) = normalized_drive_uri(s) {
120                            let original = std::mem::replace(s, normalized.clone());
121                            seen.push((normalized, original));
122                        }
123                    }
124                    _ => normalize_value(v, seen),
125                }
126            }
127        }
128        _ => {}
129    }
130}
131
132/// Puts back the client's spelling under `uri`-suffixed keys and in the
133/// URI-keyed `changes` map of a `WorkspaceEdit`.
134fn restore_value(value: &mut Value, spellings: &HashMap<String, String>) {
135    match value {
136        Value::Array(items) => items.iter_mut().for_each(|v| restore_value(v, spellings)),
137        Value::Object(map) => {
138            for (key, v) in map.iter_mut() {
139                match v {
140                    Value::String(s) if is_uri_key(key) => {
141                        if let Some(original) = spellings.get(s.as_str()) {
142                            s.clone_from(original);
143                        }
144                    }
145                    Value::Object(changes) if key == "changes" => {
146                        *changes = std::mem::take(changes)
147                            .into_iter()
148                            .map(|(uri, mut edits)| {
149                                restore_value(&mut edits, spellings);
150                                (spellings.get(&uri).cloned().unwrap_or(uri), edits)
151                            })
152                            .collect::<Map<_, _>>();
153                    }
154                    _ => restore_value(v, spellings),
155                }
156            }
157        }
158        _ => {}
159    }
160}
161
162/// `Some` when `s` is a drive-letter `file` URI not already in canonical form.
163fn normalized_drive_uri(s: &str) -> Option<String> {
164    let rest = s.strip_prefix("file:///")?;
165    let drive = *rest.as_bytes().first()?;
166    if !drive.is_ascii_alphabetic() {
167        return None;
168    }
169    let after = &rest[1..];
170    let tail = after
171        .strip_prefix(':')
172        .or_else(|| after.strip_prefix("%3A"))
173        .or_else(|| after.strip_prefix("%3a"))?;
174    if !(tail.is_empty() || tail.starts_with('/')) {
175        return None;
176    }
177    let normalized = format!("file:///{}%3A{tail}", drive.to_ascii_uppercase() as char);
178    (normalized != s).then_some(normalized)
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn drive_spellings_normalize_to_upper_case_encoded_colon() {
187        for input in [
188            "file:///c%3A/proj/a.php",
189            "file:///c:/proj/a.php",
190            "file:///C:/proj/a.php",
191            "file:///c%3a/proj/a.php",
192        ] {
193            assert_eq!(
194                normalized_drive_uri(input).as_deref(),
195                Some("file:///C%3A/proj/a.php"),
196                "{input}"
197            );
198        }
199        assert_eq!(normalized_drive_uri("file:///C%3A/proj/a.php"), None);
200        assert_eq!(normalized_drive_uri("file:///home/a.php"), None);
201        assert_eq!(normalized_drive_uri("file:///ab:/x"), None);
202        assert_eq!(normalized_drive_uri("<?php file:///c:/x"), None);
203    }
204
205    #[test]
206    fn document_text_holding_a_uri_is_untouched() {
207        let params = serde_json::json!({
208            "textDocument": {"uri": "file:///c:/a.php", "version": 2},
209            "contentChanges": [{"text": "file:///c:/pasted"}]
210        });
211        let request = Request::build("textDocument/didChange")
212            .params(params)
213            .finish();
214        let params = ClientSpellings::default()
215            .normalize_request(request)
216            .into_parts()
217            .2
218            .unwrap();
219        assert_eq!(params["textDocument"]["uri"], "file:///C%3A/a.php");
220        assert_eq!(params["contentChanges"][0]["text"], "file:///c:/pasted");
221    }
222
223    #[test]
224    fn nested_params_are_rewritten() {
225        let request = Request::build("workspace/didRenameFiles")
226            .params(serde_json::json!({
227                "files": [{"oldUri": "file:///d:/a.php", "newUri": "file:///d%3A/b.php"}]
228            }))
229            .finish();
230        let params = ClientSpellings::default()
231            .normalize_request(request)
232            .into_parts()
233            .2
234            .unwrap();
235        assert_eq!(
236            params,
237            serde_json::json!({
238                "files": [{"oldUri": "file:///D%3A/a.php", "newUri": "file:///D%3A/b.php"}]
239            })
240        );
241    }
242
243    #[test]
244    fn outgoing_uris_use_the_client_spelling() {
245        let spellings = ClientSpellings::default();
246        spellings.normalize_request(
247            Request::build("textDocument/didOpen")
248                .params(serde_json::json!({"textDocument": {"uri": "file:///c%3A/a.php"}}))
249                .finish(),
250        );
251        let request = Request::build("workspace/applyEdit")
252            .params(serde_json::json!({
253                "edit": {"changes": {"file:///C%3A/a.php": [], "file:///C%3A/b.php": []}},
254                "uri": "file:///C%3A/a.php"
255            }))
256            .finish();
257        let params = spellings.restore_request(request).into_parts().2.unwrap();
258        assert_eq!(
259            params,
260            serde_json::json!({
261                "edit": {"changes": {"file:///c%3A/a.php": [], "file:///C%3A/b.php": []}},
262                "uri": "file:///c%3A/a.php"
263            })
264        );
265    }
266}