use std::io::{self, Read, Write};
use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::segments::Segment;
pub const REQUEST_SCHEMA: &str = "text-mirror/adapter-request@1";
pub const RESPONSE_SCHEMA: &str = "text-mirror/adapter-response@1";
pub const MAX_REQUEST_BYTES: u32 = 1024 * 1024;
macro_rules! literal_schema {
($name:ident, $value:expr) => {
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct $name;
impl Serialize for $name {
fn serialize<S: Serializer>(
&self,
serializer: S,
) -> std::result::Result<S::Ok, S::Error> {
serializer.serialize_str($value)
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D: Deserializer<'de>>(
deserializer: D,
) -> std::result::Result<Self, D::Error> {
let value = String::deserialize(deserializer)?;
if value == $value {
Ok($name)
} else {
Err(D::Error::custom(format!(
"unrecognized schema {value:?}, expected {:?}",
$value
)))
}
}
}
};
}
literal_schema!(RequestSchema, REQUEST_SCHEMA);
literal_schema!(ResponseSchema, RESPONSE_SCHEMA);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Request {
pub schema: RequestSchema,
pub adapter: String,
pub payload: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Response {
pub schema: ResponseSchema,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub ok: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub error: Option<WireError>,
}
impl Response {
pub fn ok(value: serde_json::Value) -> Response {
Response {
schema: ResponseSchema,
ok: Some(value),
error: None,
}
}
pub fn err(code: &str, message: String) -> Response {
Response {
schema: ResponseSchema,
ok: None,
error: Some(WireError {
code: code.to_string(),
message,
}),
}
}
pub fn validate(&self) -> std::result::Result<(), String> {
match (&self.ok, &self.error) {
(Some(_), None) | (None, Some(_)) => Ok(()),
(Some(_), Some(_)) => Err("response carries both ok and error".to_string()),
(None, None) => Err("response carries neither ok nor error".to_string()),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WireError {
pub code: String,
pub message: String,
}
#[derive(Debug)]
pub enum FrameRead {
Complete(Vec<u8>),
Empty,
Truncated,
Oversized {
declared: u64,
},
}
pub fn read_frame(reader: &mut impl Read, max_bytes: u32) -> io::Result<FrameRead> {
let mut header = [0u8; 4];
match read_exact_or_eof(reader, &mut header)? {
Fill::Empty => return Ok(FrameRead::Empty),
Fill::Partial => return Ok(FrameRead::Truncated),
Fill::Full => {}
}
let declared = u32::from_be_bytes(header);
if declared > max_bytes {
return Ok(FrameRead::Oversized {
declared: u64::from(declared),
});
}
let mut payload = vec![0u8; declared as usize];
match read_exact_or_eof(reader, &mut payload)? {
Fill::Full => Ok(FrameRead::Complete(payload)),
Fill::Empty | Fill::Partial => Ok(FrameRead::Truncated),
}
}
enum Fill {
Full,
Partial,
Empty,
}
fn read_exact_or_eof(reader: &mut impl Read, buffer: &mut [u8]) -> io::Result<Fill> {
let mut filled = 0;
while filled < buffer.len() {
match reader.read(&mut buffer[filled..]) {
Ok(0) => {
return Ok(if filled == 0 {
Fill::Empty
} else {
Fill::Partial
});
}
Ok(n) => filled += n,
Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
Err(e) => return Err(e),
}
}
Ok(Fill::Full)
}
pub fn write_frame<T: Serialize>(writer: &mut impl Write, value: &T) -> io::Result<()> {
let payload = serde_json::to_vec(value).map_err(io::Error::other)?;
let length = u32::try_from(payload.len())
.map_err(|_| io::Error::other("frame payload exceeds the u32 length prefix"))?;
writer.write_all(&length.to_be_bytes())?;
writer.write_all(&payload)?;
writer.flush()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_non_recovered_pdf_response_omits_the_recovered_field() {
use bodies::PdfOk;
let plain = PdfOk {
text: "hi".to_string(),
warnings: Vec::new(),
segments: Vec::new(),
recovered: false,
};
let encoded = serde_json::to_string(&plain).unwrap();
assert!(
!encoded.contains("recovered"),
"false recovered is off the wire: {encoded}"
);
let back: PdfOk = serde_json::from_str(&encoded).unwrap();
assert!(!back.recovered);
let recovered = PdfOk {
recovered: true,
..plain
};
let encoded = serde_json::to_string(&recovered).unwrap();
assert!(
encoded.contains("\"recovered\":true"),
"a recovered outcome carries the field: {encoded}"
);
}
#[test]
fn frames_roundtrip() {
let mut buffer = Vec::new();
let request = Request {
schema: RequestSchema,
adapter: "pdf".to_string(),
payload: serde_json::json!({"input": "input.pdf"}),
};
write_frame(&mut buffer, &request).unwrap();
let mut cursor = std::io::Cursor::new(buffer);
let FrameRead::Complete(payload) = read_frame(&mut cursor, MAX_REQUEST_BYTES).unwrap()
else {
panic!("expected a complete frame");
};
let back: Request = serde_json::from_slice(&payload).unwrap();
assert_eq!(back.adapter, "pdf");
}
#[test]
fn oversized_declarations_are_refused_before_allocation() {
let mut bytes = u32::MAX.to_be_bytes().to_vec();
bytes.extend_from_slice(b"junk");
let mut cursor = std::io::Cursor::new(bytes);
let FrameRead::Oversized { declared } = read_frame(&mut cursor, 1024).unwrap() else {
panic!("expected an oversized refusal");
};
assert_eq!(declared, u64::from(u32::MAX));
}
#[test]
fn truncation_is_detected_in_header_and_payload() {
let mut cursor = std::io::Cursor::new(vec![0u8, 0, 0]);
assert!(matches!(
read_frame(&mut cursor, 1024).unwrap(),
FrameRead::Truncated
));
let mut bytes = 10u32.to_be_bytes().to_vec();
bytes.extend_from_slice(b"half");
let mut cursor = std::io::Cursor::new(bytes);
assert!(matches!(
read_frame(&mut cursor, 1024).unwrap(),
FrameRead::Truncated
));
let mut cursor = std::io::Cursor::new(Vec::<u8>::new());
assert!(matches!(
read_frame(&mut cursor, 1024).unwrap(),
FrameRead::Empty
));
}
#[test]
fn responses_require_exactly_one_body() {
assert!(Response::ok(serde_json::json!({})).validate().is_ok());
assert!(Response::err("io", "detail".to_string()).validate().is_ok());
let both = Response {
schema: ResponseSchema,
ok: Some(serde_json::json!({})),
error: Some(WireError {
code: "io".to_string(),
message: "detail".to_string(),
}),
};
assert!(both.validate().is_err());
let neither = Response {
schema: ResponseSchema,
ok: None,
error: None,
};
assert!(neither.validate().is_err());
}
#[test]
fn wrong_schema_values_are_rejected() {
let json = serde_json::to_string(&Response::ok(serde_json::json!({}))).unwrap();
let bumped = json.replace("adapter-response@1", "adapter-response@2");
assert!(serde_json::from_str::<Response>(&bumped).is_err());
}
}
pub mod bodies {
use super::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PdfRequest {
pub input: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PdfOk {
pub text: String,
pub warnings: Vec<String>,
pub segments: Vec<Segment>,
#[serde(default, skip_serializing_if = "is_false")]
pub recovered: bool,
}
fn is_false(value: &bool) -> bool {
!*value
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RecordsRequest {
pub input: String,
pub format: String,
pub max_records: u64,
pub max_tables: u64,
pub max_output_bytes: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RecordsOk {
pub text: String,
pub warnings: Vec<String>,
pub segments: Vec<Segment>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub enum OcrInput {
Image,
PageRender {
page: u32,
dpi: u32,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OcrRequest {
pub input: String,
pub kind: OcrInput,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OcrSpan {
pub text: String,
pub confidence: f64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OcrOk {
pub spans: Vec<OcrSpan>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AsrRequest {
pub input: String,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub language: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AsrSegment {
pub start_seconds: f64,
pub end_seconds: f64,
pub speaker: u32,
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AsrOk {
pub segments: Vec<AsrSegment>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub language: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub duration_seconds: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VideoRequest {
pub input: String,
pub sample_interval_seconds: f64,
pub max_frames: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ScreenState {
pub first_seen_seconds: f64,
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VideoOk {
pub states: Vec<ScreenState>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub duration_seconds: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProbeReport {
pub attempts: Vec<ProbeAttempt>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProbeAttempt {
pub name: String,
pub outcome: String,
pub detail: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ProbeNetRequest {
pub unix_target: String,
pub tcp4_target: String,
pub tcp6_target: String,
}
}