Skip to main content

pdfboss_write/
sink.rs

1//! The byte-sink abstraction shared by the synchronous and asynchronous
2//! write APIs.
3//!
4//! `pdfboss-write` keeps exactly one implementation of file emission —
5//! the algorithm behind [`crate::Writer::finish_into_with`] — and several
6//! ways of accepting its bytes. [`AsyncByteSink`] is what an asynchronous
7//! consumer provides; `Vec<u8>` is a sink in its own right for the
8//! in-memory path; [`Immediate`] presents any [`std::io::Write`] as a sink
9//! whose futures are already complete, which is how the synchronous entry
10//! points share the asynchronous implementation through
11//! [`pdfboss_core::block_on`] — exactly the pattern the read side's
12//! `pdfboss_core::source` module documents.
13//!
14//! Emission follows that module's three signing rules, mirrored for
15//! writing: entry points take the sink by value, carry no `Send`/`Sync`
16//! bounds of their own, and call the trait method rather than a free twin.
17
18use std::io::Write;
19
20use pdfboss_core::source::BoxFuture;
21
22use crate::error::{Error, Result};
23
24/// Accepts emitted file bytes, awaiting whatever I/O that takes.
25///
26/// This is the trait the shared emission algorithm is written against.
27/// [`BoxFuture`] is `Send`-bounded, so an implementation over a non-`Send`
28/// writer must do its work eagerly and return an already-complete future —
29/// as [`Immediate`] does — rather than capture the writer.
30pub trait AsyncByteSink {
31    /// Writes all of `buf`, erroring if any byte cannot be accepted.
32    fn write_all<'a>(&'a mut self, buf: &'a [u8]) -> BoxFuture<'a, Result<()>>;
33}
34
35/// An exclusive reference to a sink is itself a sink, forwarding the
36/// write — the write-side counterpart of `pdfboss_core::source`'s `&T`
37/// impl, and what lets a caller keep one sink across several by-value
38/// entry-point calls.
39impl<S: AsyncByteSink + ?Sized> AsyncByteSink for &mut S {
40    fn write_all<'a>(&'a mut self, buf: &'a [u8]) -> BoxFuture<'a, Result<()>> {
41        (**self).write_all(buf)
42    }
43}
44
45/// The in-memory sink: bytes accumulate in the vector and the returned
46/// future is already complete.
47impl AsyncByteSink for Vec<u8> {
48    fn write_all<'a>(&'a mut self, buf: &'a [u8]) -> BoxFuture<'a, Result<()>> {
49        self.extend_from_slice(buf);
50        Box::pin(std::future::ready(Ok(())))
51    }
52}
53
54/// Presents a synchronous [`std::io::Write`] as an [`AsyncByteSink`]
55/// whose futures are already complete.
56///
57/// The write happens eagerly, when the method is called; the returned
58/// future merely reports its result. That keeps the future free of any
59/// borrow of the writer — so it is `Send` whatever the writer is — and a
60/// future tree built over this type completes on its first poll, never
61/// parking inside [`pdfboss_core::block_on`]. No flush is ever performed:
62/// the writer comes back (or drops) exactly as buffered.
63#[derive(Debug, Clone)]
64pub struct Immediate<W>(pub W);
65
66impl<W: Write> AsyncByteSink for Immediate<W> {
67    fn write_all<'a>(&'a mut self, buf: &'a [u8]) -> BoxFuture<'a, Result<()>> {
68        let outcome = self.0.write_all(buf).map_err(Error::from);
69        Box::pin(std::future::ready(outcome))
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use std::cell::RefCell;
76    use std::io::Write;
77    use std::rc::Rc;
78
79    use pdfboss_core::block_on;
80
81    use super::{AsyncByteSink, Immediate};
82
83    #[test]
84    fn a_vec_is_a_sink() {
85        let mut sink = Vec::new();
86        block_on(AsyncByteSink::write_all(&mut sink, b"abc")).expect("a Vec accepts everything");
87        block_on(AsyncByteSink::write_all(&mut sink, b"def")).expect("a Vec accepts everything");
88        assert_eq!(sink, b"abcdef");
89    }
90
91    #[test]
92    fn a_reference_to_a_sink_is_a_sink() {
93        fn feed<S: AsyncByteSink>(mut sink: S) -> S {
94            block_on(sink.write_all(b"xy")).expect("the test sinks accept everything");
95            sink
96        }
97
98        let mut sink = Vec::new();
99        feed(&mut sink);
100        let sink = feed(sink);
101        assert_eq!(sink, b"xyxy");
102    }
103
104    #[test]
105    fn immediate_writes_through_to_the_writer() {
106        let mut sink = Immediate(Vec::new());
107        block_on(sink.write_all(b"hello")).expect("a Vec accepts everything");
108        assert_eq!(sink.0, b"hello");
109    }
110
111    /// A writer that shares state through `Rc` — deliberately not `Send` —
112    /// so this pins the design point: `Immediate`'s eager write keeps the
113    /// future `Send` without capturing the writer.
114    struct SharedWriter(Rc<RefCell<Vec<u8>>>);
115
116    impl Write for SharedWriter {
117        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
118            self.0.borrow_mut().extend_from_slice(buf);
119            Ok(buf.len())
120        }
121
122        fn flush(&mut self) -> std::io::Result<()> {
123            Ok(())
124        }
125    }
126
127    #[test]
128    fn immediate_futures_are_send_even_over_a_non_send_writer() {
129        fn assert_send<T: Send>(_: &T) {}
130
131        let shared = Rc::new(RefCell::new(Vec::new()));
132        let mut sink = Immediate(SharedWriter(Rc::clone(&shared)));
133        let future = sink.write_all(b"eager");
134        assert_send(&future);
135        block_on(future).expect("the shared writer accepts everything");
136        assert_eq!(*shared.borrow(), b"eager");
137    }
138
139    /// The write happens when the method is called, not when the future is
140    /// polled — the same eagerness divergence `pdfboss_core::source`
141    /// documents for its `Immediate`.
142    #[test]
143    fn immediate_writes_eagerly() {
144        let shared = Rc::new(RefCell::new(Vec::new()));
145        let mut sink = Immediate(SharedWriter(Rc::clone(&shared)));
146        let unpolled = sink.write_all(b"already there");
147        assert_eq!(*shared.borrow(), b"already there");
148        drop(unpolled);
149    }
150
151    /// A writer that refuses everything, so the error path is covered.
152    struct Refusing;
153
154    impl Write for Refusing {
155        fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
156            Err(std::io::Error::other("refused"))
157        }
158
159        fn flush(&mut self) -> std::io::Result<()> {
160            Ok(())
161        }
162    }
163
164    #[test]
165    fn immediate_surfaces_write_errors() {
166        let mut sink = Immediate(Refusing);
167        let err = block_on(sink.write_all(b"x")).unwrap_err();
168        assert!(matches!(err, crate::error::Error::Io(_)));
169    }
170}