superdoc-sdk 2.25.0

Beta Rust SDK for the SuperDoc document runtime
Documentation
use crate::{Error, Result};
use serde_json::Value;
use tokio::io::{AsyncBufRead, AsyncBufReadExt};

/// Read an LF-delimited frame without buffering an unlimited line or decoding a
/// partial UTF-8 sequence. The bound applies to bytes, not Unicode characters.
pub(crate) async fn read_frame<R: AsyncBufRead + Unpin>(
    input: &mut R,
    limit: usize,
) -> Result<Option<Value>> {
    let mut bytes = Vec::new();
    loop {
        let available = input
            .fill_buf()
            .await
            .map_err(|_| Error::new("HOST_DISCONNECTED", "host stdout could not be read"))?;
        if available.is_empty() {
            return if bytes.is_empty() {
                Ok(None)
            } else {
                Err(Error::new(
                    "HOST_PROTOCOL_ERROR",
                    "host exited inside a frame",
                ))
            };
        }
        let newline = available.iter().position(|byte| *byte == b'\n');
        let consumed = newline.map_or(available.len(), |index| index + 1);
        if bytes.len().saturating_add(consumed) > limit {
            return Err(Error::new(
                "HOST_PROTOCOL_ERROR",
                "host frame exceeded the configured byte limit",
            ));
        }
        bytes.extend_from_slice(&available[..consumed]);
        input.consume(consumed);
        if newline.is_some() {
            // Existing hosts/adapters tolerate unrelated diagnostic lines on
            // stdout. Malformed JSON-looking frames must still fail visibly.
            let line = std::str::from_utf8(&bytes)
                .map_err(|_| Error::new("HOST_PROTOCOL_ERROR", "host frame was not UTF-8"))?
                .trim_matches(|character| matches!(character, ' ' | '\t' | '\r' | '\n'));
            if line.is_empty() || (!line.starts_with('{') && !line.starts_with('[')) {
                bytes.clear();
                continue;
            }
            return serde_json::from_str(line)
                .map(Some)
                .map_err(|_| Error::new("HOST_PROTOCOL_ERROR", "host sent malformed JSON"));
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;
    use tokio::io::{AsyncWriteExt, BufReader};

    #[tokio::test]
    async fn split_unicode_coalesced_frames_and_diagnostics() {
        let (mut write, read) = tokio::io::duplex(16);
        let task = tokio::spawn(async move {
            let bytes = "noise\n{\"text\":\"é 😀\u{2028}\u{2029}\\nend\"}\r\n{\"value\":false}\n"
                .as_bytes();
            for byte in bytes {
                write.write_all(&[*byte]).await.unwrap();
            }
        });
        let mut input = BufReader::new(read);
        assert_eq!(
            read_frame(&mut input, 1024).await.unwrap(),
            Some(json!({"text":"é 😀\u{2028}\u{2029}\nend"}))
        );
        assert_eq!(
            read_frame(&mut input, 1024).await.unwrap(),
            Some(json!({"value":false}))
        );
        assert_eq!(read_frame(&mut input, 1024).await.unwrap(), None);
        task.await.unwrap();
    }

    #[tokio::test]
    async fn malformed_oversize_and_truncated_frames_fail() {
        for bytes in [b"{bad}\n".as_slice(), b"{}", b"{\"long\":123456789}\n"] {
            let mut input = BufReader::new(bytes);
            assert_eq!(
                read_frame(&mut input, 12).await.unwrap_err().code,
                "HOST_PROTOCOL_ERROR"
            );
        }
    }
    #[tokio::test]
    async fn seeded_chunk_and_malformed_input_corpus_stays_bounded_and_panic_free() {
        let cases = std::env::var("RUST_TEST_FUZZ_CASES")
            .ok()
            .map(|value| value.parse::<usize>().unwrap())
            .unwrap_or(2000);
        assert!((2000..=1000000).contains(&cases));
        let mut seed = 0x5D59_8701_u64;
        let mut next = || {
            seed ^= seed << 13;
            seed ^= seed >> 7;
            seed ^= seed << 17;
            seed
        };
        for index in 0..cases {
            let number = next() % 100000;
            let expected = json!({"index":index,"text":"é 😀\n\r\n\u{2028}\u{2029}","zero":0,"absentOrNull":null,"false":false,"number":number});
            let mut bytes = b"diagnostic\n".to_vec();
            bytes.extend(serde_json::to_vec(&expected).unwrap());
            bytes.extend(b"\r\n");
            let capacity = (next() % 64 + 1) as usize;
            let mut input = BufReader::with_capacity(capacity, bytes.as_slice());
            assert_eq!(
                read_frame(&mut input, 1024).await.unwrap(),
                Some(expected),
                "seed case {index}, capacity {capacity}"
            );
            assert_eq!(read_frame(&mut input, 1024).await.unwrap(), None);
            let length = (next() % 512) as usize;
            let mut arbitrary: Vec<u8> = (0..length).map(|_| next() as u8).collect();
            arbitrary.push(b'\n');
            let mut input = BufReader::with_capacity(capacity, arbitrary.as_slice());
            if let Ok(Some(value)) = read_frame(&mut input, 128).await {
                assert!(value.is_object() || value.is_array());
            }
        }
        let deep = format!("{}0{}\n", "[".repeat(256), "]".repeat(256));
        assert_eq!(
            read_frame(&mut BufReader::new(deep.as_bytes()), 4096)
                .await
                .unwrap_err()
                .code,
            "HOST_PROTOCOL_ERROR"
        );
    }
}