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<(), String> {
124 match (&self.ok, &self.error) {
125 (Some(_), None) | (None, Some(_)) => Ok(()),
126 (Some(_), Some(_)) => Err("response carries both ok and error".to_string()),
127 (None, None) => Err("response carries neither ok nor error".to_string()),
128 }
129 }
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134#[serde(deny_unknown_fields)]
135pub struct WireError {
136 pub code: String,
138 pub message: String,
140}
141
142#[derive(Debug)]
144pub enum FrameRead {
145 Complete(Vec<u8>),
147 Empty,
149 Truncated,
151 Oversized {
154 declared: u64,
156 },
157}
158
159pub fn read_frame(reader: &mut impl Read, max_bytes: u32) -> io::Result<FrameRead> {
162 let mut header = [0u8; 4];
163 match read_exact_or_eof(reader, &mut header)? {
164 Fill::Empty => return Ok(FrameRead::Empty),
165 Fill::Partial => return Ok(FrameRead::Truncated),
166 Fill::Full => {}
167 }
168 let declared = u32::from_be_bytes(header);
169 if declared > max_bytes {
170 return Ok(FrameRead::Oversized {
171 declared: u64::from(declared),
172 });
173 }
174 let mut payload = vec![0u8; declared as usize];
175 match read_exact_or_eof(reader, &mut payload)? {
176 Fill::Full => Ok(FrameRead::Complete(payload)),
177 Fill::Empty | Fill::Partial => Ok(FrameRead::Truncated),
178 }
179}
180
181enum Fill {
182 Full,
183 Partial,
184 Empty,
185}
186
187fn read_exact_or_eof(reader: &mut impl Read, buffer: &mut [u8]) -> io::Result<Fill> {
188 let mut filled = 0;
189 while filled < buffer.len() {
190 match reader.read(&mut buffer[filled..]) {
191 Ok(0) => {
192 return Ok(if filled == 0 {
193 Fill::Empty
194 } else {
195 Fill::Partial
196 });
197 }
198 Ok(n) => filled += n,
199 Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
200 Err(e) => return Err(e),
201 }
202 }
203 Ok(Fill::Full)
204}
205
206pub fn write_frame<T: Serialize>(writer: &mut impl Write, value: &T) -> io::Result<()> {
208 let payload = serde_json::to_vec(value).map_err(io::Error::other)?;
209 let length = u32::try_from(payload.len())
210 .map_err(|_| io::Error::other("frame payload exceeds the u32 length prefix"))?;
211 writer.write_all(&length.to_be_bytes())?;
212 writer.write_all(&payload)?;
213 writer.flush()
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn a_non_recovered_pdf_response_omits_the_recovered_field() {
222 use bodies::PdfOk;
223 let plain = PdfOk {
228 text: "hi".to_string(),
229 warnings: Vec::new(),
230 segments: Vec::new(),
231 recovered: false,
232 };
233 let encoded = serde_json::to_string(&plain).unwrap();
234 assert!(
235 !encoded.contains("recovered"),
236 "false recovered is off the wire: {encoded}"
237 );
238 let back: PdfOk = serde_json::from_str(&encoded).unwrap();
240 assert!(!back.recovered);
241
242 let recovered = PdfOk {
243 recovered: true,
244 ..plain
245 };
246 let encoded = serde_json::to_string(&recovered).unwrap();
247 assert!(
248 encoded.contains("\"recovered\":true"),
249 "a recovered outcome carries the field: {encoded}"
250 );
251 }
252
253 #[test]
254 fn frames_roundtrip() {
255 let mut buffer = Vec::new();
256 let request = Request {
257 schema: RequestSchema,
258 adapter: "pdf".to_string(),
259 payload: serde_json::json!({"input": "input.pdf"}),
260 };
261 write_frame(&mut buffer, &request).unwrap();
262 let mut cursor = std::io::Cursor::new(buffer);
263 let FrameRead::Complete(payload) = read_frame(&mut cursor, MAX_REQUEST_BYTES).unwrap()
264 else {
265 panic!("expected a complete frame");
266 };
267 let back: Request = serde_json::from_slice(&payload).unwrap();
268 assert_eq!(back.adapter, "pdf");
269 }
270
271 #[test]
272 fn oversized_declarations_are_refused_before_allocation() {
273 let mut bytes = u32::MAX.to_be_bytes().to_vec();
274 bytes.extend_from_slice(b"junk");
275 let mut cursor = std::io::Cursor::new(bytes);
276 let FrameRead::Oversized { declared } = read_frame(&mut cursor, 1024).unwrap() else {
277 panic!("expected an oversized refusal");
278 };
279 assert_eq!(declared, u64::from(u32::MAX));
280 }
281
282 #[test]
283 fn truncation_is_detected_in_header_and_payload() {
284 let mut cursor = std::io::Cursor::new(vec![0u8, 0, 0]);
285 assert!(matches!(
286 read_frame(&mut cursor, 1024).unwrap(),
287 FrameRead::Truncated
288 ));
289
290 let mut bytes = 10u32.to_be_bytes().to_vec();
291 bytes.extend_from_slice(b"half");
292 let mut cursor = std::io::Cursor::new(bytes);
293 assert!(matches!(
294 read_frame(&mut cursor, 1024).unwrap(),
295 FrameRead::Truncated
296 ));
297
298 let mut cursor = std::io::Cursor::new(Vec::<u8>::new());
299 assert!(matches!(
300 read_frame(&mut cursor, 1024).unwrap(),
301 FrameRead::Empty
302 ));
303 }
304
305 #[test]
306 fn responses_require_exactly_one_body() {
307 assert!(Response::ok(serde_json::json!({})).validate().is_ok());
308 assert!(Response::err("io", "detail".to_string()).validate().is_ok());
309 let both = Response {
310 schema: ResponseSchema,
311 ok: Some(serde_json::json!({})),
312 error: Some(WireError {
313 code: "io".to_string(),
314 message: "detail".to_string(),
315 }),
316 };
317 assert!(both.validate().is_err());
318 let neither = Response {
319 schema: ResponseSchema,
320 ok: None,
321 error: None,
322 };
323 assert!(neither.validate().is_err());
324 }
325
326 #[test]
327 fn wrong_schema_values_are_rejected() {
328 let json = serde_json::to_string(&Response::ok(serde_json::json!({}))).unwrap();
329 let bumped = json.replace("adapter-response@1", "adapter-response@2");
330 assert!(serde_json::from_str::<Response>(&bumped).is_err());
331 }
332}
333
334pub mod bodies {
337 use super::*;
338
339 #[derive(Debug, Clone, Serialize, Deserialize)]
341 #[serde(deny_unknown_fields)]
342 pub struct PdfRequest {
343 pub input: String,
345 }
346
347 #[derive(Debug, Clone, Serialize, Deserialize)]
349 #[serde(deny_unknown_fields)]
350 pub struct PdfOk {
351 pub text: String,
353 pub warnings: Vec<String>,
355 pub segments: Vec<Segment>,
357 #[serde(default, skip_serializing_if = "is_false")]
368 pub recovered: bool,
369 }
370
371 fn is_false(value: &bool) -> bool {
373 !*value
374 }
375
376 #[derive(Debug, Clone, Serialize, Deserialize)]
384 #[serde(deny_unknown_fields)]
385 pub struct RecordsRequest {
386 pub input: String,
388 pub format: String,
390 pub max_records: u64,
393 pub max_tables: u64,
395 pub max_output_bytes: u64,
397 }
398
399 #[derive(Debug, Clone, Serialize, Deserialize)]
401 #[serde(deny_unknown_fields)]
402 pub struct RecordsOk {
403 pub text: String,
405 pub warnings: Vec<String>,
407 pub segments: Vec<Segment>,
410 }
411
412 #[derive(Debug, Clone, Serialize, Deserialize)]
414 #[serde(rename_all = "snake_case", deny_unknown_fields)]
415 pub enum OcrInput {
416 Image,
418 PageRender {
420 page: u32,
422 dpi: u32,
424 },
425 }
426
427 #[derive(Debug, Clone, Serialize, Deserialize)]
429 #[serde(deny_unknown_fields)]
430 pub struct OcrRequest {
431 pub input: String,
433 pub kind: OcrInput,
435 }
436
437 #[derive(Debug, Clone, Serialize, Deserialize)]
439 #[serde(deny_unknown_fields)]
440 pub struct OcrSpan {
441 pub text: String,
443 pub confidence: f64,
445 }
446
447 #[derive(Debug, Clone, Serialize, Deserialize)]
449 #[serde(deny_unknown_fields)]
450 pub struct OcrOk {
451 pub spans: Vec<OcrSpan>,
453 }
454
455 #[derive(Debug, Clone, Serialize, Deserialize)]
457 #[serde(deny_unknown_fields)]
458 pub struct AsrRequest {
459 pub input: String,
461 #[serde(skip_serializing_if = "Option::is_none", default)]
463 pub language: Option<String>,
464 }
465
466 #[derive(Debug, Clone, Serialize, Deserialize)]
468 #[serde(deny_unknown_fields)]
469 pub struct AsrSegment {
470 pub start_seconds: f64,
472 pub end_seconds: f64,
474 pub speaker: u32,
476 pub text: String,
478 }
479
480 #[derive(Debug, Clone, Serialize, Deserialize)]
482 #[serde(deny_unknown_fields)]
483 pub struct AsrOk {
484 pub segments: Vec<AsrSegment>,
486 #[serde(skip_serializing_if = "Option::is_none", default)]
488 pub language: Option<String>,
489 #[serde(skip_serializing_if = "Option::is_none", default)]
491 pub duration_seconds: Option<f64>,
492 }
493
494 #[derive(Debug, Clone, Serialize, Deserialize)]
496 #[serde(deny_unknown_fields)]
497 pub struct VideoRequest {
498 pub input: String,
500 pub sample_interval_seconds: f64,
502 pub max_frames: u32,
504 }
505
506 #[derive(Debug, Clone, Serialize, Deserialize)]
508 #[serde(deny_unknown_fields)]
509 pub struct ScreenState {
510 pub first_seen_seconds: f64,
512 pub text: String,
514 }
515
516 #[derive(Debug, Clone, Serialize, Deserialize)]
518 #[serde(deny_unknown_fields)]
519 pub struct VideoOk {
520 pub states: Vec<ScreenState>,
523 #[serde(skip_serializing_if = "Option::is_none", default)]
525 pub duration_seconds: Option<f64>,
526 }
527
528 #[derive(Debug, Clone, Serialize, Deserialize)]
530 #[serde(deny_unknown_fields)]
531 pub struct ProbeReport {
532 pub attempts: Vec<ProbeAttempt>,
534 }
535
536 #[derive(Debug, Clone, Serialize, Deserialize)]
538 #[serde(deny_unknown_fields)]
539 pub struct ProbeAttempt {
540 pub name: String,
542 pub outcome: String,
546 pub detail: String,
548 }
549
550 #[derive(Debug, Clone, Serialize, Deserialize)]
557 #[serde(deny_unknown_fields)]
558 pub struct ProbeNetRequest {
559 pub unix_target: String,
563 pub tcp4_target: String,
566 pub tcp6_target: String,
569 }
570}