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