use std::borrow::Cow;
use crate::{
exceptions::{ExcType, MontyException},
resource::ResourceError,
};
pub const DEFAULT_MAX_PRINT_COLLECT_BYTES: usize = 10 * 1024 * 1024;
pub const COLLECT_STREAMS_ENTRY_OVERHEAD: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PrintStream {
Stdout,
Stderr,
}
pub enum PrintWriter<'a> {
Disabled,
Stdout,
CollectString(&'a mut String, Option<usize>),
CollectStreams(&'a mut CollectedStreams, Option<usize>),
Callback(&'a mut dyn PrintWriterCallback),
}
impl PrintWriter<'_> {
pub fn collect_string(buf: &mut String) -> PrintWriter<'_> {
PrintWriter::CollectString(buf, Some(DEFAULT_MAX_PRINT_COLLECT_BYTES))
}
pub fn collect_streams(buf: &mut CollectedStreams) -> PrintWriter<'_> {
PrintWriter::CollectStreams(buf, Some(DEFAULT_MAX_PRINT_COLLECT_BYTES))
}
pub fn reborrow(&mut self) -> PrintWriter<'_> {
match self {
Self::Disabled => PrintWriter::Disabled,
Self::Stdout => PrintWriter::Stdout,
Self::CollectString(buf, max) => PrintWriter::CollectString(buf, *max),
Self::CollectStreams(buf, max) => PrintWriter::CollectStreams(buf, *max),
Self::Callback(cb) => PrintWriter::Callback(&mut **cb),
}
}
pub fn write(&mut self, stream: PrintStream, output: Cow<'_, str>) -> Result<(), MontyException> {
match self {
Self::Disabled => Ok(()),
Self::Stdout => {
match stream {
PrintStream::Stdout => print!("{output}"),
PrintStream::Stderr => eprint!("{output}"),
}
Ok(())
}
Self::CollectString(buf, max_bytes) => {
check_print_collect_limit(buf.len(), output.len(), *max_bytes)?;
buf.push_str(&output);
Ok(())
}
Self::CollectStreams(buf, max_bytes) => buf.push_str(stream, &output, *max_bytes),
Self::Callback(cb) => match stream {
PrintStream::Stdout => cb.stdout_write(output),
PrintStream::Stderr => cb.stderr_write(output),
},
}
}
pub fn push(&mut self, stream: PrintStream, end: char) -> Result<(), MontyException> {
match self {
Self::Disabled => Ok(()),
Self::Stdout => {
match stream {
PrintStream::Stdout => print!("{end}"),
PrintStream::Stderr => eprint!("{end}"),
}
Ok(())
}
Self::CollectString(buf, max_bytes) => {
check_print_collect_limit(buf.len(), end.len_utf8(), *max_bytes)?;
buf.push(end);
Ok(())
}
Self::CollectStreams(buf, max_bytes) => buf.push_char(stream, end, *max_bytes),
Self::Callback(cb) => match stream {
PrintStream::Stdout => cb.stdout_push(end),
PrintStream::Stderr => cb.stderr_push(end),
},
}
}
pub fn stdout_write(&mut self, output: Cow<'_, str>) -> Result<(), MontyException> {
self.write(PrintStream::Stdout, output)
}
pub fn stdout_push(&mut self, end: char) -> Result<(), MontyException> {
self.push(PrintStream::Stdout, end)
}
#[must_use]
pub fn wants_poll(&self) -> bool {
matches!(self, Self::Callback(_))
}
pub fn poll_flush(&mut self) -> Result<(), MontyException> {
match self {
Self::Callback(cb) => cb.poll_flush(),
_ => Ok(()),
}
}
}
#[derive(Debug, Default)]
pub struct CollectedStreams {
entries: Vec<(PrintStream, String)>,
charged_bytes: usize,
}
impl CollectedStreams {
#[must_use]
pub fn entries(&self) -> &[(PrintStream, String)] {
&self.entries
}
#[must_use]
pub fn into_entries(self) -> Vec<(PrintStream, String)> {
self.entries
}
fn push_str(&mut self, stream: PrintStream, text: &str, max_bytes: Option<usize>) -> Result<(), MontyException> {
let starts_entry = self.starts_entry(stream);
self.charge(text.len(), starts_entry, max_bytes)?;
match self.entries.last_mut() {
Some((s, existing)) if *s == stream => existing.push_str(text),
_ => self.entries.push((stream, text.to_owned())),
}
Ok(())
}
fn push_char(&mut self, stream: PrintStream, ch: char, max_bytes: Option<usize>) -> Result<(), MontyException> {
let starts_entry = self.starts_entry(stream);
self.charge(ch.len_utf8(), starts_entry, max_bytes)?;
match self.entries.last_mut() {
Some((s, existing)) if *s == stream => existing.push(ch),
_ => self.entries.push((stream, String::from(ch))),
}
Ok(())
}
fn starts_entry(&self, stream: PrintStream) -> bool {
!matches!(self.entries.last(), Some((s, _)) if *s == stream)
}
fn charge(&mut self, add: usize, starts_entry: bool, max_bytes: Option<usize>) -> Result<(), MontyException> {
let add = if starts_entry {
add.saturating_add(COLLECT_STREAMS_ENTRY_OVERHEAD)
} else {
add
};
check_print_collect_limit(self.charged_bytes, add, max_bytes)?;
self.charged_bytes = self.charged_bytes.saturating_add(add);
Ok(())
}
}
pub fn check_print_collect_limit(
current_len: usize,
add: usize,
max_bytes: Option<usize>,
) -> Result<(), MontyException> {
let Some(limit) = max_bytes else {
return Ok(());
};
let used = current_len.saturating_add(add);
if used > limit {
Err(MontyException::new(
ExcType::MemoryError,
Some(ResourceError::Memory { limit, used }.to_string()),
))
} else {
Ok(())
}
}
pub trait PrintWriterCallback {
fn stdout_write(&mut self, output: Cow<'_, str>) -> Result<(), MontyException>;
fn stdout_push(&mut self, end: char) -> Result<(), MontyException>;
fn stderr_write(&mut self, output: Cow<'_, str>) -> Result<(), MontyException> {
self.stdout_write(output)
}
fn stderr_push(&mut self, end: char) -> Result<(), MontyException> {
self.stdout_push(end)
}
fn poll_flush(&mut self) -> Result<(), MontyException> {
Ok(())
}
}