use std::{
any::Any,
cell::Cell,
ffi::OsStr,
fmt,
path::Path,
sync::{
mpsc::{self, Receiver, Sender},
Mutex, PoisonError,
},
};
use crate::{
logger::Logger, utilities::from_fn, BuildMessage, BuildMessageKind, BuildMessageLogger,
CompileCommand,
};
#[derive(Debug)]
pub struct CompileCommandCollector {
sender: Mutex<Sender<CompileCommand>>,
forward_to: Option<Logger<Box<dyn BuildMessageLogger>>>,
}
impl CompileCommandCollector {
pub fn new() -> (Self, CompileCommands) {
let (sender, receiver) = mpsc::channel();
let collector = Self {
sender: Mutex::new(sender),
forward_to: None,
};
(collector, CompileCommands(receiver))
}
pub fn forward_to(mut self, logger: Box<dyn BuildMessageLogger>) -> Self {
self.forward_to = Some(Logger(logger));
self
}
}
impl BuildMessageLogger for CompileCommandCollector {
fn log(&self, kind: BuildMessageKind, msg: BuildMessage<'_>, extra: &dyn Any) {
if kind == BuildMessageKind::CompileCommand {
if let Some(command) = extra.downcast_ref::<CompileCommand>() {
let command = command.clone();
let _ = self
.sender
.lock()
.unwrap_or_else(PoisonError::into_inner)
.send(command);
}
}
if let Some(logger) = &self.forward_to {
logger.0.log(kind, msg, extra);
}
}
}
#[derive(Debug)]
pub struct CompileCommands(Receiver<CompileCommand>);
impl Iterator for CompileCommands {
type Item = CompileCommand;
fn next(&mut self) -> Option<CompileCommand> {
self.0.try_recv().ok()
}
}
#[must_use]
pub fn json_compilation_database(
commands: impl IntoIterator<Item = CompileCommand>,
) -> impl fmt::Display {
let commands = Cell::new(Some(commands.into_iter()));
from_fn(move |f| {
let mut commands = commands
.take()
.expect("a `json_compilation_database` can only be formatted once");
write_json_compilation_database(f, &mut commands)
})
}
fn write_json_compilation_database(
f: &mut fmt::Formatter<'_>,
commands: &mut dyn Iterator<Item = CompileCommand>,
) -> fmt::Result {
f.write_str("[")?;
for (i, command) in commands.enumerate() {
f.write_str(if i == 0 { "\n" } else { ",\n" })?;
write_json_entry(f, &command)?;
}
f.write_str("\n]\n")
}
fn write_json_entry(f: &mut fmt::Formatter<'_>, command: &CompileCommand) -> fmt::Result {
f.write_str(" {\n ")?;
f.write_json_field("directory", &*command.directory)?;
f.write_str(",\n ")?;
f.write_json_field("file", &*command.file)?;
f.write_str(",\n ")?;
f.write_json_field("arguments", &*command.arguments)?;
f.write_str(",\n ")?;
f.write_json_field("output", &*command.output)?;
f.write_str("\n }")
}
enum JsonValue<'a> {
String(&'a OsStr),
StringArray(&'a [Box<OsStr>]),
}
impl<'a> From<&'a Path> for JsonValue<'a> {
fn from(path: &'a Path) -> Self {
Self::String(path.as_os_str())
}
}
impl<'a> From<&'a [Box<OsStr>]> for JsonValue<'a> {
fn from(strings: &'a [Box<OsStr>]) -> Self {
Self::StringArray(strings)
}
}
trait JsonWriteExt {
fn write_json_field<'a>(&mut self, name: &str, value: impl Into<JsonValue<'a>>) -> fmt::Result;
fn write_json_string(&mut self, s: &OsStr) -> fmt::Result;
}
impl JsonWriteExt for fmt::Formatter<'_> {
fn write_json_field<'a>(&mut self, name: &str, value: impl Into<JsonValue<'a>>) -> fmt::Result {
fn inner(f: &mut fmt::Formatter<'_>, name: &str, value: JsonValue<'_>) -> fmt::Result {
f.write_json_string(OsStr::new(name))?;
f.write_str(": ")?;
match value {
JsonValue::String(s) => f.write_json_string(s),
JsonValue::StringArray(strings) => {
f.write_str("[")?;
for (i, s) in strings.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
f.write_json_string(s)?;
}
f.write_str("]")
}
}
}
inner(self, name, value.into())
}
fn write_json_string(&mut self, s: &OsStr) -> fmt::Result {
let s = s.to_string_lossy();
self.write_str("\"")?;
let mut unescaped = 0;
for (i, c) in s.char_indices() {
if c == '"' || c == '\\' || c < ' ' {
self.write_str(&s[unescaped..i])?;
if c < ' ' {
write!(self, "\\u{:04x}", u32::from(c))?;
} else {
write!(self, "\\{c}")?;
}
unescaped = i + c.len_utf8();
}
}
self.write_str(&s[unescaped..])?;
self.write_str("\"")
}
}
#[cfg(test)]
mod tests {
use super::*;
fn command(directory: &str, file: &str, arguments: &[&str], output: &str) -> CompileCommand {
CompileCommand {
directory: Path::new(directory).into(),
file: Path::new(file).into(),
arguments: arguments.iter().map(|arg| OsStr::new(arg).into()).collect(),
output: Path::new(output).into(),
}
}
#[test]
fn json_of_no_commands_is_an_empty_array() {
assert_eq!(json_compilation_database([]).to_string(), "[\n]\n");
}
#[test]
#[should_panic(expected = "can only be formatted once")]
fn json_formats_only_once() {
let json = json_compilation_database([]);
assert_eq!(json.to_string(), "[\n]\n");
let _ = json.to_string();
}
#[test]
fn json_escapes_quotes_backslashes_and_control_characters() {
let commands = [
command(
r"C:\Users\me\crate",
r"C:\Users\me\crate\src\foo.c",
&[
r"C:\Program Files\LLVM\bin\clang-cl.exe",
r"\\server\share\foo.c",
r#"-DGREETING="hello world""#,
"-DTAB=\t",
"-DCONTROL=\0\x1b\x1f",
"-DKEPT=ü",
],
r"C:\Users\me\crate\target\out\foo.o",
),
command(
"/crate",
"/crate/bar.c",
&["cc", "-c", "bar.c"],
"/out/bar.o",
),
];
let expected = r#"[
{
"directory": "C:\\Users\\me\\crate",
"file": "C:\\Users\\me\\crate\\src\\foo.c",
"arguments": ["C:\\Program Files\\LLVM\\bin\\clang-cl.exe", "\\\\server\\share\\foo.c", "-DGREETING=\"hello world\"", "-DTAB=\u0009", "-DCONTROL=\u0000\u001b\u001f", "-DKEPT=ü"],
"output": "C:\\Users\\me\\crate\\target\\out\\foo.o"
},
{
"directory": "/crate",
"file": "/crate/bar.c",
"arguments": ["cc", "-c", "bar.c"],
"output": "/out/bar.o"
}
]
"#;
assert_eq!(json_compilation_database(commands).to_string(), expected);
}
#[cfg(any(unix, windows))]
#[test]
fn json_replaces_invalid_unicode() {
#[cfg(unix)]
let invalid = {
use std::os::unix::ffi::OsStrExt;
OsStr::from_bytes(b"foo\xff.c").to_os_string()
};
#[cfg(windows)]
let invalid = {
use std::{ffi::OsString, os::windows::ffi::OsStringExt};
OsString::from_wide(&[0x66, 0x6f, 0x6f, 0xd800, 0x2e, 0x63])
};
let json = from_fn(|f| f.write_json_string(&invalid)).to_string();
assert_eq!(json, "\"foo\u{fffd}.c\"");
}
}