xref: /aosp_15_r20/external/toolchain-utils/rust-analyzer-chromiumos-wrapper/src/main.rs (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1 // Copyright 2022 The ChromiumOS Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 use std::env;
6 use std::ffi::OsStr;
7 use std::fs::File;
8 use std::io::{self, BufRead, BufReader, BufWriter, Write};
9 use std::os::unix::process::CommandExt;
10 use std::path::{Path, PathBuf};
11 use std::process::{self, Child};
12 use std::str::from_utf8;
13 use std::thread;
14 
15 use anyhow::{anyhow, bail, Context, Result};
16 use log::{trace, warn};
17 
18 use simplelog::{Config, LevelFilter, WriteLogger};
19 
20 use serde_json::{from_slice, to_writer, Value};
21 use url::Url;
22 
23 const SERVER_FILENAME: &str = "rust-analyzer-chromiumos-wrapper";
24 const CHROOT_SERVER_PATH: &str = "/usr/bin/rust-analyzer";
25 
main() -> Result<()>26 fn main() -> Result<()> {
27     let args = env::args().skip(1);
28 
29     let d = env::current_dir()?;
30     let chromiumos_root = match find_chromiumos_root(&d) {
31         Some(x) => x,
32         None => {
33             // It doesn't appear that we're in a chroot. Run the
34             // regular rust-analyzer.
35             bail!(process::Command::new("rust-analyzer").args(args).exec());
36         }
37     };
38 
39     let args: Vec<String> = args.collect();
40     if !args.is_empty() {
41         // We've received command line arguments, and there are 3 possibilities:
42         // * We just forward the arguments to rust-analyzer and exit.
43         // * We don't support the arguments, so we bail.
44         // * We still need to do our path translation in the LSP protocol.
45         fn run(args: &[String]) -> Result<()> {
46             bail!(process::Command::new("cros_sdk")
47                 .args(["--", "rust-analyzer"])
48                 .args(args)
49                 .exec());
50         }
51 
52         if args.iter().any(|x| {
53             matches!(
54                 x.as_str(),
55                 "--version" | "--help" | "-h" | "--print-config-schema"
56             )
57         }) {
58             // With any of these options rust-analyzer will just print something and exit.
59             return run(&args);
60         }
61 
62         if !args[0].starts_with('-') {
63             // It's a subcommand, and seemingly none of these need the path translation
64             // rust-analyzer-chromiumos-wrapper provides.
65             return run(&args);
66         }
67 
68         if args.iter().any(|x| x == "--log-file") {
69             bail!("rust-analyzer-chromiums_wrapper doesn't support --log-file");
70         }
71 
72         // Otherwise it seems we're probably OK to proceed.
73     }
74 
75     init_log()?;
76 
77     // Get the rust sysroot, this is needed to translate filepaths to sysroot
78     // related files, e.g. crate sources.
79     let outside_rust_sysroot = {
80         let output = process::Command::new("rustc")
81             .arg("--print")
82             .arg("sysroot")
83             .output()?;
84         if !output.status.success() {
85             bail!("Unable to find rustc installation outside of sysroot");
86         }
87         std::str::from_utf8(&output.stdout)?.to_owned()
88     };
89     let outside_rust_sysroot = outside_rust_sysroot.trim();
90 
91     // The /home path inside the chroot is visible outside through "<chromiumos-root>/out/home".
92     let outside_home: &'static str =
93         Box::leak(format!("{}/out/home", chromiumos_root.display()).into_boxed_str());
94 
95     let outside_prefix: &'static str = {
96         let mut path = chromiumos_root
97             .to_str()
98             .ok_or_else(|| anyhow!("Path is not valid UTF-8"))?
99             .to_owned();
100 
101         if Some(&b'/') == path.as_bytes().last() {
102             let _ = path.pop();
103         }
104 
105         // No need to ever free this memory, so let's get a static reference.
106         Box::leak(path.into_boxed_str())
107     };
108 
109     trace!("Found chromiumos root {}", outside_prefix);
110 
111     let outside_sysroot_prefix: &'static str =
112         Box::leak(format!("{outside_rust_sysroot}/lib/rustlib").into_boxed_str());
113     let inside_prefix: &'static str = "/mnt/host/source";
114 
115     let cmd = "cros_sdk";
116     let all_args = ["--", CHROOT_SERVER_PATH]
117         .into_iter()
118         .chain(args.iter().map(|x| x.as_str()));
119     let mut child = KillOnDrop(run_command(cmd, all_args)?);
120 
121     let mut child_stdin = BufWriter::new(child.0.stdin.take().unwrap());
122     let mut child_stdout = BufReader::new(child.0.stdout.take().unwrap());
123 
124     let replacement_map = [
125         (outside_prefix, inside_prefix),
126         (outside_sysroot_prefix, "/usr/lib/rustlib"),
127         (outside_home, "/home"),
128     ];
129 
130     let join_handle = {
131         let rm = replacement_map;
132         thread::spawn(move || {
133             let mut stdin = io::stdin().lock();
134             stream_with_replacement(&mut stdin, &mut child_stdin, &rm)
135                 .context("Streaming from stdin into rust-analyzer")
136         })
137     };
138 
139     // For the mapping between inside to outside, we just reverse the map.
140     let replacement_map_rev = replacement_map.map(|(k, v)| (v, k));
141     let mut stdout = BufWriter::new(io::stdout().lock());
142     stream_with_replacement(&mut child_stdout, &mut stdout, &replacement_map_rev)
143         .context("Streaming from rust-analyzer into stdout")?;
144 
145     join_handle.join().unwrap()?;
146 
147     let code = child.0.wait().context("Running rust-analyzer")?.code();
148     std::process::exit(code.unwrap_or(127));
149 }
150 
init_log() -> Result<()>151 fn init_log() -> Result<()> {
152     if !cfg!(feature = "no_debug_log") {
153         let filename = env::var("RUST_ANALYZER_CHROMIUMOS_WRAPPER_LOG")
154             .context("Obtaining RUST_ANALYZER_CHROMIUMOS_WRAPPER_LOG environment variable")?;
155         let file = File::create(&filename).with_context(|| {
156             format!(
157                 "Opening log file `{}` (value of RUST_ANALYZER_WRAPPER_LOG)",
158                 filename
159             )
160         })?;
161         WriteLogger::init(LevelFilter::Trace, Config::default(), file)
162             .with_context(|| format!("Creating WriteLogger with log file `{}`", filename))?;
163     }
164     Ok(())
165 }
166 
167 #[derive(Debug, Default)]
168 struct Header {
169     length: Option<usize>,
170     other_fields: Vec<u8>,
171 }
172 
173 /// Read the `Content-Length` (if present) into `header.length`, and the text of every other header
174 /// field into `header.other_fields`.
read_header<R: BufRead>(r: &mut R, header: &mut Header) -> Result<()>175 fn read_header<R: BufRead>(r: &mut R, header: &mut Header) -> Result<()> {
176     header.length = None;
177     header.other_fields.clear();
178     const CONTENT_LENGTH: &[u8] = b"Content-Length:";
179     let slen = CONTENT_LENGTH.len();
180     loop {
181         let index = header.other_fields.len();
182 
183         // HTTP header spec says line endings are supposed to be '\r\n' but recommends
184         // implementations accept just '\n', so let's not worry whether a '\r' is present.
185         r.read_until(b'\n', &mut header.other_fields)
186             .context("Reading a header")?;
187 
188         let new_len = header.other_fields.len();
189 
190         if new_len <= index + 2 {
191             // Either we've just received EOF, or just a newline, indicating end of the header.
192             return Ok(());
193         }
194         if header
195             .other_fields
196             .get(index..index + slen)
197             .map_or(false, |v| v == CONTENT_LENGTH)
198         {
199             let s = from_utf8(&header.other_fields[index + slen..])
200                 .context("Parsing Content-Length")?;
201             header.length = Some(s.trim().parse().context("Parsing Content-Length")?);
202             header.other_fields.truncate(index);
203         }
204     }
205 }
206 
207 // The url crate's percent decoding helper returns a Path, while for non-url strings we don't
208 // want to decode all of them as a Path since most of them are non-path strings.
209 // We opt for not sharing the code paths as the handling of plain strings and Paths are slightly
210 // different (notably that Path normalizes away trailing slashes), but otherwise the two functions
211 // are functionally equal.
replace_uri(s: &str, replacement_map: &[(&str, &str)]) -> Result<String>212 fn replace_uri(s: &str, replacement_map: &[(&str, &str)]) -> Result<String> {
213     let uri = Url::parse(s).with_context(|| format!("while parsing path {s:?}"))?;
214     let is_dir = uri.as_str().ends_with('/');
215     let path = uri
216         .to_file_path()
217         .map_err(|()| anyhow!("while converting {s:?} to file path"))?;
218 
219     // Always replace the server path everywhere.
220     if path.file_name() == Some(OsStr::new(SERVER_FILENAME)) {
221         return Ok(CHROOT_SERVER_PATH.into());
222     }
223 
224     fn path_to_url(path: &Path, is_dir: bool) -> Result<String> {
225         let url = if is_dir {
226             Url::from_directory_path(path)
227         } else {
228             Url::from_file_path(path)
229         };
230         url.map_err(|()| anyhow!("while converting {path:?} to url"))
231             .map(|p| p.into())
232     }
233 
234     // Replace by the first prefix match.
235     for (pattern, replacement) in replacement_map {
236         if let Ok(rest) = path.strip_prefix(pattern) {
237             let new_path = Path::new(replacement).join(rest);
238             return path_to_url(&new_path, is_dir);
239         }
240     }
241 
242     Ok(s.into())
243 }
244 
replace_path(s: &str, replacement_map: &[(&str, &str)]) -> String245 fn replace_path(s: &str, replacement_map: &[(&str, &str)]) -> String {
246     // Always replace the server path everywhere.
247     if s.strip_suffix(SERVER_FILENAME)
248         .is_some_and(|s| s.ends_with('/'))
249     {
250         return CHROOT_SERVER_PATH.into();
251     }
252 
253     // Replace by the first prefix match.
254     for (pattern, replacement) in replacement_map {
255         if let Some(rest) = s.strip_prefix(pattern) {
256             if rest.is_empty() || rest.starts_with('/') {
257                 return [replacement, rest].concat();
258             }
259         }
260     }
261 
262     s.into()
263 }
264 
265 /// Extend `dest` with `contents`, replacing any occurrence of patterns in a json string in
266 /// `contents` with a replacement.
replace(contents: &[u8], replacement_map: &[(&str, &str)], dest: &mut Vec<u8>) -> Result<()>267 fn replace(contents: &[u8], replacement_map: &[(&str, &str)], dest: &mut Vec<u8>) -> Result<()> {
268     fn map_value(val: Value, replacement_map: &[(&str, &str)]) -> Value {
269         match val {
270             Value::String(mut s) => {
271                 if s.starts_with("file:") {
272                     // rust-analyzer uses LSP paths most of the time, which are encoded with the
273                     // file: URL scheme.
274                     s = replace_uri(&s, replacement_map).unwrap_or_else(|e| {
275                         warn!("replace_uri failed: {e:?}");
276                         s
277                     });
278                 } else {
279                     // For certain config items, paths may be used instead of URIs.
280                     s = replace_path(&s, replacement_map);
281                 }
282                 Value::String(s)
283             }
284             Value::Array(mut v) => {
285                 for val_ref in v.iter_mut() {
286                     let value = std::mem::replace(val_ref, Value::Null);
287                     *val_ref = map_value(value, replacement_map);
288                 }
289                 Value::Array(v)
290             }
291             Value::Object(mut map) => {
292                 // Surely keys can't be paths.
293                 for val_ref in map.values_mut() {
294                     let value = std::mem::replace(val_ref, Value::Null);
295                     *val_ref = map_value(value, replacement_map);
296                 }
297                 Value::Object(map)
298             }
299             x => x,
300         }
301     }
302 
303     let init_val: Value = from_slice(contents).with_context(|| match from_utf8(contents) {
304         Err(_) => format!(
305             "JSON parsing content of length {} that's not valid UTF-8",
306             contents.len()
307         ),
308         Ok(s) => format!("JSON parsing content of length {}:\n{}", contents.len(), s),
309     })?;
310     let mapped_val = map_value(init_val, replacement_map);
311     to_writer(dest, &mapped_val)?;
312     Ok(())
313 }
314 
315 /// Read LSP messages from `r`, replacing each occurrence of patterns in a json string in the
316 /// payload with replacements, adjusting the `Content-Length` in the header to match, and writing
317 /// the result to `w`.
stream_with_replacement<R: BufRead, W: Write>( r: &mut R, w: &mut W, replacement_map: &[(&str, &str)], ) -> Result<()>318 fn stream_with_replacement<R: BufRead, W: Write>(
319     r: &mut R,
320     w: &mut W,
321     replacement_map: &[(&str, &str)],
322 ) -> Result<()> {
323     let mut head = Header::default();
324     let mut buf = Vec::with_capacity(1024);
325     let mut buf2 = Vec::with_capacity(1024);
326     loop {
327         read_header(r, &mut head)?;
328         if head.length.is_none() && head.other_fields.is_empty() {
329             // No content in the header means we're apparently done.
330             return Ok(());
331         }
332         let len = head
333             .length
334             .ok_or_else(|| anyhow!("No Content-Length in header"))?;
335 
336         trace!("Received header with length {}", head.length.unwrap());
337         trace!(
338             "Received header with contents\n{}",
339             from_utf8(&head.other_fields)?
340         );
341 
342         buf.resize(len, 0);
343         r.read_exact(&mut buf)
344             .with_context(|| format!("Reading payload expecting size {}", len))?;
345 
346         trace!("Received payload\n{}", from_utf8(&buf)?);
347 
348         buf2.clear();
349         replace(&buf, replacement_map, &mut buf2)?;
350 
351         trace!("After replacements payload\n{}", from_utf8(&buf2)?);
352 
353         write!(w, "Content-Length: {}\r\n", buf2.len())?;
354         w.write_all(&head.other_fields)?;
355         w.write_all(&buf2)?;
356         w.flush()?;
357     }
358 }
359 
run_command<'a, I>(cmd: &'a str, args: I) -> Result<process::Child> where I: IntoIterator<Item = &'a str>,360 fn run_command<'a, I>(cmd: &'a str, args: I) -> Result<process::Child>
361 where
362     I: IntoIterator<Item = &'a str>,
363 {
364     Ok(process::Command::new(cmd)
365         .args(args)
366         .stdin(process::Stdio::piped())
367         .stdout(process::Stdio::piped())
368         .spawn()?)
369 }
370 
find_chromiumos_root(start: &Path) -> Option<PathBuf>371 fn find_chromiumos_root(start: &Path) -> Option<PathBuf> {
372     let mut buf = start.to_path_buf();
373     loop {
374         buf.push(".chroot_lock");
375         if buf.exists() {
376             buf.pop();
377             return Some(buf);
378         }
379         buf.pop();
380         if !buf.pop() {
381             return None;
382         }
383     }
384 }
385 
386 struct KillOnDrop(Child);
387 
388 impl Drop for KillOnDrop {
drop(&mut self)389     fn drop(&mut self) {
390         let _ = self.0.kill();
391     }
392 }
393 
394 #[cfg(test)]
395 mod test {
396     use super::*;
397 
test_stream_with_replacement( read: &str, replacement_map: &[(&str, &str)], json_expected: &str, ) -> Result<()>398     fn test_stream_with_replacement(
399         read: &str,
400         replacement_map: &[(&str, &str)],
401         json_expected: &str,
402     ) -> Result<()> {
403         let mut w = Vec::new();
404         let input = format!("Content-Length: {}\r\n\r\n{}", read.as_bytes().len(), read);
405         stream_with_replacement(&mut input.as_bytes(), &mut w, &replacement_map)?;
406 
407         // serde_json may not format the json output the same as we do, so we can't just compare
408         // as strings or slices.
409 
410         let (w1, w2) = {
411             let mut split = w.rsplitn(2, |&c| c == b'\n');
412             let w2 = split.next().unwrap();
413             (split.next().unwrap(), w2)
414         };
415 
416         assert_eq!(
417             from_utf8(w1)?,
418             format!("Content-Length: {}\r\n\r", w2.len())
419         );
420 
421         let v1: Value = from_slice(w2)?;
422         let v2: Value = serde_json::from_str(json_expected)?;
423         assert_eq!(v1, v2);
424 
425         Ok(())
426     }
427 
428     #[test]
test_stream_with_replacement_simple() -> Result<()>429     fn test_stream_with_replacement_simple() -> Result<()> {
430         test_stream_with_replacement(
431             r#"{
432                 "somekey": {
433                     "somepath": "/XYZXYZ/",
434                     "anotherpath": "/some/string"
435                 },
436                 "anotherkey": "/XYZXYZ/def"
437             }"#,
438             &[("/XYZXYZ", "/REPLACE")],
439             r#"{
440                 "somekey": {
441                     "somepath": "/REPLACE/",
442                     "anotherpath": "/some/string"
443                 },
444                 "anotherkey": "/REPLACE/def"
445             }"#,
446         )
447     }
448 
449     #[test]
test_stream_with_replacement_file_uri() -> Result<()>450     fn test_stream_with_replacement_file_uri() -> Result<()> {
451         test_stream_with_replacement(
452             r#"{
453                 "key0": "file:///ABCDEF/",
454                 "key1": {
455                     "key2": 5,
456                     "key3": "file:///ABCDEF/text"
457                 },
458                 "key4": 1
459             }"#,
460             &[("/ABCDEF", "/replacement")],
461             r#"{
462                 "key0": "file:///replacement/",
463                 "key1": {
464                     "key2": 5,
465                     "key3": "file:///replacement/text"
466                 },
467                 "key4": 1
468             }"#,
469         )
470     }
471 
472     #[test]
test_stream_with_replacement_self_binary() -> Result<()>473     fn test_stream_with_replacement_self_binary() -> Result<()> {
474         test_stream_with_replacement(
475             r#"{
476                 "path": "/my_folder/rust-analyzer-chromiumos-wrapper"
477             }"#,
478             &[],
479             r#"{
480                 "path": "/usr/bin/rust-analyzer"
481             }"#,
482         )
483     }
484 
485     #[test]
test_stream_with_replacement_replace_once() -> Result<()>486     fn test_stream_with_replacement_replace_once() -> Result<()> {
487         test_stream_with_replacement(
488             r#"{
489                 "path": "/mnt/home/file"
490             }"#,
491             &[("/mnt/home", "/home"), ("/home", "/foo")],
492             r#"{
493                 "path": "/home/file"
494             }"#,
495         )
496     }
497 }
498