Skip to content

feat!: typed errors, a Stream-based streaming API, and a narrower public API - #65

Merged
tycho merged 4 commits into
mainfrom
steven/feat/typed-api
Oct 5, 2026
Merged

tycho merged 4 commits into
mainfrom
steven/feat/typed-api

Conversation

@tycho

@tycho tycho commented Oct 5, 2026 •

Copy link
Copy Markdown
Member

Breaking API changes for 0.3.0, making the library easier to consume:

  • The public API is now the conversion API at the crate root, plus verify and image; the merge engine's internal modules are private.
  • Fallible functions return ocirender::Error, a non-exhaustive enum whose variants say what failed (the image layout, a particular layer, the caller's layer source, mksquashfs, the output, ...), instead of anyhow::Error. Failures are now pinned on the right side of a merge: a corrupt layer is reported as that layer's error, and mksquashfs dying is reported with its stderr rather than as a broken pipe.
  • convert_streaming takes any Stream of layers plus an ImageSpec, replacing the three convert_*_streaming functions that took a tokio channel.

Also fixes the streaming merge silently dropping layers delivered with an out-of-range or duplicate index.

Existing StreamingPacker callers should mostly compile unchanged: notify_error accepts any error type, anyhow::Error included, and ocirender::Error converts into anyhow::Error with ?. Protect's OCI crate builds against this branch without changes.

Fixes #15
Fixes #16

@tycho
tycho added this pull request to stack #66 October 5, 2026 18:03
@tycho
tycho force-pushed the steven/feat/typed-api branch from 13206f6 to cc8ce8b Compare October 5, 2026 18:06
Base automatically changed from steven/feat/manifest-json-image-manifest to main October 5, 2026 18:10
tycho added 4 commits October 5, 2026 11:16
Every module was public, including the merge engine's internals
(`canonical`, `overlay`, `tracker`, `layers`) and the per-format output
sinks (`dir`, `squashfs`, `tar`), even though their own documentation
describes them as pipeline internals. All of it was API to keep stable,
and the move to a typed error would have had to cover every one.

Keep public only what consumers use: the conversion API at the crate
root, `verify`, and `image` for parsing image layouts (the CLI's tests
check its fetched layouts with `image::load_manifest`). `LayerBlob` is
also re-exported at the crate root, where the streaming API uses it.
The output sinks' convenience wrappers that nothing calls any more are
removed, and the batch merge and `CanonicalTarHeader::path`, which only
the tests use, are compiled for tests only.

The tests that exercise internal modules move from `tests/` into the
crate, as unit tests under `src/tests/`.

BREAKING CHANGE: the `canonical`, `dir`, `layers`, `overlay`,
`squashfs`, `tar` and `tracker` modules are no longer public; use the
conversion API at the crate root instead.

Signed-off-by: Steven Noonan <steven@edera.dev>
Every fallible function returned `anyhow::Error`, which suits an
application but not a library: a caller could only print the error or
guess at downcasts, could not tell a corrupt layer from a missing
mksquashfs or a failed download, and was pushed into anyhow itself
(`StreamingPacker::notify_error` took an `anyhow::Error`).

Return `ocirender::Error`, a non-exhaustive enum built with thiserror
whose variants say what failed: `ImageLayout`, `Layer` (with the
layer's index and blob), `LayerSource` (the caller's own error, as its
source), `MissingLayers`, `LayerIndexOutOfRange`, `PackerStopped`,
`MksquashfsSpawn`, `Mksquashfs` (with its stderr), `Output` and
`Verify`. As is conventional, `Display` describes only that level and
`source()` carries the cause. `notify_error` and the streaming channels
take any error as a `BoxError`, so a caller's own error type, anyhow's
included, still passes straight through.

Typing the errors meant deciding what to blame when a merge stops,
which the old code muddled. A corrupt layer usually surfaces while its
data is being copied to the sink, where `tar::Builder` reports a read
failure, a write failure and an entry it cannot encode as the same kind
of `io::Error`. The sink is now wrapped to record its own failures, so
only those are output errors and anything else is the layer's. With
that, when mksquashfs dies its failure and stderr are reported rather
than the broken pipe the merge then hits, while a corrupt layer or a
failed download still takes precedence over the mksquashfs exit it
causes.

Along the way: the mksquashfs output path is passed as an `OsStr`,
dropping a needless UTF-8 requirement; a panic in the blocking
conversion task is resumed in the caller rather than turned into an
error; and `image::strip_digest_prefix` returns an `Option`, so that
the error for an unsupported digest names the file it appeared in.

BREAKING CHANGE: fallible functions return `ocirender::Result` with
`ocirender::Error` instead of `anyhow::Result`. `notify_error` and the
streaming receivers take a `BoxError` (anything converting into
`Box<dyn Error + Send + Sync>`, including `anyhow::Error`), and
`image::strip_digest_prefix` returns `Option<&str>`.

Signed-off-by: Steven Noonan <steven@edera.dev>
The streaming merge counted every layer it was handed towards the
image's total without checking the layer's index. A layer with an
index the image doesn't have was buffered and never processed, and a
layer delivered twice overwrote the first delivery; either way the
count still reached the total, the merge stopped waiting, and the
conversion succeeded with layers silently missing from the output.

Check each delivery: an index outside the image fails the merge with
`Error::LayerIndexOutOfRange`, and a second delivery of the same layer
with the new `Error::DuplicateLayer`. `StreamingPacker` already
rejected out-of-range indices itself, but neither it nor the streaming
conversion functions caught duplicates.

Signed-off-by: Steven Noonan <steven@edera.dev>
The streaming conversion functions took a `tokio::sync::mpsc::Receiver`,
which tied callers to tokio's channel even when their layers came from
some other source, and came as three per-format variants
(`convert_mksquashfs_streaming`, `convert_tar_streaming` and
`convert_dir_streaming`) of what `ImageSpec` already expresses.

Replace them with a single `convert_streaming(layers, total_layers,
spec)`, mirroring `convert(image_dir, spec)`, that takes any `Stream`
of `Result<LayerBlob, E>` where `E` converts into a `BoxError`: a tokio
channel through tokio-stream's `ReceiverStream`, a `futures` channel,
or any stream combinator. An `Err` item still aborts the conversion,
and comes back as the source of `Error::LayerSource`. That is the
usual convention for consuming a fallible stream (`reqwest`'s
`Body::wrap_stream` works the same way), and it saves a caller from
pairing a "stream ended early" error with a failure it would otherwise
have to keep track of separately. A stream that cannot fail is passed
as `stream.map(Ok::<_, Infallible>)`.

The `Stream` trait comes from futures-core, the small, dependency-free
crate that tokio-stream, futures and other stream libraries build on.
`StreamingPacker`'s push-based API is unchanged.

BREAKING CHANGE: `convert_mksquashfs_streaming`, `convert_tar_streaming`
and `convert_dir_streaming` are replaced by `convert_streaming`, which
takes a `Stream` of layers and an `ImageSpec`; a tokio receiver can be
wrapped in `tokio_stream::wrappers::ReceiverStream`.

Signed-off-by: Steven Noonan <steven@edera.dev>
@tycho
tycho force-pushed the steven/feat/typed-api branch from cc8ce8b to 68396fc Compare October 5, 2026 18:19
@tycho
tycho requested a review from azenla October 5, 2026 18:24
@tycho
tycho marked this pull request as ready for review October 5, 2026 18:27

@azenla azenla left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM

@tycho
tycho merged commit 68396fc into main Oct 5, 2026
2 checks passed
@tycho
tycho deleted the steven/feat/typed-api branch October 5, 2026 18:33
@tycho
tycho deployed to release October 5, 2026 18:33 — with GitHub Actions Active
@tycho
tycho deployed to release October 5, 2026 18:33 — with GitHub Actions Active

This branch was successfully deployed

1 active deployment
release — 68396fcd Deployed Oct 5, 2026 by tycho via Release-plz PR #35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streaming API should take a Stream Remove anyhow from API surface area

2 participants