Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
name: CI

on:
push:
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
CARGO_BUILD_JOBS: 3
CARGO_TERM_COLOR: always

jobs:
style:
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@7e38f4b43b4db5c8dd498af069a4f6196df1d067
with:
toolchain: stable
components: rustfmt,clippy
- name: Install tools
run: |
sudo apt-get update
sudo apt-get install -y nasm pipx
pipx install clang-format==23.1.1
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Format and lint
run: |
./scripts/stylecheck.sh
git diff --exit-code

tests:
runs-on: ubuntu-24.04
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
asm: ['true', 'false']
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: halidecx/wpd-test-data
ref: f8c31341db3ab4400f048a96e7b3736fed303b34
path: wpd-test-data
persist-credentials: false
- uses: dtolnay/rust-toolchain@7e38f4b43b4db5c8dd498af069a4f6196df1d067
with:
toolchain: stable
- name: Install tools
run: |
sudo apt-get update
sudo apt-get install -y pipx ninja-build nasm cmake
pipx install meson==1.12.1
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Tests, C API, parity and checkasm
env:
ASM: ${{ matrix.asm }}
run: |
meson setup build -Denable_asm="$ASM" -Dtrim_dsp=false \
-Dtestdata_tests=true -Dlibwebp=subproject -Dwuffs=disabled
if [ "$ASM" = true ]; then
meson test -C build --list | grep -q checkasm
fi
meson test -C build --print-errorlogs --num-processes 3

scripts:
runs-on: ubuntu-24.04
timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: halidecx/wpd-test-data
ref: f8c31341db3ab4400f048a96e7b3736fed303b34
path: wpd-test-data
persist-credentials: false
- uses: dtolnay/rust-toolchain@7e38f4b43b4db5c8dd498af069a4f6196df1d067
with:
toolchain: stable
- name: Install tools
run: |
sudo apt-get update
sudo apt-get install -y pipx ninja-build nasm cmake webp
pipx install meson==1.12.1
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Build assembly and fallback tools
run: |
meson setup build -Dtrim_dsp=false -Dtestdata_tests=true \
-Dlibwebp=subproject -Dwuffs=disabled
meson compile -C build -j 3 libwebpdec
meson setup build-noasm -Denable_asm=false \
-Dlibwebp=disabled -Dwuffs=disabled
meson compile -C build-noasm -j 3
- name: Existing correctness checks
run: |
./scripts/testdata.sh
./scripts/animcheck.sh
./scripts/threadcheck.sh
./scripts/md5check.sh ./build-noasm/wpd ./build/wpd
./scripts/clicheck.sh ./build-noasm/wpd ./build/wpd
./scripts/rac32.sh --print-errorlogs --num-processes 3

c-sanitizers:
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: halidecx/wpd-test-data
ref: f8c31341db3ab4400f048a96e7b3736fed303b34
path: wpd-test-data
persist-credentials: false
- uses: dtolnay/rust-toolchain@7e38f4b43b4db5c8dd498af069a4f6196df1d067
with:
toolchain: stable
- name: Install tools
run: |
sudo apt-get update
sudo apt-get install -y pipx ninja-build nasm cmake
pipx install meson==1.12.1
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: C harnesses and damaged-input smoke
run: |
./scripts/sanitize.sh --print-errorlogs --num-processes 3
./scripts/fuzz.sh 8 wpd-test-data/odd_lossy.webp \
wpd-test-data/odd_a_lossy.webp wpd-test-data/palette_rgb.webp \
wpd-test-data/mixed_codecs.webp

nightly:
runs-on: ubuntu-24.04
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
check: [rustsan, tsan, miri, fuzz]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: halidecx/wpd-test-data
ref: f8c31341db3ab4400f048a96e7b3736fed303b34
path: wpd-test-data
persist-credentials: false
- uses: dtolnay/rust-toolchain@7e38f4b43b4db5c8dd498af069a4f6196df1d067
with:
toolchain: nightly
components: rust-src,miri
- name: Install tools
run: |
sudo apt-get update
sudo apt-get install -y pipx ninja-build nasm cmake clang
pipx install meson==1.12.1
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Nightly check
env:
CHECK: ${{ matrix.check }}
run: |
case "$CHECK" in
rustsan)
meson setup build -Dlibwebp=disabled -Dwuffs=disabled
meson compile -C build -j 3
./scripts/rustsan.sh
;;
tsan) ./scripts/tsan.sh ;;
miri) ./scripts/miri.sh --lib container::tests ;;
fuzz)
cargo install cargo-fuzz --version 0.13.2 --locked
./scripts/fuzz-smoke.sh
;;
esac
- name: Save fuzz failures
if: failure() && matrix.check == 'fuzz'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: fuzz-failures
path: fuzz/artifacts
if-no-files-found: ignore
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Changelog

This is a draft for the first tagged release. There are no historical release
dates to record. The proposed first tag is `v0.2.0`, matching the current Cargo
workspace version, after the maintainer merges the patch series and completes
the release checks. Nothing here announces a tag or crates.io publication.

## Unreleased — 0.2.0

### Existing decoder functionality

- Decode lossy VP8, lossless VP8L, alpha, and animated WebP in Rust, with
optional handwritten assembly and worker threads.
- Provide Rust and C APIs, incremental decoding, composited animation or raw
subframes, frame timing, metadata access, scaling, cropping, and frame size
limits.
- Provide a CLI with packed RGB/RGBA and planar YUV output, raw, PAM, PPM,
YUV4MPEG2 and MD5 muxers, replay, benchmark repeats, and input/output byte
budgets.
- Build static and shared C libraries with Meson, including headers and
pkg-config metadata. The project uses the BSD-2-Clause license.

### Build and CI

- Repair the end-to-end fuzz target's decoder options initializer so all four
coverage-guided targets build again.
- Correct the optional Wuffs dependency name in the Meson build.
- Add one GitHub Actions workflow for tests, rustfmt/clippy, fuzz target builds,
seeded smoke runs, and the existing correctness and sanitizer checks.
- Bound fuzz-harness pictures to one megapixel so mutated dimensions fit the
smoke run's memory budget without changing decoder limits.

### CLI additions

- Add `--info=json` with dimensions, frame count, raw frame durations, loop
count, alpha, ICC presence, and an ordered chunk list. Keep text `--info`.
- Add `--icc-out`, `--exif-out`, and `--xmp-out` for original metadata bytes.
- Add `--muxer frames` for numbered RGBA PAM files and a JSON timing manifest.
Publish the final manifest after successful decoding and output flushing,
refuse existing directories, and include the manifest in the output budget.
- Keep JSON separate from the recognized stdout paths and cover streaming,
truncation, binary metadata, replay, scaling, and output errors in CLI tests.

### Validation scope

The local patch series has been tested with Rust 1.98 and 1.99. This does not
establish the declared Rust 1.82 minimum or claim crates.io packaging support.
103 changes: 103 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,91 @@ produces `libwpd-sealed.a` alongside `libwpd.a`. The sealed static lib aborts
instead of unwinding on an internal panic. The build merges every object into
one monolithic object for downstream consumers.

## Release status

[CHANGELOG.md](CHANGELOG.md) is a draft for the first release. The workspace is
already version 0.2.0; `v0.2.0` is the proposed first tag after the maintainer
merges these changes and completes the checks. No release date or crates.io
publication is claimed. This series has been tested with Rust 1.98 and 1.99;
that does not establish compatibility with the declared Rust 1.82 minimum.

## CLI metadata

```sh
build/wpd --info=json input.webp
build/wpd --icc-out profile.icc --exif-out exif.bin --xmp-out xmp.bin input.webp
```

`--info` retains its text output. `--info=json` writes one JSON object to stdout
after the complete image has decoded successfully:

```json
{
"width": 2,
"height": 1,
"frame_count": 1,
"loop_count": 0,
"has_alpha": false,
"has_icc": false,
"durations_ms": [0],
"chunks": [
{ "fourcc": "VP8L", "offset": 12, "size": 17, "complete": true }
]
}
```

Dimensions describe the original canvas, including with `--scale`. Durations are
the original milliseconds, without a minimum playback delay; a still has one
duration of 0. An animation loop count of 0 means infinite repetition. The
ordered chunk list describes top-level RIFF chunks; offsets point to their
FourCC, sizes exclude the header and padding, and `complete` includes padding.
Raw VP8/VP8L input has an empty chunk list. Arbitrary FourCC bytes outside
printable ASCII are escaped as `\u00XX`.

Metadata outputs contain the original chunk payload, without its header or
padding. Missing metadata produces an empty file. Extraction alone needs no
pixel output. It does not interpret EXIF orientation or apply colour profiles.
These options also work with `--stream`, `--loops`, and `--repeat`; JSON and
metadata are written once. JSON can accompany a pixel output file, but the pixel
output cannot also use stdout. Metadata paths must name files. `-`,
`/dev/stdout`, `/dev/fd/1`, and `/proc/self/fd/1` are recognized as stdout;
custom links to stdout must not be used as output paths with JSON info.

The existing input and frame size limits apply. `--max-output` limits decoded
pixel output; metadata is bounded by `--max-input`. Exit codes are 0 for
success, 1 for input, decoding, limits or output errors, and 2 for invalid
arguments. Failed decodes write no JSON or metadata, and leave existing metadata
output files untouched.

## CLI frame sequences

```sh
build/wpd --muxer frames input.webp output-frames
```

The `frames` muxer creates a new directory, writes `frame-000000.pam`,
`frame-000001.pam`, and so on, and publishes `manifest.json` after successful
decoding and output flushing. Every PAM file has straight RGBA pixels and its
own header. An existing output directory is refused.

The manifest has `canvas_width`, `canvas_height`, `loop_count`, `composited`,
`frame_count`, and an ordered `frames` array. Each frame has `file`, `width`,
`height`, `duration_ms`, `timestamp_ms`, `x`, and `y`. Durations are the
original milliseconds, including 0; timestamps are the start of each frame in
the first pass, starting at 0. A still has one frame with duration and
timestamp 0.

Frames are composited canvases by default. `--subframe` keeps raw frame sizes
and canvas offsets, and sets `composited` to false. `--scale` changes the frame
dimensions while the manifest's canvas dimensions describe the original file.
`--stream` is supported; only the first pass of `--loops` or `--repeat` is
written. The muxer requires RGBA output and a directory path, and `--max-output`
covers all PAM bytes and the complete manifest together.

A failed decode or write leaves partial output with `manifest.json.part`,
without a final `manifest.json`. Consumers should require the final manifest
before using a sequence.

## Library

`meson install -C build` installs the static/shared libraries, `wpd.h`, and
Expand Down Expand Up @@ -90,6 +175,24 @@ Test data is maintained at
into the `wpd/` root. `./scripts/testdata.sh` runs end-to-end assembly and
fallback checks.

GitHub Actions runs assembly and fallback tests, checkasm, format/lint checks,
the correctness scripts, C and Rust sanitizers, and a container Miri smoke
check. The corpus revision is pinned in `.github/workflows/ci.yml`. CI compares
the assembly and fallback tools with `md5check.sh` and `clicheck.sh`; comparing
an older release still needs an explicit baseline binary. Timing scripts
(`bench.sh` and `cmpbench.sh`) remain manual because shared CI runners do not
provide stable performance measurements.

`./scripts/fuzz-smoke.sh [seconds-per-target] [corpus-directory]` builds every
fuzz target and runs each for 15 seconds by default. It requires nightly Rust,
Python 3 and cargo-fuzz 0.13.2. It derives container and raw VP8/VP8L seeds from
the test corpus without changing its WebP files, and leaves generated seeds and
failure artifacts under `fuzz/`. Longer fuzzing and the full safe-core Miri
suite (`./scripts/miri.sh`) are useful local checks before releases. The
decoding harnesses bound pictures to 1,048,576 pixels so mutated dimensions fit
the smoke run's memory budget; larger pictures remain in ordinary corpus tests.
This is a harness limit, not a decoder limit.

For libwebp parity testing and benchmarking against alternative WebP decoders,
build the optional third-party test binaries:

Expand Down
13 changes: 13 additions & 0 deletions fuzz/budget.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
pub const MAX_PIXELS: u32 = 1 << 20;

pub fn fits(data: &[u8]) -> bool {
// Keep malformed headers in coverage; only a successfully read size can
// exceed the harness budget. Raw codecs have no decoder options limit.
wpd::api::info(data).map_or(true, |info| {
wpd::api::Options {
frame_size_limit: MAX_PIXELS,
..Default::default()
}
.fits(info.width, info.height)
})
}
1 change: 0 additions & 1 deletion fuzz/fuzz_targets/container.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

#![no_main]

use libfuzzer_sys::fuzz_target;
Expand Down
Loading