pdfboss_core/source.rs
1//! The object-source abstraction shared by the synchronous and asynchronous
2//! APIs.
3//!
4//! `pdfboss` keeps exactly one implementation of every algorithm that reads a
5//! document — text extraction, rasterization — and two ways of delivering
6//! bytes to it. [`ObjectSource`] is what a caller holding the whole file
7//! provides. [`AsyncObjectSource`] is what a caller streaming from a file or
8//! a network provides. Algorithms are written once against the asynchronous
9//! trait and reach synchronous callers through [`Immediate`] and
10//! [`block_on`].
11//!
12//! [`block_on`] is a complete single-threaded driver: it polls, and on
13//! [`Poll::Pending`] parks the calling thread until a waker unparks it. A
14//! future built over [`Immediate`] resolves every leaf against a
15//! [`std::future::Ready`] and so completes on its first poll, never reaching
16//! the parking path — but that is an optimisation, not a precondition. Any
17//! future whose wakeup does not depend on an external reactor is driven
18//! correctly; one that does will park forever, so see [`block_on`]'s own
19//! documentation before handing it a future from elsewhere.
20//!
21//! Nothing here needs an async runtime: `Future`, `Pin`, `Box`, `Arc`,
22//! `Wake` and `std::future::ready` are all in the standard library, so
23//! `pdfboss-core` stays free of executor dependencies.
24//!
25//! # Signing a shared algorithm
26//!
27//! Every algorithm written against [`AsyncObjectSource`] follows the same three
28//! rules. They are not stylistic: each one is what makes a single
29//! implementation serve both a synchronous and an asynchronous caller.
30//!
31//! 1. **Entry points take the source by value** — `src: S`, never `&S`. A future
32//! holding `&'a S` is `Send` but never `'static`, and the asynchronous
33//! consumers (spawning onto a runtime, crossing into the Python bindings) need
34//! both. It costs nothing: an asynchronous document is an `Arc` handle, and
35//! [`Immediate`] over a borrowed document is `Copy`.
36//!
37//! An entry point is a function a consumer awaits directly. Helpers *inside*
38//! one take `&S`, because the entry point owns the source and a future may
39//! borrow across its own awaits freely — the `'static` question is settled at
40//! the outermost boundary, not at every internal call. That does put `S: Sync`
41//! on the entry point's future being `Send`, which costs nothing either:
42//! [`resolve_with`] already requires `Sync` of every genuinely asynchronous
43//! source. One entry point calling another passes `&src`, which works because
44//! `&S` is itself an [`AsyncObjectSource`].
45//! 2. **Put no `Send` or `Sync` bound on the function.** Auto traits are
46//! inferred per instantiation, so one function yields a `Send` future over
47//! an asynchronous source and a non-`Send` future over
48//! `Immediate<&Document>` — which is correct, because [`block_on`] drives
49//! the latter on the calling thread and never sends it anywhere.
50//! 3. **Call `src.resolve(o)`, not [`resolve_with`]**, unless the algorithm
51//! genuinely needs `S: Sync` for other reasons. `resolve_with` requires
52//! `Sync` and so excludes `Immediate<&Document>`; reaching for it out of
53//! habit silently breaks the synchronous path.
54//!
55//! [`crate::document::page_content_with`] is the reference example.
56
57use std::future::Future;
58use std::pin::Pin;
59use std::sync::atomic::{AtomicBool, Ordering};
60use std::sync::Arc;
61use std::task::{Context, Poll, Wake, Waker};
62
63use crate::error::Result;
64use crate::object::{ObjRef, Object, Stream};
65
66/// Reference-chase depth limit for every reference chase in the crate:
67/// [`resolve_sync_with`], [`resolve_with`], the provided
68/// [`ObjectSource::resolve`] and [`crate::document::Document::resolve`].
69///
70/// This is a denial-of-service guard — a file can encode a reference chain of
71/// any length, and a malicious one can encode a cycle — so it is deliberately
72/// one definition. It is public because the cap is part of the observable
73/// contract: a caller that hands `pdfboss` a legitimately deep chain needs to
74/// know where the crate stops chasing and reports
75/// [`crate::error::Error::CircularReference`] instead.
76pub const MAX_RESOLVE_DEPTH: usize = 32;
77
78/// A boxed future, as returned by every [`AsyncObjectSource`] method.
79///
80/// Boxing keeps the trait object-safe (`dyn AsyncObjectSource` is usable) at
81/// the cost of one allocation per object fetch. Fetches are per *resource* —
82/// a font, an image, a form — never per glyph or per pixel, and the hot
83/// non-fetching helpers keep taking already-loaded `&Object`, so the
84/// allocation does not enter the rasterizer's inner loops.
85///
86/// `Send` is required so that a future built over one of these can cross
87/// `tokio::spawn` and reach the Python bindings, which need `'static + Send`
88/// streams.
89pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
90
91/// Reads indirect objects and decodes streams, with the whole file already
92/// available.
93///
94/// Implement `get` and `stream_data`; `resolve` has a provided implementation
95/// that chases reference chains through `get` via [`resolve_sync_with`]. An
96/// implementor should override `resolve` only if its chasing genuinely
97/// differs; one that just wants the shared chase — as `Document` does — needs
98/// nothing.
99pub trait ObjectSource {
100 /// Fetches an indirect object by reference.
101 fn get(&self, r: ObjRef) -> Result<Object>;
102
103 /// Decodes a stream's data through its filter chain, resolving indirect
104 /// filter parameters against this source.
105 fn stream_data(&self, s: &Stream) -> Result<Vec<u8>>;
106
107 /// Chases reference chains, depth-capped at [`MAX_RESOLVE_DEPTH`].
108 ///
109 /// Lenient: a reference to a missing or unreadable object resolves to
110 /// [`Object::Null`]. Exceeding the depth cap is
111 /// [`crate::error::Error::CircularReference`].
112 ///
113 /// The loop itself lives in [`resolve_sync_with`], which this delegates
114 /// to; an implementor overriding this method should delegate there too
115 /// unless its chasing genuinely differs.
116 fn resolve(&self, o: &Object) -> Result<Object> {
117 resolve_sync_with(self, o)
118 }
119}
120
121/// A shared reference to a source is itself a source, forwarding every method.
122///
123/// This is what lets a synchronous entry point that only holds `&self` wrap
124/// itself for the asynchronous implementation — `Immediate(self)` builds an
125/// `Immediate<&Self>` — without cloning or owning the document. Forwarding
126/// `resolve` explicitly keeps the wrapped implementor's own reference chasing
127/// rather than falling back to the provided loop over `get`.
128impl<T: ObjectSource + ?Sized> ObjectSource for &T {
129 fn get(&self, r: ObjRef) -> Result<Object> {
130 (**self).get(r)
131 }
132
133 fn stream_data(&self, s: &Stream) -> Result<Vec<u8>> {
134 (**self).stream_data(s)
135 }
136
137 fn resolve(&self, o: &Object) -> Result<Object> {
138 (**self).resolve(o)
139 }
140}
141
142/// Reads indirect objects and decodes streams, awaiting whatever I/O that
143/// takes.
144///
145/// This is the trait the shared algorithms are written against. A caller who
146/// already holds the whole file reaches them through [`Immediate`], which
147/// implements this trait with futures that are already complete.
148pub trait AsyncObjectSource {
149 /// Fetches an indirect object by reference.
150 fn get(&self, r: ObjRef) -> BoxFuture<'_, Result<Object>>;
151
152 /// Decodes a stream's data through its filter chain, resolving indirect
153 /// filter parameters against this source.
154 fn stream_data<'a>(&'a self, s: &'a Stream) -> BoxFuture<'a, Result<Vec<u8>>>;
155
156 /// Chases reference chains, depth-capped at [`MAX_RESOLVE_DEPTH`].
157 ///
158 /// Lenient in the same way as [`ObjectSource::resolve`]: a reference to a
159 /// missing or unreadable object resolves to [`Object::Null`].
160 ///
161 /// There is no default body. The shared chasing loop lives in
162 /// [`resolve_with`], which needs `Self: Sync` because it holds `&self`
163 /// across an await; a `where` clause on a provided method would bind
164 /// overriding implementors too, and [`Immediate`] wraps sources that are
165 /// deliberately not `Sync`. An implementor that *is* `Sync` should
166 /// delegate to [`resolve_with`].
167 fn resolve<'a>(&'a self, o: &'a Object) -> BoxFuture<'a, Result<Object>>;
168}
169
170/// A shared reference to an asynchronous source is itself one, forwarding every
171/// method — the counterpart of the [`ObjectSource`] impl above.
172///
173/// This is what makes "# Signing a shared algorithm" rule 1 cheap rather than
174/// merely mandatory. An entry point owns its source so that its future can be
175/// `'static`, and it still needs to hand that source to shared helpers which own
176/// theirs for the same reason — [`crate::document::page_content_with`] is the one
177/// every page-reading algorithm starts with. Without this impl the owner's only
178/// options are to copy the helper's body or to give the helper a `&S` signature
179/// that no consumer can spawn.
180///
181/// All three methods forward explicitly because [`AsyncObjectSource::resolve`]
182/// has no default body, and forwarding is what keeps the wrapped implementor's
183/// own reference chasing rather than substituting the shared loop over `get`.
184impl<T: AsyncObjectSource + ?Sized> AsyncObjectSource for &T {
185 fn get(&self, r: ObjRef) -> BoxFuture<'_, Result<Object>> {
186 (**self).get(r)
187 }
188
189 fn stream_data<'a>(&'a self, s: &'a Stream) -> BoxFuture<'a, Result<Vec<u8>>> {
190 (**self).stream_data(s)
191 }
192
193 fn resolve<'a>(&'a self, o: &'a Object) -> BoxFuture<'a, Result<Object>> {
194 (**self).resolve(o)
195 }
196}
197
198/// Chases reference chains against a synchronous source, depth-capped at
199/// [`MAX_RESOLVE_DEPTH`].
200///
201/// This is the one canonical synchronous chase: the provided
202/// [`ObjectSource::resolve`] and [`crate::document::Document::resolve`] both
203/// delegate here, so their leniency and their error reporting cannot drift
204/// apart. [`resolve_with`] is the same algorithm awaiting its fetches.
205///
206/// Lenient: a reference whose target is missing or unreadable resolves to
207/// [`Object::Null`], because a real-world file routinely dangles a reference
208/// into nothing and ISO 32000 makes such a reference equivalent to `null`.
209/// A non-reference is returned unchanged without a fetch.
210///
211/// # Errors
212///
213/// Returns [`crate::error::Error::CircularReference`] when the chain exceeds
214/// `MAX_RESOLVE_DEPTH` hops — naming the last reference followed — or when
215/// the underlying source reports one, which is propagated unchanged rather
216/// than flattened to `Null`.
217pub fn resolve_sync_with<S>(src: &S, o: &Object) -> Result<Object>
218where
219 S: ObjectSource + ?Sized,
220{
221 let mut current = o.clone();
222 let mut last_num = 0;
223 for _ in 0..MAX_RESOLVE_DEPTH {
224 match current {
225 Object::Ref(r) => {
226 last_num = r.num;
227 current = match src.get(r) {
228 Ok(object) => object,
229 Err(crate::error::Error::CircularReference(n)) => {
230 return Err(crate::error::Error::CircularReference(n))
231 }
232 Err(_) => return Ok(Object::Null),
233 };
234 }
235 other => return Ok(other),
236 }
237 }
238 Err(crate::error::Error::CircularReference(last_num))
239}
240
241/// Chases reference chains against an asynchronous source, depth-capped at
242/// [`MAX_RESOLVE_DEPTH`].
243///
244/// Lenient in the same way as [`ObjectSource::resolve`]: a reference to a
245/// missing or unreadable object resolves to [`Object::Null`]. An implementor
246/// that is `Sync` can satisfy [`AsyncObjectSource::resolve`] by delegating
247/// here; one that is not — such as [`Immediate`] over a source with
248/// thread-local interior state — supplies its own.
249///
250/// # Errors
251///
252/// Returns [`crate::error::Error::CircularReference`] when the chain exceeds
253/// `MAX_RESOLVE_DEPTH` hops, or when the underlying source reports one.
254pub async fn resolve_with<S>(src: &S, o: &Object) -> Result<Object>
255where
256 S: AsyncObjectSource + Sync + ?Sized,
257{
258 let mut current = o.clone();
259 let mut last_num = 0;
260 for _ in 0..MAX_RESOLVE_DEPTH {
261 match current {
262 Object::Ref(r) => {
263 last_num = r.num;
264 current = match src.get(r).await {
265 Ok(object) => object,
266 Err(crate::error::Error::CircularReference(n)) => {
267 return Err(crate::error::Error::CircularReference(n))
268 }
269 Err(_) => return Ok(Object::Null),
270 };
271 }
272 other => return Ok(other),
273 }
274 }
275 Err(crate::error::Error::CircularReference(last_num))
276}
277
278/// Presents a synchronous [`ObjectSource`] as an [`AsyncObjectSource`] whose
279/// futures are already complete.
280///
281/// Wrapping a source in `Immediate` is what lets the synchronous entry points
282/// share the asynchronous implementation. Because every future this produces
283/// is [`std::future::Ready`], a future tree built over it completes on its
284/// first poll and never parks — see [`block_on`].
285///
286/// Note the divergence from a genuinely asynchronous source: these futures do
287/// their work eagerly, when the method is called, rather than lazily on first
288/// poll. Constructing one and dropping it unpolled still performs the read.
289/// That is invisible to an algorithm that awaits what it constructs, but it
290/// means `Immediate` is not a timing-faithful stand-in for a streaming source.
291#[derive(Debug, Clone, Copy)]
292pub struct Immediate<S>(pub S);
293
294impl<S: ObjectSource> AsyncObjectSource for Immediate<S> {
295 fn get(&self, r: ObjRef) -> BoxFuture<'_, Result<Object>> {
296 Box::pin(std::future::ready(self.0.get(r)))
297 }
298
299 fn stream_data<'a>(&'a self, s: &'a Stream) -> BoxFuture<'a, Result<Vec<u8>>> {
300 Box::pin(std::future::ready(self.0.stream_data(s)))
301 }
302
303 fn resolve<'a>(&'a self, o: &'a Object) -> BoxFuture<'a, Result<Object>> {
304 Box::pin(std::future::ready(self.0.resolve(o)))
305 }
306}
307
308/// Unparks the thread blocked inside one particular [`block_on`] call.
309///
310/// `notified` is what makes the wakeup belong to that call rather than to the
311/// thread at large. Unpark tokens are per-thread and not counted: a single
312/// token satisfies the next `park` whoever set it, so without a per-call flag
313/// a nested `block_on` would consume the outer call's token and leave the
314/// outer call parked forever.
315///
316/// A stray token in the other direction is accepted deliberately. Because
317/// [`block_on`] must test this flag before parking to survive a nested call
318/// stealing the thread's single token, a wake that arrives during a poll can
319/// leave its token unconsumed, and a `Waker` clone outliving its call can set
320/// one afterwards. Either way a later unrelated `park` on this thread may
321/// return spuriously — which the standard library permits, and which is
322/// strictly better than the hang the opposite order produces. [`block_on`]'s
323/// `Poll::Ready` arm drains the token in the common case; the two remaining
324/// paths are documented there and cannot be closed from inside the call.
325struct ThreadWaker {
326 thread: std::thread::Thread,
327 notified: AtomicBool,
328}
329
330impl ThreadWaker {
331 /// Records the wakeup and unparks, but only on the `false -> true`
332 /// transition, so however many times a future wakes this waker at most
333 /// one unpark token is outstanding.
334 fn notify(&self) {
335 // `swap` rather than a load-then-store: the read-modify-write is what
336 // makes the transition test atomic, so two concurrent wakers cannot
337 // both observe `false` and both unpark.
338 //
339 // `Release` publishes everything the waking side wrote before waking
340 // — the state that made the future ready. It pairs with the `Acquire`
341 // swap in `block_on`'s park loop below, giving the driver a
342 // happens-before edge to that state before it re-polls. `park`/
343 // `unpark` synchronize on their own, but the flag is what the loop
344 // actually trusts to decide it may stop waiting, so the edge has to
345 // exist on the flag too. Nothing needs to be acquired *from* the flag
346 // here, so the read half stays relaxed.
347 if !self.notified.swap(true, Ordering::Release) {
348 self.thread.unpark();
349 }
350 }
351}
352
353impl Wake for ThreadWaker {
354 fn wake(self: Arc<Self>) {
355 self.notify();
356 }
357
358 fn wake_by_ref(self: &Arc<Self>) {
359 self.notify();
360 }
361}
362
363/// Runs `future` to completion on the current thread.
364///
365/// This is how the synchronous entry points run the shared asynchronous
366/// implementation. A future built over [`Immediate`] resolves every leaf
367/// against a [`std::future::Ready`], so it completes on its first poll and
368/// the parking path below is never entered.
369///
370/// Any other future is driven correctly *provided something eventually wakes
371/// it*. This is a bare driver, not a runtime: it owns no reactor, no timer
372/// wheel and no I/O registration. A future that returns
373/// [`Poll::Pending`] while waiting on a wakeup that only an external reactor
374/// could deliver — a socket becoming readable, a timer firing — **blocks this
375/// thread forever**. Such a future belongs on the runtime that owns its
376/// reactor; pass it to that runtime's own driver instead.
377///
378/// One case fails louder than blocking, and is worth naming because the type
379/// system does not catch it. A future built over the *asynchronous* document
380/// API is not merely unwakeable here — that crate reads files through its
381/// runtime's blocking pool, so driving one of its futures outside a runtime
382/// context **panics** rather than hanging. `block_on` is for futures built over
383/// [`Immediate`]; hand anything from the asynchronous API to the runtime that
384/// owns it.
385///
386/// Nesting is safe: an inner `block_on` on the same thread cannot steal the
387/// outer call's wakeup, because each call waits on a flag private to itself
388/// rather than on the thread's shared unpark token.
389pub fn block_on<F: Future>(future: F) -> F::Output {
390 // The `Arc` is kept alongside the `Waker` so the loop can read and clear
391 // the flag the waker sets; `Waker::from` would otherwise consume it.
392 let waker_state = Arc::new(ThreadWaker {
393 thread: std::thread::current(),
394 notified: AtomicBool::new(false),
395 });
396 let waker = Waker::from(Arc::clone(&waker_state));
397 let mut cx = Context::from_waker(&waker);
398 let mut future = std::pin::pin!(future);
399 loop {
400 match future.as_mut().poll(&mut cx) {
401 Poll::Ready(value) => {
402 // A future may legally wake and then complete in the same
403 // poll, which leaves the flag set and an unpark token
404 // outstanding that no `park` above ever consumed. Drain it,
405 // so a completed call cannot make an unrelated later `park`
406 // on this thread return spuriously. `park_timeout` with a
407 // zero duration consumes a waiting token and returns at once
408 // either way, so this never blocks.
409 if waker_state.notified.swap(false, Ordering::Acquire) {
410 std::thread::park_timeout(std::time::Duration::ZERO);
411 }
412 return value;
413 }
414 // Wait for *this* call's wakeup, testing the flag BEFORE parking.
415 // That order is load-bearing and the alternative deadlocks.
416 //
417 // An unpark token is a single per-thread boolean, not a count. So
418 // when this future woke during its own poll and then, still inside
419 // that poll, drove a nested `block_on` whose future also parked,
420 // the nested call consumed the one token — including the share
421 // this call was relying on. Our flag is set but no token remains.
422 // Parking first would block forever;
423 // `nested_block_on_does_not_strand_the_outer_call` is that
424 // deadlock, and it fails within its 30 s budget if this is
425 // reordered.
426 //
427 // The cost of testing first is that when nobody stole the token it
428 // goes unconsumed, so a later unrelated `park` on this thread can
429 // return spuriously. That is permitted by the standard library and
430 // is strictly preferable to a hang; the `Poll::Ready` arm drains
431 // the token in the common case anyway.
432 //
433 // Looping absorbs a spurious `park` return: the flag is still
434 // false, so it parks again. `Acquire` pairs with the `Release` in
435 // `ThreadWaker::notify`.
436 Poll::Pending => {
437 while !waker_state.notified.swap(false, Ordering::Acquire) {
438 std::thread::park();
439 }
440 }
441 }
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 use std::future::Future;
448 use std::pin::Pin;
449 use std::task::{Context, Poll};
450
451 use crate::document::Document;
452 use crate::error::{Error, Result};
453 use crate::object::{ObjRef, Object, Stream};
454 use crate::source::{
455 block_on, resolve_sync_with, resolve_with, AsyncObjectSource, BoxFuture, Immediate,
456 ObjectSource, MAX_RESOLVE_DEPTH,
457 };
458
459 /// Fetches the fixture's page content stream, which `simple_doc` writes as
460 /// object 4.
461 fn content_stream(doc: &Document) -> Stream {
462 match ObjectSource::get(doc, ObjRef { num: 4, gen: 0 }).unwrap() {
463 Object::Stream(s) => s,
464 other => panic!("expected object 4 to be the content stream, got {other:?}"),
465 }
466 }
467
468 /// The trait impl must be a pure forward to the inherent methods: the
469 /// same reference read both ways yields the same object.
470 #[test]
471 fn document_trait_get_matches_inherent_get() {
472 let doc = Document::load(pdfboss_testkit::simple_doc("Hello")).unwrap();
473 let r = ObjRef { num: 1, gen: 0 };
474 let inherent = doc.get(r).unwrap();
475 let through_trait = ObjectSource::get(&doc, r).unwrap();
476 assert_eq!(inherent, through_trait);
477 }
478
479 /// A reference to a missing object resolves to Null rather than erroring
480 /// (Document::resolve is lenient); the trait must preserve that.
481 #[test]
482 fn document_trait_resolve_is_lenient_about_missing_targets() {
483 let doc = Document::load(pdfboss_testkit::simple_doc("Hello")).unwrap();
484 let missing = Object::Ref(ObjRef { num: 9_999, gen: 0 });
485 assert_eq!(
486 ObjectSource::resolve(&doc, &missing).unwrap(),
487 Object::Null,
488 "a dangling reference must resolve to Null through the trait, \
489 matching Document::resolve"
490 );
491 }
492
493 /// Reading through Immediate must agree with reading synchronously.
494 #[test]
495 fn immediate_get_matches_the_sync_source() {
496 let doc = Document::load(pdfboss_testkit::simple_doc("Hello")).unwrap();
497 let r = ObjRef { num: 1, gen: 0 };
498 let expected = ObjectSource::get(&doc, r).unwrap();
499
500 let src = Immediate(&doc);
501 let actual = block_on(src.get(r)).unwrap();
502
503 assert_eq!(actual, expected);
504 }
505
506 /// `Immediate`'s `resolve` override delegates to the wrapped source, so it
507 /// must reproduce `Document::resolve`'s leniency: a dangling reference
508 /// becomes Null, not an error.
509 #[test]
510 fn immediate_resolve_is_lenient_about_missing_targets() {
511 let doc = Document::load(pdfboss_testkit::simple_doc("Hello")).unwrap();
512 let missing = Object::Ref(ObjRef { num: 9_999, gen: 0 });
513
514 let src = Immediate(&doc);
515 assert_eq!(block_on(src.resolve(&missing)).unwrap(), Object::Null);
516 }
517
518 /// Decoding a stream through Immediate must agree with decoding it
519 /// synchronously.
520 #[test]
521 fn immediate_stream_data_matches_the_sync_source() {
522 let doc = Document::load(pdfboss_testkit::simple_doc("Hello")).unwrap();
523 let stream = content_stream(&doc);
524 let expected = ObjectSource::stream_data(&doc, &stream).unwrap();
525
526 let src = Immediate(&doc);
527 let actual = block_on(src.stream_data(&stream)).unwrap();
528
529 assert_eq!(actual, expected);
530 assert!(
531 !actual.is_empty(),
532 "the fixture's content stream must decode to something"
533 );
534 }
535
536 /// `block_on` completes a nested future tree — several awaits deep, which
537 /// is the shape the shared algorithms produce.
538 #[test]
539 fn block_on_drives_a_nested_future_tree() {
540 let doc = Document::load(pdfboss_testkit::simple_doc("Hello")).unwrap();
541 let src = Immediate(&doc);
542
543 async fn depth_three<S: AsyncObjectSource>(src: &S, r: ObjRef) -> Object {
544 async fn inner<S: AsyncObjectSource>(src: &S, r: ObjRef) -> Object {
545 src.resolve(&Object::Ref(r)).await.unwrap()
546 }
547 inner(src, r).await
548 }
549
550 let got = block_on(depth_three(&src, ObjRef { num: 1, gen: 0 }));
551 assert!(
552 !matches!(got, Object::Null),
553 "object 1 of a simple document must resolve to something"
554 );
555 }
556
557 /// A source whose every object is a reference to itself, so chasing a
558 /// chain never terminates and must hit the depth cap. Reaching the cap is
559 /// unreachable through `Immediate`, whose `resolve` delegates to the
560 /// wrapped source, so this stub is what exercises `resolve_with`'s loop.
561 struct SelfReferential;
562
563 impl AsyncObjectSource for SelfReferential {
564 fn get(&self, r: ObjRef) -> BoxFuture<'_, Result<Object>> {
565 Box::pin(std::future::ready(Ok(Object::Ref(r))))
566 }
567
568 fn stream_data<'a>(&'a self, s: &'a Stream) -> BoxFuture<'a, Result<Vec<u8>>> {
569 Box::pin(std::future::ready(Ok(s.data.clone())))
570 }
571
572 fn resolve<'a>(&'a self, o: &'a Object) -> BoxFuture<'a, Result<Object>> {
573 Box::pin(resolve_with(self, o))
574 }
575 }
576
577 /// `resolve_with` must stop at the depth cap and name the last reference
578 /// it followed, rather than looping forever.
579 #[test]
580 fn resolve_with_stops_at_the_depth_cap() {
581 let chain = Object::Ref(ObjRef { num: 7, gen: 0 });
582 let err = block_on(resolve_with(&SelfReferential, &chain)).unwrap_err();
583 assert!(
584 matches!(err, Error::CircularReference(7)),
585 "a self-referential chain must exhaust the cap and report \
586 CircularReference for the last reference seen, got {err:?}"
587 );
588 }
589
590 /// A non-reference passes straight through `resolve_with` without a fetch.
591 #[test]
592 fn resolve_with_returns_a_direct_object_unchanged() {
593 let direct = Object::Int(42);
594 assert_eq!(
595 block_on(resolve_with(&SelfReferential, &direct)).unwrap(),
596 Object::Int(42)
597 );
598 }
599
600 /// An owned source holding heap state, standing in for a real asynchronous
601 /// document (which owns an `Arc`). The heap field is load-bearing for the
602 /// test below: a unit struct would be const-promoted, so `&UnitStub` would
603 /// be a `&'static` reference and would satisfy a `'static` assertion even
604 /// under a by-reference signature — making the assertion useless as a gate.
605 /// A `Vec` cannot be promoted, so the borrow is genuinely non-`'static`.
606 struct OwnedStub {
607 payload: Vec<u8>,
608 }
609
610 impl AsyncObjectSource for OwnedStub {
611 fn get(&self, _r: ObjRef) -> BoxFuture<'_, Result<Object>> {
612 Box::pin(std::future::ready(Ok(Object::Int(
613 self.payload.len() as i64
614 ))))
615 }
616
617 fn stream_data<'a>(&'a self, s: &'a Stream) -> BoxFuture<'a, Result<Vec<u8>>> {
618 Box::pin(std::future::ready(Ok(s.data.clone())))
619 }
620
621 fn resolve<'a>(&'a self, o: &'a Object) -> BoxFuture<'a, Result<Object>> {
622 Box::pin(resolve_with(self, o))
623 }
624 }
625
626 /// Stands in for a shared algorithm: generic over the source, taking it by
627 /// value, and carrying no `Send` or `Sync` bound of its own.
628 async fn fetch_one<S: AsyncObjectSource>(src: S, r: ObjRef) -> Result<Object> {
629 src.get(r).await
630 }
631
632 /// An algorithm that owns its source produces a future a runtime's `spawn`
633 /// and the Python bindings will both accept. Taking `&S` instead produces
634 /// one that is `Send` but borrowed, which both reject — so this is what
635 /// keeps the by-value rule in the module documentation enforceable rather
636 /// than advisory. Confirmed to be a real gate, not a tautology: changing
637 /// `fetch_one` to take `src: &S` makes this stop compiling with `E0716
638 /// temporary value dropped while borrowed`. That check only works because
639 /// `OwnedStub` holds heap state — see its own note.
640 #[test]
641 fn an_owned_source_yields_a_spawnable_future() {
642 fn assert_send_static<F: Future + Send + 'static>(_: &F) {}
643
644 let future = fetch_one(
645 OwnedStub {
646 payload: vec![1, 2, 3],
647 },
648 ObjRef { num: 3, gen: 0 },
649 );
650 assert_send_static(&future);
651 assert!(block_on(future).is_ok());
652 }
653
654 /// The same generic function must still serve a synchronous caller, whose
655 /// source is neither `Send` nor `'static`. Auto traits are inferred per
656 /// instantiation, so no bound on the function has to choose between the two
657 /// callers — which is the whole premise of sharing one implementation.
658 #[test]
659 fn the_same_function_serves_a_borrowed_synchronous_source() {
660 let doc = Document::load(pdfboss_testkit::simple_doc("Hello")).unwrap();
661 let r = ObjRef { num: 1, gen: 0 };
662 let expected = ObjectSource::get(&doc, r).unwrap();
663
664 let got = block_on(fetch_one(Immediate(&doc), r)).unwrap();
665
666 assert_eq!(got, expected);
667 }
668
669 /// What lets one by-value entry point call another. An algorithm that owns
670 /// its source — which rule 1 requires of every entry point — can only reach a
671 /// shared helper with the same by-value signature by handing out `&src`, so
672 /// `&S` has to be a source in its own right. Without this the choice is
673 /// between duplicating the helper and giving it a signature no consumer can
674 /// spawn.
675 #[test]
676 fn a_reference_to_a_source_is_a_source() {
677 let stub = OwnedStub {
678 payload: vec![7, 7],
679 };
680 let r = ObjRef { num: 1, gen: 0 };
681
682 let borrowed = block_on(fetch_one(&stub, r)).unwrap();
683 let owned = block_on(fetch_one(stub, r)).unwrap();
684
685 assert_eq!(borrowed, Object::Int(2));
686 assert_eq!(borrowed, owned);
687 }
688
689 /// The synchronous mirror of `SelfReferential`. Reaching the cap is
690 /// unreachable through a real `Document`, whose own re-entrancy guard
691 /// fires first, so this stub is what exercises `resolve_sync_with`'s loop.
692 struct SyncSelfReferential;
693
694 impl ObjectSource for SyncSelfReferential {
695 fn get(&self, r: ObjRef) -> Result<Object> {
696 Ok(Object::Ref(r))
697 }
698
699 fn stream_data(&self, s: &Stream) -> Result<Vec<u8>> {
700 Ok(s.data.clone())
701 }
702 }
703
704 /// Reports a cycle for every fetch, so the propagating branch is covered.
705 struct CircularSource;
706
707 impl ObjectSource for CircularSource {
708 fn get(&self, r: ObjRef) -> Result<Object> {
709 Err(Error::CircularReference(r.num))
710 }
711
712 fn stream_data(&self, s: &Stream) -> Result<Vec<u8>> {
713 Ok(s.data.clone())
714 }
715 }
716
717 /// Fails every fetch for a reason that is *not* a cycle, so the lenient
718 /// branch is covered.
719 struct UnreadableSource;
720
721 impl ObjectSource for UnreadableSource {
722 fn get(&self, _: ObjRef) -> Result<Object> {
723 Err(Error::MissingKey("Length"))
724 }
725
726 fn stream_data(&self, s: &Stream) -> Result<Vec<u8>> {
727 Ok(s.data.clone())
728 }
729 }
730
731 /// A finite chain: object `n` refers to object `n - 1`, and object 0 is a
732 /// direct integer. Chasing from `Ref(k)` therefore terminates after a
733 /// known number of hops, which is what lets the cap be measured.
734 struct Chain;
735
736 impl ObjectSource for Chain {
737 fn get(&self, r: ObjRef) -> Result<Object> {
738 if r.num == 0 {
739 Ok(Object::Int(0))
740 } else {
741 Ok(Object::Ref(ObjRef {
742 num: r.num - 1,
743 gen: 0,
744 }))
745 }
746 }
747
748 fn stream_data(&self, s: &Stream) -> Result<Vec<u8>> {
749 Ok(s.data.clone())
750 }
751 }
752
753 /// `resolve_sync_with` must stop at the depth cap and name the last
754 /// reference it followed, rather than looping forever.
755 #[test]
756 fn resolve_sync_with_stops_at_the_depth_cap() {
757 let chain = Object::Ref(ObjRef { num: 7, gen: 0 });
758 let err = resolve_sync_with(&SyncSelfReferential, &chain).unwrap_err();
759 assert!(
760 matches!(err, Error::CircularReference(7)),
761 "a self-referential chain must exhaust the cap and report \
762 CircularReference for the last reference seen, got {err:?}"
763 );
764 }
765
766 /// A non-reference passes straight through `resolve_sync_with` without a
767 /// fetch.
768 #[test]
769 fn resolve_sync_with_returns_a_direct_object_unchanged() {
770 assert_eq!(
771 resolve_sync_with(&SyncSelfReferential, &Object::Int(42)).unwrap(),
772 Object::Int(42)
773 );
774 }
775
776 /// The two halves of the shared chase's error handling, both load-bearing
777 /// for `Document::resolve`: an unreadable target becomes `Null`, while a
778 /// cycle reported by the source propagates unchanged.
779 #[test]
780 fn resolve_sync_with_is_lenient_but_propagates_cycles() {
781 let r = Object::Ref(ObjRef { num: 5, gen: 0 });
782 assert_eq!(
783 resolve_sync_with(&UnreadableSource, &r).unwrap(),
784 Object::Null,
785 "a fetch failing for any reason other than a cycle must flatten \
786 to Null"
787 );
788 assert!(matches!(
789 resolve_sync_with(&CircularSource, &r).unwrap_err(),
790 Error::CircularReference(5)
791 ));
792 }
793
794 /// The provided `ObjectSource::resolve` must *be* the shared chase, not a
795 /// second copy of it: same answer on a direct object and same cap error.
796 #[test]
797 fn the_provided_resolve_is_the_shared_chase() {
798 let direct = Object::Int(9);
799 assert_eq!(
800 ObjectSource::resolve(&SyncSelfReferential, &direct).unwrap(),
801 resolve_sync_with(&SyncSelfReferential, &direct).unwrap()
802 );
803
804 let chain = Object::Ref(ObjRef { num: 3, gen: 0 });
805 assert!(matches!(
806 ObjectSource::resolve(&SyncSelfReferential, &chain).unwrap_err(),
807 Error::CircularReference(3)
808 ));
809 }
810
811 /// The synchronous and asynchronous chases must share one cap, which is
812 /// the point of `MAX_RESOLVE_DEPTH` having a single definition. Chasing
813 /// the longest chain that fits succeeds both ways; one hop more fails both
814 /// ways, with the same reference named.
815 #[test]
816 fn both_chases_share_one_depth_cap() {
817 // From `Ref(k)` the loop follows k + 1 references and spends one more
818 // iteration returning the direct object it lands on: k + 2 iterations
819 // for a cap of MAX_RESOLVE_DEPTH.
820 let longest = u32::try_from(MAX_RESOLVE_DEPTH - 2).expect("the cap fits in a u32");
821 let fits = Object::Ref(ObjRef {
822 num: longest,
823 gen: 0,
824 });
825 let too_long = Object::Ref(ObjRef {
826 num: longest + 1,
827 gen: 0,
828 });
829
830 assert_eq!(resolve_sync_with(&Chain, &fits).unwrap(), Object::Int(0));
831 assert_eq!(
832 block_on(resolve_with(&Immediate(&Chain), &fits)).unwrap(),
833 Object::Int(0)
834 );
835
836 assert!(matches!(
837 resolve_sync_with(&Chain, &too_long).unwrap_err(),
838 Error::CircularReference(0)
839 ));
840 assert!(matches!(
841 block_on(resolve_with(&Immediate(&Chain), &too_long)).unwrap_err(),
842 Error::CircularReference(0)
843 ));
844 }
845
846 /// The parking path must actually resume. This future returns Pending
847 /// once — waking itself first, so the unpark token is already set and the
848 /// test cannot deadlock — then Ready.
849 #[test]
850 fn block_on_resumes_after_parking() {
851 struct YieldOnce {
852 yielded: bool,
853 }
854
855 impl Future for YieldOnce {
856 type Output = u32;
857
858 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<u32> {
859 if self.yielded {
860 Poll::Ready(7)
861 } else {
862 self.yielded = true;
863 cx.waker().wake_by_ref();
864 Poll::Pending
865 }
866 }
867 }
868
869 assert_eq!(
870 block_on(YieldOnce { yielded: false }),
871 7,
872 "block_on must re-poll after parking rather than panicking or hanging"
873 );
874 }
875
876 /// A nested `block_on` on the same thread must not consume the outer
877 /// call's wakeup. The outer future arms its own waker and then, still
878 /// inside that same poll, runs a complete nested driver before yielding.
879 /// Unpark tokens are per-thread and not counted, so a driver that trusted
880 /// `park` alone let the inner call swallow the outer wake and left the
881 /// outer call parked forever. Driven on a worker thread with a bounded
882 /// wait, so a regression fails this test instead of hanging the suite.
883 #[test]
884 fn nested_block_on_does_not_strand_the_outer_call() {
885 /// Yields once, waking itself first so the nested driver has a wakeup
886 /// to observe.
887 struct SelfWaking {
888 yielded: bool,
889 }
890
891 impl Future for SelfWaking {
892 type Output = u32;
893
894 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<u32> {
895 if self.yielded {
896 return Poll::Ready(7);
897 }
898 self.yielded = true;
899 cx.waker().wake_by_ref();
900 Poll::Pending
901 }
902 }
903
904 struct Outer {
905 polled: bool,
906 inner: u32,
907 }
908
909 impl Future for Outer {
910 type Output = u32;
911
912 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<u32> {
913 if self.polled {
914 return Poll::Ready(self.inner);
915 }
916 self.polled = true;
917 cx.waker().wake_by_ref();
918 self.inner = block_on(SelfWaking { yielded: false });
919 Poll::Pending
920 }
921 }
922
923 let (tx, rx) = std::sync::mpsc::channel();
924 std::thread::spawn(move || {
925 let _ = tx.send(block_on(Outer {
926 polled: false,
927 inner: 0,
928 }));
929 });
930
931 match rx.recv_timeout(std::time::Duration::from_secs(30)) {
932 Ok(inner) => assert_eq!(
933 inner, 7,
934 "the nested block_on must have run its future to completion"
935 ),
936 Err(e) => panic!(
937 "the outer block_on never finished ({e:?}): the nested call \
938 consumed its wakeup"
939 ),
940 }
941 }
942
943 /// `BoxFuture` promises `Send`, which the asynchronous API depends on to
944 /// cross `tokio::spawn` and reach the Python bindings. Pin that here: the
945 /// futures must be `Send` even though `Document` — with its `Rc`/`RefCell`
946 /// caches — is itself neither `Send` nor `Sync`.
947 #[test]
948 fn immediate_futures_are_send() {
949 fn assert_send<T: Send>(_: &T) {}
950
951 let doc = Document::load(pdfboss_testkit::simple_doc("Hello")).unwrap();
952 let stream = content_stream(&doc);
953 let src = Immediate(&doc);
954 let object = Object::Ref(ObjRef { num: 1, gen: 0 });
955
956 assert_send(&src.get(ObjRef { num: 1, gen: 0 }));
957 assert_send(&src.stream_data(&stream));
958 assert_send(&src.resolve(&object));
959 }
960
961 /// `BoxFuture`'s documentation claims `dyn AsyncObjectSource` is usable.
962 /// Hold the trait to it, so a later signature change cannot quietly break
963 /// object-safety.
964 #[test]
965 fn async_object_source_is_object_safe() {
966 fn assert_dyn(_: &dyn AsyncObjectSource) {}
967
968 let doc = Document::load(pdfboss_testkit::simple_doc("Hello")).unwrap();
969 assert_dyn(&Immediate(&doc));
970 assert_dyn(&SelfReferential);
971 }
972}