Skip to content

Add .pptx and .pdf, and move the module onto TinyBus streams - #6

Merged
senamakel merged 14 commits into
tinyhumansai:mainfrom
senamakel:tinydocs-port
Aug 11, 2026
Merged

Add .pptx and .pdf, and move the module onto TinyBus streams#6
senamakel merged 14 commits into
tinyhumansai:mainfrom
senamakel:tinydocs-port

Conversation

@senamakel

@senamakel senamakel commented Aug 11, 2026

Copy link
Copy Markdown
Member

Summary

Teaches TinyDocs the other two document formats and makes the TinyBus module the
way a host reaches all three. .pptx synthesis and .pdf text extraction are
ported in behind their own feature gates, the spec types move out from behind the
format gates so a host can share the wire contract without any writer, and the
module's bus surface is rebuilt on TinyBus streams.

The motivating consumer is OpenHuman, which carries docx-rs, ppt-rs and
pdf-extract today purely to support three agent tools. With this landed it
carries none of them: 39 crates leave its lockfile and its product profile drops
from 505 to 448 unique crate names.

Related issue

None.

API or behavior changes

Additive on the library side. tinydocs::spec is new and is where every spec
type, limit and validate now lives — compiled in every build, including
--no-default-features, with serde and the crate error type as its only
dependencies. The format modules re-export what they consume, so
tinydocs::docx::DocumentSpec still names the same type and existing code keeps
compiling. New pptx and pdf modules behind new default-ON features. New
Error::ExtractionFailed variant (Error is #[non_exhaustive], so this does
not break a match).

Breaking on the module's bus surface. ai.tinyhumans.tinydocs.Docx is retired
and replaced by ai.tinyhumans.tinydocs.Documents. Returning a handle where
callers expect inline bytes is a breaking contract, and FOR_AGENTS.md is explicit
that those get a new interface name.

It is replaced rather than served alongside because module_export! attaches
its methods list to the first entry in provides and leaves any others empty —
a second fully-declared interface is not expressible today, and a manifest that
under-declared its members would break the invariant that manifest methods and
dispatch members stay identical. Noted under Open questions in the spec.

Validation

Commands actually run, with their outcome:

  • cargo fmt --all -- --check — clean
  • cargo clippy --all-targets --all-features -- -D warnings — clean
  • cargo build --all-targets --all-features — clean
  • cargo test --all-features — 121 passed, 0 failed, 1 ignored (the loader E2E)
  • .github/scripts/check-file-coverage.sh 90 coverage.json — every file ≥ 90%

Also run, because they are the ones that actually prove this change:

  • cargo check -p tinydocs --no-default-features — the spec-only build compiles,
    and cargo tree -p tinydocs --no-default-features -i docx-rs finds nothing.
    If this ever fails, the carve-out is broken and no host can take the contract
    without a codec.
  • The full feature matrix: no-default, docx, pptx, pdf, all-features.
  • TINYDOCS_TEST_MODULE=… cargo test -p tinydocs-module --test module_e2e -- --ignored — the built cdylib through the real dynamic loader and a real
    broker.

Tests

121 unit tests plus the loader E2E.

The blob/stream work is weighted towards refusals rather than happy paths,
deliberately: a module is never unloaded, so what decides whether an abandoned
transfer leaks is the bounds and the expiry, not whether bytes move. Every bound
has a test that trips it, and the TTL is tested in both directions — an abandoned
output is reaped, a slow consumer reading in chunks never is. The clock is a
parameter rather than Instant::now, which is what makes those deterministic.

The E2E covers all three formats through the real loader and moves an image pair
across a stream in several chunks, because a single-chunk transfer would not prove
the reassembly. It also asserts the refusal case: an image stream that does not
match the lengths the deck declares.

Two deliberate gaps:

  • The streaming methods have no unit tests. A stream needs two connected peers and
    a broker between them, so there is no honest way to test one against a bare
    struct; they are covered in the E2E instead.
  • The .pdf fixture is a PDF assembled in-process, cross-reference offsets
    computed from the bytes emitted, rather than a checked-in binary that could go
    stale.

Documentation

  • README.md — the separable-spec section, the presentation limits table, the
    rewritten TinyBus module section, feature flags, layout.
  • docs/specs/tinybus-module.md — rewritten for the new interface, the transfer
    asymmetry, and the bounds.
  • Module-level rustdoc on every new module, with the reasoning rather than the
    mechanics: why images are bytes and not references, why the output store exists
    at all, why transfers are append-only.

Checklist

  • The change is focused on one logical change
  • No new #[allow(...)], #[ignore], or relaxed lints
  • No secrets, tokens, or .env contents in the diff or the description

On the second box — one #[allow] was added, and I would rather flag it than
tick the box.
crates/tinydocs-module/src/service/mod.rs carries
#[allow(clippy::unused_async)] on the interface block. tinybus::interface
rejects a non-async method outright, so the two output methods are async because
the dispatch contract requires it, not because they await anything — the lint can
never be actionable in that block. It is scoped to the one block with a reason,
not a crate-level relax. Happy to take a different approach if you would rather.

The one #[ignore] is the pre-existing loader E2E, which cannot run without a
built artifact.

Note for the reviewer

The consuming OpenHuman PR pins this branch's head as its vendor/tinydocs
gitlink and cannot merge until this lands and v0.2.0 is cut — its module
registry pins per-platform SHA-256 digests from the release, and the ones there
now are v0.1.11's, which predate every commit here.

Summary by CodeRabbit

  • New Features
    • Added PowerPoint (.pptx) generation with text, bullets, notes, captions, and PNG/JPEG images.
    • Added PDF text extraction with validation for malformed, oversized, or empty files.
    • Added staged output downloads in chunks, integrity verification, and explicit output release.
    • Added reusable document and presentation specifications with clear validation limits.
  • Improvements
    • Expanded image handling with format and dimension detection.
    • Updated the service interface and documentation to support DOCX, PPTX, and PDF workflows.
  • Bug Fixes
    • Improved error reporting for invalid inputs, transfer failures, and extraction errors.

senamakel and others added 7 commits August 11, 2026 00:56
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The spec types, their limits, and `DocumentSpec::validate` were reachable only
with the `docx` feature on, because they lived in `src/docx/types.rs` behind a
gated module. That coupled the wire contract to the OOXML writer: a host whose
synthesis happens elsewhere — in another process, or behind the TinyBus module
in this repo — had to either pull `docx-rs` in to name `DocumentSpec`, or
re-declare the spec and let the two definitions drift.

Move them to a new ungated `src/spec/`, which depends on nothing but `serde` and
the crate error type, and re-export from `docx` so `tinydocs::docx::DocumentSpec`
still names the same type. `cargo check -p tinydocs --no-default-features` now
compiles the whole contract with `docx-rs` absent from the dependency graph.

Validation moves with the types, so it is no longer gated either — which is the
point: validating at a host boundary is the cheap half, and it should not
require the codec.

Public API: purely additive. `tinydocs::spec` is new; every existing path
resolves to the same item.

Tests split along the same line. `src/spec/test.rs` owns validation, the
blank/aggregate rules and the JSON contract, and must pass with every format
feature off; `src/docx/test.rs` keeps the OOXML mapping assertions. Two new
cases cover the aggregate-budget branches that only a bullet or a heading can
cross, taking `src/spec/document.rs` to 100% line coverage.

Co-authored-by: Medulla <medulla@tinyhumans.ai>
Ports the presentation engine from the OpenHuman host that already drove
`ppt-rs` directly, on the same split the docx path uses: the spec and its
validation go in ungated `spec/presentation`, the OOXML mapping goes in a gated
`pptx` module, and the host keeps its executor and deadline policy.

Three things are deliberately different from the code this came from.

Images are bytes, not references. The host's spec named an image by artifact id
or filesystem path, and resolving either is policy this crate must not hold —
which directories an agent may read, whether an identifier belongs to the
caller. `SlideImage` therefore carries the bytes, format and dimensions, and
`SlideImage::from_bytes` does the mechanical half of the hand-off. Because
identification lives in the ungated `spec::image`, a host can build and validate
a whole deck with `pptx` off.

Validation re-derives an image's format and dimensions from its bytes and
rejects any disagreement with what the spec declares. `from_bytes` keeps those
fields consistent by construction, but a spec can also arrive as JSON, where the
three are independent: a wrong format yields a part the reader refuses to open,
and wrong dimensions distort the image silently. Both now fail by name.

`fit_within` is integer arithmetic in u64 rather than f64 scaling. The float
spelling made the result depend on binary64 rounding for no benefit — an EMU is
1/914,400 inch, so a one-unit difference is invisible — and it could not satisfy
this repo's cast lints without an allow.

The host's `GenerationFailed { exit_code, stderr_truncated }` collapses onto
`Error::GenerationFailed { detail }`; `exit_code` was a vestige of a retired
python-pptx subprocess and was always -1. Timeout and cancellation stay host-
side, where the deadline is known.

Also fixes a mistake in the previous commit: `spec/document`'s tests were moved
without a `mod test;` declaration, so all 20 stopped running and the module's
coverage read 50%. Declared, they run again and it is back to 100%.

Public API: additive. `spec::presentation`, `spec::image` and `pptx` are new.
Format limits are reached through their own module rather than re-exported flat
from `spec`, because `MAX_TEXT_CHARS` means a different number per format.

Co-authored-by: Medulla <medulla@tinyhumans.ai>
Completes the document surface a host needs from this crate: it writes .docx and
.pptx, and now reads .pdf. Ported from the OpenHuman host, which called
`pdf_extract::extract_text_from_mem` directly from its multimodal ingest path.

This is the only module here that reads rather than writes, and the asymmetry
shapes it. Everything else turns a spec the caller authored into bytes, so there
is a contract to validate; extraction takes a document somebody else produced,
so the input is arbitrary and often damaged. Two consequences:

`ExtractionFailed` is a new `Error` variant rather than a reuse of
`GenerationFailed`. The two have opposite causes and opposite remedies —
generation fails on our output path and usually means a bug or an exhausted
resource, whereas extraction fails on someone else's input and usually means the
document is encrypted, damaged, or has no text layer. A caller that retries one
should not retry the other. `Error` is `#[non_exhaustive]`, so adding it does not
break a match.

The boundary checks are `InvalidInput`, not extraction failures: empty input,
input over `MAX_DOCUMENT_BYTES`, and input with no `%PDF-` signature all name the
offending field instead of surfacing a parser's phrasing for "you handed me a
JPEG". `MAX_DOCUMENT_BYTES` exists because extraction allocates well past the
input size while parsing, and the caller is handing over something it did not
produce; a host wanting a tighter bound applies it first.

A document that parses but carries no text layer — a scan — yields an empty
string rather than an error. Nothing to extract is not a failure to retry, and
OCR is out of scope.

The 60s timeout and the host's decision to degrade a failed extraction to a file
reference stay where they were: this call is synchronous and holds no opinion
about deadlines, and here that matters more than for synthesis, because the cost
is set by the input rather than by a spec this crate has already bounded.

Tests build a real single-page PDF in-process, computing its cross-reference
offsets from the bytes emitted, so extraction is exercised end to end without a
checked-in binary fixture that could go stale.

Co-authored-by: Medulla <medulla@tinyhumans.ai>
The module could only ever return a `.docx` inline, and that stops working the
moment the other two formats arrive. A TinyBus frame is a 16 MiB JSON document
and a `Vec<u8>` serialises as an array of integers — roughly 3.5 bytes of frame
per byte of payload — so the real inline ceiling is a few megabytes. A deck may
legally carry 8 images of 5 MiB, and a `.pdf` handed in for extraction is bounded
only by what the host accepted. TinyBus says as much itself: large payloads are
meant to travel as paths, not inline.

So bytes now move through a staging area in base64 chunks (1.34x rather than
3.5x), addressed by opaque blob ids, and no method returns bytes inline. A caller
stages a document, calls a format method, and reads the result back the same way;
frame size stops being part of the contract, and the caller's code path is the
same regardless of size.

Every bound in `blobs` is load-bearing rather than defensive. A module is trusted
in-process code that TinyBus never unloads, so an abandoned upload is never
reclaimed by a process exit that does not come — hence four independent limits
(per chunk, per blob, total staged, blob count) and expiry of untouched blobs.
The budget is reserved at `BeginBlob` rather than counted on arrival, so an
admitted transfer can always finish instead of failing halfway when somebody else
fills the area. Transfers are append-only: `offset` must equal the bytes received
so far, which makes "complete" mean "length reached" and turns a lost or
duplicated chunk into a named error at the moment it happens rather than a
corrupt blob discovered later. Completion verifies the caller's SHA-256 before
the blob becomes readable.

Expiry takes the clock as a parameter instead of calling `Instant::now`, which is
what makes the TTL rules testable at all; the tests drive time explicitly and
cover both directions — an abandoned blob is reaped, a slow but live transfer
never is.

Transfer errors are grouped by what the caller should do next rather than by what
went wrong internally: `UnknownBlob` means restart, `TransferRefused` means a
budget is full and the same request may work later, `TransferFailed` means
re-send. `BlobStore`'s `Debug` is written by hand so staged bytes cannot reach a
log line.

This replaces `ai.tinyhumans.tinydocs.Docx` rather than extending it. Returning a
`BlobRef` where callers expect bytes is a breaking contract, and TinyBus's module
guidance is explicit that those get a new interface name. It is not served
alongside the old one because `module_export!` attaches its method list to the
first entry in `provides` and leaves any others empty, so a second
fully-declared interface would have to under-declare its members and break the
invariant that manifest methods and dispatch members stay identical. Serving both
needs a TinyBus change; retiring one at a pre-1.0 minor bump does not.

The loader E2E test now covers all three formats through the real dynamic loader
and moves an image across several chunks — a single-chunk transfer would not
prove the offsets line up, which is the whole point of the change.

The one `#[allow]` added is `clippy::unused_async` on the interface block: the
macro rejects a non-async method outright, so the four transfer methods are async
because the dispatch contract requires it, and the lint can never be actionable
there.

Co-authored-by: Medulla <medulla@tinyhumans.ai>
TinyBus gained chunked, flow-controlled streams (tinyhumansai/tinybus#9), which
is the facility the hand-rolled staging area in the previous commit was standing
in for. Inbound payloads now use it, and the half of that code they replace is
deleted.

What goes away is the risky half. `BeginBlob`, `PutChunk`, the append-only offset
protocol, the upload digest check, the reserve-at-begin budget and the inbound
TTL all existed to answer one question — what happens when a caller starts
sending a document and never finishes — and TinyBus now answers it, per peer,
with a window, a size cap and an idle timeout. A stream is also tied to the call
that opened it and writable only by the peer that opened it, which is
authorisation the blob ids never had: any peer that guessed an id could have
written to somebody else's transfer.

What stays is the output half, because replies cannot stream. `Interface::call`
receives a member name and a JSON body — no caller identity, no connection — so
a served object cannot open a stream back to whoever called it. A produced
document is still held and pulled with `ReadOutput`, since returning it inline
would put it through a 16 MiB JSON frame where a `Vec<u8>` costs ~3.5 bytes per
byte. `outputs` is what is left of `blobs` once only that direction remains:
still bounded four ways and still expiring, because TinyBus never unloads a
module. A reply-stream seam upstream would delete it, and the spec now says so.

A deck's images share one stream, concatenated in slide order, because a call has
one stream and a deck has many pictures. Each image declares its `byte_len` in
the spec rather than framing itself in the stream, which is what makes a
truncated or over-long transfer a named rejection instead of a deck containing a
picture assembled from two different images. A text-only deck passes no stream at
all rather than opening an empty one.

The E2E test moves an image pair across a real stream through the real dynamic
loader and asserts the mismatch case, which is the only place the streaming paths
can be tested honestly: a stream needs two connected peers and a broker, so a
unit test against a bare struct cannot reach one.

Net: 5 methods instead of 7, and the module no longer implements transfer.
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The bus-facing deck shape — the one whose images name a length in a stream rather
than carrying bytes — lived in the private module crate. It belongs beside the
spec it mirrors: a host driving the module over a bus needs those types, and the
module crate is `publish = false`, so the host would have had to re-declare them.

That is exactly the drift the `spec` carve-out exists to prevent. The wire deck is
`serde` and nothing else, so it sits in `spec::presentation::wire` and is compiled
in every build, including `--no-default-features`. The module crate re-exports it
rather than owning it, and both sides now share one definition of the shape.

Co-authored-by: Medulla <medulla@tinyhumans.ai>
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@senamakel, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 19 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 53af7780-2b03-43e5-befa-9d17be1c6316

📥 Commits

Reviewing files that changed from the base of the PR and between 8860df5 and c97d2ac.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (12)
  • Cargo.toml
  • crates/tinydocs-module/Cargo.toml
  • crates/tinydocs-module/src/outputs/mod.rs
  • crates/tinydocs-module/src/outputs/test.rs
  • crates/tinydocs-module/src/service/mod.rs
  • crates/tinydocs-module/tests/module_e2e.rs
  • deny.toml
  • docs/specs/tinybus-module.md
  • src/pptx/mod.rs
  • src/spec/image/mod.rs
  • src/spec/image/test.rs
  • src/spec/presentation/wire.rs
📝 Walkthrough

Walkthrough

The change expands the crate from DOCX generation to validated DOCX/PPTX generation and PDF text extraction. It adds shared specifications, staged output storage, streamed TinyBus inputs, chunked output reads, release operations, feature flags, and comprehensive unit and end-to-end tests.

Changes

Shared specification contracts

Layer / File(s) Summary
Shared specifications and validation
src/spec/*, src/error/mod.rs, src/lib.rs, src/docx/*
Adds reusable document, image, presentation, and wire specifications with validation and format-independent exports.
Feature and crate configuration
Cargo.toml, crates/tinydocs-module/Cargo.toml
Adds optional PPTX/PDF dependencies and enables all three format features by default.

PPTX and PDF writers

Layer / File(s) Summary
PPTX generation
src/pptx/*
Generates presentations with title slides, text, notes, captions, embedded images, and bounded aspect-ratio layout.
PDF extraction
src/pdf/*
Validates PDF input, extracts text, and reports malformed documents with ExtractionFailed.

Staged output storage

Layer / File(s) Summary
Output handles and retrieval
crates/tinydocs-module/src/outputs/*
Stores generated bytes with size, count, TTL, chunk, release, SHA-256, and poisoned-lock handling. Tests cover limits, expiry, reads, release, and redacted debugging.

Documents TinyBus service

Layer / File(s) Summary
Service contract and integration
crates/tinydocs-module/src/service/*, crates/tinydocs-module/tests/module_e2e.rs
Replaces Docx with Documents, resolves streamed inputs, runs format operations, stages outputs, and exposes five methods. Unit and broker-backed tests cover DOCX, PPTX, PDF, chunked reads, release, and transfer errors.
Contract documentation
README.md, docs/specs/tinybus-module.md
Documents the new formats, validation limits, stream framing, output retrieval, feature flags, and service methods.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Poem

A rabbit sees slides hop into view,
PDF words emerge, and DOCX joins too.
Bytes wait in stores, then chunks travel light,
Digests guard outputs through day and night.
“Sniff!” says the rabbit, “The formats now grow!” 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: adding PPTX and PDF support and moving the module to TinyBus streams.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (1)
crates/tinydocs-module/tests/module_e2e.rs (1)

240-266: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the wire error name, not only failure.

mismatched.is_err() also passes if the call fails for an unrelated reason, such as a transfer or dispatch failure. Check the name so the test proves the length rejection.

♻️ Proposed change
-    assert!(
-        mismatched.is_err(),
-        "a stream shorter than the declared images should be refused"
-    );
+    let err = mismatched.expect_err("a stream shorter than the declared images should be refused");
+    assert_eq!(
+        err.wire_name(),
+        "ai.tinyhumans.tinydocs.Error.InvalidInput",
+        "the refusal should name the spec/stream mismatch"
+    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tinydocs-module/tests/module_e2e.rs` around lines 240 - 266, Update
refuses_a_stream_that_contradicts_the_spec to inspect the returned error’s
wire-level name instead of asserting only mismatched.is_err(). Verify it matches
the expected stream-length validation error, while preserving the existing test
setup and failure message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/tinydocs-module/src/outputs/mod.rs`:
- Around line 271-274: Replace the predictable counter-based ID generation in
allocate_id with a cryptographically secure random capability for every output,
while preserving the String return type and uniqueness expectations. Update the
related output creation and ReadOutput/ReleaseOutput paths to use the generated
capability unchanged, rather than exposing or accepting predictable out-N
identifiers.
- Around line 207-211: Update ReadOutput’s read_chunk flow to refresh
output.last_read only when the computed range is non-empty (end > start), while
preserving the existing slice and return behavior. Add a regression test that
performs repeated zero-byte/end-of-document reads beyond IDLE_TTL and verifies
the output is reaped rather than kept alive.

In `@crates/tinydocs-module/src/service/mod.rs`:
- Around line 173-215: Update the image-length handling around the expected-byte
calculation and SlideImage construction: use checked addition when summing
WireSlideImage.byte_len values and when computing cursor + len, returning
BusError::MethodFailed with INVALID_INPUT_ERROR on overflow. Replace direct
payload slicing with payload.get(cursor..end), mapping an invalid range to the
same INVALID_INPUT_ERROR instead of panicking.

In `@docs/specs/tinybus-module.md`:
- Around line 34-43: Update the method count in the module description to match
the five methods listed in the code block, without changing the method
signatures or module details.
- Around line 59-69: Update the documented transfer error names to
Error.UnknownOutput and Error.OutputRefused, matching the definitions in the
module service. In the surrounding lifecycle description, replace the retired
“blobs” terminology with “outputs” while preserving the existing staging,
bounds, and expiration behavior.

In `@src/pdf/mod.rs`:
- Around line 83-84: Replace the PDF parsing dependency used by the extraction
flow around pdf_extract::extract_text_from_mem so the resolved lopdf version is
at least 0.42.0, or configure and enforce an equivalent nesting-depth limit
before parsing. Preserve the existing Error::extraction_failed mapping and
successful text extraction behavior.

In `@src/pptx/mod.rs`:
- Around line 207-212: Update the image sizing flow around fit_within so the
returned dimensions never exceed the image’s natural EMU width and height,
preserving the documented no-upscaling contract. Keep the existing slot
constraints for larger images and use the computed natural dimensions from
img.width_px and img.height_px.

In `@src/spec/image/mod.rs`:
- Around line 103-107: Treat the JPEG TEM marker (0x01) as standalone in the
marker parsing condition alongside SOI, EOI, RSTn, and fill bytes, so it does
not consume a segment length; update src/spec/image/mod.rs lines 103-107. Add a
regression test covering TEM before SOF in src/spec/image/test.rs lines 113-124.

In `@src/spec/presentation/wire.rs`:
- Around line 52-54: Update the documentation comment for the images field in
the wire presentation type to state that each WireSlideImage describes a byte
range in the concatenated image stream, rather than naming a staged blob.
- Around line 26-28: Update resolve_presentation to validate every
WireSlideImage.byte_len against MAX_IMAGE_BYTES before calling read_stream or
slicing, and accumulate the expected stream length with checked_add. Reject
oversized values and addition overflow as invalid input, preventing cursor + len
from wrapping or panicking.

---

Nitpick comments:
In `@crates/tinydocs-module/tests/module_e2e.rs`:
- Around line 240-266: Update refuses_a_stream_that_contradicts_the_spec to
inspect the returned error’s wire-level name instead of asserting only
mismatched.is_err(). Verify it matches the expected stream-length validation
error, while preserving the existing test setup and failure message.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6e8bea62-c2c5-42f8-acbe-8e4bcce14a6a

📥 Commits

Reviewing files that changed from the base of the PR and between 8e9f317 and 8860df5.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (28)
  • .gitignore
  • Cargo.toml
  • README.md
  • crates/tinydocs-module/Cargo.toml
  • crates/tinydocs-module/src/lib.rs
  • crates/tinydocs-module/src/outputs/mod.rs
  • crates/tinydocs-module/src/outputs/test.rs
  • crates/tinydocs-module/src/service/mod.rs
  • crates/tinydocs-module/src/service/test.rs
  • crates/tinydocs-module/tests/module_e2e.rs
  • docs/specs/tinybus-module.md
  • src/docx/mod.rs
  • src/docx/test.rs
  • src/error/mod.rs
  • src/lib.rs
  • src/pdf/mod.rs
  • src/pdf/test.rs
  • src/pptx/mod.rs
  • src/pptx/test.rs
  • src/spec/document/mod.rs
  • src/spec/document/test.rs
  • src/spec/image/mod.rs
  • src/spec/image/test.rs
  • src/spec/mod.rs
  • src/spec/presentation/mod.rs
  • src/spec/presentation/test.rs
  • src/spec/presentation/wire.rs
  • vendor/tinybus

Comment thread crates/tinydocs-module/src/outputs/mod.rs Outdated
Comment thread crates/tinydocs-module/src/outputs/mod.rs
Comment thread crates/tinydocs-module/src/service/mod.rs Outdated
Comment thread docs/specs/tinybus-module.md
Comment thread docs/specs/tinybus-module.md Outdated
Comment thread src/pdf/mod.rs
Comment thread src/pptx/mod.rs
Comment thread src/spec/image/mod.rs Outdated
Comment thread src/spec/presentation/wire.rs
Comment thread src/spec/presentation/wire.rs
senamakel and others added 7 commits August 11, 2026 08:49
Update the optional pdf-extract dependency from 0.10 to 0.12, pulling in its newer transitive dependencies including an upgraded lopdf, rand, and getrandom stack. The lockfile is regenerated to reflect the new crate versions and to remove several intermediate dependencies that are no longer required.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Downgrade the windows-sys dependency from 0.61.2 to 0.52.0 across all transitive dependencies in the lockfile, and relax the ppt-rs version requirement from exact 0.2.14 to the more permissive 0.2 range to allow compatible patch updates.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Set `default-features = false` on the `ppt-rs` optional dependency to disable its `pdf-native` feature, which pulled in `pdfrs` and a chain of transitive dependencies including `syntect`, `yaml-rust`, `bincode`, and `ttf-parser`. This eliminates three RUSTSEC unmaintained advisories and removes over 400 lines of lock-file entries, since nothing in this crate calls the PDF export functionality that the subtree existed to support.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The ppt-rs dependency previously had `default-features = false` to avoid pulling in the pdf-native feature and its transitive dependencies, which carry unmaintained RUSTSEC advisories. However, ppt-rs 0.2.24 unconditionally imports `pdfrs` in its slide render module without a cfg guard, causing a build failure when the feature is disabled. Default features are now enabled to restore compilation, and the advisory exemptions are documented in deny.toml until ppt-rs fixes the conditional compilation.

Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Auto-committed-on: dragonfly
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Ten findings from the review, all legitimate. Grouped by what they actually were.

**A real vulnerability, fixed rather than ignored.** `pdf-extract` was pinned at
0.10, which resolves lopdf 0.38 — RUSTSEC-2026-0187, an uncatchable stack-overflow
SIGABRT from a ~21 KB PDF with deeply nested objects. The `%PDF-` and size checks
do nothing against it. Pinning `pdf-extract = "0.12"` resolves lopdf 0.42.0, where
it is fixed. This matters more than the advisory count suggests: the extraction
path is the one that sees untrusted user attachments.

**Caller-controlled arithmetic could wrap.** Every `byte_len` in a wire deck comes
from the caller, and the total was summed unchecked: `u64::MAX + 1` wraps to zero
in a release build, a zero-byte stream then satisfies the aggregate check, and the
first slice panics. Each length is now bounded against `MAX_IMAGE_BYTES` before it
is summed, the sum and the cursor use `checked_add`, and slicing goes through
`payload.get(..)` so a bad range is an `InvalidInput` rather than a panic.

**Output ids were authorisation, and were guessable.** `ReadOutput` and
`ReleaseOutput` take an id and nothing else — a method receives no caller
identity, so the store cannot bind an output to whoever produced it. That makes
the id the whole authorisation story, and it was `out-1`, `out-2`. Any peer on the
bus could take another peer's document. Ids are now 128 bits of OS randomness. My
own comment claiming ids were "never authorisation tokens" was the tell that this
was wrong; it is corrected rather than deleted.

**Empty reads refreshed the TTL.** A zero-length read, or one at the exact end of
a document, returns nothing and costs nothing — and reset `last_read`, so
repeating it pinned an output in the store forever. Only a read that returned
bytes counts as activity now.

**A valid JPEG could be rejected.** TEM (`0xFF01`) is a standalone marker with no
length field. The walk read the following two bytes as one, desynchronised, and
gave up on a file that places TEM before its frame header.

**Two of my own doc comments contradicted the code**, which is worth fixing
loudly. The pptx module claimed images are never upscaled past their natural size;
the implementation scales to fill the slot in both directions, which is what the
code this was ported from did and what its test asserts — so the sentence was
wrong, not the behaviour. And the spec said "seven methods" over a list of five,
named `Error.UnknownBlob` / `Error.TransferRefused` where the code defines
`UnknownOutput` / `OutputRefused` (a caller matching the documented names would
never match), and still called outputs "blobs" after that concept was removed.

**Supply chain.** Three unmaintained advisories remain, ignored in `deny.toml`
with the exposure and the plan written out, per that file's own rule. Two of them
(`yaml-rust`, `bincode`) arrive through ppt-rs's `pdf-native` default feature — a
PDF exporter behind a syntax highlighter that `.pptx` synthesis never reaches. It
cannot simply be turned off: ppt-rs 0.2.24 fails to build with
`default-features = false` because `src/export/slide_render.rs` has an unguarded
`use pdfrs::...`. When that is fixed upstream, one line drops `pdfrs`, `syntect`,
`yaml-rust`, `bincode` and `pulldown-cmark` together. `CC0-1.0` is added to the
license allowlist for `constant_time_eq` — a public-domain dedication, strictly
more permissive than everything already on the list.

The E2E's mismatch case now asserts the wire error name instead of only that the
call failed, which would also have passed if it failed for an unrelated reason.

New regression tests: empty reads not extending the TTL, ids being unguessable
and unique, and a TEM-before-SOF JPEG.

Co-authored-by: Medulla <medulla@tinyhumans.ai>
@senamakel
senamakel merged commit 3483b42 into tinyhumansai:main Aug 11, 2026
6 checks passed

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tinysweeper found nothing blocking. Approving.

             $0.3794 · 245,489 in / 97,152 out · 59,664 cached (24%) · z-ai/glm-5.2
critique:    $0.1427 · 58,746 in  / 49,605 out · 32,347 cached (55%) · z-ai/glm-5.2
security:    $0.0930 · 51,232 in  / 29,957 out · 27,189 cached (53%) · z-ai/glm-5.2
tests:       $0.0829 · 67,034 in  / 13,611 out · 64 cached (0%)      · z-ai/glm-5.2
description: $0.0609 · 68,477 in  / 3,979 out  · 64 cached (0%)      · z-ai/glm-5.2

Comment thread Cargo.toml

[features]
default = ["docx"]
default = ["docx", "pptx", "pdf"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique likely

Keep pdf-extract out of the default feature set

pdf-extract is added to the default feature set via default = ["docx", "pptx", "pdf"], but its own dependency comment argues the opposite. The comment at # \.pdf` text extraction via `pdf-extract`.says the parser stack is "only the extraction path needs, so a host that never reads a PDF should not carry it." Puttingpdfindefaultmeans every default consumer pullslopdfand the CFF/Type1/CMap parsers regardless, contradicting that rationale. Unlike thepptxentry, which explicitly explains why it is forced on, thepdfentry gives no reason it must be a default rather than an opt-in feature. Movepdfout ofdefault` so the extraction stack is opt-in as the comment intends.

[RULE] Enable only needed dependency features, using default-features = false to trim the tree; gate optional deps behind Cargo features. ·


```text
GenerateDocx(DocumentSpec) -> Vec<u8>
GenerateDocx(DocumentSpec) -> OutputRef

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique likely

Define how OutputRef relates to the output_id parameter

The generation and extraction methods return OutputRef, but ReadOutput and ReleaseOutput take output_id. The spec never states whether OutputRef is the output ID, contains it, or wraps it. An implementer cannot determine which value to pass back to ReadOutput from what the interface listing shows. Either rename the return type to match (OutputId) or add a sentence defining OutputRef and which field the caller supplies as output_id.

**[RULE] ** ·

method list to the first entry in `provides` and leaves the rest empty, so a
second fully-declared interface is not expressible without a TinyBus change.

## Invariants and constraints

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium critique uncertain

Add output-ID unguessability to the constraints section

The prior finding "Make output IDs unguessable so they can serve as access capabilities" still stands at the spec level. ReadOutput(output_id, offset, len) returns document content to any caller that supplies a valid output_id; if the IDs are sequential or otherwise predictable, one D-Bus caller can enumerate and read another's staged documents. The "Invariants and constraints" section lists per-document bounds, a total cap, a count cap, and an idle TTL, but says nothing about the ID format. Add a constraint that output IDs must be unguessable (e.g., 128-bit random tokens).

**[RULE] ** ·

Comment thread Cargo.toml

[features]
default = ["docx"]
default = ["docx", "pptx", "pdf"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

priority medium security likely

Keep the pdf-extract untrusted-parser stack out of the default feature set

The pdf feature is included in default = ["docx", "pptx", "pdf"]. The pdf-extract crate brings lopdf and CFF/Type1/CMap parsers that consume untrusted PDF input. Making this opt-out rather than opt-in means every downstream consumer inherits a parser-for-untrusted-data attack surface even when they never read a PDF. The previous default was docx alone; the new parsing dependency should stay behind an explicit feature flag, not be pulled in by default.

[RULE] Enable only needed dependency features, using default-features = false to trim the tree; gate optional deps behind Cargo features. ·

@tinysweeper

tinysweeper Bot commented Aug 11, 2026

Copy link
Copy Markdown

What this change touches

28 files, +4146 -417 across 9 components. The code graph knows nothing about these files yet — normal for newly added files, and a cold index otherwise.

flowchart LR
  n0["crates/tinydocs-module<br/>7 files +1507 -62"]:::changed
  n1["src/spec<br/>8 files +1549 -2"]:::changed
  n2["src/pptx<br/>2 files +586 -0"]:::changed
  n3["src/docx<br/>2 files +13 -312"]:::changed
  n4["src/pdf<br/>2 files +203 -0"]:::changed
  n5["root<br/>4 files +166 -19<br/>2 findings"]:::flagged
  n6["docs/specs<br/>1 file +63 -19<br/>2 findings"]:::flagged
  n7["src<br/>1 file +34 -3"]:::changed
  n8["src/error<br/>1 file +25 -0"]:::changed
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading

Green: changed. Grey: untouched, reached through an import or a call. Orange: has findings. Red: has a finding that blocks the merge.

Component Files Lines Findings
crates/tinydocs-module changed 7 +1507 -62
src/spec changed 8 +1549 -2
src/pptx changed 2 +586 -0
src/docx changed 2 +13 -312
src/pdf changed 2 +203 -0
(root) changed 4 +166 -19 2 (medium)
docs/specs changed 1 +63 -19 2 (medium)
src changed 1 +34 -3
src/error changed 1 +25 -0
Changed files

crates/tinydocs-module

  • crates/tinydocs-module/Cargo.toml
  • crates/tinydocs-module/src/lib.rs
  • crates/tinydocs-module/src/outputs/mod.rs
  • crates/tinydocs-module/src/outputs/test.rs
  • crates/tinydocs-module/src/service/mod.rs
  • crates/tinydocs-module/src/service/test.rs
  • crates/tinydocs-module/tests/module_e2e.rs

src/spec

  • src/spec/document/mod.rs
  • src/spec/document/test.rs
  • src/spec/image/mod.rs
  • src/spec/image/test.rs
  • src/spec/mod.rs
  • src/spec/presentation/mod.rs
  • src/spec/presentation/test.rs
  • src/spec/presentation/wire.rs

src/pptx

  • src/pptx/mod.rs
  • src/pptx/test.rs

src/docx

  • src/docx/mod.rs
  • src/docx/test.rs

src/pdf

  • src/pdf/mod.rs
  • src/pdf/test.rs

(root)

  • .gitignore
  • Cargo.toml
  • README.md
  • deny.toml

docs/specs

  • docs/specs/tinybus-module.md

src

  • src/lib.rs

src/error

  • src/error/mod.rs

tinysweeper 0.1.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant