1use crate::RetryPolicy;
8use ledgence_worker_api::{Error, ErrorKind, ProgramRef, Result, decode_json, validate_wire_value};
9use serde::{
10 Deserialize, Deserializer, Serialize,
11 de::{self, DeserializeOwned, MapAccess, SeqAccess, Visitor},
12};
13use serde_json::{Map, Number, Value};
14use std::{fmt, io};
15
16pub const SUBMISSION_DATA_MAX_BYTES: usize = ledgence_worker_api::APPLICATION_INPUT_MAX_BYTES;
22
23pub const SUBMISSION_MAX_BYTES: usize = 2 * 1024 * 1024;
25
26#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(deny_unknown_fields)]
46pub struct SubmitTask {
47 pub tenant_id: String,
48 pub namespace: String,
49 pub queue: String,
50 pub program: ProgramRef,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub correlation_key: Option<String>,
53 pub data: Value,
54 #[serde(default)]
55 pub retry_policy: RetryPolicy,
56 #[serde(default = "default_attempt_timeout_ms")]
57 pub attempt_timeout_ms: u64,
58}
59
60const fn default_attempt_timeout_ms() -> u64 {
61 300_000
62}
63
64impl SubmitTask {
65 pub fn validate(&self) -> Result<()> {
70 for (name, value) in [
71 ("tenant_id", self.tenant_id.as_str()),
72 ("namespace", self.namespace.as_str()),
73 ("queue", self.queue.as_str()),
74 ] {
75 if value.is_empty() || value.len() > 128 || value.chars().any(char::is_control) {
76 return Err(invalid(format!(
77 "{name} must contain 1–128 UTF-8 bytes and no control characters"
78 )));
79 }
80 }
81 if self
82 .tenant_id
83 .chars()
84 .chain(self.namespace.chars())
85 .chain(self.queue.chars())
86 .any(|character| {
87 let code = u32::from(character);
88 (0xfdd0..=0xfdef).contains(&code) || code & 0xffff >= 0xfffe
89 })
90 {
91 return Err(invalid(
92 "tenant_id, namespace, and queue must not contain Unicode noncharacters",
93 ));
94 }
95 if let Some(key) = &self.correlation_key
96 && (key.len() > 512 || key.chars().any(char::is_control))
97 {
98 return Err(invalid(
99 "correlation_key must contain at most 512 UTF-8 bytes and no control characters",
100 ));
101 }
102 self.program.validate()?;
103 self.retry_policy.validate()?;
104 if !(60_000..=86_400_000).contains(&self.attempt_timeout_ms) {
105 return Err(invalid(
106 "attempt_timeout_ms must be between 60000 and 86400000",
107 ));
108 }
109 validate_wire_value(&self.data)
110 .map_err(|_| invalid("submission data must not exceed 64 nested arrays or objects"))?;
111 check_encoded_size(&self.data, SUBMISSION_DATA_MAX_BYTES, "submission data")?;
112 check_encoded_size(self, SUBMISSION_MAX_BYTES, "submission")
113 }
114
115 pub fn decode(bytes: &[u8]) -> Result<Self> {
122 let submission: Self = decode_unique_json(bytes, SUBMISSION_MAX_BYTES)?;
123 submission.validate()?;
124 Ok(submission)
125 }
126
127 pub fn canonical_bytes(&self) -> Result<Vec<u8>> {
139 self.validate()?;
140 let value = serde_json::to_value(self)
141 .map_err(|error| invalid(format!("cannot encode submission: {error}")))?;
142 canonical_owned_json_bytes(value)
143 }
144
145 pub fn semantically_matches(&self, other: &Self) -> Result<bool> {
150 Ok(self.canonical_bytes()? == other.canonical_bytes()?)
151 }
152}
153
154pub fn decode_unique_json<T: DeserializeOwned>(bytes: &[u8], max_bytes: usize) -> Result<T> {
163 if bytes.len() > max_bytes {
164 return Err(invalid(format!(
165 "JSON request exceeds its {max_bytes}-byte limit"
166 )));
167 }
168 let StrictValue(value) = decode_json(bytes)?;
169 serde_json::from_value(value).map_err(|error| invalid(format!("invalid JSON command: {error}")))
170}
171
172pub fn canonical_json_bytes(value: &Value) -> Result<Vec<u8>> {
185 canonical_owned_json_bytes(value.clone())
186}
187
188fn canonical_owned_json_bytes(mut value: Value) -> Result<Vec<u8>> {
189 sort_objects(&mut value);
190 serde_json::to_vec(&value)
191 .map_err(|error| invalid(format!("cannot encode canonical JSON: {error}")))
192}
193
194fn invalid(message: impl Into<String>) -> Error {
195 Error::new(ErrorKind::InvalidInput, message)
196}
197
198pub(crate) fn check_encoded_size(value: &impl Serialize, limit: usize, label: &str) -> Result<()> {
199 let mut counter = ByteCounter { written: 0, limit };
200 serde_json::to_writer(&mut counter, value)
201 .map_err(|_| invalid(format!("{label} exceeds its {limit}-byte JSON limit")))
202}
203
204struct ByteCounter {
205 written: usize,
206 limit: usize,
207}
208
209impl io::Write for ByteCounter {
210 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
211 if bytes.len() > self.limit - self.written {
212 return Err(io::Error::other("JSON byte limit exceeded"));
213 }
214 self.written += bytes.len();
215 Ok(bytes.len())
216 }
217
218 fn flush(&mut self) -> io::Result<()> {
219 Ok(())
220 }
221}
222
223fn sort_objects(value: &mut Value) {
224 match value {
225 Value::Array(values) => values.iter_mut().for_each(sort_objects),
226 Value::Object(values) => {
227 let mut sorted: Vec<_> = std::mem::take(values).into_iter().collect();
228 sorted.sort_unstable_by(|(left, _), (right, _)| left.cmp(right));
229 for (key, mut value) in sorted {
230 sort_objects(&mut value);
231 values.insert(key, value);
232 }
233 }
234 _ => {}
235 }
236}
237
238struct StrictValue(Value);
240
241impl<'de> Deserialize<'de> for StrictValue {
242 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
243 struct StrictVisitor;
244
245 impl<'de> Visitor<'de> for StrictVisitor {
246 type Value = StrictValue;
247
248 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
249 formatter.write_str("a JSON value without duplicate object keys")
250 }
251
252 fn visit_bool<E: de::Error>(self, value: bool) -> std::result::Result<Self::Value, E> {
253 Ok(StrictValue(Value::Bool(value)))
254 }
255
256 fn visit_i64<E: de::Error>(self, value: i64) -> std::result::Result<Self::Value, E> {
257 Ok(StrictValue(Value::Number(value.into())))
258 }
259
260 fn visit_u64<E: de::Error>(self, value: u64) -> std::result::Result<Self::Value, E> {
261 Ok(StrictValue(Value::Number(value.into())))
262 }
263
264 fn visit_f64<E: de::Error>(self, value: f64) -> std::result::Result<Self::Value, E> {
265 Number::from_f64(value)
266 .map(|number| StrictValue(Value::Number(number)))
267 .ok_or_else(|| E::custom("nonfinite JSON number"))
268 }
269
270 fn visit_str<E: de::Error>(self, value: &str) -> std::result::Result<Self::Value, E> {
271 Ok(StrictValue(Value::String(value.to_owned())))
272 }
273
274 fn visit_string<E: de::Error>(
275 self,
276 value: String,
277 ) -> std::result::Result<Self::Value, E> {
278 Ok(StrictValue(Value::String(value)))
279 }
280
281 fn visit_unit<E: de::Error>(self) -> std::result::Result<Self::Value, E> {
282 Ok(StrictValue(Value::Null))
283 }
284
285 fn visit_seq<A: SeqAccess<'de>>(
286 self,
287 mut sequence: A,
288 ) -> std::result::Result<Self::Value, A::Error> {
289 let mut values = Vec::new();
290 while let Some(StrictValue(value)) = sequence.next_element()? {
291 values.push(value);
292 }
293 Ok(StrictValue(Value::Array(values)))
294 }
295
296 fn visit_map<A: MapAccess<'de>>(
297 self,
298 mut object: A,
299 ) -> std::result::Result<Self::Value, A::Error> {
300 let mut values = Map::new();
301 while let Some(key) = object.next_key::<String>()? {
302 if values.contains_key(&key) {
303 return Err(de::Error::custom("duplicate JSON object key"));
304 }
305 let StrictValue(value) = object.next_value()?;
306 values.insert(key, value);
307 }
308 Ok(StrictValue(Value::Object(values)))
309 }
310 }
311
312 deserializer.deserialize_any(StrictVisitor)
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use serde_json::json;
320
321 fn request(data: &str) -> Vec<u8> {
322 format!(
323 r#"{{"tenant_id":"acme","namespace":"billing","queue":"python","program":{{"id":"invoice","version":"1.0.0"}},"data":{data}}}"#
324 )
325 .into_bytes()
326 }
327
328 fn submission(data: &str) -> SubmitTask {
329 SubmitTask::decode(&request(data)).unwrap()
330 }
331
332 #[test]
333 fn omitted_optional_settings_use_explicit_defaults() {
334 let implicit = submission("null");
335 assert_eq!(implicit.correlation_key, None);
336 assert_eq!(implicit.retry_policy.max_attempts, 3);
337 assert_eq!(implicit.retry_policy.retry_delay_ms, 5_000);
338 assert_eq!(implicit.attempt_timeout_ms, 300_000);
339 let mut explicit: Value = decode_json(&request("null")).unwrap();
340 explicit["correlation_key"] = Value::Null;
341 explicit["retry_policy"] = json!({"max_attempts": 3, "retry_delay_ms": 5000});
342 explicit["attempt_timeout_ms"] = json!(300_000);
343 let explicit = SubmitTask::decode(&serde_json::to_vec(&explicit).unwrap()).unwrap();
344 assert!(implicit.semantically_matches(&explicit).unwrap());
345 }
346
347 #[test]
348 fn duplicate_keys_are_rejected_before_collapse_at_every_depth() {
349 for data in [
350 r#"{"x":1,"x":2}"#,
351 r#"[{"outer":{"x":1,"\u0078":1}}]"#,
352 r#"{"\u0000":null,"\u0000":true}"#,
353 ] {
354 let error = SubmitTask::decode(&request(data)).unwrap_err();
355 assert_eq!(error.kind, ErrorKind::InvalidInput);
356 assert!(error.message.contains("duplicate JSON object key"));
357 }
358 let duplicated = String::from_utf8(request("null")).unwrap().replacen(
359 "\"tenant_id\":\"acme\"",
360 "\"tenant_id\":\"acme\",\"tenant_id\":\"acme\"",
361 1,
362 );
363 assert!(SubmitTask::decode(duplicated.as_bytes()).is_err());
364 let duplicate_program = String::from_utf8(request("null")).unwrap().replace(
365 "\"id\":\"invoice\"",
366 "\"id\":\"invoice\",\"id\":\"invoice\"",
367 );
368 assert!(SubmitTask::decode(duplicate_program.as_bytes()).is_err());
369 assert!(submission(r#"[{"x":1},{"x":2}]"#).validate().is_ok());
370 }
371
372 #[test]
373 fn integer_limits_and_exact_large_integer_roundtrip() {
374 let accepted = submission("[-9223372036854775808,18446744073709551615,9007199254740993]");
375 assert_eq!(accepted.data[0].as_i64(), Some(i64::MIN));
376 assert_eq!(accepted.data[1].as_u64(), Some(u64::MAX));
377 assert_eq!(accepted.data[2].as_u64(), Some(9_007_199_254_740_993));
378 let roundtrip = SubmitTask::decode(&accepted.canonical_bytes().unwrap()).unwrap();
379 assert!(accepted.semantically_matches(&roundtrip).unwrap());
380 for rejected in ["-9223372036854775809", "18446744073709551616"] {
381 assert!(SubmitTask::decode(&request(rejected)).is_err());
382 }
383 }
384
385 #[test]
386 fn user_strings_and_keys_preserve_nul_and_numeric_looking_text() {
387 let task = submission(
388 r#"{"\u0000":"left\u0000right","$serde_json::private::Number":"18446744073709551616"}"#,
389 );
390 assert_eq!(task.data["\0"], json!("left\0right"));
391 let roundtrip = SubmitTask::decode(&task.canonical_bytes().unwrap()).unwrap();
392 assert!(task.semantically_matches(&roundtrip).unwrap());
393 }
394
395 #[test]
396 fn binary64_spellings_normalize_without_erasing_number_kinds_or_signed_zero() {
397 for (left, right) in [("1e0", "1.0"), ("2.5e2", "250.0"), ("-0", "-0.0")] {
398 assert!(
399 submission(left)
400 .semantically_matches(&submission(right))
401 .unwrap()
402 );
403 }
404 for (left, right) in [("1", "1.0"), ("0", "0.0"), ("0.0", "-0.0"), ("0", "-0")] {
405 assert!(
406 !submission(left)
407 .semantically_matches(&submission(right))
408 .unwrap()
409 );
410 }
411 let task = submission("[2.291712365432881e-09,-1.527077339613215e-236,-0.0]");
412 let restored = SubmitTask::decode(&task.canonical_bytes().unwrap()).unwrap();
413 for (actual, expected) in restored.data.as_array().unwrap().iter().zip([
414 2.291712365432881e-09_f64,
415 -1.527077339613215e-236_f64,
416 -0.0_f64,
417 ]) {
418 assert_eq!(actual.as_f64().unwrap().to_bits(), expected.to_bits());
419 }
420 }
421
422 #[test]
423 fn comparison_ignores_object_order_but_preserves_arrays_and_values() {
424 let left = submission(r#"{"b":[{"z":0,"a":1},2],"a":"same"}"#);
425 let reordered = submission(r#"{"a":"same","b":[{"a":1,"z":0},2]}"#);
426 assert!(left.semantically_matches(&reordered).unwrap());
427 assert!(
428 !left
429 .semantically_matches(&submission(r#"{"a":"same","b":[2,{"a":1,"z":0}]}"#))
430 .unwrap()
431 );
432 assert!(
433 !left
434 .semantically_matches(&submission(r#"{"a":"different","b":[{"a":1,"z":0},2]}"#))
435 .unwrap()
436 );
437 }
438
439 #[test]
440 fn shared_json_comparison_preserves_nested_report_number_classes() {
441 let left: Value =
442 decode_json(br#"{"outcome":{"z":-0.0,"a":9007199254740993},"attempt":1}"#).unwrap();
443 let reordered: Value =
444 decode_json(br#"{"attempt":1,"outcome":{"a":9007199254740993,"z":-0e0}}"#).unwrap();
445 assert_eq!(
446 canonical_json_bytes(&left).unwrap(),
447 canonical_json_bytes(&reordered).unwrap()
448 );
449 for changed in [
450 br#"{"attempt":1,"outcome":{"a":9007199254740993,"z":0.0}}"#.as_slice(),
451 br#"{"attempt":1.0,"outcome":{"a":9007199254740993,"z":-0.0}}"#.as_slice(),
452 ] {
453 let changed: Value = decode_json(changed).unwrap();
454 assert_ne!(
455 canonical_json_bytes(&left).unwrap(),
456 canonical_json_bytes(&changed).unwrap()
457 );
458 }
459 }
460
461 #[test]
462 fn all_submission_settings_participate_in_comparison() {
463 let original = submission("null");
464 let mut changed = Vec::new();
465 let mut task = original.clone();
466 task.program.version = "2.0.0".into();
467 changed.push(task);
468 let mut task = original.clone();
469 task.retry_policy.max_attempts = 2;
470 changed.push(task);
471 let mut task = original.clone();
472 task.retry_policy.retry_delay_ms += 1;
473 changed.push(task);
474 let mut task = original.clone();
475 task.correlation_key = Some("invoice:1".into());
476 changed.push(task);
477 let mut task = original.clone();
478 task.attempt_timeout_ms += 1;
479 changed.push(task);
480 let mut task = original.clone();
481 task.tenant_id = "other".into();
482 changed.push(task);
483 let mut task = original.clone();
484 task.namespace = "other".into();
485 changed.push(task);
486 let mut task = original.clone();
487 task.queue = "other".into();
488 changed.push(task);
489 for task in changed {
490 assert!(!original.semantically_matches(&task).unwrap());
491 }
492 }
493
494 #[test]
495 fn metadata_uses_utf8_byte_limits_and_rejects_controls() {
496 let mut task = submission("null");
497 task.tenant_id = "é".repeat(64);
498 task.correlation_key = Some("é".repeat(256));
499 assert!(task.validate().is_ok());
500 task.tenant_id.push('é');
501 assert!(task.validate().is_err());
502 task.tenant_id = "acme".into();
503 task.correlation_key.as_mut().unwrap().push('é');
504 assert!(task.validate().is_err());
505 task.correlation_key = Some("business\u{85}reference".into());
506 assert!(task.validate().is_err());
507 task.correlation_key = None;
508 for value in ["", "line\nbreak", "nul\0value"] {
509 task.queue = value.into();
510 assert!(task.validate().is_err());
511 }
512 }
513
514 #[test]
515 fn platform_identifiers_reject_noncharacters_before_the_task_can_be_accepted() {
516 for character in [
517 '\u{fdd0}',
518 '\u{fdef}',
519 '\u{fffe}',
520 '\u{ffff}',
521 '\u{1fffe}',
522 '\u{10ffff}',
523 ] {
524 let mut task = submission("null");
525 task.tenant_id = format!("tenant{character}");
526 assert!(task.validate().is_err());
527 task.tenant_id = "acme".into();
528 task.namespace = format!("namespace{character}");
529 assert!(task.validate().is_err());
530 task.namespace = "billing".into();
531 task.queue = format!("queue{character}");
532 assert!(task.validate().is_err());
533 }
534 let mut task = submission("null");
535 task.correlation_key = Some("reference\u{10ffff}".into());
536 assert!(task.validate().is_ok());
537 }
538
539 #[test]
540 fn generic_command_decoder_rejects_nested_duplicate_keys_and_respects_limits() {
541 let bytes = br#"{"report":{"output":{"value":1,"\u0076alue":2}}}"#;
542 assert!(decode_unique_json::<Value>(bytes, bytes.len()).is_err());
543 let valid = br#"{"report":{"output":9007199254740993}}"#;
544 assert!(decode_unique_json::<Value>(valid, valid.len() - 1).is_err());
545 let decoded: Value = decode_unique_json(valid, valid.len()).unwrap();
546 assert_eq!(
547 decoded["report"]["output"].as_u64(),
548 Some(9_007_199_254_740_993)
549 );
550 assert!(
551 decode_unique_json::<Value>(br#"{"report":{"output":18446744073709551616}}"#, 1000)
552 .is_err()
553 );
554 }
555
556 #[test]
557 fn data_size_counts_compact_json_bytes_including_string_escapes() {
558 let mut task = submission("null");
559 task.data = Value::String("x".repeat(SUBMISSION_DATA_MAX_BYTES - 2));
560 assert!(task.validate().is_ok());
561 task.data = Value::String("x".repeat(SUBMISSION_DATA_MAX_BYTES - 1));
562 assert!(task.validate().is_err());
563 task.data = Value::String("\0".repeat(SUBMISSION_DATA_MAX_BYTES / 6 + 1));
564 assert!(task.validate().is_err());
565 }
566
567 #[test]
568 fn whole_request_limit_applies_to_incoming_whitespace() {
569 let mut bytes = request("null");
570 bytes.resize(SUBMISSION_MAX_BYTES, b' ');
571 assert!(SubmitTask::decode(&bytes).is_ok());
572 bytes.push(b' ');
573 assert!(SubmitTask::decode(&bytes).is_err());
574 }
575
576 #[test]
577 fn data_depth_64_is_allowed_and_65_is_rejected() {
578 let mut data = Value::Null;
579 for _ in 0..64 {
580 data = Value::Array(vec![data]);
581 }
582 let encoded = serde_json::to_string(&data).unwrap();
583 assert!(SubmitTask::decode(&request(&encoded)).is_ok());
584 data = Value::Array(vec![data]);
585 let encoded = serde_json::to_string(&data).unwrap();
586 assert!(SubmitTask::decode(&request(&encoded)).is_err());
587 }
588
589 #[test]
590 fn attempt_timeout_endpoints_are_inclusive() {
591 let mut task = submission("null");
592 for valid in [60_000, 86_400_000] {
593 task.attempt_timeout_ms = valid;
594 assert!(task.validate().is_ok());
595 }
596 for invalid in [0, 1, 59_999, 86_400_001] {
597 task.attempt_timeout_ms = invalid;
598 assert!(task.validate().is_err());
599 }
600 }
601
602 #[test]
603 fn invalid_schema_syntax_and_nonfinite_values_are_rejected() {
604 for data in ["1e400", "NaN", "[1 2]", "\"\\ud800\""] {
605 assert!(SubmitTask::decode(&request(data)).is_err());
606 }
607 let mut value: Value = decode_json(&request("null")).unwrap();
608 value["unexpected"] = json!(true);
609 assert!(SubmitTask::decode(&serde_json::to_vec(&value).unwrap()).is_err());
610 value.as_object_mut().unwrap().remove("unexpected");
611 value["program"]["version"] = json!("../invalid");
612 assert!(SubmitTask::decode(&serde_json::to_vec(&value).unwrap()).is_err());
613 }
614}