1pub const MAGIC_REQ: &[u8; 4] = b"\0REQ";
24pub const MAGIC_RES: &[u8; 4] = b"\0RES";
25pub const HEADER_LEN: usize = 12;
26
27pub const MAX_PACKET_BYTES: usize = 1024 * 1024;
32
33pub const MAX_ADMIN_LINE: usize = 1024;
35
36pub const MAX_FUNCTION_NAME: usize = 512;
38pub const MAX_UNIQUE: usize = 64;
39
40pub const CAN_DO: u32 = 1;
42pub const CANT_DO: u32 = 2;
43pub const RESET_ABILITIES: u32 = 3;
44pub const PRE_SLEEP: u32 = 4;
45pub const SUBMIT_JOB: u32 = 7;
46pub const JOB_CREATED: u32 = 8;
47pub const GRAB_JOB: u32 = 9;
48pub const WORK_STATUS: u32 = 12;
49pub const WORK_COMPLETE: u32 = 13;
50pub const WORK_FAIL: u32 = 14;
51pub const GET_STATUS: u32 = 15;
52pub const ECHO_REQ: u32 = 16;
53pub const ECHO_RES: u32 = 17;
54pub const SUBMIT_JOB_BG: u32 = 18;
55pub const ERROR: u32 = 19;
56pub const STATUS_RES: u32 = 20;
57pub const SUBMIT_JOB_HIGH: u32 = 21;
58pub const SET_CLIENT_ID: u32 = 22;
59pub const CAN_DO_TIMEOUT: u32 = 23;
60pub const ALL_YOURS: u32 = 24;
61pub const WORK_EXCEPTION: u32 = 25;
62pub const OPTION_REQ: u32 = 26;
63pub const OPTION_RES: u32 = 27;
64pub const WORK_DATA: u32 = 28;
65pub const WORK_WARNING: u32 = 29;
66pub const GRAB_JOB_UNIQ: u32 = 30;
67pub const SUBMIT_JOB_HIGH_BG: u32 = 32;
68pub const SUBMIT_JOB_LOW: u32 = 33;
69pub const SUBMIT_JOB_LOW_BG: u32 = 34;
70pub const GRAB_JOB_ALL: u32 = 39;
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct Header {
75 pub request: bool,
77 pub packet_type: u32,
78 pub size: u32,
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum HeaderError {
83 BadMagic,
84 TooLarge(u32),
86}
87
88pub fn parse_header(bytes: &[u8]) -> Result<Header, HeaderError> {
90 if bytes.len() < HEADER_LEN {
91 return Err(HeaderError::BadMagic);
92 }
93 let request = match &bytes[..4] {
94 m if m == MAGIC_REQ => true,
95 m if m == MAGIC_RES => false,
96 _ => return Err(HeaderError::BadMagic),
97 };
98 let packet_type = u32::from_be_bytes(bytes[4..8].try_into().unwrap());
99 let size = u32::from_be_bytes(bytes[8..12].try_into().unwrap());
100 if size as usize > MAX_PACKET_BYTES {
101 return Err(HeaderError::TooLarge(size));
102 }
103 Ok(Header {
104 request,
105 packet_type,
106 size,
107 })
108}
109
110pub fn encode(request: bool, packet_type: u32, args: &[&[u8]]) -> Vec<u8> {
112 let size: usize = args.iter().map(|a| a.len()).sum::<usize>() + args.len().saturating_sub(1);
113 let mut out = Vec::with_capacity(HEADER_LEN + size);
114 out.extend_from_slice(if request { MAGIC_REQ } else { MAGIC_RES });
115 out.extend_from_slice(&packet_type.to_be_bytes());
116 out.extend_from_slice(&(size as u32).to_be_bytes());
117 for (i, arg) in args.iter().enumerate() {
118 if i > 0 {
119 out.push(0);
120 }
121 out.extend_from_slice(arg);
122 }
123 out
124}
125
126pub fn response(packet_type: u32, args: &[&[u8]]) -> Vec<u8> {
128 encode(false, packet_type, args)
129}
130
131pub fn split_args(data: &[u8], n: usize) -> Option<Vec<&[u8]>> {
134 if n == 0 {
135 return data.is_empty().then(Vec::new);
136 }
137 let mut out = Vec::with_capacity(n);
138 let mut rest = data;
139 for _ in 0..n - 1 {
140 let at = rest.iter().position(|b| *b == 0)?;
141 out.push(&rest[..at]);
142 rest = &rest[at + 1..];
143 }
144 out.push(rest);
145 Some(out)
146}
147
148pub fn request_arg_count(packet_type: u32) -> Option<usize> {
150 Some(match packet_type {
151 SUBMIT_JOB | SUBMIT_JOB_BG | SUBMIT_JOB_HIGH | SUBMIT_JOB_HIGH_BG | SUBMIT_JOB_LOW
152 | SUBMIT_JOB_LOW_BG => 3,
153 GET_STATUS | ECHO_REQ | OPTION_REQ | SET_CLIENT_ID => 1,
154 _ => return None,
155 })
156}
157
158pub fn is_worker_packet(packet_type: u32) -> bool {
161 matches!(
162 packet_type,
163 CAN_DO
164 | CANT_DO
165 | RESET_ABILITIES
166 | PRE_SLEEP
167 | GRAB_JOB
168 | WORK_STATUS
169 | WORK_COMPLETE
170 | WORK_FAIL
171 | CAN_DO_TIMEOUT
172 | ALL_YOURS
173 | WORK_EXCEPTION
174 | WORK_DATA
175 | WORK_WARNING
176 | GRAB_JOB_UNIQ
177 | GRAB_JOB_ALL
178 )
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
182pub enum Priority {
183 Normal,
184 High,
185 Low,
186}
187
188impl Priority {
189 pub fn name(self) -> &'static str {
190 match self {
191 Priority::Normal => "normal",
192 Priority::High => "high",
193 Priority::Low => "low",
194 }
195 }
196}
197
198#[derive(Debug, Clone, PartialEq, Eq)]
200pub enum Request {
201 Submit {
202 function: String,
203 unique: String,
204 workload: Vec<u8>,
205 priority: Priority,
206 background: bool,
207 },
208 GetStatus(Vec<u8>),
209 Echo(Vec<u8>),
210 Option(Vec<u8>),
211 SetClientId,
212 Worker(u32),
214 Unsupported(u32),
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub enum ArgError {
222 WrongArgCount,
224 BadName,
226}
227
228pub fn parse_request(header: &Header, data: &[u8]) -> Result<Request, ArgError> {
230 let t = header.packet_type;
231 if !header.request {
232 return Ok(Request::Unsupported(t));
233 }
234 if is_worker_packet(t) {
235 return Ok(Request::Worker(t));
236 }
237 let Some(n) = request_arg_count(t) else {
238 return Ok(Request::Unsupported(t));
239 };
240 let args = split_args(data, n).ok_or(ArgError::WrongArgCount)?;
241 Ok(match t {
242 SUBMIT_JOB | SUBMIT_JOB_BG | SUBMIT_JOB_HIGH | SUBMIT_JOB_HIGH_BG | SUBMIT_JOB_LOW
243 | SUBMIT_JOB_LOW_BG => {
244 let (function, unique) = (args[0], args[1]);
245 if function.is_empty()
246 || function.len() > MAX_FUNCTION_NAME
247 || unique.len() > MAX_UNIQUE
248 {
249 return Err(ArgError::BadName);
250 }
251 Request::Submit {
252 function: String::from_utf8_lossy(function).into_owned(),
253 unique: String::from_utf8_lossy(unique).into_owned(),
254 workload: args[2].to_vec(),
255 priority: match t {
256 SUBMIT_JOB_HIGH | SUBMIT_JOB_HIGH_BG => Priority::High,
257 SUBMIT_JOB_LOW | SUBMIT_JOB_LOW_BG => Priority::Low,
258 _ => Priority::Normal,
259 },
260 background: matches!(t, SUBMIT_JOB_BG | SUBMIT_JOB_HIGH_BG | SUBMIT_JOB_LOW_BG),
261 }
262 }
263 GET_STATUS => Request::GetStatus(args[0].to_vec()),
264 ECHO_REQ => Request::Echo(args[0].to_vec()),
265 OPTION_REQ => Request::Option(args[0].to_vec()),
266 _ => Request::SetClientId,
267 })
268}
269
270fn digits(n: u64) -> Vec<u8> {
272 n.to_string().into_bytes()
273}
274
275pub fn job_created(handle: &[u8]) -> Vec<u8> {
276 response(JOB_CREATED, &[handle])
277}
278pub fn work_status(handle: &[u8], numerator: u64, denominator: u64) -> Vec<u8> {
279 response(
280 WORK_STATUS,
281 &[handle, &digits(numerator), &digits(denominator)],
282 )
283}
284pub fn work_data(handle: &[u8], data: &[u8]) -> Vec<u8> {
285 response(WORK_DATA, &[handle, data])
286}
287pub fn work_complete(handle: &[u8], result: &[u8]) -> Vec<u8> {
288 response(WORK_COMPLETE, &[handle, result])
289}
290pub fn work_fail(handle: &[u8]) -> Vec<u8> {
291 response(WORK_FAIL, &[handle])
292}
293pub fn work_exception(handle: &[u8], text: &[u8]) -> Vec<u8> {
294 response(WORK_EXCEPTION, &[handle, text])
295}
296pub fn status_res(handle: &[u8], known: bool, running: bool, num: u64, den: u64) -> Vec<u8> {
297 response(
298 STATUS_RES,
299 &[
300 handle,
301 if known { b"1" } else { b"0" },
302 if running { b"1" } else { b"0" },
303 &digits(num),
304 &digits(den),
305 ],
306 )
307}
308
309pub fn error(code: &str, text: &str) -> Result<Vec<u8>, String> {
312 if code.is_empty()
313 || code.len() > 64
314 || !code.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'_')
315 {
316 return Err(format!(
317 "error code must be 1-64 letters, digits or '_', got {code:?}"
318 ));
319 }
320 if text.len() > 1024 || text.contains('\0') {
321 return Err("error text must be at most 1024 bytes and contain no NUL".to_string());
322 }
323 Ok(response(ERROR, &[code.as_bytes(), text.as_bytes()]))
324}
325
326pub fn read_response(packet: &[u8]) -> Option<(u32, Vec<Vec<u8>>)> {
330 let header = parse_header(packet).ok()?;
331 if header.request || packet.len() != HEADER_LEN + header.size as usize {
332 return None;
333 }
334 let n = match header.packet_type {
335 JOB_CREATED | WORK_FAIL | ECHO_RES | OPTION_RES => 1,
336 WORK_COMPLETE | WORK_DATA | WORK_WARNING | WORK_EXCEPTION | ERROR => 2,
337 WORK_STATUS => 3,
338 STATUS_RES => 5,
339 _ => return None,
340 };
341 let args = split_args(&packet[HEADER_LEN..], n)?;
342 Some((
343 header.packet_type,
344 args.into_iter().map(<[u8]>::to_vec).collect(),
345 ))
346}
347
348#[derive(Debug, Clone, PartialEq, Eq)]
350pub enum AdminCommand {
351 Status,
352 Workers,
353 Version,
354 MaxQueue,
356 Shutdown,
358 Unknown,
359}
360
361pub fn parse_admin(line: &str) -> AdminCommand {
362 match line.split_whitespace().next().unwrap_or("") {
363 "status" => AdminCommand::Status,
364 "workers" => AdminCommand::Workers,
365 "version" => AdminCommand::Version,
366 "maxqueue" => AdminCommand::MaxQueue,
367 "shutdown" => AdminCommand::Shutdown,
368 _ => AdminCommand::Unknown,
369 }
370}
371
372pub fn admin_error(code: &str, text: &str) -> String {
374 format!("ERR {code} {}\n", text.replace(' ', "+"))
375}
376
377pub fn admin_status(rows: &[(String, u64, u64, u64)]) -> String {
379 let mut out = String::new();
380 for (function, total, running, workers) in rows {
381 let name = crate::utils::sanitize::line_field(function);
382 out.push_str(&format!("{name}\t{total}\t{running}\t{workers}\n"));
383 }
384 out.push_str(".\n");
385 out
386}