1use serde::{Deserialize, Serialize};
46use tatara_lisp::DeriveTataraDomain;
47
48use crate::SpecError;
49
50#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
55#[tatara(keyword = "defworker-protocol")]
56pub struct WorkerProtocol {
57 pub name: String,
58 pub version: u32,
59 #[serde(rename = "magicClient")]
61 pub magic_client: String,
62 #[serde(rename = "magicServer")]
64 pub magic_server: String,
65}
66
67#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
71#[tatara(keyword = "defworker-opcode")]
72pub struct WorkerOpcode {
73 pub name: String,
74 pub code: u32,
75 pub direction: OpcodeDirection,
76 #[serde(rename = "requestFields")]
78 pub request_fields: Vec<WireType>,
79 #[serde(default, rename = "responseFields")]
81 pub response_fields: Vec<WireType>,
82 #[serde(default, rename = "sinceVersion")]
84 pub since_version: u32,
85 #[serde(default)]
87 pub deprecated: bool,
88}
89
90#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
92pub enum OpcodeDirection {
93 ClientToDaemon,
95 DaemonToClient,
97}
98
99#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
103pub enum WireType {
104 U64,
106 Str,
108 Bytes,
110 Bool,
112 StorePath,
115 StringList,
117 StorePathList,
119 PathInfo,
121 ValidPathInfo,
123 DerivationOutputs,
125 RealisationsMap,
127 KeyedBuildResult,
129 Substitutables,
131 KeyValueAttrs,
133 BuildMode,
135}
136
137pub const CANONICAL_WORKER_PROTOCOL_LISP: &str =
140 include_str!("../specs/worker_protocol.lisp");
141
142pub fn load_canonical_protocols() -> Result<Vec<WorkerProtocol>, SpecError> {
148 crate::loader::load_all::<WorkerProtocol>(CANONICAL_WORKER_PROTOCOL_LISP)
149}
150
151pub fn load_canonical_opcodes() -> Result<Vec<WorkerOpcode>, SpecError> {
157 crate::loader::load_all::<WorkerOpcode>(CANONICAL_WORKER_PROTOCOL_LISP)
158}
159
160pub fn load_opcode_named(name: &str) -> Result<WorkerOpcode, SpecError> {
166 load_canonical_opcodes()?
167 .into_iter()
168 .find(|o| o.name == name)
169 .ok_or_else(|| SpecError::Load(format!("no (defworker-opcode) with :name {name:?}")))
170}
171
172pub fn load_opcode_by_code(code: u32) -> Result<WorkerOpcode, SpecError> {
179 load_canonical_opcodes()?
180 .into_iter()
181 .find(|o| o.code == code)
182 .ok_or_else(|| SpecError::Load(format!("no (defworker-opcode) with :code {code}")))
183}
184
185#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct DispatchOutcome {
190 pub opcode_name: String,
192 pub opcode_code: u32,
194 pub response_body: Vec<u8>,
197}
198
199pub trait WorkerProtocolHandler {
208 fn dispatch(&self, opcode: &WorkerOpcode, body: &[u8])
216 -> Result<Vec<u8>, String>
217 {
218 Err(format!(
219 "unhandled opcode `{}` (code {}) — handler must override dispatch \
220 or implement an explicit case",
221 opcode.name, opcode.code,
222 ))
223 }
224}
225
226pub fn apply<H: WorkerProtocolHandler>(
236 code: u32,
237 body: &[u8],
238 handler: &H,
239) -> Result<DispatchOutcome, SpecError> {
240 let opcode = load_opcode_by_code(code).map_err(|_| SpecError::Interp {
241 phase: "unknown-opcode".into(),
242 message: format!(
243 "received opcode {code} but no (defworker-opcode :code {code}) \
244 is authored in the canonical worker-protocol spec",
245 ),
246 })?;
247 let response_body = handler.dispatch(&opcode, body).map_err(|e| SpecError::Interp {
248 phase: "dispatch-failed".into(),
249 message: format!("opcode `{}` (code {code}): {e}", opcode.name),
250 })?;
251 Ok(DispatchOutcome {
252 opcode_name: opcode.name,
253 opcode_code: code,
254 response_body,
255 })
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261 use std::collections::{HashMap, HashSet};
262
263 #[test]
264 fn canonical_protocol_parses() {
265 let protos = load_canonical_protocols().unwrap();
266 assert!(!protos.is_empty());
267 }
268
269 #[test]
270 fn canonical_opcodes_parse() {
271 let opcodes = load_canonical_opcodes().unwrap();
272 assert!(
273 opcodes.len() >= 25,
274 "canonical worker protocol must declare at least 25 opcodes, got {}",
275 opcodes.len(),
276 );
277 }
278
279 #[test]
280 fn opcodes_have_unique_codes() {
281 let opcodes = load_canonical_opcodes().unwrap();
282 let mut by_code: HashMap<u32, Vec<&str>> = HashMap::new();
283 for op in &opcodes {
284 by_code.entry(op.code).or_default().push(op.name.as_str());
285 }
286 for (code, names) in &by_code {
287 assert_eq!(
288 names.len(),
289 1,
290 "opcode {code} has multiple authored entries: {names:?}",
291 );
292 }
293 }
294
295 #[test]
296 fn essential_opcodes_present() {
297 let opcodes = load_canonical_opcodes().unwrap();
298 let names: HashSet<&str> = opcodes.iter().map(|o| o.name.as_str()).collect();
299 for required in [
301 "IsValidPath",
302 "QueryPathInfo",
303 "AddToStore",
304 "BuildPaths",
305 "BuildDerivation",
306 "QueryReferrers",
307 "QueryValidPaths",
308 "CollectGarbage",
309 "NarFromPath",
310 "AddTempRoot",
311 ] {
312 assert!(
313 names.contains(required),
314 "canonical worker protocol missing opcode `{required}`",
315 );
316 }
317 }
318
319 #[test]
320 fn query_path_info_has_correct_shape() {
321 let op = load_opcode_named("QueryPathInfo").unwrap();
322 assert_eq!(op.code, 26);
323 assert_eq!(op.direction, OpcodeDirection::ClientToDaemon);
324 assert_eq!(op.request_fields, vec![WireType::StorePath]);
325 }
326
327 #[test]
328 fn ca_realisation_opcodes_present() {
329 let names: HashSet<String> = load_canonical_opcodes()
331 .unwrap()
332 .into_iter()
333 .map(|o| o.name)
334 .collect();
335 assert!(names.contains("QueryRealisation"));
336 assert!(names.contains("RegisterDrvOutput"));
337 }
338
339 use std::cell::RefCell;
342
343 struct LoggingHandler {
344 seen: RefCell<Vec<(String, u32)>>,
345 response_for: HashMap<u32, Vec<u8>>,
346 }
347
348 impl LoggingHandler {
349 fn new() -> Self {
350 Self {
351 seen: RefCell::new(Vec::new()),
352 response_for: HashMap::new(),
353 }
354 }
355 fn responds(mut self, code: u32, body: &[u8]) -> Self {
356 self.response_for.insert(code, body.to_vec());
357 self
358 }
359 }
360
361 impl WorkerProtocolHandler for LoggingHandler {
362 fn dispatch(&self, opcode: &WorkerOpcode, _body: &[u8])
363 -> Result<Vec<u8>, String>
364 {
365 self.seen.borrow_mut().push((opcode.name.clone(), opcode.code));
366 self.response_for
367 .get(&opcode.code)
368 .cloned()
369 .ok_or_else(|| format!("no response configured for code {}", opcode.code))
370 }
371 }
372
373 #[test]
374 fn dispatch_known_opcode_succeeds() {
375 let handler = LoggingHandler::new().responds(1, b"\x01"); let outcome = apply(1, b"", &handler).unwrap();
377 assert_eq!(outcome.opcode_name, "IsValidPath");
378 assert_eq!(outcome.opcode_code, 1);
379 assert_eq!(outcome.response_body, vec![1u8]);
380 let log = handler.seen.borrow();
381 assert_eq!(log.len(), 1);
382 assert_eq!(log[0].0, "IsValidPath");
383 }
384
385 #[test]
386 fn dispatch_unknown_opcode_errors() {
387 let handler = LoggingHandler::new();
388 let err = apply(9999, b"", &handler).unwrap_err();
389 match err {
390 SpecError::Interp { phase, message } => {
391 assert_eq!(phase, "unknown-opcode");
392 assert!(message.contains("9999"));
393 }
394 _ => panic!("expected unknown-opcode"),
395 }
396 }
397
398 #[test]
399 fn dispatch_handler_failure_surfaces_as_dispatch_failed() {
400 let handler = LoggingHandler::new(); let err = apply(1, b"", &handler).unwrap_err();
403 match err {
404 SpecError::Interp { phase, message } => {
405 assert_eq!(phase, "dispatch-failed");
406 assert!(message.contains("IsValidPath"));
407 }
408 _ => panic!("expected dispatch-failed"),
409 }
410 }
411
412 #[test]
413 fn load_opcode_by_code_finds_known() {
414 let op = load_opcode_by_code(26).unwrap(); assert_eq!(op.name, "QueryPathInfo");
416 }
417
418 #[test]
419 fn load_opcode_by_code_errors_on_missing() {
420 let err = load_opcode_by_code(99999).unwrap_err();
421 match err {
422 SpecError::Load(msg) => assert!(msg.contains("99999")),
423 _ => panic!("expected Load error"),
424 }
425 }
426
427 #[test]
428 fn default_handler_returns_unhandled() {
429 struct Empty;
430 impl WorkerProtocolHandler for Empty {}
431 let err = apply(1, b"", &Empty).unwrap_err();
432 match err {
433 SpecError::Interp { phase, .. } => assert_eq!(phase, "dispatch-failed"),
434 _ => panic!("expected dispatch-failed"),
435 }
436 }
437}