use std::collections::HashMap;
use std::io::{self, Write};
use std::sync::Arc;
use rapid_structure_rs::OcrOutput;
use serde::Serialize;
use super::error::ErrorBody;
#[derive(Debug, Clone)]
pub(super) struct Succeeded {
pub output: Arc<OcrOutput>,
pub serialized_bytes: u64,
}
impl Succeeded {
pub fn new(output: Arc<OcrOutput>, serialized_bytes: u64) -> Self {
Self {
output,
serialized_bytes,
}
}
}
#[derive(Debug, Clone)]
pub(super) enum Outcome {
Succeeded(Succeeded),
Failed(u16, ErrorBody),
}
impl Outcome {
pub fn bytes(&self) -> u64 {
match self {
Self::Succeeded(succeeded) => succeeded.serialized_bytes,
Self::Failed(_, body) => {
(body.code.len() + body.message.len() + body.detail.to_string().len()) as u64
}
}
}
}
#[derive(Debug)]
pub(super) struct ResultStore {
max_entries: usize,
max_bytes: u64,
bytes: u64,
entries: HashMap<String, Outcome>,
}
impl ResultStore {
pub fn new(max_entries: usize, max_bytes: u64) -> Self {
Self {
max_entries,
max_bytes,
bytes: 0,
entries: HashMap::new(),
}
}
pub fn insert(&mut self, id: &str, outcome: Outcome) -> bool {
if self.entries.contains_key(id) {
return false;
}
let size = outcome.bytes();
if self.entries.len() >= self.max_entries
|| self.bytes.saturating_add(size) > self.max_bytes
{
return false;
}
self.bytes = self.bytes.saturating_add(size);
self.entries.insert(id.to_string(), outcome);
true
}
pub fn get(&self, id: &str) -> Option<&Outcome> {
self.entries.get(id)
}
pub fn retain_only(&mut self, keep: impl Fn(&str) -> bool) -> usize {
let before = self.entries.len();
let mut removed = 0_u64;
self.entries.retain(|id, outcome| {
if keep(id) {
return true;
}
removed = removed.saturating_add(outcome.bytes());
false
});
self.bytes = self.bytes.saturating_sub(removed);
before - self.entries.len()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn bytes(&self) -> u64 {
self.bytes
}
}
struct BoundedWriter {
buffer: Vec<u8>,
limit: usize,
exceeded: bool,
}
impl BoundedWriter {
fn new(limit: u64) -> Self {
let reserve = usize::try_from(limit).unwrap_or(usize::MAX).min(64 * 1024);
Self {
buffer: Vec::with_capacity(reserve),
limit: usize::try_from(limit).unwrap_or(usize::MAX),
exceeded: false,
}
}
}
impl Write for BoundedWriter {
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
if self.buffer.len().saturating_add(data.len()) > self.limit {
self.exceeded = true;
return Err(io::Error::other("serialized result exceeds the limit"));
}
self.buffer.extend_from_slice(data);
Ok(data.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[derive(Debug)]
pub(super) enum SerializeError {
TooLarge,
Internal(String),
}
pub(super) fn serialize_bounded<T: Serialize>(
value: &T,
limit: u64,
) -> Result<Vec<u8>, SerializeError> {
let mut writer = BoundedWriter::new(limit);
let outcome = {
let mut serializer = serde_json::Serializer::new(&mut writer);
value.serialize(&mut serializer)
};
match outcome {
Ok(()) => Ok(writer.buffer),
Err(_) if writer.exceeded => Err(SerializeError::TooLarge),
Err(error) => Err(SerializeError::Internal(error.to_string())),
}
}
pub(super) fn text_bounded(text: &str, limit: u64) -> Result<Vec<u8>, SerializeError> {
let mut writer = BoundedWriter::new(limit);
match writer.write_all(text.as_bytes()) {
Ok(()) => Ok(writer.buffer),
Err(_) if writer.exceeded => Err(SerializeError::TooLarge),
Err(error) => Err(SerializeError::Internal(error.to_string())),
}
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use serde_json::json;
use super::{Outcome, ResultStore, SerializeError, Succeeded, serialize_bounded, text_bounded};
use crate::serve::tests::scripted_output;
#[test]
fn a_result_over_the_limit_aborts_without_building_a_large_buffer() {
let value = json!({ "text": "x".repeat(4096) });
let error = serialize_bounded(&value, 512).expect_err("must abort");
assert!(matches!(error, SerializeError::TooLarge), "{error:?}");
let small = json!({ "n": 1 });
let exact = serialize_bounded(&small, 7).expect("fits exactly");
assert_eq!(exact.len(), 7);
assert!(serialize_bounded(&small, 6).is_err());
}
#[test]
fn rendered_documents_are_bounded_and_never_truncated() {
assert_eq!(text_bounded("abcdef", 6).expect("exact fit").len(), 6);
let error = text_bounded("abcdef", 5).expect_err("one byte over must abort");
assert!(matches!(error, SerializeError::TooLarge), "{error:?}");
assert!(text_bounded("", 0).is_ok());
}
#[test]
fn the_store_counts_bytes_and_refuses_to_overflow() {
let mut store = ResultStore::new(2, 8);
assert!(store.insert("a", succeed(4)));
assert!(store.insert("b", succeed(4)));
assert_eq!(store.len(), 2);
assert_eq!(store.bytes(), 8);
assert!(!store.insert("c", succeed(1)));
assert!(!store.insert("a", succeed(1)));
store.retain_only(|id| id == "a");
assert_eq!(store.len(), 1);
assert_eq!(store.bytes(), 4);
assert!(store.get("b").is_none());
assert!(store.insert("b", succeed(4)));
}
#[test]
fn failures_keep_their_status_and_code() {
let mut store = ResultStore::new(4, 1024);
let body = super::ErrorBody {
code: "result_too_large",
message: "too big".to_string(),
detail: json!(null),
};
assert!(store.insert("a", Outcome::Failed(413, body.clone())));
match store.get("a").expect("inserted") {
Outcome::Failed(status, stored) => {
assert_eq!(*status, 413);
assert_eq!(stored.code, "result_too_large");
}
other => panic!("expected a failure outcome, got {other:?}"),
}
assert!(store.bytes() > 0);
}
#[test]
fn the_succeeded_payload_accounts_for_its_measured_serialization() {
let outcome = succeed(1234);
assert_eq!(outcome.bytes(), 1234);
match &outcome {
Outcome::Succeeded(succeeded) => {
assert_eq!(succeeded.serialized_bytes, 1234);
assert_eq!(succeeded.output.image.original_size.width, 100);
}
other => panic!("expected success, got {other:?}"),
}
}
fn succeed(bytes: u64) -> Outcome {
Outcome::Succeeded(Succeeded::new(Arc::new(scripted_output(1, 0)), bytes))
}
}