use serde::{Deserialize, Serialize};
use tatara_lisp::DeriveTataraDomain;
use crate::SpecError;
#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
#[tatara(keyword = "defworker-protocol")]
pub struct WorkerProtocol {
pub name: String,
pub version: u32,
#[serde(rename = "magicClient")]
pub magic_client: String,
#[serde(rename = "magicServer")]
pub magic_server: String,
}
#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
#[tatara(keyword = "defworker-opcode")]
pub struct WorkerOpcode {
pub name: String,
pub code: u32,
pub direction: OpcodeDirection,
#[serde(rename = "requestFields")]
pub request_fields: Vec<WireType>,
#[serde(default, rename = "responseFields")]
pub response_fields: Vec<WireType>,
#[serde(default, rename = "sinceVersion")]
pub since_version: u32,
#[serde(default)]
pub deprecated: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OpcodeDirection {
ClientToDaemon,
DaemonToClient,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum WireType {
U64,
Str,
Bytes,
Bool,
StorePath,
StringList,
StorePathList,
PathInfo,
ValidPathInfo,
DerivationOutputs,
RealisationsMap,
KeyedBuildResult,
Substitutables,
KeyValueAttrs,
BuildMode,
}
pub const CANONICAL_WORKER_PROTOCOL_LISP: &str =
include_str!("../specs/worker_protocol.lisp");
pub fn load_canonical_protocols() -> Result<Vec<WorkerProtocol>, SpecError> {
crate::loader::load_all::<WorkerProtocol>(CANONICAL_WORKER_PROTOCOL_LISP)
}
pub fn load_canonical_opcodes() -> Result<Vec<WorkerOpcode>, SpecError> {
crate::loader::load_all::<WorkerOpcode>(CANONICAL_WORKER_PROTOCOL_LISP)
}
pub fn load_opcode_named(name: &str) -> Result<WorkerOpcode, SpecError> {
load_canonical_opcodes()?
.into_iter()
.find(|o| o.name == name)
.ok_or_else(|| SpecError::Load(format!("no (defworker-opcode) with :name {name:?}")))
}
pub fn load_opcode_by_code(code: u32) -> Result<WorkerOpcode, SpecError> {
load_canonical_opcodes()?
.into_iter()
.find(|o| o.code == code)
.ok_or_else(|| SpecError::Load(format!("no (defworker-opcode) with :code {code}")))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DispatchOutcome {
pub opcode_name: String,
pub opcode_code: u32,
pub response_body: Vec<u8>,
}
pub trait WorkerProtocolHandler {
fn dispatch(&self, opcode: &WorkerOpcode, body: &[u8])
-> Result<Vec<u8>, String>
{
Err(format!(
"unhandled opcode `{}` (code {}) — handler must override dispatch \
or implement an explicit case",
opcode.name, opcode.code,
))
}
}
pub fn apply<H: WorkerProtocolHandler>(
code: u32,
body: &[u8],
handler: &H,
) -> Result<DispatchOutcome, SpecError> {
let opcode = load_opcode_by_code(code).map_err(|_| SpecError::Interp {
phase: "unknown-opcode".into(),
message: format!(
"received opcode {code} but no (defworker-opcode :code {code}) \
is authored in the canonical worker-protocol spec",
),
})?;
let response_body = handler.dispatch(&opcode, body).map_err(|e| SpecError::Interp {
phase: "dispatch-failed".into(),
message: format!("opcode `{}` (code {code}): {e}", opcode.name),
})?;
Ok(DispatchOutcome {
opcode_name: opcode.name,
opcode_code: code,
response_body,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::{HashMap, HashSet};
#[test]
fn canonical_protocol_parses() {
let protos = load_canonical_protocols().unwrap();
assert!(!protos.is_empty());
}
#[test]
fn canonical_opcodes_parse() {
let opcodes = load_canonical_opcodes().unwrap();
assert!(
opcodes.len() >= 25,
"canonical worker protocol must declare at least 25 opcodes, got {}",
opcodes.len(),
);
}
#[test]
fn opcodes_have_unique_codes() {
let opcodes = load_canonical_opcodes().unwrap();
let mut by_code: HashMap<u32, Vec<&str>> = HashMap::new();
for op in &opcodes {
by_code.entry(op.code).or_default().push(op.name.as_str());
}
for (code, names) in &by_code {
assert_eq!(
names.len(),
1,
"opcode {code} has multiple authored entries: {names:?}",
);
}
}
#[test]
fn essential_opcodes_present() {
let opcodes = load_canonical_opcodes().unwrap();
let names: HashSet<&str> = opcodes.iter().map(|o| o.name.as_str()).collect();
for required in [
"IsValidPath",
"QueryPathInfo",
"AddToStore",
"BuildPaths",
"BuildDerivation",
"QueryReferrers",
"QueryValidPaths",
"CollectGarbage",
"NarFromPath",
"AddTempRoot",
] {
assert!(
names.contains(required),
"canonical worker protocol missing opcode `{required}`",
);
}
}
#[test]
fn query_path_info_has_correct_shape() {
let op = load_opcode_named("QueryPathInfo").unwrap();
assert_eq!(op.code, 26);
assert_eq!(op.direction, OpcodeDirection::ClientToDaemon);
assert_eq!(op.request_fields, vec![WireType::StorePath]);
}
#[test]
fn ca_realisation_opcodes_present() {
let names: HashSet<String> = load_canonical_opcodes()
.unwrap()
.into_iter()
.map(|o| o.name)
.collect();
assert!(names.contains("QueryRealisation"));
assert!(names.contains("RegisterDrvOutput"));
}
use std::cell::RefCell;
struct LoggingHandler {
seen: RefCell<Vec<(String, u32)>>,
response_for: HashMap<u32, Vec<u8>>,
}
impl LoggingHandler {
fn new() -> Self {
Self {
seen: RefCell::new(Vec::new()),
response_for: HashMap::new(),
}
}
fn responds(mut self, code: u32, body: &[u8]) -> Self {
self.response_for.insert(code, body.to_vec());
self
}
}
impl WorkerProtocolHandler for LoggingHandler {
fn dispatch(&self, opcode: &WorkerOpcode, _body: &[u8])
-> Result<Vec<u8>, String>
{
self.seen.borrow_mut().push((opcode.name.clone(), opcode.code));
self.response_for
.get(&opcode.code)
.cloned()
.ok_or_else(|| format!("no response configured for code {}", opcode.code))
}
}
#[test]
fn dispatch_known_opcode_succeeds() {
let handler = LoggingHandler::new().responds(1, b"\x01"); let outcome = apply(1, b"", &handler).unwrap();
assert_eq!(outcome.opcode_name, "IsValidPath");
assert_eq!(outcome.opcode_code, 1);
assert_eq!(outcome.response_body, vec![1u8]);
let log = handler.seen.borrow();
assert_eq!(log.len(), 1);
assert_eq!(log[0].0, "IsValidPath");
}
#[test]
fn dispatch_unknown_opcode_errors() {
let handler = LoggingHandler::new();
let err = apply(9999, b"", &handler).unwrap_err();
match err {
SpecError::Interp { phase, message } => {
assert_eq!(phase, "unknown-opcode");
assert!(message.contains("9999"));
}
_ => panic!("expected unknown-opcode"),
}
}
#[test]
fn dispatch_handler_failure_surfaces_as_dispatch_failed() {
let handler = LoggingHandler::new(); let err = apply(1, b"", &handler).unwrap_err();
match err {
SpecError::Interp { phase, message } => {
assert_eq!(phase, "dispatch-failed");
assert!(message.contains("IsValidPath"));
}
_ => panic!("expected dispatch-failed"),
}
}
#[test]
fn load_opcode_by_code_finds_known() {
let op = load_opcode_by_code(26).unwrap(); assert_eq!(op.name, "QueryPathInfo");
}
#[test]
fn load_opcode_by_code_errors_on_missing() {
let err = load_opcode_by_code(99999).unwrap_err();
match err {
SpecError::Load(msg) => assert!(msg.contains("99999")),
_ => panic!("expected Load error"),
}
}
#[test]
fn default_handler_returns_unhandled() {
struct Empty;
impl WorkerProtocolHandler for Empty {}
let err = apply(1, b"", &Empty).unwrap_err();
match err {
SpecError::Interp { phase, .. } => assert_eq!(phase, "dispatch-failed"),
_ => panic!("expected dispatch-failed"),
}
}
}