use std::io;
#[cfg(feature = "pdf")]
use std::sync::Arc;
use tokio::{
io::{AsyncWrite, AsyncWriteExt},
sync::mpsc,
task::JoinHandle,
};
use crate::djvm::{DjvmError, DjvmSpool, DjvmStreamWriter};
#[cfg(feature = "pdf")]
use crate::djvu_document::DjVuDocument;
#[cfg(feature = "pdf")]
use crate::pdf::{PdfError, PdfOptions, djvu_to_pdf_to_writer};
const CHANNEL_CAPACITY: usize = 8;
const CHANNEL_CHUNK_BYTES: usize = 64 * 1024;
struct ChannelWriter {
sender: mpsc::Sender<Vec<u8>>,
}
impl ChannelWriter {
fn new(sender: mpsc::Sender<Vec<u8>>) -> Self {
Self { sender }
}
}
impl io::Write for ChannelWriter {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
for chunk in bytes.chunks(CHANNEL_CHUNK_BYTES) {
self.sender.blocking_send(chunk.to_vec()).map_err(|_| {
io::Error::new(
io::ErrorKind::BrokenPipe,
"async export sink stopped receiving output",
)
})?;
}
Ok(bytes.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
enum DrainError<E> {
Writer(E),
Sink(io::Error),
Join(String),
}
async fn drain_channel<W, E>(
mut receiver: mpsc::Receiver<Vec<u8>>,
task: JoinHandle<Result<(), E>>,
sink: &mut W,
) -> Result<(), DrainError<E>>
where
W: AsyncWrite + Unpin,
{
while let Some(chunk) = receiver.recv().await {
if let Err(error) = sink.write_all(&chunk).await {
drop(receiver);
let _ = task.await;
return Err(DrainError::Sink(error));
}
}
match task.await {
Ok(Ok(())) => sink.flush().await.map_err(DrainError::Sink),
Ok(Err(error)) => Err(DrainError::Writer(error)),
Err(error) => Err(DrainError::Join(error.to_string())),
}
}
#[cfg(feature = "pdf")]
#[derive(Debug, thiserror::Error)]
pub enum AsyncPdfError {
#[error("PDF export error: {0}")]
Pdf(#[source] PdfError),
#[error("async sink I/O error: {0}")]
Sink(#[source] io::Error),
#[error("spawn_blocking join error: {0}")]
Join(String),
}
#[cfg(feature = "pdf")]
pub async fn djvu_to_pdf_to_async_writer<W: AsyncWrite + Unpin>(
doc: Arc<DjVuDocument>,
opts: &PdfOptions,
sink: &mut W,
) -> Result<(), AsyncPdfError> {
let opts = opts.clone();
let (sender, receiver) = mpsc::channel(CHANNEL_CAPACITY);
let task = tokio::task::spawn_blocking(move || {
djvu_to_pdf_to_writer(&doc, &opts, ChannelWriter::new(sender))
});
drain_channel(receiver, task, sink)
.await
.map_err(|error| match error {
DrainError::Writer(error) => AsyncPdfError::Pdf(error),
DrainError::Sink(error) => AsyncPdfError::Sink(error),
DrainError::Join(error) => AsyncPdfError::Join(error),
})
}
#[derive(Debug, thiserror::Error)]
pub enum AsyncDjvmError {
#[error("DJVM export error: {0}")]
Djvm(#[source] DjvmError),
#[error("async sink I/O error: {0}")]
Sink(#[source] io::Error),
#[error("spawn_blocking join error: {0}")]
Join(String),
}
pub async fn stream_djvm_to_async_writer<W, C, D>(
components: C,
document_chunks: D,
spool: DjvmSpool,
sink: &mut W,
) -> Result<(), AsyncDjvmError>
where
W: AsyncWrite + Unpin,
C: IntoIterator<Item = (String, u8, Vec<u8>)> + Send + 'static,
D: IntoIterator<Item = ([u8; 4], Vec<u8>)> + Send + 'static,
{
let (sender, receiver) = mpsc::channel(CHANNEL_CAPACITY);
let task = tokio::task::spawn_blocking(move || -> Result<(), DjvmError> {
let mut writer = DjvmStreamWriter::new(ChannelWriter::new(sender), spool)?;
for (id, flag, bytes) in components {
writer.add_component(&id, flag, &bytes)?;
}
for (chunk_id, bytes) in document_chunks {
writer.add_document_chunk(chunk_id, &bytes)?;
}
writer.finish()?;
Ok(())
});
drain_channel(receiver, task, sink)
.await
.map_err(|error| match error {
DrainError::Writer(error) => AsyncDjvmError::Djvm(error),
DrainError::Sink(error) => AsyncDjvmError::Sink(error),
DrainError::Join(error) => AsyncDjvmError::Join(error),
})
}
#[cfg(test)]
mod tests {
use std::{
io,
pin::Pin,
sync::Arc,
task::{Context, Poll},
time::Duration,
};
use super::*;
#[cfg(feature = "pdf")]
use crate::pdf::PdfOptions;
struct FailingAsyncWriter {
bytes_until_failure: usize,
}
impl FailingAsyncWriter {
fn after(bytes_until_failure: usize) -> Self {
Self {
bytes_until_failure,
}
}
}
impl AsyncWrite for FailingAsyncWriter {
fn poll_write(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
bytes: &[u8],
) -> Poll<io::Result<usize>> {
if self.bytes_until_failure == 0 {
return Poll::Ready(Err(io::Error::other("injected async sink failure")));
}
let written = bytes.len().min(self.bytes_until_failure);
self.bytes_until_failure -= written;
Poll::Ready(Ok(written))
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
}
#[derive(Default)]
struct AsyncVecWriter(Vec<u8>);
impl AsyncWrite for AsyncVecWriter {
fn poll_write(
mut self: Pin<&mut Self>,
_cx: &mut Context<'_>,
bytes: &[u8],
) -> Poll<io::Result<usize>> {
self.0.extend_from_slice(bytes);
Poll::Ready(Ok(bytes.len()))
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
}
#[cfg(feature = "pdf")]
fn load_pdf_fixture() -> DjVuDocument {
let bytes = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/chicken.djvu"),
)
.expect("read fixture");
DjVuDocument::parse(&bytes).expect("parse fixture")
}
#[cfg(feature = "pdf")]
#[tokio::test]
async fn async_pdf_failing_sink_returns_sink_error_without_hanging() {
let doc = Arc::new(load_pdf_fixture());
let mut sink = FailingAsyncWriter::after(64);
let result = tokio::time::timeout(
Duration::from_secs(10),
djvu_to_pdf_to_async_writer(doc, &PdfOptions::default(), &mut sink),
)
.await
.expect("async PDF export must shut down its blocking task");
assert!(matches!(
result,
Err(AsyncPdfError::Sink(error)) if error.kind() == io::ErrorKind::Other
));
}
#[tokio::test]
async fn async_djvm_adapter_streams_a_valid_bundle() {
let component = std::fs::read(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/chicken.djvu"),
)
.expect("read fixture");
let mut sink = AsyncVecWriter::default();
stream_djvm_to_async_writer(
vec![("page.djvu".to_owned(), 1, component)],
Vec::<([u8; 4], Vec<u8>)>::new(),
DjvmSpool::Memory,
&mut sink,
)
.await
.expect("stream bundle to async sink");
let doc = DjVuDocument::parse(&sink.0).expect("parse async bundle");
assert_eq!(doc.page_count(), 1);
}
}