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