use std::io::{Read, Write};
use serde::{Deserialize, Serialize};
use crate::ir::{CompilerIr, Unavailability, UnitRef};
pub const PROTOCOL_VERSION: u32 = 1;
pub const MAX_FRAME_BYTES: u32 = 64 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[repr(u8)]
pub enum Encoding {
Json = 0,
}
impl Encoding {
#[must_use]
pub const fn tag(self) -> u8 {
self as u8
}
#[must_use]
pub const fn from_tag(tag: u8) -> Option<Self> {
match tag {
0 => Some(Self::Json),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Capability {
Types,
NameResolution,
CallTargets,
MirCfg,
MacroExpansion,
TemplateInstantiation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Absence {
Degrade,
Refuse,
}
impl Capability {
pub const ALL: [Self; 6] = [
Self::Types,
Self::NameResolution,
Self::CallTargets,
Self::MirCfg,
Self::MacroExpansion,
Self::TemplateInstantiation,
];
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Types => "types",
Self::NameResolution => "name_resolution",
Self::CallTargets => "call_targets",
Self::MirCfg => "mir_cfg",
Self::MacroExpansion => "macro_expansion",
Self::TemplateInstantiation => "template_instantiation",
}
}
#[must_use]
pub const fn absence(self) -> Absence {
match self {
Self::Types | Self::NameResolution => Absence::Refuse,
Self::CallTargets
| Self::MirCfg
| Self::MacroExpansion
| Self::TemplateInstantiation => Absence::Degrade,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Request {
pub protocol_version: u32,
pub id: u64,
pub body: RequestBody,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RequestBody {
Handshake(ClientIdentity),
DescribeBuild(DescribeBuild),
Analyze(Analyze),
Shutdown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DescribeBuild {
pub root: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct BuildDescription {
pub features: Vec<String>,
pub cfgs: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Analyze {
pub unit: UnitRef,
pub compile_command: Option<CompileCommandSelector>,
pub read_boundary: Option<String>,
pub want: Vec<Capability>,
pub permitted: Vec<Execution>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompileCommandSelector {
pub file: String,
pub directory: Option<String>,
pub arguments: Vec<String>,
}
impl CompileCommandSelector {
#[must_use]
pub fn names_the_same_entry(&self, other: &Self) -> bool {
fn one_path(left: &str, right: &str) -> bool {
crate::ir::ordinary(std::path::Path::new(left))
== crate::ir::ordinary(std::path::Path::new(right))
}
self.arguments == other.arguments
&& one_path(&self.file, &other.file)
&& match (self.directory.as_deref(), other.directory.as_deref()) {
(Some(mine), Some(theirs)) => one_path(mine, theirs),
(None, None) => true,
_ => false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Execution {
BuildScript,
ProcMacro,
Configure,
CompilerWrapper,
GeneratedSource,
}
impl Execution {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::BuildScript => "build-script",
Self::ProcMacro => "proc-macro",
Self::Configure => "configure",
Self::CompilerWrapper => "compiler-wrapper",
Self::GeneratedSource => "generated-source",
}
}
#[must_use]
pub fn from_name(name: &str) -> Option<Self> {
[
Self::BuildScript,
Self::ProcMacro,
Self::Configure,
Self::CompilerWrapper,
Self::GeneratedSource,
]
.into_iter()
.find(|class| class.name() == name)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClientIdentity {
pub client: String,
pub client_version: String,
pub protocol: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Response {
pub protocol_version: u32,
pub id: u64,
pub body: ResponseBody,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ResponseBody {
Handshake(Box<HelperIdentity>),
Build(Box<BuildDescription>),
Analyzed(Box<CompilerIr>),
Unavailable {
unit: UnitRef,
reason: Unavailability,
},
Shutdown,
Failed(Failure),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HelperIdentity {
pub name: String,
pub version: String,
pub protocol: u32,
pub toolchains: Vec<String>,
pub capabilities: Vec<Capability>,
pub executes: Vec<Execution>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Failure {
pub code: String,
pub message: String,
}
#[derive(Debug, thiserror::Error)]
pub enum FrameError {
#[error("the stream ended mid-frame")]
Truncated,
#[error("a frame declared {declared} bytes, over the {MAX_FRAME_BYTES} ceiling")]
TooLarge {
declared: u32,
},
#[error("a frame arrived in unknown encoding {tag}")]
UnknownEncoding {
tag: u8,
},
#[error("a frame's payload did not parse: {0}")]
Malformed(#[from] serde_json::Error),
#[error("the stream failed: {0}")]
Io(#[from] std::io::Error),
}
const HEADER_BYTES: usize = 5;
pub fn write_frame<W: Write, T: Serialize>(writer: &mut W, value: &T) -> Result<(), FrameError> {
let payload = serde_json::to_vec(value)?;
let length =
u32::try_from(payload.len()).map_err(|_| FrameError::TooLarge { declared: u32::MAX })?;
if length > MAX_FRAME_BYTES {
return Err(FrameError::TooLarge { declared: length });
}
let mut header = [0u8; HEADER_BYTES];
header[..4].copy_from_slice(&length.to_be_bytes());
header[4] = Encoding::Json.tag();
writer.write_all(&header)?;
writer.write_all(&payload)?;
writer.flush()?;
Ok(())
}
pub fn read_frame<R: Read, T: for<'de> Deserialize<'de>>(
reader: &mut R,
) -> Result<Option<T>, FrameError> {
let mut header = [0u8; HEADER_BYTES];
match read_exact_or_eof(reader, &mut header)? {
Read0::Eof => return Ok(None),
Read0::Partial => return Err(FrameError::Truncated),
Read0::Full => {}
}
let length = u32::from_be_bytes([header[0], header[1], header[2], header[3]]);
if length > MAX_FRAME_BYTES {
return Err(FrameError::TooLarge { declared: length });
}
if Encoding::from_tag(header[4]).is_none() {
return Err(FrameError::UnknownEncoding { tag: header[4] });
}
let mut payload = vec![0u8; length as usize];
match read_exact_or_eof(reader, &mut payload)? {
Read0::Full => {}
Read0::Eof | Read0::Partial => return Err(FrameError::Truncated),
}
Ok(Some(serde_json::from_slice(&payload)?))
}
enum Read0 {
Eof,
Partial,
Full,
}
fn read_exact_or_eof<R: Read>(reader: &mut R, buffer: &mut [u8]) -> Result<Read0, std::io::Error> {
let mut filled = 0;
while filled < buffer.len() {
let read = reader.read(&mut buffer[filled..])?;
if read == 0 {
return Ok(if filled == 0 {
Read0::Eof
} else {
Read0::Partial
});
}
filled += read;
}
Ok(Read0::Full)
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
fn identity() -> HelperIdentity {
HelperIdentity {
name: "mock".into(),
version: "0.1.0".into(),
protocol: PROTOCOL_VERSION,
toolchains: vec!["rustc 1.85.0".into()],
capabilities: vec![Capability::Types, Capability::CallTargets],
executes: vec![Execution::BuildScript],
}
}
#[test]
fn a_frame_survives_the_round_trip() {
let message = Response {
protocol_version: PROTOCOL_VERSION,
id: 7,
body: ResponseBody::Handshake(Box::new(identity())),
};
let mut buffer = Vec::new();
write_frame(&mut buffer, &message).unwrap();
let back: Option<Response> = read_frame(&mut buffer.as_slice()).unwrap();
assert_eq!(back, Some(message));
}
#[test]
fn frames_are_read_one_at_a_time_from_one_stream() {
let mut buffer = Vec::new();
for id in 0..3u64 {
write_frame(
&mut buffer,
&Response {
protocol_version: PROTOCOL_VERSION,
id,
body: ResponseBody::Shutdown,
},
)
.unwrap();
}
let mut stream = buffer.as_slice();
for id in 0..3u64 {
let message: Response = read_frame(&mut stream).unwrap().unwrap();
assert_eq!(message.id, id);
}
assert!(read_frame::<_, Response>(&mut stream).unwrap().is_none());
}
#[test]
fn a_stream_that_ends_between_frames_is_not_an_error() {
let empty: &[u8] = &[];
assert!(read_frame::<_, Response>(&mut { empty }).unwrap().is_none());
}
#[test]
fn a_stream_that_ends_inside_a_frame_is_an_error() {
let mut buffer = Vec::new();
write_frame(
&mut buffer,
&Response {
protocol_version: PROTOCOL_VERSION,
id: 1,
body: ResponseBody::Shutdown,
},
)
.unwrap();
buffer.truncate(buffer.len() - 1);
let error = read_frame::<_, Response>(&mut buffer.as_slice()).unwrap_err();
assert!(matches!(error, FrameError::Truncated), "{error:?}");
}
#[test]
fn an_oversized_header_is_refused_before_anything_is_allocated() {
let mut header = [0u8; HEADER_BYTES];
header[..4].copy_from_slice(&(MAX_FRAME_BYTES + 1).to_be_bytes());
let error = read_frame::<_, Response>(&mut header.as_slice()).unwrap_err();
assert!(matches!(error, FrameError::TooLarge { .. }), "{error:?}");
}
#[test]
fn an_encoding_this_build_lacks_is_refused_rather_than_guessed() {
let mut buffer = Vec::new();
write_frame(
&mut buffer,
&Response {
protocol_version: PROTOCOL_VERSION,
id: 1,
body: ResponseBody::Shutdown,
},
)
.unwrap();
buffer[4] = 9;
let error = read_frame::<_, Response>(&mut buffer.as_slice()).unwrap_err();
assert!(
matches!(error, FrameError::UnknownEncoding { tag: 9 }),
"{error:?}"
);
}
#[test]
fn what_a_capability_is_called_is_what_it_is_sent_as() {
for capability in Capability::ALL {
let sent = serde_json::to_string(&capability).unwrap();
assert_eq!(sent, format!("\"{}\"", capability.name()));
}
}
#[test]
fn what_an_execution_class_is_called_is_what_it_is_sent_as() {
for class in [
Execution::BuildScript,
Execution::ProcMacro,
Execution::Configure,
Execution::CompilerWrapper,
Execution::GeneratedSource,
] {
let sent = serde_json::to_string(&class).unwrap();
assert_eq!(sent, format!("\"{}\"", class.name()));
}
}
#[test]
fn a_class_nobody_can_name_is_not_read_as_the_one_with_no_name() {
assert_eq!(
Execution::from_name("build-script"),
Some(Execution::BuildScript)
);
assert_eq!(Execution::from_name("build-scripts"), None);
assert_eq!(Execution::from_name("unknown"), None);
assert!(
serde_json::from_str::<Vec<Execution>>(r#"["build-script","something-newer"]"#)
.is_err()
);
}
#[test]
fn a_capability_this_build_cannot_name_is_rejected() {
assert!(
serde_json::from_str::<Vec<Capability>>(r#"["types","overload_resolution"]"#).is_err()
);
}
#[test]
fn what_a_comparison_is_made_of_is_worth_refusing_over() {
assert_eq!(Capability::Types.absence(), Absence::Refuse);
assert_eq!(Capability::NameResolution.absence(), Absence::Refuse);
for capability in [
Capability::CallTargets,
Capability::MirCfg,
Capability::MacroExpansion,
Capability::TemplateInstantiation,
] {
assert_eq!(capability.absence(), Absence::Degrade, "{capability:?}");
}
}
fn selector(file: &str, directory: Option<&str>) -> CompileCommandSelector {
CompileCommandSelector {
file: file.to_owned(),
directory: directory.map(ToOwned::to_owned),
arguments: vec!["clang++".into(), "-c".into(), "a.cpp".into()],
}
}
#[test]
fn one_entry_resolved_by_two_programs_is_one_entry() {
let plain = selector("C:/w/a.cpp", Some("C:/w"));
let verbatim = selector(r"\\?\C:/w/a.cpp", Some(r"\\?\C:/w"));
assert!(plain.names_the_same_entry(&verbatim));
assert!(verbatim.names_the_same_entry(&plain));
assert!(plain.names_the_same_entry(&plain));
}
#[test]
fn two_commands_over_one_source_are_two_entries() {
let mut other = selector("C:/w/a.cpp", Some("C:/w"));
other.arguments.push("-DWIDE".into());
assert!(!selector("C:/w/a.cpp", Some("C:/w")).names_the_same_entry(&other));
assert!(
!selector("C:/w/a.cpp", Some("C:/w"))
.names_the_same_entry(&selector("C:/w/b.cpp", Some("C:/w")))
);
assert!(
!selector("C:/w/a.cpp", Some("C:/w"))
.names_the_same_entry(&selector("C:/w/a.cpp", None))
);
}
}