use serde::{Deserialize, Serialize};
use std::ffi::{OsStr, OsString};
pub mod process;
pub mod unix;
pub mod windows;
pub mod workspace;
pub const EXECUTION_VERSION: u32 = 2;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "encoding", content = "units", rename_all = "snake_case")]
pub enum NativeString {
Unix(Vec<u8>),
Windows(Vec<u16>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NativeStringError {
ForeignEncoding,
Nul,
}
impl std::fmt::Display for NativeStringError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(match self {
Self::ForeignEncoding => "native string encoding belongs to another platform",
Self::Nul => "native string contains a NUL",
})
}
}
impl std::error::Error for NativeStringError {}
impl NativeString {
pub fn capture(value: &OsStr) -> Self {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
Self::Unix(value.as_bytes().to_owned())
}
#[cfg(windows)]
{
use std::os::windows::ffi::OsStrExt;
Self::Windows(value.encode_wide().collect())
}
}
pub fn decode(&self) -> Result<OsString, NativeStringError> {
match self {
#[cfg(unix)]
Self::Unix(bytes) => {
use std::os::unix::ffi::OsStringExt;
if bytes.contains(&0) {
return Err(NativeStringError::Nul);
}
Ok(OsString::from_vec(bytes.clone()))
}
#[cfg(windows)]
Self::Windows(units) => {
use std::os::windows::ffi::OsStringExt;
if units.contains(&0) {
return Err(NativeStringError::Nul);
}
Ok(OsString::from_wide(units))
}
_ => Err(NativeStringError::ForeignEncoding),
}
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExecutionRequest {
pub version: u32,
#[serde(default)]
pub entry: ExecutionEntry,
pub client_version: String,
pub client_pid: u32,
#[serde(default)]
pub creation_mask: Option<u32>,
pub argv: Vec<NativeString>,
pub env: Vec<(NativeString, NativeString)>,
pub cwd: NativeString,
pub home: Option<NativeString>,
pub temp: NativeString,
pub cache: Option<NativeString>,
pub executable: NativeString,
pub startup_micros: u64,
pub channels: ExecutionChannels,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionEntry {
#[default]
Original,
Compiler,
}
impl std::fmt::Debug for ExecutionRequest {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ExecutionRequest")
.field("version", &self.version)
.field("client_pid", &self.client_pid)
.field("argv_count", &self.argv.len())
.field("env_count", &self.env.len())
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "platform", rename_all = "snake_case")]
pub enum ExecutionChannels {
Unix { terminal: bool, jobserver: Vec<i32> },
UnixClient {
terminal: bool,
jobserver: Vec<i32>,
#[serde(default)]
trusted_pipeline: bool,
},
Windows {
stdin: u64,
stdout: u64,
stderr: u64,
#[serde(default)]
process_broker: bool,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "execution", rename_all = "snake_case")]
pub enum ExecutionReply {
Ready {
version: u32,
descriptors: Option<DescriptorTicket>,
},
Prepared,
Accepted,
Complete {
exit_code: i32,
},
Declined {
reason: String,
},
Failed {
message: String,
},
WindowsControl {
command: windows::Command,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ExecutionCommand {
Commit,
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DescriptorTicket {
pub endpoint: NativeString,
pub key: [u8; 16],
}
impl std::fmt::Debug for DescriptorTicket {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("DescriptorTicket")
.field("endpoint", &self.endpoint)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ExecutionStatus {
#[serde(default)]
pub protocol_version: u32,
pub eligible: bool,
pub client_trust_enabled: bool,
pub daemon_trust_enabled: bool,
pub detail: String,
}
impl ExecutionStatus {
pub fn compatible(&self) -> bool {
self.protocol_version == EXECUTION_VERSION
}
}
#[cfg(test)]
mod tests {
#[test]
fn execution_diagnostics_reject_legacy_and_foreign_protocols() {
let mut status: super::ExecutionStatus = serde_json::from_str(r#"{"eligible":true,"client_trust_enabled":false,"daemon_trust_enabled":false,"detail":"old daemon"}"#).unwrap();
assert!(!status.compatible());
for (version, compatible) in [(0, false), (1, false), (2, true), (3, false)] {
status.protocol_version = version;
assert_eq!(status.compatible(), compatible);
}
}
use super::*;
#[test]
fn request_debug_does_not_include_compiler_arguments_or_environment() {
let native = |text: &str| NativeString::capture(OsStr::new(text));
let request = ExecutionRequest {
version: EXECUTION_VERSION,
entry: ExecutionEntry::Original,
client_version: "fixture".into(),
client_pid: 42,
creation_mask: Some(0o022),
argv: vec![native("compiler"), native("private-argument")],
env: vec![(native("TOKEN"), native("private-value"))],
cwd: native("private-directory"),
home: None,
temp: native("private-temp"),
cache: None,
executable: native("private-executable"),
startup_micros: 0,
channels: ExecutionChannels::Unix {
terminal: false,
jobserver: Vec::new(),
},
};
let debug = format!("{request:?}");
assert!(!debug.contains("private"));
assert!(!debug.contains("TOKEN"));
assert!(debug.contains("argv_count: 2"));
assert!(debug.contains("env_count: 1"));
let envelope = crate::Request::ExecuteV1(Box::new(request));
assert_eq!(envelope.client_version(), Some("fixture"));
assert!(!envelope.is_build_activity());
assert_eq!(crate::Request::Health.client_version(), None);
assert!(
crate::Request::CompileFinished(crate::CompileFinishedRequest {
pid: 42,
started_at_ms: 123,
})
.is_build_activity()
);
}
#[test]
fn native_string_errors_explain_why_a_request_cannot_be_transferred() {
assert_eq!(
NativeStringError::ForeignEncoding.to_string(),
"native string encoding belongs to another platform"
);
assert_eq!(
NativeStringError::Nul.to_string(),
"native string contains a NUL"
);
}
#[test]
fn descriptor_ticket_debug_keeps_the_endpoint_and_redacts_the_one_use_key() {
let ticket = DescriptorTicket {
endpoint: NativeString::capture("descriptor-endpoint".as_ref()),
key: [173; 16],
};
let debug = format!("{ticket:?}");
assert!(debug.starts_with("DescriptorTicket"));
assert!(debug.contains("endpoint"));
assert!(!debug.contains("173"));
}
#[test]
fn native_strings_round_trip_without_loss_and_reject_nuls() {
#[cfg(unix)]
let (native, nul, foreign) = {
use std::os::unix::ffi::OsStringExt;
(
OsString::from_vec(vec![b'x', 255, 128]),
NativeString::Unix(vec![0]),
NativeString::Windows(vec![120]),
)
};
#[cfg(windows)]
let (native, nul, foreign) = {
use std::os::windows::ffi::OsStringExt;
(
OsString::from_wide(&[120, 0xd800, 0xdc00, 0xdfff]),
NativeString::Windows(vec![0]),
NativeString::Unix(vec![120]),
)
};
let captured = NativeString::capture(&native);
let json = serde_json::to_vec(&captured).unwrap();
let decoded: NativeString = serde_json::from_slice(&json).unwrap();
assert_eq!(decoded.decode().unwrap(), native);
assert_eq!(nul.decode(), Err(NativeStringError::Nul));
assert_eq!(foreign.decode(), Err(NativeStringError::ForeignEncoding));
}
#[test]
fn handoff_states_have_distinct_wire_messages() {
let prepared = serde_json::to_string(&ExecutionReply::Prepared).unwrap();
let accepted = serde_json::to_string(&ExecutionReply::Accepted).unwrap();
assert_eq!(prepared, r#"{"execution":"prepared"}"#);
assert_eq!(accepted, r#"{"execution":"accepted"}"#);
assert_eq!(
serde_json::to_string(&ExecutionCommand::Commit).unwrap(),
"\"commit\""
);
}
}