use std::borrow::Cow;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Default)]
pub struct SourceCache {
cache: HashMap<String, Option<(String, Vec<usize>)>>,
home_path: Option<Option<PathBuf>>,
rustc_sources: Option<Vec<(String, PathBuf)>>,
}
impl SourceCache {
#[allow(clippy::unused_self)]
fn home_path_uncached(&self) -> Option<PathBuf> {
let buffer = std::env::var("HOME").ok()?;
if buffer.is_empty() {
return None;
}
Some(buffer.into())
}
fn rustc_sources_uncached(&mut self) -> Option<Vec<(String, PathBuf)>> {
let mut list = Vec::new();
let toolchains_path = {
let mut buffer = self.home_path()?;
buffer.push(".rustup");
buffer.push("toolchains");
buffer
};
let iter = match std::fs::read_dir(&toolchains_path) {
Ok(iter) => iter,
Err(error) => {
log::warn!("Error reading {toolchains_path:?}: {error}");
return None;
}
};
for entry in iter {
let entry = match entry {
Ok(entry) => entry,
Err(error) => {
log::warn!("Error reading entry in {toolchains_path:?}: {error}");
continue;
}
};
let root_path = entry.path();
let rustc_path = {
let mut buffer = root_path.clone();
buffer.push("bin");
buffer.push("rustc");
buffer
};
if !rustc_path.exists() {
continue;
}
let sources_path = {
let mut buffer = root_path;
buffer.push("lib");
buffer.push("rustlib");
buffer.push("src");
buffer.push("rust");
buffer
};
if !sources_path.exists() {
continue;
}
let output = match std::process::Command::new(&rustc_path).args(["--version"]).output() {
Ok(output) => output,
Err(error) => {
log::warn!("Error extracting version from {rustc_path:?}: {error}");
continue;
}
};
if !output.status.success() {
log::warn!(
"Error extracting version from {rustc_path:?}: non successful status: {}",
output.status
);
continue;
}
let Ok(version_string) = String::from_utf8(output.stdout) else {
log::warn!("Error extracting version from {rustc_path:?}: returned version is not valid UTF-8");
continue;
};
let p = &version_string[version_string.find('(')? + 1..];
let p = &p[..p.find(' ')?];
log::debug!("Found Rust sources for hash '{p}' at: {sources_path:?}");
list.push((p.to_owned(), sources_path));
}
Some(list)
}
fn home_path(&mut self) -> Option<PathBuf> {
if let Some(ref path) = self.home_path {
return path.clone();
}
let result = self.home_path_uncached();
if let Some(ref path) = result {
log::debug!("Found HOME at: {path:?}");
} else {
log::debug!("HOME not found!");
}
self.home_path = Some(result.clone());
result
}
fn rustc_sources(&mut self) -> &[(String, PathBuf)] {
if self.rustc_sources.is_none() {
self.rustc_sources = Some(self.rustc_sources_uncached().unwrap_or_default());
}
self.rustc_sources.as_ref().unwrap()
}
fn read_source_file(&mut self, path: &str) -> Option<String> {
const HOME_PREFIX: &str = "~/";
const RUSTC_PREFIX: &str = "/rustc/";
let filesystem_path = if let Some(relative_path) = path.strip_prefix(HOME_PREFIX) {
let mut buffer = self.home_path()?;
buffer.push(relative_path);
Cow::Owned(buffer)
} else if let Some(relative_path) = path.strip_prefix(RUSTC_PREFIX) {
let p = relative_path;
let index = p.find('/')?;
let hash = &p[..index];
let relative_path = &p[index + 1..];
if relative_path.is_empty() || hash.is_empty() {
return None;
}
let sources = self.rustc_sources();
let (_, sources_root) = sources.iter().find(|(sources_hash, _)| hash.starts_with(sources_hash))?;
Cow::Owned(sources_root.join(relative_path))
} else {
Cow::Borrowed(Path::new(path))
};
match std::fs::read_to_string(&filesystem_path) {
Ok(contents) => {
log::debug!("Loaded source file: '{path}' (from {filesystem_path:?})");
Some(contents)
}
Err(error) => {
log::warn!("Failed to load source file '{path}' from {filesystem_path:?}: {error}");
None
}
}
}
pub fn lookup_source_line(&mut self, path: &str, line: u32) -> Option<&str> {
if !self.cache.contains_key(path) {
let Some(contents) = self.read_source_file(path) else {
self.cache.insert(path.to_owned(), None);
return None;
};
let mut line_to_offset = Vec::new();
line_to_offset.push(0);
for (offset, byte) in contents.bytes().enumerate() {
if byte == b'\n' {
line_to_offset.push(offset + 1);
}
}
self.cache.insert(path.to_owned(), Some((contents, line_to_offset)));
}
let cached = self.cache.get(path)?;
let (contents, line_to_offset) = cached.as_ref()?;
let line = (line as usize).wrapping_sub(1);
let offset = *line_to_offset.get(line)?;
let next_offset = line_to_offset.get(line.wrapping_add(1)).copied().unwrap_or(contents.len());
contents.get(offset..next_offset).map(|s| s.trim_end())
}
}