1use std::io::{self, Read, Write};
17
18use serde::de::Error as _;
19use serde::{Deserialize, Deserializer, Serialize, Serializer};
20
21use crate::segments::Segment;
22
23pub const REQUEST_SCHEMA: &str = "text-mirror/adapter-request@1";
25
26pub const RESPONSE_SCHEMA: &str = "text-mirror/adapter-response@1";
28
29pub const MAX_REQUEST_BYTES: u32 = 1024 * 1024;
33
34macro_rules! literal_schema {
35 ($name:ident, $value:expr) => {
36 #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
39 pub struct $name;
40
41 impl Serialize for $name {
42 fn serialize<S: Serializer>(
43 &self,
44 serializer: S,
45 ) -> std::result::Result<S::Ok, S::Error> {
46 serializer.serialize_str($value)
47 }
48 }
49
50 impl<'de> Deserialize<'de> for $name {
51 fn deserialize<D: Deserializer<'de>>(
52 deserializer: D,
53 ) -> std::result::Result<Self, D::Error> {
54 let value = String::deserialize(deserializer)?;
55 if value == $value {
56 Ok($name)
57 } else {
58 Err(D::Error::custom(format!(
59 "unrecognized schema {value:?}, expected {:?}",
60 $value
61 )))
62 }
63 }
64 }
65 };
66}
67
68literal_schema!(RequestSchema, REQUEST_SCHEMA);
69literal_schema!(ResponseSchema, RESPONSE_SCHEMA);
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
73#[serde(deny_unknown_fields)]
74pub struct Request {
75 pub schema: RequestSchema,
77 pub adapter: String,
79 pub payload: serde_json::Value,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
88#[serde(deny_unknown_fields)]
89pub struct Response {
90 pub schema: ResponseSchema,
92 #[serde(skip_serializing_if = "Option::is_none", default)]
94 pub ok: Option<serde_json::Value>,
95 #[serde(skip_serializing_if = "Option::is_none", default)]
97 pub error: Option<WireError>,
98}
99
100impl Response {
101 pub fn ok(value: serde_json::Value) -> Response {
103 Response {
104 schema: ResponseSchema,
105 ok: Some(value),
106 error: None,
107 }
108 }
109
110 pub fn err(code: &str, message: String) -> Response {
112 Response {
113 schema: ResponseSchema,
114 ok: None,
115 error: Some(WireError {
116 code: code.to_string(),
117 message,
118 }),
119 }
120 }
121
122 pub fn validate(&self) -> std::result::Result<(), ResponseValidationError> {
124 match (&self.ok, &self.error) {
125 (Some(_), None) | (None, Some(_)) => Ok(()),
126 (Some(_), Some(_)) => Err(ResponseValidationError::BothBodies),
127 (None, None) => Err(ResponseValidationError::NoBody),
128 }
129 }
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum ResponseValidationError {
135 BothBodies,
137 NoBody,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143#[serde(deny_unknown_fields)]
144pub struct WireError {
145 pub code: String,
147 pub message: String,
149}
150
151#[derive(Debug)]
153pub enum FrameRead {
154 Complete(Vec<u8>),
156 Empty,
158 Truncated,
160 Oversized {
163 declared: u64,
165 },
166}
167
168pub fn read_frame(reader: &mut impl Read, max_bytes: u32) -> io::Result<FrameRead> {
171 let mut header = [0u8; 4];
172 match read_exact_or_eof(reader, &mut header)? {
173 Fill::Empty => return Ok(FrameRead::Empty),
174 Fill::Partial => return Ok(FrameRead::Truncated),
175 Fill::Full => {}
176 }
177 let declared = u32::from_be_bytes(header);
178 if declared > max_bytes {
179 return Ok(FrameRead::Oversized {
180 declared: u64::from(declared),
181 });
182 }
183 let mut payload = vec![0u8; declared as usize];
184 match read_exact_or_eof(reader, &mut payload)? {
185 Fill::Full => Ok(FrameRead::Complete(payload)),
186 Fill::Empty | Fill::Partial => Ok(FrameRead::Truncated),
187 }
188}
189
190enum Fill {
191 Full,
192 Partial,
193 Empty,
194}
195
196fn read_exact_or_eof(reader: &mut impl Read, buffer: &mut [u8]) -> io::Result<Fill> {
197 let mut filled = 0;
198 while filled < buffer.len() {
199 match reader.read(&mut buffer[filled..]) {
200 Ok(0) => {
201 return Ok(if filled == 0 {
202 Fill::Empty
203 } else {
204 Fill::Partial
205 });
206 }
207 Ok(n) => filled += n,
208 Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
209 Err(e) => return Err(e),
210 }
211 }
212 Ok(Fill::Full)
213}
214
215pub fn write_frame<T: Serialize>(writer: &mut impl Write, value: &T) -> io::Result<()> {
217 let payload = serde_json::to_vec(value).map_err(io::Error::other)?;
218 let length = u32::try_from(payload.len())
219 .map_err(|_| io::Error::other("frame payload exceeds the u32 length prefix"))?;
220 writer.write_all(&length.to_be_bytes())?;
221 writer.write_all(&payload)?;
222 writer.flush()
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn a_non_recovered_pdf_response_omits_the_recovered_field() {
231 use bodies::PdfOk;
232 let plain = PdfOk {
237 text: "hi".to_string(),
238 warnings: Vec::new(),
239 segments: Vec::new(),
240 recovered: false,
241 };
242 let encoded = serde_json::to_string(&plain).unwrap();
243 assert!(
244 !encoded.contains("recovered"),
245 "false recovered is off the wire: {encoded}"
246 );
247 let back: PdfOk = serde_json::from_str(&encoded).unwrap();
249 assert!(!back.recovered);
250
251 let recovered = PdfOk {
252 recovered: true,
253 ..plain
254 };
255 let encoded = serde_json::to_string(&recovered).unwrap();
256 assert!(
257 encoded.contains("\"recovered\":true"),
258 "a recovered outcome carries the field: {encoded}"
259 );
260 }
261
262 #[test]
263 fn frames_roundtrip() {
264 let mut buffer = Vec::new();
265 let request = Request {
266 schema: RequestSchema,
267 adapter: "pdf".to_string(),
268 payload: serde_json::json!({"input": "input.pdf"}),
269 };
270 write_frame(&mut buffer, &request).unwrap();
271 let mut cursor = std::io::Cursor::new(buffer);
272 let FrameRead::Complete(payload) = read_frame(&mut cursor, MAX_REQUEST_BYTES).unwrap()
273 else {
274 panic!("expected a complete frame");
275 };
276 let back: Request = serde_json::from_slice(&payload).unwrap();
277 assert_eq!(back.adapter, "pdf");
278 }
279
280 #[test]
281 fn oversized_declarations_are_refused_before_allocation() {
282 let mut bytes = u32::MAX.to_be_bytes().to_vec();
283 bytes.extend_from_slice(b"junk");
284 let mut cursor = std::io::Cursor::new(bytes);
285 let FrameRead::Oversized { declared } = read_frame(&mut cursor, 1024).unwrap() else {
286 panic!("expected an oversized refusal");
287 };
288 assert_eq!(declared, u64::from(u32::MAX));
289 }
290
291 #[test]
292 fn truncation_is_detected_in_header_and_payload() {
293 let mut cursor = std::io::Cursor::new(vec![0u8, 0, 0]);
294 assert!(matches!(
295 read_frame(&mut cursor, 1024).unwrap(),
296 FrameRead::Truncated
297 ));
298
299 let mut bytes = 10u32.to_be_bytes().to_vec();
300 bytes.extend_from_slice(b"half");
301 let mut cursor = std::io::Cursor::new(bytes);
302 assert!(matches!(
303 read_frame(&mut cursor, 1024).unwrap(),
304 FrameRead::Truncated
305 ));
306
307 let mut cursor = std::io::Cursor::new(Vec::<u8>::new());
308 assert!(matches!(
309 read_frame(&mut cursor, 1024).unwrap(),
310 FrameRead::Empty
311 ));
312 }
313
314 #[test]
315 fn responses_require_exactly_one_body() {
316 assert!(Response::ok(serde_json::json!({})).validate().is_ok());
317 assert!(Response::err("io", "detail".to_string()).validate().is_ok());
318 let both = Response {
319 schema: ResponseSchema,
320 ok: Some(serde_json::json!({})),
321 error: Some(WireError {
322 code: "io".to_string(),
323 message: "detail".to_string(),
324 }),
325 };
326 assert_eq!(both.validate(), Err(ResponseValidationError::BothBodies));
327 let neither = Response {
328 schema: ResponseSchema,
329 ok: None,
330 error: None,
331 };
332 assert_eq!(neither.validate(), Err(ResponseValidationError::NoBody));
333 }
334
335 #[test]
336 fn probe_attempts_roundtrip_as_closed_wire_values() {
337 use bodies::{ProbeAttempt, ProbeName, ProbeOutcome, ProbeReport};
338
339 let report = ProbeReport {
340 attempts: crate::runner::runner_message_tests::IO_KINDS
341 .iter()
342 .copied()
343 .map(|error_kind| ProbeAttempt {
344 name: ProbeName::UnixSocket,
345 outcome: ProbeOutcome::Denied,
346 error_kind,
347 })
348 .collect(),
349 };
350 let value = serde_json::to_value(&report).unwrap();
351 let back: ProbeReport = serde_json::from_value(value).unwrap();
352 assert_eq!(
353 back.attempts.len(),
354 crate::runner::runner_message_tests::IO_KINDS.len()
355 );
356 for (attempt, expected_kind) in back
357 .attempts
358 .iter()
359 .zip(crate::runner::runner_message_tests::IO_KINDS)
360 {
361 assert_eq!(attempt.name, ProbeName::UnixSocket);
362 assert_eq!(attempt.outcome, ProbeOutcome::Denied);
363 assert_eq!(attempt.error_kind, *expected_kind);
364 }
365
366 let deadlock = serde_json::to_value(ProbeAttempt {
367 name: ProbeName::UnixSocket,
368 outcome: ProbeOutcome::Denied,
369 error_kind: io::ErrorKind::Deadlock,
370 })
371 .unwrap();
372 assert_eq!(deadlock["error_kind"], "deadlock");
373
374 let future_kind = serde_json::json!({
375 "name": "unix_socket",
376 "outcome": "denied",
377 "error_kind": "future_kind"
378 });
379 let fallback: ProbeAttempt = serde_json::from_value(future_kind).unwrap();
380 assert_eq!(fallback.error_kind, io::ErrorKind::Other);
381
382 let unknown = serde_json::json!({
383 "attempts": [{
384 "name": "unix_socket",
385 "outcome": "detail_from_worker",
386 "error_kind": "permission_denied"
387 }]
388 });
389 assert!(serde_json::from_value::<ProbeReport>(unknown).is_err());
390 }
391
392 #[test]
393 fn wrong_schema_values_are_rejected() {
394 let json = serde_json::to_string(&Response::ok(serde_json::json!({}))).unwrap();
395 let bumped = json.replace("adapter-response@1", "adapter-response@2");
396 assert!(serde_json::from_str::<Response>(&bumped).is_err());
397 }
398}
399
400pub mod bodies {
403 use std::io;
404
405 use super::*;
406
407 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
409 #[serde(rename_all = "snake_case")]
410 pub enum ProbeName {
411 Ipv4Tcp,
413 Ipv6Tcp,
415 UnixSocket,
417 UdpSend,
419 }
420
421 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
423 #[serde(rename_all = "snake_case")]
424 pub enum ProbeOutcome {
425 Connected,
427 Denied,
429 Timeout,
431 InvalidTarget,
433 }
434
435 mod error_kind_wire {
436 use super::*;
437
438 #[derive(Serialize, Deserialize)]
441 #[serde(rename_all = "snake_case")]
442 enum WireErrorKind {
443 NotFound,
444 PermissionDenied,
445 ConnectionRefused,
446 ConnectionReset,
447 HostUnreachable,
448 NetworkUnreachable,
449 ConnectionAborted,
450 NotConnected,
451 AddrInUse,
452 AddrNotAvailable,
453 NetworkDown,
454 BrokenPipe,
455 AlreadyExists,
456 WouldBlock,
457 NotADirectory,
458 IsADirectory,
459 DirectoryNotEmpty,
460 ReadOnlyFilesystem,
461 StaleNetworkFileHandle,
462 InvalidInput,
463 InvalidData,
464 TimedOut,
465 WriteZero,
466 StorageFull,
467 NotSeekable,
468 QuotaExceeded,
469 FileTooLarge,
470 ResourceBusy,
471 ExecutableFileBusy,
472 Deadlock,
473 CrossesDevices,
474 TooManyLinks,
475 InvalidFilename,
476 ArgumentListTooLong,
477 Interrupted,
478 Unsupported,
479 UnexpectedEof,
480 OutOfMemory,
481 Other,
482 #[serde(other)]
483 Unknown,
484 }
485
486 fn from_io_kind(kind: io::ErrorKind) -> Option<WireErrorKind> {
487 match kind {
488 io::ErrorKind::NotFound => Some(WireErrorKind::NotFound),
489 io::ErrorKind::PermissionDenied => Some(WireErrorKind::PermissionDenied),
490 io::ErrorKind::ConnectionRefused => Some(WireErrorKind::ConnectionRefused),
491 io::ErrorKind::ConnectionReset => Some(WireErrorKind::ConnectionReset),
492 io::ErrorKind::HostUnreachable => Some(WireErrorKind::HostUnreachable),
493 io::ErrorKind::NetworkUnreachable => Some(WireErrorKind::NetworkUnreachable),
494 io::ErrorKind::ConnectionAborted => Some(WireErrorKind::ConnectionAborted),
495 io::ErrorKind::NotConnected => Some(WireErrorKind::NotConnected),
496 io::ErrorKind::AddrInUse => Some(WireErrorKind::AddrInUse),
497 io::ErrorKind::AddrNotAvailable => Some(WireErrorKind::AddrNotAvailable),
498 io::ErrorKind::NetworkDown => Some(WireErrorKind::NetworkDown),
499 io::ErrorKind::BrokenPipe => Some(WireErrorKind::BrokenPipe),
500 io::ErrorKind::AlreadyExists => Some(WireErrorKind::AlreadyExists),
501 io::ErrorKind::WouldBlock => Some(WireErrorKind::WouldBlock),
502 io::ErrorKind::NotADirectory => Some(WireErrorKind::NotADirectory),
503 io::ErrorKind::IsADirectory => Some(WireErrorKind::IsADirectory),
504 io::ErrorKind::DirectoryNotEmpty => Some(WireErrorKind::DirectoryNotEmpty),
505 io::ErrorKind::ReadOnlyFilesystem => Some(WireErrorKind::ReadOnlyFilesystem),
506 io::ErrorKind::StaleNetworkFileHandle => {
507 Some(WireErrorKind::StaleNetworkFileHandle)
508 }
509 io::ErrorKind::InvalidInput => Some(WireErrorKind::InvalidInput),
510 io::ErrorKind::InvalidData => Some(WireErrorKind::InvalidData),
511 io::ErrorKind::TimedOut => Some(WireErrorKind::TimedOut),
512 io::ErrorKind::WriteZero => Some(WireErrorKind::WriteZero),
513 io::ErrorKind::StorageFull => Some(WireErrorKind::StorageFull),
514 io::ErrorKind::NotSeekable => Some(WireErrorKind::NotSeekable),
515 io::ErrorKind::QuotaExceeded => Some(WireErrorKind::QuotaExceeded),
516 io::ErrorKind::FileTooLarge => Some(WireErrorKind::FileTooLarge),
517 io::ErrorKind::ResourceBusy => Some(WireErrorKind::ResourceBusy),
518 io::ErrorKind::ExecutableFileBusy => Some(WireErrorKind::ExecutableFileBusy),
519 io::ErrorKind::Deadlock => Some(WireErrorKind::Deadlock),
520 io::ErrorKind::CrossesDevices => Some(WireErrorKind::CrossesDevices),
521 io::ErrorKind::TooManyLinks => Some(WireErrorKind::TooManyLinks),
522 io::ErrorKind::InvalidFilename => Some(WireErrorKind::InvalidFilename),
523 io::ErrorKind::ArgumentListTooLong => Some(WireErrorKind::ArgumentListTooLong),
524 io::ErrorKind::Interrupted => Some(WireErrorKind::Interrupted),
525 io::ErrorKind::Unsupported => Some(WireErrorKind::Unsupported),
526 io::ErrorKind::UnexpectedEof => Some(WireErrorKind::UnexpectedEof),
527 io::ErrorKind::OutOfMemory => Some(WireErrorKind::OutOfMemory),
528 io::ErrorKind::Other => Some(WireErrorKind::Other),
529 _ => None,
530 }
531 }
532
533 impl From<WireErrorKind> for io::ErrorKind {
534 fn from(kind: WireErrorKind) -> io::ErrorKind {
535 match kind {
536 WireErrorKind::NotFound => io::ErrorKind::NotFound,
537 WireErrorKind::PermissionDenied => io::ErrorKind::PermissionDenied,
538 WireErrorKind::ConnectionRefused => io::ErrorKind::ConnectionRefused,
539 WireErrorKind::ConnectionReset => io::ErrorKind::ConnectionReset,
540 WireErrorKind::HostUnreachable => io::ErrorKind::HostUnreachable,
541 WireErrorKind::NetworkUnreachable => io::ErrorKind::NetworkUnreachable,
542 WireErrorKind::ConnectionAborted => io::ErrorKind::ConnectionAborted,
543 WireErrorKind::NotConnected => io::ErrorKind::NotConnected,
544 WireErrorKind::AddrInUse => io::ErrorKind::AddrInUse,
545 WireErrorKind::AddrNotAvailable => io::ErrorKind::AddrNotAvailable,
546 WireErrorKind::NetworkDown => io::ErrorKind::NetworkDown,
547 WireErrorKind::BrokenPipe => io::ErrorKind::BrokenPipe,
548 WireErrorKind::AlreadyExists => io::ErrorKind::AlreadyExists,
549 WireErrorKind::WouldBlock => io::ErrorKind::WouldBlock,
550 WireErrorKind::NotADirectory => io::ErrorKind::NotADirectory,
551 WireErrorKind::IsADirectory => io::ErrorKind::IsADirectory,
552 WireErrorKind::DirectoryNotEmpty => io::ErrorKind::DirectoryNotEmpty,
553 WireErrorKind::ReadOnlyFilesystem => io::ErrorKind::ReadOnlyFilesystem,
554 WireErrorKind::StaleNetworkFileHandle => io::ErrorKind::StaleNetworkFileHandle,
555 WireErrorKind::InvalidInput => io::ErrorKind::InvalidInput,
556 WireErrorKind::InvalidData => io::ErrorKind::InvalidData,
557 WireErrorKind::TimedOut => io::ErrorKind::TimedOut,
558 WireErrorKind::WriteZero => io::ErrorKind::WriteZero,
559 WireErrorKind::StorageFull => io::ErrorKind::StorageFull,
560 WireErrorKind::NotSeekable => io::ErrorKind::NotSeekable,
561 WireErrorKind::QuotaExceeded => io::ErrorKind::QuotaExceeded,
562 WireErrorKind::FileTooLarge => io::ErrorKind::FileTooLarge,
563 WireErrorKind::ResourceBusy => io::ErrorKind::ResourceBusy,
564 WireErrorKind::ExecutableFileBusy => io::ErrorKind::ExecutableFileBusy,
565 WireErrorKind::Deadlock => io::ErrorKind::Deadlock,
566 WireErrorKind::CrossesDevices => io::ErrorKind::CrossesDevices,
567 WireErrorKind::TooManyLinks => io::ErrorKind::TooManyLinks,
568 WireErrorKind::InvalidFilename => io::ErrorKind::InvalidFilename,
569 WireErrorKind::ArgumentListTooLong => io::ErrorKind::ArgumentListTooLong,
570 WireErrorKind::Interrupted => io::ErrorKind::Interrupted,
571 WireErrorKind::Unsupported => io::ErrorKind::Unsupported,
572 WireErrorKind::UnexpectedEof => io::ErrorKind::UnexpectedEof,
573 WireErrorKind::OutOfMemory => io::ErrorKind::OutOfMemory,
574 WireErrorKind::Other => io::ErrorKind::Other,
575 WireErrorKind::Unknown => io::ErrorKind::Other,
576 }
577 }
578 }
579
580 pub(super) fn serialize<S: Serializer>(
581 kind: &io::ErrorKind,
582 serializer: S,
583 ) -> std::result::Result<S::Ok, S::Error> {
584 from_io_kind(*kind)
585 .ok_or_else(|| {
586 serde::ser::Error::custom("I/O error kind is absent from the wire schema")
587 })?
588 .serialize(serializer)
589 }
590
591 pub(super) fn deserialize<'de, D: Deserializer<'de>>(
592 deserializer: D,
593 ) -> std::result::Result<io::ErrorKind, D::Error> {
594 WireErrorKind::deserialize(deserializer).map(io::ErrorKind::from)
595 }
596 }
597
598 #[derive(Debug, Clone, Serialize, Deserialize)]
600 #[serde(deny_unknown_fields)]
601 pub struct PdfRequest {
602 pub input: String,
604 }
605
606 #[derive(Debug, Clone, Serialize, Deserialize)]
608 #[serde(deny_unknown_fields)]
609 pub struct PdfOk {
610 pub text: String,
612 pub warnings: Vec<String>,
614 pub segments: Vec<Segment>,
616 #[serde(default, skip_serializing_if = "is_false")]
627 pub recovered: bool,
628 }
629
630 fn is_false(value: &bool) -> bool {
632 !*value
633 }
634
635 #[derive(Debug, Clone, Serialize, Deserialize)]
643 #[serde(deny_unknown_fields)]
644 pub struct RecordsRequest {
645 pub input: String,
647 pub format: String,
649 pub max_records: u64,
652 pub max_tables: u64,
654 pub max_output_bytes: u64,
656 }
657
658 #[derive(Debug, Clone, Serialize, Deserialize)]
660 #[serde(deny_unknown_fields)]
661 pub struct RecordsOk {
662 pub text: String,
664 pub warnings: Vec<String>,
666 pub segments: Vec<Segment>,
669 }
670
671 #[derive(Debug, Clone, Serialize, Deserialize)]
679 #[serde(deny_unknown_fields)]
680 pub struct ImageMetadataRequest {
681 pub input: String,
683 pub format: String,
686 pub max_decompressed_bytes: u64,
689 pub max_xml_depth: u32,
691 pub max_xml_events: u64,
693 pub max_boxes: u64,
696 pub max_rows: u64,
698 pub max_output_bytes: u64,
701 }
702
703 #[derive(Debug, Clone, Serialize, Deserialize)]
708 #[serde(deny_unknown_fields)]
709 pub struct MetadataRow {
710 pub carrier: String,
714 pub surface: String,
717 pub path: String,
720 #[serde(skip_serializing_if = "Option::is_none", default)]
723 pub language: Option<String>,
724 pub value: String,
726 }
727
728 #[derive(Debug, Clone, Serialize, Deserialize)]
734 #[serde(deny_unknown_fields)]
735 pub struct ImageMetadataOk {
736 pub rows: Vec<MetadataRow>,
739 }
740
741 #[derive(Debug, Clone, Serialize, Deserialize)]
743 #[serde(rename_all = "snake_case", deny_unknown_fields)]
744 pub enum OcrInput {
745 Image,
747 PageRender {
749 page: u32,
751 dpi: u32,
753 },
754 }
755
756 #[derive(Debug, Clone, Serialize, Deserialize)]
758 #[serde(deny_unknown_fields)]
759 pub struct OcrRequest {
760 pub input: String,
762 pub kind: OcrInput,
764 #[serde(default, skip_serializing_if = "Vec::is_empty")]
769 pub inventory: Vec<InventoryEntry>,
770 }
771
772 #[derive(Debug, Clone, Serialize, Deserialize)]
774 #[serde(deny_unknown_fields)]
775 pub struct OcrSpan {
776 pub text: String,
778 pub confidence: f64,
780 }
781
782 #[derive(Debug, Clone, Serialize, Deserialize)]
784 #[serde(deny_unknown_fields)]
785 pub struct OcrOk {
786 pub spans: Vec<OcrSpan>,
788 #[serde(default, skip_serializing_if = "Vec::is_empty")]
792 pub warnings: Vec<String>,
793 }
794
795 #[derive(Debug, Clone, Serialize, Deserialize)]
802 #[serde(deny_unknown_fields)]
803 pub struct InventoryEntry {
804 pub role: String,
806 pub path: String,
808 pub expected_blake3: String,
810 }
811
812 #[derive(Debug, Clone, Serialize, Deserialize)]
814 #[serde(deny_unknown_fields)]
815 pub struct AsrRequest {
816 pub input: String,
818 #[serde(skip_serializing_if = "Option::is_none", default)]
821 pub language: Option<String>,
822 pub max_duration_seconds: u64,
827 #[serde(default, skip_serializing_if = "Vec::is_empty")]
831 pub inventory: Vec<InventoryEntry>,
832 }
833
834 #[derive(Debug, Clone, Serialize, Deserialize)]
836 #[serde(deny_unknown_fields)]
837 pub struct AsrSegment {
838 pub start_seconds: f64,
840 pub end_seconds: f64,
842 pub speaker: u32,
844 pub text: String,
846 }
847
848 #[derive(Debug, Clone, Serialize, Deserialize)]
850 #[serde(deny_unknown_fields)]
851 pub struct AsrOk {
852 pub segments: Vec<AsrSegment>,
854 #[serde(skip_serializing_if = "Option::is_none", default)]
856 pub language: Option<String>,
857 #[serde(skip_serializing_if = "Option::is_none", default)]
859 pub duration_seconds: Option<f64>,
860 }
861
862 #[derive(Debug, Clone, Serialize, Deserialize)]
871 #[serde(deny_unknown_fields)]
872 pub struct SvgRoleEntry {
873 pub role: String,
875 pub path: String,
877 pub expected_blake3: String,
879 pub version: String,
881 #[serde(skip_serializing_if = "Vec::is_empty", default)]
885 pub closure_roots: Vec<String>,
886 #[serde(skip_serializing_if = "Option::is_none", default)]
888 pub closure_blake3: Option<String>,
889 #[serde(skip_serializing_if = "Option::is_none", default)]
892 pub jail_temp_env: Option<String>,
893 }
894
895 #[derive(Debug, Clone, Serialize, Deserialize)]
897 #[serde(deny_unknown_fields)]
898 pub struct SvgRasterRequest {
899 pub input: String,
901 pub roles: Vec<SvgRoleEntry>,
903 }
904
905 #[derive(Debug, Clone, Serialize, Deserialize)]
909 #[serde(deny_unknown_fields)]
910 pub struct SvgRasterOk {
911 pub raster_png_hex: String,
913 pub width: u32,
915 pub height: u32,
917 }
918
919 #[derive(Debug, Clone, Serialize, Deserialize)]
921 #[serde(deny_unknown_fields)]
922 pub struct VideoRequest {
923 pub input: String,
925 pub sample_interval_seconds: f64,
927 pub max_frames: u32,
929 }
930
931 #[derive(Debug, Clone, Serialize, Deserialize)]
933 #[serde(deny_unknown_fields)]
934 pub struct ScreenState {
935 pub first_seen_seconds: f64,
937 pub text: String,
939 }
940
941 #[derive(Debug, Clone, Serialize, Deserialize)]
943 #[serde(deny_unknown_fields)]
944 pub struct VideoOk {
945 pub states: Vec<ScreenState>,
948 #[serde(skip_serializing_if = "Option::is_none", default)]
950 pub duration_seconds: Option<f64>,
951 }
952
953 #[derive(Debug, Clone, Serialize, Deserialize)]
955 #[serde(deny_unknown_fields)]
956 pub struct ProbeReport {
957 pub attempts: Vec<ProbeAttempt>,
959 }
960
961 #[derive(Debug, Clone, Serialize, Deserialize)]
963 #[serde(deny_unknown_fields)]
964 pub struct ProbeAttempt {
965 pub name: ProbeName,
967 pub outcome: ProbeOutcome,
969 #[serde(with = "error_kind_wire")]
971 pub error_kind: io::ErrorKind,
972 }
973
974 #[derive(Debug, Clone, Serialize, Deserialize)]
981 #[serde(deny_unknown_fields)]
982 pub struct ProbeNetRequest {
983 pub unix_target: String,
987 pub tcp4_target: String,
990 pub tcp6_target: String,
993 }
994}