use std::borrow::Cow;
use std::fs;
use camino::{Utf8Path, Utf8PathBuf};
use serde::Deserialize;
use crate::{HashMap, HashSet};
#[derive(Deserialize)]
pub(super) struct CargoMessage<'line> {
#[serde(borrow, default)]
pub(super) reason: Cow<'line, str>,
#[serde(borrow, default)]
pub(super) message: Option<CompilerMessage<'line>>,
#[serde(borrow, default)]
pub(super) filenames: Vec<Cow<'line, str>>,
#[serde(borrow, default)]
pub(super) manifest_path: Option<Cow<'line, str>>,
}
#[derive(Deserialize)]
pub(super) struct CompilerMessage<'line> {
#[serde(borrow, default)]
pub(super) level: Cow<'line, str>,
#[serde(borrow, default)]
pub(super) rendered: Option<Cow<'line, str>>,
#[serde(borrow, default)]
pub(super) code: Option<DiagnosticCode<'line>>,
#[serde(borrow, default)]
pub(super) spans: Vec<Span<'line>>,
#[serde(borrow, default)]
pub(super) children: Vec<Self>,
}
#[derive(Deserialize)]
pub(super) struct DiagnosticCode<'line> {
#[serde(borrow, default)]
pub(super) code: Cow<'line, str>,
}
#[derive(Deserialize)]
pub(super) struct Span<'line> {
#[serde(borrow, default)]
pub(super) file_name: Option<Cow<'line, str>>,
#[serde(default)]
pub(super) line_start: Option<u64>,
#[serde(default)]
pub(super) line_end: Option<u64>,
#[serde(default)]
pub(super) column_start: Option<u64>,
#[serde(default)]
pub(super) column_end: Option<u64>,
#[serde(default)]
pub(super) is_primary: bool,
}
pub(super) fn cargo_message(line: &str) -> Option<CargoMessage<'_>> {
serde_json::from_str(line).ok()
}
pub(super) fn compiled_sources(stdout: &str, root: &Utf8Path) -> Option<HashSet<Utf8PathBuf>> {
let mut compiled: HashSet<Utf8PathBuf> = HashSet::default();
let mut read_any = false;
for dep_file in dep_files(stdout) {
let Ok(text) = fs::read_to_string(dep_file.as_std_path()) else {
continue;
};
read_any = true;
for line in text.lines() {
let Some((_artifact, list)) = line.split_once(": ") else {
continue;
};
for path in dependencies(list) {
let path = Utf8Path::new(&path);
let relative = path.strip_prefix(root).unwrap_or(path);
let _added = compiled.insert(Utf8PathBuf::from(normalize_separators(relative.as_str())));
}
}
}
read_any.then_some(compiled)
}
fn dependencies(list: &str) -> Vec<String> {
let mut paths = Vec::new();
let mut path = String::new();
let mut characters = list.chars().peekable();
while let Some(character) = characters.next() {
match character {
'\\' if characters.peek().is_some_and(|next| next.is_whitespace()) => {
if let Some(escaped) = characters.next() {
path.push(escaped);
}
}
character if character.is_whitespace() => {
if !path.is_empty() {
paths.push(core::mem::take(&mut path));
}
}
character => path.push(character),
}
}
if !path.is_empty() {
paths.push(path);
}
paths
}
pub(super) fn dep_files(stdout: &str) -> Vec<Utf8PathBuf> {
let mut wanted: HashMap<Utf8PathBuf, HashSet<String>> = HashMap::default();
for line in stdout.lines() {
let Some(message) = cargo_message(line) else {
continue;
};
if message.reason != "compiler-artifact" {
continue;
}
for filename in &message.filenames {
let path = Utf8Path::new(filename.as_ref());
let Some((directory, stem)) = path.parent().zip(path.file_stem()) else {
continue;
};
let Some((_name, hash)) = stem.rsplit_once('-') else {
continue;
};
let _added = wanted.entry(directory.to_owned()).or_default().insert(hash.to_owned());
}
}
let mut found = Vec::new();
for (directory, hashes) in wanted {
let Ok(entries) = fs::read_dir(directory.as_std_path()) else {
continue;
};
for entry in entries.flatten() {
let Ok(path) = Utf8PathBuf::from_path_buf(entry.path()) else {
continue;
};
if path.extension() != Some("d") {
continue;
}
let matched = path
.file_stem()
.and_then(|stem| stem.rsplit_once('-'))
.is_some_and(|(_name, hash)| hashes.contains(hash));
if matched {
found.push(path);
}
}
}
found
}
pub(super) fn normalize_separators(path: &str) -> String {
path.replace('\\', "/")
}