From b9a95601b32f46e488fb420feaabb6701a419ca4 Mon Sep 17 00:00:00 2001 From: Mike Sulsenti Date: Sun, 4 Oct 2026 10:32:44 -0400 Subject: [PATCH 01/10] fuzz: initialize the current C decoder options The e2e target stopped building after 90cd9a2 added reserved2 and frame_size_limit to WPDDecoderOptions. Initialize the padding field to zero and forward the Rust options limit so the external-buffer path exercises the same configuration as the Rust driver. Validation: all four cargo-fuzz targets build with ASan and debug assertions on nightly-2026-07-10; scripts/stylecheck.sh passes on the current head. --- fuzz/fuzz_targets/e2e.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fuzz/fuzz_targets/e2e.rs b/fuzz/fuzz_targets/e2e.rs index 9832126..baaebf3 100644 --- a/fuzz/fuzz_targets/e2e.rs +++ b/fuzz/fuzz_targets/e2e.rs @@ -123,6 +123,8 @@ fn decode_external(data: &[u8], options: Options) { flip: i32::from(options.flip), reserved: 0, n_threads: options.n_threads, + reserved2: 0, + frame_size_limit: options.frame_size_limit, }; let mut frame = WPDFrame { struct_size: mem::size_of::(), From 443705cca542a103c7c98a3c2bbbe7b9a9415bf7 Mon Sep 17 00:00:00 2001 From: Mike Sulsenti Date: Sun, 4 Oct 2026 10:38:05 -0400 Subject: [PATCH 02/10] build: name the optional Wuffs dependency Meson 1.12.1 drops empty dependency names before looking up a fallback. The Wuffs dependency introduced in 9748ced therefore makes setup crash with an IndexError, including when Wuffs is disabled. Use the conventional wuffs dependency name and keep the existing pinned fallback. This allows the project to configure with current Meson without changing which optional decoder is built. Validation: meson setup build --wipe -Dtrim_dsp=false -Dtestdata_tests=true -Dlibwebp=subproject -Dwuffs=disabled succeeds with Meson 1.12.1; the same command failed before the change. scripts/stylecheck.sh passes. --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index f3eb390..66af0e2 100644 --- a/meson.build +++ b/meson.build @@ -517,7 +517,7 @@ custom_target( message('imagewebpdec: enabled (build explicitly with target imagewebpdec)') wuffs_dep = dependency( - '', + 'wuffs', fallback: ['wuffs', 'wuffs_dep'], required: get_option('wuffs'), ) From 24efec1f5db4f8c3b727e41f017dc136104bcb60 Mon Sep 17 00:00:00 2001 From: Mike Sulsenti Date: Sun, 4 Oct 2026 11:15:07 -0400 Subject: [PATCH 03/10] ci: run decoder checks and seeded fuzz smoke on GitHub Actions Add one workflow for pushes, pull requests and manual runs. Test the full pinned corpus with assembly enabled and disabled, and require checkasm in the assembly job. Run the existing format/clippy, CPU-mask, animation, threading, forced-32-bit range-coder, C/Rust sanitizer and container Miri checks. Build every declared cargo-fuzz target with ASan and debug assertions, then run each for 15 seconds with container and raw VP8/VP8L seeds extracted from the corpus. Keep generated seeds separate from developer corpora and upload failure artifacts. Pin the actions, test corpus, Meson, cargo-fuzz and clang-format; the formatter pin avoids changing existing C macro layout on Ubuntu. Compare assembly and fallback CLIs with the existing md5 and CLI scripts. Keep historical comparisons and performance timing manual, and document the bounded Miri and fuzz coverage in README. Validation: actionlint and shellcheck pass. The style and fallback Meson jobs pass under act on Ubuntu 24.04. Native Meson suites pass (235 asm, 234 fallback), as do full CPU-mask, rac32, animation, thread-count, checksum, CLI, C sanitizer and damaged-input scripts. Rust ASan checks 432 decodes per build; TSan passes 215 unit tests and 720 decodes. Miri passes 17 container tests and the C API storage regression. All four fuzz targets build and complete the seeded smoke with no failures. scripts/stylecheck.sh passes. Record build and CI changes in the draft changelog. Compatibility regression seeds are added with the compatibility-mode fuzzing change. Bound raw VP8/VP8L header geometry and e2e frame allocations to 1,048,576 pixels in the fuzz harnesses. A seeded VP8L smoke run exceeded its RSS budget on a 16384x14336 header; keep malformed header coverage while avoiding allocations outside that smoke budget. This does not change decoder limits. --- .github/workflows/ci.yml | 197 +++++++++++++++++++++++++++++++++ CHANGELOG.md | 13 +++ README.md | 18 +++ fuzz/budget.rs | 13 +++ fuzz/fuzz_targets/container.rs | 1 - fuzz/fuzz_targets/e2e.rs | 7 ++ fuzz/fuzz_targets/vp8.rs | 7 +- fuzz/fuzz_targets/vp8l.rs | 7 +- scripts/fuzz-smoke.sh | 65 +++++++++++ 9 files changed, 325 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 CHANGELOG.md create mode 100644 fuzz/budget.rs create mode 100755 scripts/fuzz-smoke.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5076716 --- /dev/null +++ b/.github/workflows/ci.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c83f2b6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +## Unreleased — 0.2.0 + +### 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. diff --git a/README.md b/README.md index 4da01c4..a789d16 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,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: diff --git a/fuzz/budget.rs b/fuzz/budget.rs new file mode 100644 index 0000000..fe1e0c6 --- /dev/null +++ b/fuzz/budget.rs @@ -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) + }) +} diff --git a/fuzz/fuzz_targets/container.rs b/fuzz/fuzz_targets/container.rs index ff1825b..85373cf 100644 --- a/fuzz/fuzz_targets/container.rs +++ b/fuzz/fuzz_targets/container.rs @@ -1,4 +1,3 @@ - #![no_main] use libfuzzer_sys::fuzz_target; diff --git a/fuzz/fuzz_targets/e2e.rs b/fuzz/fuzz_targets/e2e.rs index baaebf3..70c0a75 100644 --- a/fuzz/fuzz_targets/e2e.rs +++ b/fuzz/fuzz_targets/e2e.rs @@ -11,6 +11,9 @@ use wpd_capi::decoder::{wpd_decode_into, WPDOutputBuffer}; use wpd_capi::frame::{WPDFrame, WPDOutputPlane}; use wpd_capi::options::WPDDecoderOptions; +#[path = "../budget.rs"] +mod budget; + const FORMATS: [Format; 16] = [ Format::Yuv420p, Format::Yuva420p, @@ -40,6 +43,7 @@ fn decode_options(data: &[u8]) -> (Options, bool) { let flags = byte(data, 1); let subframe = flags & 4 != 0; let mut options = Options { + frame_size_limit: budget::MAX_PIXELS, n_threads: [1, 2, 3, 8][usize::from(flags >> 6)], bypass_filtering: flags & 8 != 0, no_fancy_upsampling: flags & 16 != 0, @@ -156,6 +160,9 @@ fn decode_external(data: &[u8], options: Options) { } fuzz_target!(|data: &[u8]| { + if !budget::fits(data) { + return; + } let Some(&first) = data.first() else { return; }; diff --git a/fuzz/fuzz_targets/vp8.rs b/fuzz/fuzz_targets/vp8.rs index dbe3ec7..f2794f6 100644 --- a/fuzz/fuzz_targets/vp8.rs +++ b/fuzz/fuzz_targets/vp8.rs @@ -1,10 +1,15 @@ - #![no_main] use libfuzzer_sys::fuzz_target; use wpd::vp8::Decoder; +#[path = "../budget.rs"] +mod budget; + fuzz_target!(|data: &[u8]| { + if !budget::fits(data) { + return; + } let mut decoder = Decoder::new(); let _ = decoder.decode_frame(data); diff --git a/fuzz/fuzz_targets/vp8l.rs b/fuzz/fuzz_targets/vp8l.rs index 48367bd..fc91499 100644 --- a/fuzz/fuzz_targets/vp8l.rs +++ b/fuzz/fuzz_targets/vp8l.rs @@ -1,14 +1,19 @@ - #![no_main] use libfuzzer_sys::fuzz_target; use wpd::vp8l::{AlphaDst, Decoder, Target}; +#[path = "../budget.rs"] +mod budget; + fuzz_target!(|data: &[u8]| { if data.len() < 2 { return; } let (head, payload) = data.split_at(2); + if !budget::fits(payload) { + return; + } let mut decoder = Decoder::new(); let alpha_chunk = head[0] & 1 != 0; diff --git a/scripts/fuzz-smoke.sh b/scripts/fuzz-smoke.sh new file mode 100755 index 0000000..9ae87e1 --- /dev/null +++ b/scripts/fuzz-smoke.sh @@ -0,0 +1,65 @@ +#!/bin/bash -eu + +SECONDS_PER_TARGET="${1:-15}" +CORPUS="${2:-wpd-test-data}" + +case "$SECONDS_PER_TARGET" in + ''|*[!0-9]*|0*) echo "fuzz-smoke.sh: seconds must be positive" >&2; exit 1 ;; +esac + +# Keep generated seeds separate from saved developer corpora and artifacts. +mkdir -p fuzz/corpus/ci/{container,e2e,vp8,vp8l} fuzz/artifacts +python3 - "$CORPUS" <<'PY' +from pathlib import Path +import sys + +corpus = Path(sys.argv[1]) +out = Path("fuzz/corpus/ci") +files = sorted(corpus.glob("*.webp")) +if not files: + sys.exit(f"no WebP files found in {corpus}") + +def chunks(data): + offset = 0 + while offset + 8 <= len(data): + tag = data[offset:offset + 4] + size = int.from_bytes(data[offset + 4:offset + 8], "little") + end = offset + 8 + size + if end > len(data): + break + payload = data[offset + 8:end] + if tag == b"ANMF": + yield from chunks(payload[16:]) + else: + yield tag, payload + offset = end + (size & 1) + +for path in files: + data = path.read_bytes() + # Large seeds make a smoke run spend its budget replaying images. + if len(data) > 65536: + continue + for target in ("container", "e2e"): + (out / target / path.stem).write_bytes(data) + for index, (tag, payload) in enumerate(chunks(data[12:])): + name = f"{path.stem}-{index}" + if tag == b"VP8 ": + (out / "vp8" / name).write_bytes(payload) + elif tag == b"VP8L": + # The target uses two leading bytes to select ARGB and threading. + for threads in (0, 1): + (out / "vp8l" / f"{name}-{threads}").write_bytes( + bytes((0, threads)) + payload) + +for target in ("container", "e2e", "vp8", "vp8l"): + if not any((out / target).iterdir()): + sys.exit(f"no seeds prepared for {target}") +PY + +# With no target argument cargo-fuzz builds every declared target. +cargo +nightly fuzz build -O --debug-assertions +for target in container vp8 vp8l e2e; do + cargo +nightly fuzz run -O --debug-assertions "$target" \ + "fuzz/corpus/ci/$target" -- -max_total_time="$SECONDS_PER_TARGET" \ + -max_len=65536 -timeout=5 -rss_limit_mb=1024 -seed=1 +done From cc0ffdba9a9c301fedd2a1628b9aaf9a3dc56d54 Mon Sep 17 00:00:00 2001 From: Mike Sulsenti Date: Sun, 4 Oct 2026 10:35:40 -0400 Subject: [PATCH 04/10] container: validate the first chunk in a RIFF file A corrupted VP8X FourCC was treated as an unknown chunk, so an extended file could silently lose its canvas and flags and still decode. libwebp rejects these files. Require the first RIFF chunk to identify VP8, VP8L or VP8X, while keeping unknown chunks after the header supported. Regression coverage checks a damaged VP8X tag through whole-buffer and split-header scans. Validation: container unit tests and scripts/stylecheck.sh pass. --- src/container.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/container.rs b/src/container.rs index eb31075..91ee448 100644 --- a/src/container.rs +++ b/src/container.rs @@ -544,6 +544,10 @@ impl Scan { let tag = rl32(buf, at); let size = rl32(buf, at + 4); + if self.pos == 12 && !matches!(tag, TAG_VP8X | TAG_VP8 | TAG_VP8L) { + log::error("RIFF must start with VP8, VP8L or VP8X"); + return Err(Error::InvalidData); + } if size == u32::MAX { self.info.truncated = true; break; @@ -751,6 +755,25 @@ mod tests { assert_eq!(get_info(b"not a webp file at all"), Err(Error::NotWebp)); } + #[test] + fn a_damaged_vp8x_tag_cannot_turn_an_extended_file_into_a_simple_one() { + let mut payload = chunk(b"VP9X", &[0x10, 0, 0, 0, 0, 0, 0, 0, 0, 0]); + + payload.extend(chunk(b"VP8L", &vp8l_header(1, 1, true))); + + let file = riff(&payload); + + assert_eq!(get_info(&file), Err(Error::InvalidData)); + + let mut scan = Scan::new(); + + assert_eq!( + scan.headers(&file[..19], 0, true, true), + Err(Error::Truncated) + ); + assert_eq!(scan.headers(&file, 0, true, true), Err(Error::InvalidData)); + } + #[test] fn oversized_vp8x_dimensions_are_refused_before_allocating() { let payload = chunk(b"VP8X", &[2, 0, 0, 0, 0, 64, 0, 0, 0, 0]); From 17dfb19497f91a39a5e0a320c94f4c621eb75255 Mon Sep 17 00:00:00 2001 From: Mike Sulsenti Date: Sun, 4 Oct 2026 11:15:07 -0400 Subject: [PATCH 05/10] container: add opt-in libwebp still compatibility Keep strict decoding by default. Options.libwebp_compat and the version-gated C options field allow the still decoder to ignore damaged trailing chunks, keep the first VP8X, and give the image codec the available payload after its header, as libwebp does even when the image chunk declares too few bytes. Metadata chunks are still collected when complete. Animation containers and declared RIFF truncation retain their checks. Older C callers retain strict mode, with explicit padding before the new field. The option must be set before opening input. Regression tests cover whole-buffer, append and update decoding, shortened image chunks, broken tails, duplicate canvases and truncated RIFF files. Validation: workspace tests without assembly, scripts/stylecheck.sh and all four cargo-fuzz builds with ASan/debug assertions on nightly-2026-07-10. Retain compatible still bytes past RIFF as well as the image chunk and wait until end_of_stream before producing a still picture, so incremental partition lengths cannot change. Defer tiny declared image headers until their real codec header arrives, match libwebp's still partition-size and simple final-padding rules, skip optional non-animation ANMF consistently, and keep reserved VP8X flag checks for animations. Additional regressions cover append/update input, underdeclared RIFF and image lengths, optional ANMF, missing padding and malformed animation flags. Rust and C option documentation states the compatible streaming EOF behavior. --- capi/src/decoder.rs | 4 + capi/src/options.rs | 26 +++++ fuzz/fuzz_targets/e2e.rs | 2 + include/wpd.h | 30 +++++- src/container.rs | 85 +++++++++++++-- src/driver/mod.rs | 221 ++++++++++++++++++++++++++++++++++++++- src/options.rs | 6 ++ 7 files changed, 362 insertions(+), 12 deletions(-) diff --git a/capi/src/decoder.rs b/capi/src/decoder.rs index e0ad8b0..4c6dd57 100644 --- a/capi/src/decoder.rs +++ b/capi/src/decoder.rs @@ -234,6 +234,7 @@ fn set_options( || !flag(options.use_cropping) || !flag(options.use_scaling) || !flag(options.flip) + || !flag(options.libwebp_compat) { return Err(decoder.fail("invalid decoder options", Error::InvalidArgument)); } @@ -299,6 +300,9 @@ entry!(fn wpd_decoder_set_options(decoder, options: *const WPDDecoderOptions) { local.frame_size_limit = unsafe { ptr::addr_of!((*options).frame_size_limit).read() }; } + if size >= WPDDecoderOptions::v4() { + local.libwebp_compat = unsafe { ptr::addr_of!((*options).libwebp_compat).read() }; + } reported(set_options(decoder, &local).map(|()| WPD_OK)) }); diff --git a/capi/src/options.rs b/capi/src/options.rs index cbd42c3..615cf26 100644 --- a/capi/src/options.rs +++ b/capi/src/options.rs @@ -24,6 +24,9 @@ pub struct WPDDecoderOptions { /// Takes the v2 struct's tail padding, for the reason `reserved` took v1's. pub reserved2: c_int, pub frame_size_limit: c_uint, + /// Takes the v3 struct's tail padding. + pub reserved3: c_int, + pub libwebp_compat: c_int, } /// What `sizeof` gave the v1 struct: it ended at `flip`, and padded out to the @@ -42,6 +45,11 @@ const V2_SIZE: usize = const _: () = assert!(V2_SIZE < WPDDecoderOptions::v3()); +const V3_SIZE: usize = + WPDDecoderOptions::v3().next_multiple_of(mem::align_of::()); + +const _: () = assert!(V3_SIZE < WPDDecoderOptions::v4()); + impl WPDDecoderOptions { pub(crate) const fn v1() -> usize { mem::offset_of!(WPDDecoderOptions, flip) + mem::size_of::() @@ -55,6 +63,10 @@ impl WPDDecoderOptions { mem::offset_of!(WPDDecoderOptions, frame_size_limit) + mem::size_of::() } + pub(crate) const fn v4() -> usize { + mem::offset_of!(WPDDecoderOptions, libwebp_compat) + mem::size_of::() + } + /// Legacy callers retain serial decoding and serial log callbacks. pub(crate) fn to_core(&self) -> Options { Options { @@ -79,6 +91,7 @@ impl WPDDecoderOptions { } else { 0 }, + libwebp_compat: self.struct_size >= Self::v4() && self.libwebp_compat != 0, } } } @@ -110,4 +123,17 @@ mod tests { options.struct_size = mem::size_of::(); assert_eq!(options.to_core().frame_size_limit, 64); } + + #[test] + fn older_options_keep_strict_decoding() { + let mut options: WPDDecoderOptions = unsafe { mem::zeroed() }; + + options.libwebp_compat = 1; + for size in [V1_SIZE, V2_SIZE, V3_SIZE] { + options.struct_size = size; + assert!(!options.to_core().libwebp_compat); + } + options.struct_size = mem::size_of::(); + assert!(options.to_core().libwebp_compat); + } } diff --git a/fuzz/fuzz_targets/e2e.rs b/fuzz/fuzz_targets/e2e.rs index 70c0a75..07a2bd5 100644 --- a/fuzz/fuzz_targets/e2e.rs +++ b/fuzz/fuzz_targets/e2e.rs @@ -129,6 +129,8 @@ fn decode_external(data: &[u8], options: Options) { n_threads: options.n_threads, reserved2: 0, frame_size_limit: options.frame_size_limit, + reserved3: 0, + libwebp_compat: i32::from(options.libwebp_compat), }; let mut frame = WPDFrame { struct_size: mem::size_of::(), diff --git a/include/wpd.h b/include/wpd.h index 7f60934..ff94859 100644 --- a/include/wpd.h +++ b/include/wpd.h @@ -260,10 +260,38 @@ typedef struct WPDDecoderOptions { * no such field, gets no limit. */ unsigned frame_size_limit; + /** Must be zero. Takes the tail padding after frame_size_limit. */ + int reserved3; + /** + * Match libwebp's still-container acceptance and portable lossy decoding + * on damaged input. 0 keeps strict decoding; 1 enables compatibility. + * Set before opening input. Animation containers retain their validation. + * Older callers retain strict decoding. + * Compatible still streams retain bytes after RIFF's declared end and + * produce their picture only after wpd_decoder_end_of_stream(). + */ + int libwebp_compat; } WPDDecoderOptions; #define WPD_DECODER_OPTIONS_INIT \ - {sizeof(WPDDecoderOptions), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + {sizeof(WPDDecoderOptions), \ + 0, \ + 0, \ + 0, \ + 0, \ + 0, \ + 0, \ + 0, \ + 0, \ + 0, \ + 0, \ + 0, \ + 0, \ + 0, \ + 0, \ + 0, \ + 0, \ + 0} /** * Set processing options. Cropping precedes scaling. diff --git a/src/container.rs b/src/container.rs index 91ee448..707477d 100644 --- a/src/container.rs +++ b/src/container.rs @@ -120,6 +120,7 @@ pub struct Info { #[derive(Default)] pub struct Scan { + libwebp_compat: bool, pos: usize, riff_end: u64, info: Info, @@ -220,6 +221,18 @@ impl Scan { }; } + pub(crate) fn set_libwebp_compat(&mut self, enabled: bool) { + self.libwebp_compat = enabled; + } + + fn compat_still(&self) -> bool { + self.libwebp_compat && self.vp8x_flags & VP8X_FLAG_ANIM == 0 + } + + pub(crate) fn is_animation_container(&self) -> bool { + self.vp8x_flags & VP8X_FLAG_ANIM != 0 + } + pub fn info(&self) -> &Info { &self.info } @@ -244,6 +257,13 @@ impl Scan { } } else { self.info.coding = Coding::Lossy; + // libwebp's header check compares the first partition length + // with the declared chunk size without subtracting the header. + let size = if self.compat_still() { + size.saturating_add(9) + } else { + size + }; if let Some((width, height)) = bitstream_size(tag, p, size) { self.info.width = width; self.info.height = height; @@ -529,6 +549,9 @@ impl Scan { return self.raw_headers(buf, partial); } self.riff_end = u64::from(rl32(buf, 4)) + 8; + if self.libwebp_compat && !(20..=0xffff_fffe).contains(&self.riff_end) { + return Err(Error::InvalidData); + } self.pos = 12; } @@ -549,15 +572,24 @@ impl Scan { return Err(Error::InvalidData); } if size == u32::MAX { - self.info.truncated = true; + self.info.truncated |= !self.compat_still() || self.info.images == 0; break; } - let padded = size as usize + (size & 1) as usize; + let mut padded = size as usize + (size & 1) as usize; + + if self.compat_still() + && !self.vp8x + && matches!(tag, TAG_VP8 | TAG_VP8L) + && self.info.end - (self.pos + 8) == size as usize + { + // The simple still parser checks the payload, not its pad. + padded = size as usize; + } if self.info.end - (self.pos + 8) < padded { let avail = self.info.end - (self.pos + 8); - self.info.truncated = true; + self.info.truncated |= !self.compat_still() || self.info.images == 0; if self.collect_frames && tag == TAG_ANMF { self.anmf(window(buf, at + 8, avail), false)?; } @@ -574,6 +606,15 @@ impl Scan { match tag { TAG_VP8X => { + // The still decoder treats later VP8X chunks as optional + // chunks; the demuxer validates them instead. + if self.compat_still() && self.vp8x { + self.pos += 8 + padded; + continue; + } + if self.compat_still() && self.info.images != 0 { + break; + } if self.vp8x || size != VP8X_CHUNK_SIZE { log::error("invalid VP8X chunk"); return Err(Error::InvalidData); @@ -582,7 +623,9 @@ impl Scan { let flags = byte(buf, at + 8); - if flags & !VP8X_FLAGS_VALID != 0 { + if (!self.libwebp_compat || flags & VP8X_FLAG_ANIM != 0) + && flags & !VP8X_FLAGS_VALID != 0 + { log::error_args(format_args!( "VP8X sets reserved flag bits (0x{flags:02x})" )); @@ -600,6 +643,10 @@ impl Scan { crate::error::check_image_size(self.info.width, self.info.height)?; } TAG_ALPH => { + if self.compat_still() && self.info.images != 0 { + self.pos += 8 + padded; + continue; + } self.still_chunk_allowed()?; if self.still_alpha_allowed() { self.info.has_alpha = true; @@ -607,6 +654,10 @@ impl Scan { } } TAG_ANIM => { + if self.compat_still() { + self.pos += 8 + padded; + continue; + } if size < ANIM_CHUNK_SIZE { log::error("ANIM chunk is too short"); return Err(Error::InvalidData); @@ -624,6 +675,10 @@ impl Scan { } } TAG_ANMF => { + if self.compat_still() { + self.pos += 8 + padded; + continue; + } if !self.anim_chunk { log::error("ANMF chunk before the ANIM header"); return Err(Error::InvalidData); @@ -645,14 +700,26 @@ impl Scan { self.still_chunk_allowed()?; let first = self.info.images == 0; + let have = if self.compat_still() { + base + buf.len() - (self.pos + 8) + } else { + size as usize + }; + + if first + && self.compat_still() + && have < if tag == TAG_VP8L { 5 } else { 10 } + { + // A shortened chunk can be complete before its actual + // codec header has arrived. Revisit it on append. + self.info.truncated = true; + partial_still = partial; + break; + } self.info.images = self.info.images.saturating_add(1); if first { - self.still_size( - tag, - window(buf, at + 8, size as usize), - size as usize, - )?; + self.still_size(tag, window(buf, at + 8, have), size as usize)?; } } _ => { diff --git a/src/driver/mod.rs b/src/driver/mod.rs index e1a9104..1f2ab6c 100644 --- a/src/driver/mod.rs +++ b/src/driver/mod.rs @@ -503,6 +503,7 @@ impl<'a> Decoder<'a> { fn rescan_headers(&mut self) -> Result<(), Error> { let base = self.input.discarded(); + self.scan.set_libwebp_compat(self.options.libwebp_compat); let walked = self .scan .headers(self.input.bytes(), base, self.streaming, true); @@ -621,6 +622,11 @@ impl<'a> Decoder<'a> { /// buffered, and a caller appending forever holds at most the file the /// header describes. A header split across appends is read from both. fn riff_room(&self, data: &[u8]) -> usize { + // The still codec in libwebp can consume bytes past RIFF's declared + // end. This mode retains them, subject to Input's byte limit. + if self.options.libwebp_compat && !self.scan.is_animation_container() { + return usize::MAX; + } let size = self.input.size(); let end = self.scan.riff_end().or_else(|| { if size >= 12 { @@ -716,6 +722,12 @@ impl Decoder<'_> { if bad_crop || bad_scale || options.n_threads < 0 { return Err(self.fail("invalid decoder options", Error::InvalidArgument)); } + if self.opened && options.libwebp_compat != self.options.libwebp_compat { + return Err(self.fail( + "compatibility must be set before opening input", + Error::InvalidArgument, + )); + } if self.anim_mode == ANIM_SUBFRAME && options.transforms() { return Err(self.fail( "cropping, scaling and flipping are defined against the canvas, \ @@ -1044,6 +1056,14 @@ impl Decoder<'_> { _ => {} } } + if decoder.options.libwebp_compat && decoder.still_done && !decoder.animation { + return Ok(false); + } + // A compatible still can need bytes outside its declared image or + // RIFF extent. Its final partition size is only known at EOF. + if decoder.options.libwebp_compat && !decoder.animation && !decoder.eos { + return Ok(false); + } if decoder.scanned().raw != Raw::No { return if decoder.still_done { Ok(false) @@ -1067,7 +1087,12 @@ impl Decoder<'_> { let size = size as usize; let padded_size = size + (size & 1); - if decoder.end - payload_pos < padded_size { + let missing_still_pad = decoder.options.libwebp_compat + && !decoder.animation + && decoder.end - payload_pos == size + && matches!(chunk_type, TAG_VP8 | TAG_VP8L); + + if decoder.end - payload_pos < padded_size && !missing_still_pad { if !decoder.eos { let avail = decoder.end - payload_pos; @@ -1106,7 +1131,7 @@ impl Decoder<'_> { Error::InvalidData, )); } - if decoder.alpha_pending { + if decoder.alpha_pending && !decoder.options.libwebp_compat { return Err(("duplicate ALPHA chunk", Error::InvalidData)); } decoder.alpha_pending = true; @@ -1123,6 +1148,13 @@ impl Decoder<'_> { if decoder.animation || decoder.still_done { continue; } + // libwebp uses all remaining bytes for the still codec, + // even when the image chunk declares a shorter payload. + let size = if decoder.options.libwebp_compat { + decoder.input.size() - payload_pos + } else { + size + }; let ret = if decoder.vp8_active { decoder.vp8_lossy_step(payload_pos, size, size).and_then( |done| done.then_some(()).ok_or(Error::InvalidData), @@ -1140,6 +1172,11 @@ impl Decoder<'_> { if decoder.animation || decoder.still_done { continue; } + let size = if decoder.options.libwebp_compat { + decoder.input.size() - payload_pos + } else { + size + }; if decoder.frame.vp8l.still_active() { decoder .lossless_step(payload_pos, size, size, true) @@ -1157,6 +1194,9 @@ impl Decoder<'_> { return decoder.emit_still_lossless(out); } TAG_ANMF => { + if decoder.options.libwebp_compat && !decoder.animation { + continue; + } if !decoder.animation || decoder.canvas_width == 0 || decoder.canvas_height == 0 @@ -1375,6 +1415,183 @@ mod tests { out } + fn compat() -> Decoder<'static> { + let mut decoder = Decoder::new(); + + decoder + .set_core_options(Options { + libwebp_compat: true, + ..Options::default() + }) + .unwrap(); + decoder + } + + #[test] + fn still_compatibility_ignores_a_broken_tail_and_uses_available_image_bytes() { + let mut broken_tail = riff_lossless(); + + broken_tail.extend_from_slice(b"JUNK\xff\xff\xff\xff"); + let len = broken_tail.len() as u32 - 8; + + broken_tail[4..8].copy_from_slice(&len.to_le_bytes()); + + let mut short_chunk = riff_lossless(); + + short_chunk[16..20].copy_from_slice(&6u32.to_le_bytes()); + + let mut strict = Decoder::new(); + + assert_eq!(strict.open(&broken_tail), Err(Error::Truncated)); + strict.open(&short_chunk).unwrap(); + assert!(strict.next_picture(&mut Handout::default()).is_err()); + + let mut short_riff = riff_lossless(); + + short_riff[4..8].copy_from_slice(&18u32.to_le_bytes()); + short_riff[16..20].copy_from_slice(&6u32.to_le_bytes()); + + for data in [broken_tail, short_chunk, short_riff] { + for mode in 0..3 { + let mut decoder = compat(); + let mut frames = 0; + + if mode == 0 { + decoder.open(&data).unwrap(); + } else { + decoder.open_stream().unwrap(); + for i in 1..=data.len() { + if mode == 1 { + decoder.append(&data[i - 1..i]).unwrap(); + } else { + decoder.update(&data[..i]).unwrap(); + } + frames += usize::from( + decoder.next_picture(&mut Handout::default()).unwrap(), + ); + } + decoder.end_of_stream().unwrap(); + } + frames += + usize::from(decoder.next_picture(&mut Handout::default()).unwrap()); + assert_eq!(frames, 1, "mode {mode}"); + assert!(!decoder.next_picture(&mut Handout::default()).unwrap()); + assert_eq!((decoder.canvas_width, decoder.canvas_height), (2, 2)); + } + } + } + + #[test] + fn still_compatibility_keeps_the_first_vp8x_and_the_riff_size_check() { + let mut payload = chunk(b"VP8X", &[0, 0, 0, 0, 1, 0, 0, 1, 0, 0]); + + payload.extend(chunk(b"VP8X", &[0xff; 10])); + payload.extend(chunk(b"VP8L", RAW_LOSSLESS)); + + let mut data = b"RIFF".to_vec(); + + data.extend_from_slice(&(payload.len() as u32 + 4).to_le_bytes()); + data.extend_from_slice(b"WEBP"); + data.extend(payload); + assert_eq!(Decoder::new().open(&data), Err(Error::InvalidData)); + + let mut decoder = compat(); + + decoder.open(&data).unwrap(); + assert!(decoder.next_picture(&mut Handout::default()).unwrap()); + assert!(!decoder.next_picture(&mut Handout::default()).unwrap()); + let len = data.len() as u32; + + data[4..8].copy_from_slice(&len.to_le_bytes()); + assert_eq!(decoder.open(&data), Err(Error::Truncated)); + } + + #[test] + fn container_compatibility_cannot_change_after_opening_input() { + let mut decoder = Decoder::new(); + + decoder.open(&riff_lossless()).unwrap(); + assert_eq!( + decoder.set_core_options(Options { + libwebp_compat: true, + ..Options::default() + }), + Err(Error::InvalidArgument) + ); + } + + #[test] + fn still_compatibility_waits_for_the_actual_header_of_an_empty_chunk() { + for size in [0u32, 1, 4] { + let mut data = riff_lossless(); + + data[16..20].copy_from_slice(&size.to_le_bytes()); + + for update in [false, true] { + let mut decoder = compat(); + + decoder.open_stream().unwrap(); + for i in 1..=data.len() { + if update { + decoder.update(&data[..i]).unwrap(); + } else { + decoder.append(&data[i - 1..i]).unwrap(); + } + assert!(!decoder.next_picture(&mut Handout::default()).unwrap()); + } + decoder.end_of_stream().unwrap(); + assert!(decoder.next_picture(&mut Handout::default()).unwrap()); + assert!(!decoder.next_picture(&mut Handout::default()).unwrap()); + } + } + } + + #[test] + fn still_compatibility_accepts_a_simple_images_missing_final_pad() { + let mut data = riff_lossless(); + + data.push(0); + let len = data.len() as u32 - 8; + + data[4..8].copy_from_slice(&len.to_le_bytes()); + data[16..20].copy_from_slice(&13u32.to_le_bytes()); + assert_eq!(Decoder::new().open(&data), Err(Error::Truncated)); + let mut decoder = compat(); + + decoder.open(&data).unwrap(); + assert!(decoder.next_picture(&mut Handout::default()).unwrap()); + decoder.open_stream().unwrap(); + for byte in &data { + decoder.append(std::slice::from_ref(byte)).unwrap(); + } + decoder.end_of_stream().unwrap(); + assert!(decoder.next_picture(&mut Handout::default()).unwrap()); + } + + #[test] + fn still_compatibility_skips_optional_anmf_and_keeps_animation_validation() { + let mut data = riff_lossless(); + let mut payload = chunk(b"VP8X", &[0, 0, 0, 0, 1, 0, 0, 1, 0, 0]); + + payload.extend(chunk(b"ANMF", &[0; 16])); + payload.extend_from_slice(&data[12..]); + data.truncate(12); + data[4..8].copy_from_slice(&(payload.len() as u32 + 4).to_le_bytes()); + data.extend(payload); + + let mut decoder = compat(); + + decoder.open(&data).unwrap(); + assert!(decoder.next_picture(&mut Handout::default()).unwrap()); + assert!(!decoder.next_picture(&mut Handout::default()).unwrap()); + for flags in [3, 0x42, 0x82] { + let mut data = animation(&chunk(b"VP8L", RAW_LOSSLESS), 2, 2); + + data[20] = flags; + assert_eq!(compat().open(&data), Err(Error::InvalidData)); + } + } + #[test] fn detached_update_buffers_suspend_decoding_and_resume_at_the_same_position() { let mut decoder = Decoder::new(); diff --git a/src/options.rs b/src/options.rs index a1e9d35..3607b51 100644 --- a/src/options.rs +++ b/src/options.rs @@ -14,6 +14,12 @@ pub struct Options { /// decoded, so a caller facing untrusted input can bound the memory and /// time a few bytes of header may ask for; dav1d's `frame_size_limit`. pub frame_size_limit: u32, + /// Match libwebp's still-container acceptance and portable lossy decoding + /// on damaged input. Set before opening the input; animations retain + /// their container validation. The default keeps strict decoding. + /// Compatible still streams retain bytes past the declared RIFF end and + /// produce their picture after `end_of_stream`. + pub libwebp_compat: bool, } impl Options { From ffb717aa3aa48b0caaf9201b15f99d02f514cf29 Mon Sep 17 00:00:00 2001 From: Mike Sulsenti Date: Sun, 4 Oct 2026 11:15:07 -0400 Subject: [PATCH 06/10] tests: compare directories against libwebp 1.6.0 tools Add scripts/webpcompare.py for still dwebp and composited anim_dump comparisons, including extensionless fuzz corpora. Require reference version 1.6.0, enforce a per-file watchdog, report acceptance and frame/pixel hashes as JSON lines, and distinguish visible differences from RGB under zero alpha. Abnormal exits are failures, not ordinary rejections. Use --libwebp-compat for the proposed matching mode and --noasm for the deterministic portable-C oracle. Decoded outputs use an owned temporary directory under wpd-test-data and input files are never modified. Validation: original test-data corpus against native 1.6.0; invalid input against -noasm; scripts/stylecheck.sh. Restrict --noasm to still dwebp comparisons: anim_dump 1.6.0 has no such option and otherwise treats it as an input filename. Animation comparisons keep the native reference implementation, and --help states this distinction. --- scripts/webpcompare.py | 190 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100755 scripts/webpcompare.py diff --git a/scripts/webpcompare.py b/scripts/webpcompare.py new file mode 100755 index 0000000..39c0dd5 --- /dev/null +++ b/scripts/webpcompare.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +"""Compare stills and composited animations with libwebp 1.6.0. + +Emit one JSON record per input and a summary. RGB differences under zero alpha +are counted separately. Input files, including extensionless fuzz seeds, are +never changed. Decoded files live in a temporary directory under wpd-test-data. +""" + +import argparse +import hashlib +import json +import os +from pathlib import Path +import re +import subprocess +import tempfile + + +def run(command, timeout): + try: + p = subprocess.run(command, stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, timeout=timeout) + return p.returncode, p.stderr[-4096:].decode("utf-8", "replace") + except subprocess.TimeoutExpired: + return "timeout", "wall-clock limit exceeded" + + +def check_version(tool, pattern): + p = subprocess.run([tool, "-version"], capture_output=True, text=True, + check=True, timeout=10) + version = (p.stdout + p.stderr).strip() + if not re.search(pattern, version): + raise ValueError(f"{tool}: expected libwebp 1.6.0, got {version!r}") + return version + + +def animated(data): + return (len(data) >= 30 and data[:4] == b"RIFF" and + data[8:16] == b"WEBPVP8X" and data[20] & 2 != 0) + + +def pams(path): + """Read concatenated PAM frames and normalize RGB to straight RGBA.""" + frames = [] + with path.open("rb") as source: + while True: + magic = source.readline(4) + if not magic: + return frames + if magic != b"P7\n": + raise ValueError("invalid PAM magic") + fields = {} + for _ in range(32): + line = source.readline(256) + if line == b"ENDHDR\n": + break + pair = line.split() + if len(pair) == 2: + fields[pair[0]] = pair[1] + else: + raise ValueError("invalid PAM header") + width, height, depth = [int(fields[key]) for key in + (b"WIDTH", b"HEIGHT", b"DEPTH")] + if width <= 0 or height <= 0 or depth not in (3, 4): + raise ValueError("invalid PAM dimensions or depth") + size = width * height * depth + if size > 2 ** 30 or fields.get(b"MAXVAL") != b"255": + raise ValueError("PAM exceeds comparison limit") + pixels = source.read(size) + if len(pixels) != size: + raise ValueError("short PAM frame") + if depth == 3: + rgba = bytearray(width * height * 4) + for channel in range(3): + rgba[channel::4] = pixels[channel::3] + rgba[3::4] = b"\xff" * (width * height) + pixels = bytes(rgba) + frames.append((width, height, pixels)) + + +def digest(frames): + h = hashlib.sha256() + for width, height, pixels in frames: + h.update(width.to_bytes(4, "little")) + h.update(height.to_bytes(4, "little")) + h.update(pixels) + return h.hexdigest() + + +def difference(wpd, reference): + if [(w, h) for w, h, _ in wpd] != [(w, h) for w, h, _ in reference]: + return "geometry", 0, 0 + visible = transparent = 0 + for (_, _, a), (_, _, b) in zip(wpd, reference): + if a == b: + continue + for i in range(0, len(a), 4): + if a[i:i + 4] == b[i:i + 4]: + continue + if a[i + 3] == b[i + 3] == 0: + transparent += 1 + else: + visible += 1 + return ("visible" if visible else "transparent" if transparent else "identical", + visible, transparent) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("directory", type=Path) + parser.add_argument("--wpd", default="./build/wpd") + parser.add_argument("--dwebp", default="dwebp") + parser.add_argument("--anim-dump", default="anim_dump") + parser.add_argument("--libwebp-compat", action="store_true") + parser.add_argument("--noasm", action="store_true", + help="use portable C for dwebp; anim_dump remains native") + parser.add_argument("--timeout", type=float, default=60) + parser.add_argument("--work-dir", type=Path, default=Path("wpd-test-data")) + args = parser.parse_args() + if not args.directory.is_dir() or args.timeout <= 0: + parser.error("a directory and a positive timeout are required") + args.wpd = os.path.abspath(args.wpd) + args.work_dir.mkdir(parents=True, exist_ok=True) + versions = { + "dwebp": check_version(args.dwebp, r"^1\.6\.0$"), + "anim_dump": check_version(args.anim_dump, r"Decoder version: 1\.6\.0\b"), + } + counts = {key: 0 for key in ("identical", "transparent", "visible", "geometry", + "wpd_only", "libwebp_only", "neither", "abnormal")} + files = sorted(p for p in args.directory.rglob("*") if p.is_file() and + ".git" not in p.relative_to(args.directory).parts) + for path in files: + data = path.read_bytes() + record = {"path": str(path), "sha256": hashlib.sha256(data).hexdigest(), + "animation": animated(data)} + with tempfile.TemporaryDirectory(prefix="webpcompare-", dir=args.work_dir) as td: + work = Path(td) + wpath = work / "wpd.pam" + wcmd = [args.wpd, "--fmt", "rgba", "--muxer", "pam", "--threads", "1"] + if args.libwebp_compat: + wcmd.append("--libwebp-compat") + wstatus, werror = run(wcmd + [str(path), str(wpath)], args.timeout) + if record["animation"]: + lcmd = [args.anim_dump, "-pam", "-folder", str(work), "-prefix", "ref"] + else: + lcmd = [args.dwebp, "-quiet", "-pam", "-o", str(work / "ref0.pam")] + if args.noasm and not record["animation"]: + lcmd.append("-noasm") + lstatus, lerror = run(lcmd + [str(path)], args.timeout) + record.update(wpd_status=wstatus, libwebp_status=lstatus) + if (not isinstance(wstatus, int) or not isinstance(lstatus, int) or + wstatus < 0 or lstatus < 0): + outcome = "abnormal" + elif wstatus == lstatus == 0: + try: + a = pams(wpath) + references = sorted(work.glob("ref*.pam"), key=lambda p: + int(re.search(r"(\d+)\.pam$", p.name)[1])) + b = [frame for p in references for frame in pams(p)] + if not a or not b: + raise ValueError("success without decoded frames") + outcome, visible, transparent = difference(a, b) + record.update(wpd_pixel_sha256=digest(a), + libwebp_pixel_sha256=digest(b), + visible_pixels=visible, transparent_pixels=transparent, + frames=len(a)) + except (ValueError, KeyError) as error: + outcome = "abnormal" + record["output_error"] = str(error) + else: + outcome = ("wpd_only" if wstatus == 0 else + "libwebp_only" if lstatus == 0 else "neither") + if wstatus != 0: + record["wpd_error"] = werror.strip() + if lstatus != 0: + record["libwebp_error"] = lerror.strip() + record["outcome"] = outcome + counts[outcome] += 1 + print(json.dumps(record), flush=True) + print(json.dumps({"summary": counts, "files": len(files), "versions": versions, + "libwebp_compat": args.libwebp_compat, "noasm": args.noasm})) + return int(not files or any(counts[key] for key in + ("visible", "geometry", "wpd_only", "libwebp_only", "abnormal"))) + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (OSError, ValueError, subprocess.SubprocessError) as error: + raise SystemExit(str(error)) From b02e99103c7b63e1c7b89570dea71b4cd321841e Mon Sep 17 00:00:00 2001 From: Mike Sulsenti Date: Sun, 4 Oct 2026 10:48:01 -0400 Subject: [PATCH 07/10] vp8: match portable libwebp transforms in compatibility mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mutated lossy streams can overflow the signed 16-bit first-pass buffers described by RFC 6386. Keep the existing default arithmetic, and let libwebp_compat select the wider intermediates used by libwebp 1.6.0's portable C decoder. Apply the same safe Rust transforms with and without assembly, including DC-only paths, whole frames and incremental decoding. All eleven reported lossy cases now match dwebp -noasm in YUV. Native x86 libwebp SIMD itself differs from its C decoder on ten of those cases, so this option deliberately defines a portable oracle instead of claiming universal browser parity. Fixed-point multiplication wraps explicitly for larger damaged coefficients, avoiding debug/fuzz panics. Pin the eleven results using ten public or synthetic raw VP8 fixtures, check thread counts and incremental decoding, and verify that valid project corpus pixels stay unchanged. Document fixture provenance and the arithmetic distinction. Validation: all-feature and no-assembly compatibility tests; scalar transform boundary tests; the project's stylecheck. Save each decoder’s original strict DSP tables before replacing compatibility transforms and restore those saved tables when the mode is disabled, without resampling global CPU flags. --- src/driver/lossy.rs | 5 + src/driver/slot.rs | 1 + src/dsp/vp8.rs | 102 +++++++++-- src/vp8/mod.rs | 16 ++ ...386ead209a72d4411d4d0eadd0f9ed6235730c.vp8 | Bin 0 -> 7638 bytes ...06e5ea9977c4e83826977b05a664ba311fd396.vp8 | Bin 0 -> 34980 bytes ...a90d04ce17374a075194e72510319d4f12e5f1.vp8 | Bin 0 -> 7092 bytes ...ad5278df83871478b68166dc93f71a9d6ca96a.vp8 | Bin 0 -> 7092 bytes ...641847c558f1e8e204ae6b9ae7a39829df71e8.vp8 | Bin 0 -> 7638 bytes ...232e41e66c25526a68befb54c530f39385239e.vp8 | Bin 0 -> 7092 bytes ...c6d1e69830bad6ca4e19014559805c7309a07a.vp8 | Bin 0 -> 7092 bytes tests/data/vp8-compat/README.md | 30 ++++ ...e4675457e402210e2e72ea9e61f93c40a81a68.vp8 | Bin 0 -> 7638 bytes ...4b9d6c836f6b8c5d6e6705e4e98261dfabb0be.vp8 | Bin 0 -> 34980 bytes ...df820775b3266dc39aeadc299d34df5c895299.vp8 | Bin 0 -> 34980 bytes tests/vp8_compat.rs | 158 ++++++++++++++++++ tests/vp8_mode_reset.rs | 52 ++++++ 17 files changed, 349 insertions(+), 15 deletions(-) create mode 100644 tests/data/vp8-compat/05386ead209a72d4411d4d0eadd0f9ed6235730c.vp8 create mode 100644 tests/data/vp8-compat/1106e5ea9977c4e83826977b05a664ba311fd396.vp8 create mode 100644 tests/data/vp8-compat/2fa90d04ce17374a075194e72510319d4f12e5f1.vp8 create mode 100644 tests/data/vp8-compat/31ad5278df83871478b68166dc93f71a9d6ca96a.vp8 create mode 100644 tests/data/vp8-compat/3d641847c558f1e8e204ae6b9ae7a39829df71e8.vp8 create mode 100644 tests/data/vp8-compat/41232e41e66c25526a68befb54c530f39385239e.vp8 create mode 100644 tests/data/vp8-compat/45c6d1e69830bad6ca4e19014559805c7309a07a.vp8 create mode 100644 tests/data/vp8-compat/README.md create mode 100644 tests/data/vp8-compat/d4e4675457e402210e2e72ea9e61f93c40a81a68.vp8 create mode 100644 tests/data/vp8-compat/fd4b9d6c836f6b8c5d6e6705e4e98261dfabb0be.vp8 create mode 100644 tests/data/vp8-compat/fddf820775b3266dc39aeadc299d34df5c895299.vp8 create mode 100644 tests/vp8_compat.rs create mode 100644 tests/vp8_mode_reset.rs diff --git a/src/driver/lossy.rs b/src/driver/lossy.rs index 704df4b..bab869c 100644 --- a/src/driver/lossy.rs +++ b/src/driver/lossy.rs @@ -219,10 +219,12 @@ impl FrameSlot { ) -> Result<()> { { let bypass = env.bypass_filtering; + let compat = env.libwebp_compat; let chunk = env.input.chunk(offset, size); let vp8 = self.vp8_decoder()?; vp8.bypass_filtering = bypass; + vp8.libwebp_compat = compat; if vp8.frame_init(chunk, size, size)? == Status::NeedMore { return Err(Error::InvalidData); } @@ -310,6 +312,7 @@ impl<'a> Decoder<'a> { pub(crate) fn frame_settings(&self) -> super::slot::FrameSettings { super::slot::FrameSettings { bypass_filtering: self.filter_bypass(), + libwebp_compat: self.options.libwebp_compat, no_fancy_upsampling: self.options.no_fancy_upsampling, to_argb: self.frame_to_argb(), premultiply: self.frame_premultiply(), @@ -355,10 +358,12 @@ impl<'a> Decoder<'a> { ) -> Result { if !self.vp8_active { let bypass = self.filter_bypass(); + let compat = self.options.libwebp_compat; let Self { frame, input, .. } = self; let vp8 = frame.vp8_decoder()?; vp8.bypass_filtering = bypass; + vp8.libwebp_compat = compat; match vp8.frame_init(input.chunk(offset, avail), avail, size)? { Status::NeedMore => return Ok(false), diff --git a/src/driver/slot.rs b/src/driver/slot.rs index 0b64d7e..880e26e 100644 --- a/src/driver/slot.rs +++ b/src/driver/slot.rs @@ -33,6 +33,7 @@ impl std::ops::Deref for FrameEnv<'_, '_> { #[derive(Clone, Copy, Default, PartialEq, Eq)] pub(crate) struct FrameSettings { pub(crate) bypass_filtering: bool, + pub(crate) libwebp_compat: bool, pub(crate) no_fancy_upsampling: bool, /// The output format alone decides the frame must become ARGB, whatever /// the frames before it did, so the conversion can happen off the walk. diff --git a/src/dsp/vp8.rs b/src/dsp/vp8.rs index ff58d4d..122da87 100644 --- a/src/dsp/vp8.rs +++ b/src/dsp/vp8.rs @@ -161,20 +161,32 @@ pub fn loop_filter_simple(buf: &mut [u8], stride: usize, flim: } } -pub fn luma_dc_wht(block: &mut [[i16; 16]; 16], dc: &mut [i16; 16]) { +fn transform_intermediate(value: i32) -> i32 { + if LIBWEBP { + value + } else { + i32::from(value as i16) + } +} + +fn luma_dc_wht_tmpl( + block: &mut [[i16; 16]; 16], + dc: &mut [i16; 16], +) { + let mut tmp = [0i32; 16]; for i in 0..4 { let d = |k: usize| i32::from(dc[k * 4 + i]); let (t0, t1) = (d(0) + d(3), d(1) + d(2)); let (t2, t3) = (d(1) - d(2), d(0) - d(3)); - dc[i] = (t0 + t1) as i16; - dc[4 + i] = (t3 + t2) as i16; - dc[8 + i] = (t0 - t1) as i16; - dc[12 + i] = (t3 - t2) as i16; + tmp[i] = transform_intermediate::(t0 + t1); + tmp[4 + i] = transform_intermediate::(t3 + t2); + tmp[8 + i] = transform_intermediate::(t0 - t1); + tmp[12 + i] = transform_intermediate::(t3 - t2); } for i in 0..4 { - let d = |k: usize| i32::from(dc[i * 4 + k]); + let d = |k: usize| tmp[i * 4 + k]; let (t0, t1) = (d(0) + d(3) + 3, d(1) + d(2)); let (t2, t3) = (d(1) - d(2), d(0) - d(3) + 3); @@ -187,6 +199,10 @@ pub fn luma_dc_wht(block: &mut [[i16; 16]; 16], dc: &mut [i16; 16]) { } } +pub fn luma_dc_wht(block: &mut [[i16; 16]; 16], dc: &mut [i16; 16]) { + luma_dc_wht_tmpl::(block, dc); +} + pub fn luma_dc_wht_dc(block: &mut [[i16; 16]; 16], dc: &mut [i16; 16]) { let val = ((i32::from(dc[0]) + 3) >> 3) as i16; @@ -197,15 +213,19 @@ pub fn luma_dc_wht_dc(block: &mut [[i16; 16]; 16], dc: &mut [i16; 16]) { } fn mul_20091(a: i32) -> i32 { - ((a * 20091) >> 16) + a + ((a.wrapping_mul(20091)) >> 16) + a } fn mul_35468(a: i32) -> i32 { - (a * 35468) >> 16 + (a.wrapping_mul(35468)) >> 16 } -pub fn idct_add(dst: &mut [u8], stride: usize, block: &mut [i16; 16]) { - let mut tmp = [0i16; 16]; +fn idct_add_tmpl( + dst: &mut [u8], + stride: usize, + block: &mut [i16; 16], +) { + let mut tmp = [0i32; 16]; for i in 0..4 { let b = |k: usize| i32::from(block[k * 4 + i]); @@ -218,14 +238,14 @@ pub fn idct_add(dst: &mut [u8], stride: usize, block: &mut [i16; 16]) { block[k * 4 + i] = 0; } - tmp[i * 4] = (t0 + t3) as i16; - tmp[i * 4 + 1] = (t1 + t2) as i16; - tmp[i * 4 + 2] = (t1 - t2) as i16; - tmp[i * 4 + 3] = (t0 - t3) as i16; + tmp[i * 4] = transform_intermediate::(t0 + t3); + tmp[i * 4 + 1] = transform_intermediate::(t1 + t2); + tmp[i * 4 + 2] = transform_intermediate::(t1 - t2); + tmp[i * 4 + 3] = transform_intermediate::(t0 - t3); } for i in 0..4 { - let t = |k: usize| i32::from(tmp[k * 4 + i]); + let t = |k: usize| tmp[k * 4 + i]; let t0 = t(0) + t(2); let t1 = t(0) - t(2); let t2 = mul_35468(t(1)) - mul_20091(t(3)); @@ -239,6 +259,10 @@ pub fn idct_add(dst: &mut [u8], stride: usize, block: &mut [i16; 16]) { } } +pub fn idct_add(dst: &mut [u8], stride: usize, block: &mut [i16; 16]) { + idct_add_tmpl::(dst, stride, block); +} + // LLVM 19 recognizes a saturating pack when the upper bound is clipped // first, whereas i16::clamp produces extra vector clamps. #[allow(clippy::manual_clamp)] @@ -331,6 +355,32 @@ mod tests { assert_eq!(block[0], 0); } + #[test] + fn full_width_transforms_handle_extreme_coefficients() { + for value in [i16::MIN, i16::MAX] { + let mut block = [value; 16]; + let mut dst = [128u8; 16]; + + idct_add_tmpl::(&mut dst, 4, &mut block); + assert_eq!(block, [0; 16]); + } + } + + #[test] + fn compatibility_dc_rounding_does_not_narrow_before_shifting() { + let mut dsp = Vp8Dsp::new(); + let mut blocks = [[0i16; 16]; 16]; + let mut dc = [0i16; 16]; + + dsp.libwebp_transforms(); + dc[0] = i16::MAX; + (dsp.luma_dc_wht_dc)(&mut blocks, &mut dc); + for block in &blocks { + assert_eq!(block[0], 4096); + } + assert_eq!(dc, [0; 16]); + } + #[test] fn the_transform_clears_its_coefficients() { let mut block = [7i16; 16]; @@ -354,6 +404,7 @@ pub type LfAllFn = fn(&mut [u8], usize, usize, i32, i32, i32, i32, u32); pub type LfUvAllFn = fn(&mut [u8], usize, &mut [u8], usize, usize, i32, i32, i32, i32, u32); +#[derive(Clone, Copy)] pub struct Vp8Dsp { pub luma_dc_wht: WhtFn, pub luma_dc_wht_dc: WhtFn, @@ -402,6 +453,14 @@ fn idct_add_c(p: &mut [u8], o: usize, s: usize, block: &mut [i16; 16]) { idct_add(&mut p[o..], s, block); } +fn wht_libwebp_c(block: &mut [[i16; 16]; 16], dc: &mut [i16; 16]) { + luma_dc_wht_tmpl::(block, dc); +} + +fn idct_add_libwebp_c(p: &mut [u8], o: usize, s: usize, block: &mut [i16; 16]) { + idct_add_tmpl::(&mut p[o..], s, block); +} + fn idct_dc_add_c(p: &mut [u8], o: usize, s: usize, block: &mut [i16; 16]) { idct_dc_add(&mut p[o..], s, block); } @@ -558,6 +617,19 @@ impl Vp8Dsp { } } + /// libwebp's portable C transforms keep the first pass in 32-bit + /// integers. RFC 6386 describes 16-bit intermediates; damaged + /// coefficients can make the two produce different pixels. Select the + /// same arithmetic with and without assembly. + pub(crate) fn libwebp_transforms(&mut self) { + self.luma_dc_wht = wht_libwebp_c; + self.luma_dc_wht_dc = wht_dc_c; + self.idct_add = idct_add_libwebp_c; + self.idct_dc_add = idct_dc_add_c; + self.idct_dc_add4y = idct_dc_add4y_c; + self.idct_dc_add4uv = idct_dc_add4uv_c; + } + pub fn new() -> Self { #[allow(unused_mut)] let mut table = Self::scalar(); diff --git a/src/vp8/mod.rs b/src/vp8/mod.rs index f255e13..353a345 100644 --- a/src/vp8/mod.rs +++ b/src/vp8/mod.rs @@ -291,6 +291,9 @@ pub struct Decoder { pub width: i32, pub height: i32, pub bypass_filtering: bool, + /// Match the full-width intermediates of libwebp's portable C transforms. + pub libwebp_compat: bool, + strict_dsp: Option<(Vp8Dsp, Vp8Dsp)>, mb_width: usize, mb_height: usize, @@ -1523,6 +1526,19 @@ impl Decoder { .inspect_err(|_| crate::log::error("Frame allocation failed"))?; } + if self.libwebp_compat { + if self.strict_dsp.is_none() { + // CPU detection or its global mask may have changed since + // this decoder was created. Restore its own tables later. + self.strict_dsp = Some((self.recon.dsp, self.coeffs.dsp)); + self.recon.dsp.libwebp_transforms(); + self.coeffs.dsp.libwebp_transforms(); + } + } else if let Some((recon, coeffs)) = self.strict_dsp.take() { + self.recon.dsp = recon; + self.coeffs.dsp = coeffs; + } + self.recon.deblock_filter = self.recon.filter.level != 0 && !self.bypass_filtering; diff --git a/tests/data/vp8-compat/05386ead209a72d4411d4d0eadd0f9ed6235730c.vp8 b/tests/data/vp8-compat/05386ead209a72d4411d4d0eadd0f9ed6235730c.vp8 new file mode 100644 index 0000000000000000000000000000000000000000..55bc6d8dec8b6e810be118f791bd1347a04e335c GIT binary patch literal 7638 zcmeI$b6Xt_pa9_PRxK}YdD&Vg8_PbqWw#biC+p;8w`^zsSP!u|6;&+mD^ z#hZ`-mj%z93zrKAcmgv;I)y$f;q<+T@ghNTBK?@39Mn}Dp0=L_Z=Thl%}-RRRf#tY z2kcK>8(p8@q2BRtw@}C$)jiqUqb7h0*09+CJO=NAtvepTzwQKK4vWag_y?^p_D6&5 z#fQTgM1|h(?clem?ZeZQ8bB=Q>qA$+(-Q1FdqmXYq4}BOWcD2P576LO3I@WQpvDtA zjbqo%H^OJcSFoMUaTq?V?tgu`H=n!E?iytepi&dncxQh}IpcqDdIKMWsD&3{UTySS z>=BK7xoD0vUZlWi9iG>8T?ao%@_=V;%T%$mk7N>a%hUcjs%o2izKaW=8x>22_`hh2 zMTeyHDXMa7oSZ1g_-=I=IT_aA3|{ygV#K<0!4$yx1t)}w&k*h5GlcA-Fkhc~=1=dBs`Jy< zOL#rBKY1tvWs%oWh8;=_BA@Qqk~WGh6IGV-z?{alqj{Z9O}urV(m$PjRI1JdY)>gj z|5g@mRN{7W6LEQi4gTF<1ZVSH?|Yid};($17>?NF38JvPUMn){{*I-Ywc3$KQo2P*7PxwS@S?< zy#o;do_daPXw_~Ys$so!-E`Lu?vr2wd6*LAz>m83s>cs9tDueXKD31AuZKmEr844= z2DgdCK@>v|f9znAiglR>6!wBlr^p|Q!BhETMPnz!?s|PrJAVs10X1IMALg_3b)xaK zXvy2cWpE}Thq-GH2MfM7_#f!(t2|3gZoFFaS`=V-Z4UZ*W9qqd;gb8-0Ys{&5&D`a z$%5?n)n#$1KA^{k-R(Zy%FE!^{oDa6^(nsb*{~ar`KyuO(<#S3V%VFqj{DL`HrrIE zmi2*&&F-7e#Evr45Ch4^vE|I(b5}^udzzpR-V_io=(aPa=M(D77ERmw8&b&pen^)U zV!bKdiv80u&~Ko{tgzg{+A+e!`}1u{eTc7P5kU~?lF~3{Bi+5xD*2=12@QQM)T8iB zCh@yOKPZleDW|)01NCyIRNvtOmDMz}iwn-&kNhRNPb6|{c$Vvo>!yeRdALheC%~oU zvI|AQjNm5Jl*H#TZnrV+UJS(sVuol`&>I#XmElR}f3vE!zaqRX(_c{GOn^C2lFRRNBE(L}hAh zjXroR+9}dCnsdmktu)!gHC`67(vyElEPyqXCLd~cZ19h3wu*gx8!0@WeV6{Bu3>ka zL9=tA>rFVk6j3fl_O;?uM5s$Ap_zxk-HNEVLt}AKHUh)_J=F>x2eaK2s^*utbkIfIML)>HZrtC?_h7YYCd6um5uY^$PATZD0N99w0$!{X&_swbHOvJuh}M zmgCjJf6O5ouW@%SCL-OHMNG7+8;@*gL4w>OC^bWnyf%2KYn~r$2t7d>=3&C$-rAzL zIwoBsfo~UOx+fHHEY%YM(wR8xlsxAJ5cKoh0;>~1bz^1(5_MHUkkBC1f>z7~1f4jf zWo`_@%+D7-b@STRVuo;q;org=%%{s}mbU?#>7HlhOLF;t<-CaLuZ?POSjMa!C1Hm; zaq_MSfYFmu_S(%&2uNDB5G$~^xoS4N3qvoh&4d`gAU(c>Y-?GE4IY)XX$1vY`KCc- zKH(q>bp?kxQ@=Lh)1sedjXuKgLdBQU+PRiB1(NDi4EA=1DbT=9Onj z!%?`XG2I2{^n&&6y`4jF7}uGf;cK^o%b$u-KW1($*t5#@^QB>}Z`LP*acuRj&1_Fg z&t(;^69dLyO-v+`9?tjshWWjvfNo*&`D>tGGv(g(z4D*F>2?(U-h{gIj_qn>qzv;Lt#rq!mVtzQI z>D*ukon`jw=OXWO!wgx##V^f=9_N$J#Jv=g>R0lLwn;k9eO{7DX(HcV+dgo0s{+Z;B!)FjOEhd2J z9{#Ywi3uf>NRBt0>QS?V?;(8!OMhae5k844^_TAE@CyN?9C^{lZ5$2U`S&dkL$%dP z$dEo=0g1lQuS8bbkugwNwaL#<%Aelta%4|949~QSrDaGS? ze~B5g@G03)Lha1yay`uWp7wiGN!gjD=`X-vbV#48eCTHz%(qI5_s9r(w%cA7a|j$z zQ288v#H?oQk>2fE_QDG7IgGe)mM$6x9?Mz-dg4)7p6iU{oZ%|QpWL3fq+>f5r z>!CJ80pdeiZmYl4t3(MtM*-`#H5U8g__W4d;)|9cgMumuvCDwI)JH14SmbQ=KxJ0i z$WU)t9y&zoz#Q^X?CweiNHurDsZ!W-0utf;7P?mAOxuLYNtA&+N>;DMrN8msqKx>( z4?!7>rE+Ip!`(0?C@o|2C=@b%|Fg5qb3gUrucnToKM_L)v4s4$M)MCnYS?K$xo zI;ZdV%6$?zfH!8ydjVXV?tER(7$_DaZpKkdXqiG2Q@J!^n;rW6wb~eSu{pw^0YnXK z1!U(Fvs#*n{F0JYlrSUMJgCfrm!s2!Sc3v*^SDsV{zgV&Ng6|zX{7UnSfu)O_R8hB zaT8kN-X^rE4J0PA9~4Z30I@@+lc)OSYJ648W;&=F@C8yFLCe>2i@SR__Rp?(2-?L< zU11!h^TiWL6XtdHNNC}*5tg?Nf+w;dsx$dc+l4J>j-4F(ZPD<=m;vKwGUQzP6cwJ@ z?dE(9KZ&9JuMumK*2FAuPDA)l+ke=S4<2u#92QBnHDw=|lSp-gNFV(AxzPgxRYPiibt28rAru3Z-L&^B zyR>kR+>T9kS+eWQAqtMj))=P{{V_b1urZqbf(b07tPc6?H#)MRsH44~$iC1u*qmvKng(V_eis z{Wv?Ph*!O7ELqPGr1~AFmo`mgn&C*!dySUQBpbX5nXW`}|M-cz=hOPLmv=s(>?CDJ z+iuId{vBD9+d;7Zna{m-tYA;P;kWA(pO3^X!x`R6|iVFPU;y zphX#4JNiB@6)hbo3XJLUXvQOF}GM0KlnXZ;vg!b6Ti`&3N>4|L|bV zXx~qRp3zmc&F$zok>tq!~wwnFbTtS09>tm&Ve9CB1 zl{b&{IT`(TT5eVmI2Sa(Pk2M>R~I{rV&$V(F(NX0A&|+{%sjj*t8!Pr@}=5BK9+R_ z?3CfQfSAVZthw!OS4CF9Jjt_7q)RZv&D_4#Vw0;mzF}n-B@ta)rfRX$w;;dLw^nXQ z?Q95;c}zS~MjhXI?skLyrx_5+C~<(oDZ(0ea#!gUG?{Ka#vw^i<~@Lxtzg;>wz}CA zqUBFed(t!;VllV1UztM0hGqk83RfGS+nK3!KAdjx)5_Jp4Kd~mn5!4moi^a%R8z+8Z(e-4e6en#| zWnJI4G|HOozs)O#TiNCde3bm98m(e6eZm6|*WGyHO7Px_-qP+Fw7k*TL)vO=6T-6E zSJiy}^Gm#wdt@|s>jE$-v7S`4t=ZfUggw%)UVI|NdN%7Mp7(6+qsw`T;15vb3eqz= z?IJ=A`#j|4_)icMU=$9-XAZEdDn6_hH1p*}=NzKlq)(`rBN*-nUFpFwCj;;q{q$`V z)&hPK)G1Jmz2;BB_oN>l3Ye zZ^A~k2>#M-$aKxSYW$;ECSC1zIYWs7V+mNa$B#@&eMDkTaQ`dNjkziz z3Nq=tac4YA=GZgz?fd)2fOpI}O6L$CM;7c!>#ZQr0;}h*M%d3+w;xY!_&%UB2zn@R z&PM8M7TY>KycM@Wdt}NVIq=;p%9+W*;TkfFBhxdwy{JML*$kB6#a;qhdD3takz|Q+ z7oF`~)Z#~9zj&1}+3|=l#1s`?zjwodWN-L8;NYY@2O;HCXDF+U-{?BXS+5&AjJEqI zQmLQB3n=}a_vrd)zn*wVBqN5YOvZ;U+%!NraZr5P!uQc$#4ca|W1b{%#?@p(x1)-8 z6FrJccS6BFvo)k+vpXW`=I~p!5h=TiPc6>GPmA}dN)x**+vMhy^wncmDbyrNjI+iN zA32k3)Mo|IDc|ur$r*Es6(0Tw=yc>Nk?>&jEI=RT#5QJY&ndb~SuSXA@X_t94|*&G2a6Fcjcy|CPGv;X@Kvdh32&hWmI?K7^*r3!-ISMJJZy}*x9 z+6+<@+PydOh!?rfc)>(+31rzs`;Hs#HgFLqgLvbTa2i@-N?S2Wc`PTy#@*8cnrFe1Qf9ane1iVTQv0R&pbPdHAhVbHte2CB79Ky< z@`q{xrLT*E>}aY0z-{Tl#z{ZB3LvT@&!#Y0hI@zPPRE1k9DAE|D4A}J!5@`1CVyFm zNY{sD;HQn7dE~ev0u=>YjX2=TN(|)2&#DN18aRLZy*1DXEhsakufrDo3rDmR4-8L) zi(7UKS<<^Wn+}9d=lhVobBtXJaIC*9RRus>z*}I(_w!T64Tec=ohq!cq3MhHOOMKY z`{;A4tQ80z4)YWB*k@&0mYXzgyI(=BG&^^QZXIUa2$Eq8G7;&62Mf-8B>^VYT7N=9 z7E`$o50rhDSw%+KYIf%<)z-2)RSIL=7gk4s`0jze2)%RF{x6MS)Gq!d%--s&0U895qk2(IVvg&=5bTx~ zZh{QO?M#(^@QXwVhN@@uTMXM;%oz`c25Q0A5QTv?Y;Mozg-%}m9ys2D1OBwH?NSxR zt!1|jntZ=Tdt;7GE*^<7&6BDB?e#>&oWBwoNez{tn!hHL!iWC$48fDVQi;J)LnVb# zH+v(7`5WjCIChm5G@UvbKLYNm#*oo^g!b@jltx_KW`{2NuRB{xSz6{la2GG;<0X;v zIh9)R#7a8S7IDC({}ZHxiQ(><1{@OV7s6c%V6E+whI$uB6%lsJ1yZaAm+d#JBStqZ|@479!-!|zM_b~e209;eGB{JcIBf@nu#YsfHPR_BOnS5 z#(pD|HezF1yL0r#s8CN?k6z%{qfb?5qWW^c45j|1VIh#y^8#H*cpt2G;-^}5#u!Kv z?sniic6MA^aSj3y$ZS40{vk4nbzgA1s>G3i^XzEg{o|0q+)FEgU_L;&kNFP610-WS z%VD@47z=&fWjnlpB+U1QmTKadB1v@ks?@jlKsK(ZJ3b{aP$`~4@F)|bXg5h`w2=L-Xug>*qxKQtJyMC(q^g7C#(ka;BlB5rDQFOi za+&@pGw6mz4$`LxMbU$mb~j;=+d5<#d3`r^YL+`&@sO!VHQr#4Rs#&ALJw}pLT#() z&#AceQVS)TIlTgM(D+b1)<7u?9fu36UUzurfns&AoeENzNw}-p-*n-#`m?m)i9Ko2 zED`Y(VKvCCLtR;uy^v_YmK?EcSRlcHuB7OwZDODI{%E|Xjp%ivcf3mGApv$$G|I^k2~(D9S`=i@HTZ_nVmf?^Fc;=!Y9}qf?1` zo*RN%8J=pm{9x3}49_14YZ}IMg}9$g?b-M%gwkJgn|nzQ)Nl{j=UrA^XcFicXmKe1 z;fx)I{29c#2vU-zoVjw|ulni)_!;?8p~79a?>8E3&Fr^$lGbj|h5lb6l#hNNp{+6C zZitIF-luQ{BS}PO$^>ZQ6ObCS1<~DQIdU$1l&-8D=qDp0UuAGgcntO=l$rs1EWd4$ zQuu%3CD**!O4)1zzU?UW8PdbgOkEZOY0IljX=|H&fKMTeyy~Pc!Q&X%88K?5CDnUQ z8#iCb*9!vNZRehC_dUUpsddvzd|GvZUmbTpocXko)Z^&~jw{~03$#xpc%n`A#~yP? z>+<6v6-M4)sYrI%3cGe9y(5v>R6HEpbp;OPyd2_X@G41_+#SkB3|zXcNN1~tTj<>c zAztMo+qf#KQSdN;b{wQLhWBtHT@I0&#RW%`W^+N#A(P<>jH7Zc-{uo z!%=D&EGSfBiB?&b7NUr?CDKU9|MtgePw|DWM@h|)R{yqaYopIGG%L{Q6!(JNxVTX< z=0^eY5PeaLN-Yb&-$`+d4-R7@yZcd~)agG7Bq1R;M|FuC#Pq7&{vqi7F}CEhtoZ>L zjH(*U|50mXpLh)sd%VfZF3C!9EG4X`hl}3{?pO73o_w?DUNve1U7$sX8(KS9z`jiYb0%RORBX4cw0U zPY;y2nMFaG*2`a)Ka?45Tg)tBL96PCrm z_oJcn3O|fKZ#sD$%@+~Dp84gD1%b8@Ct|&1>`?-WtvH`PJPnj{b1#Hpx3*o}*=Dd0 zZ`rP+N8hh7hw|X^7C1#jn=h~h1h&RTm}C3N5&@673%g|mMzJbBp5vk{INU*K6%>>@ vrok&kD_*$YFRT>X3>hPW=L>)4eG@718~Hz-{Ez=m{=bv|@8thqp8Wp+O;LV*6j3+ToZq8B%a%N40xs4=eD46itjYp?9h3ZP=TFvCZ(n*$9c=*rJWRl^ z9iSHgP-PV#aD>`dU|u7f0rWdX%=F~@0C$^zYJ9}2_K6UlbOo^stb@kD7trwfHy!!E z&VBwC(~@=jXHQAcf9-MgegplFM%~+fe^?p1Zsq`9NIvm^U8laEFOc6%z>%K-KV{vY z_Z|M;?p(Bdz>is{pa9^@+bRB&Fj}A<&;q#o-AfQ?^%npDzVqLEQ3L?vdwT%D@fu(@ zyzl1$@>AjWBhVkv=ROMnOnL%-;{mk*KssQu?JF0H5b&-C0DyvXYTSABd+#&YE+~K5 zP472ybEi0G>u@C+ClB|nv4Mg(P);{3Pvh?Ps0c`yy5EbiAN7XmDzDxp5?FRa3XUn1 zsFv#^51fK%@ZT+c+LHvTbk|zvHc>2OEerN}%bn9cuZ84Rm{_t!X+j^0Nu6^7kCah+ zQng^b1ds8D0?>LZF`jc7p`{e?e3JGF$#dvPZLqI|{qmYUX|@EGh1d_tN+b6fDF#J? zVgpB=Hg5et$^WfYv0tvb&$^vZ@00mu%Qg|M)NrKV<7{_YN0fW{2Sldw*F})WW<4O7 zD;I-gtjtGYA=l+POgTnb&_E0K^k%`JipXW2{=TY}3w!`yH( z?}7WC#e!Q07?BuE;MWS-uw+FMn35RsCnyj z$swneYDT7_F4zj;WO-aI$igr%`L}-qe3daqvff2{)ozD~kk&HZ8E>}KB`Fk}2P|aO z)$vFTEFmgOe<#DiW+Eq$`&-tpVbGS&AL5OqO_9yt-_%;1IGTY$A9@ftk#_+&k)ddg9A@2bp6y4;98G{4crs`e;6L9@J97(e9o(g;y2J170|Ag}K@?o89)sr~*Cm;*+i>&aZxQCED4!|vr$(?}{t;aUeeR5E+It_19gOqh zc;2tpOM8cL$6L5Te}c$3bWUn@tmbvVyS+jgDvVr!b&D)KMwE`8Smi)naD;CDvOGXoboNpa5- zOM{7T*q+;UMbn+D*#n2=CnV}gp-)=GIT(&y0DFx50wjCCgy<=bsOTkT7@$%crN3En zgkCP!MpoEI@XdS3^jc{zrE5}rF*{<-8P2pxAcM`n*2<;{Np7$giC6usN$z7aL3B zq5|F=%CDh>&aoDMQOxgCIlF^m@ZsR<;&!;eF2rQu8T2*+yHR2?wW~>nGORWR1D>0y zH>MY6Z8QuX*R5jLC|mBB{P*5};{i$0BePFKa)GUKz}|EbfZYa-S}sT7DRtwgy?kfZ z%M5hIix5?>HN*kI+sgq|Fol<}*7|*u7I($;9cRRy6DhVf`OwcO1Dv%V!NdXsU99&; zlptn;hJTTTgl@FFKE?qe83_nzB>733m^YGp^k*mR%#h?twrck4arj19aMY3==(wSq zrOuC&#|i5op;@?d%Bcsak~p3Vsh`Ff1!nsw4UMLAsyCskcZ*d!fpZa^;qQnd#X(f~ zZCTRs5H$sGGYvI`P93u4Kds6>7EfgK&w(TrEa;TOz4I%NWHndSh%&E(qu`e_p4g+YJGmKc}t50$1Aincqyf({p8Il*zT!c+cM6LVyM z{wZYd(w3>9ih&G|WGAMtdB&HL^2*u}W%SJKsmx$gb4L4<#0Gbv`kZZ(g~|=j=Uq;Vy8lhX$k@ya2jR%{09^# zDg5n})`p1zNb7Fxs;P|r6~u|kHG=3gsYp<)&qB!_?23!cG`NkPjmY$`COtSaOwVBD zWSuf6Nhn!hl$kjL!J$*1Ywq#2fJJLV_&EsE*H+-$Yk;Ycg{>$jA!ofjRGtOO`e8GY zA?^7&b-R85AcMBk;j3ZwHj-q_gGf^fzX5D%IF_dW)>qqJS(HN=ylCQ%IPxDtTyyyv z<8jmPE%aM%F76Y@`+?=Wbpu}J9MQ8$_2QUEy1lABtfqN@e44K1EDl#*U+JbqyG>mSyc@OEUd|>Ot9xc6aD4+UMN68KO`|fHOf&QV&iOy zS(;fv;l5^FH70+{iC}>UNXDY@8`w*lqIHhUf^SGN<~GMjp` zM4cO*DQ{;-gWk0rp?Ny~-f}FLD%{KDZ(YQzb?-q9zx`o(?vv}5m}f3tKc@>_O;vbk za_B0LsWYL@E8fj};w}N4wesbutd?~7`;@-wTp%VAOy3NPM(EtjrxOP2OoPH*fOUTk1ftclX`WAPt44Ho;0guC!L-^(iLi@51Errh19Y!6MOQNv{kzQhqaR6rp{p}FFF|E6 zN)C<6DXQ`3Gl)n2ac;4cplfLQa%&oV?h6fc0|C{_@GFUQG5oXtBwNcB4iN0EUgL8p z;`WdY<@fV90v1BPm_JWukistw6;gXdiqGjvQx)5< zCch68csx(|G0%@UzTkvG)v9k*5sl6xxuc^*E9xipy+yrgUs)Z+anlPHoKr;poP^~j z7^iU-kUmSzrNEnTiGDB=s++$Yb?t3WsWHA;V9CT{N(cb~#_XC`)Ej~MsSe3iC8E`+ zxPrs!SQc;)m9}}uZb$>ANuf79(bikTHsCbXo$bktOv<$(Z}Kq*vS&HyBj(eY81!V0 z?8M|thz7Yn>Oy4Di0jA|da%-fcCiRU{G7>mb*t_i_bz5ZcC8#0tGosz!QxG1jBMO} z`faLV!wOg>DQd1J{SX#a>}&SRkA%dm>6^1>D27r^nOx^DLE?ef%1FTe@G34RVx3f% zncU78(r13EbS>S?AXO|W!cB;a8oiLezDTBzCtvb=5n57_N3ge3NiLT_M)A>UgnDIo z(?$<+lged=ctBrzjG&HHmFQ(HIVfLit=a3C%ys2zU=C8eu?YQwzG~fj35OxmK_H{`xJRwncRL&>bA5>a4XIc6MN;))lb|A!77p@LEnBPXNHIz#CIZ&>Vq$Q;aXe z($vhjTnAwtd%)kLPVF{>hEiVZ%$@PKX!+|YfB)eVRv1XZ14k*=k_3hD@k{^QbN*gZ z&$1k`;<^H-!Ef1OJjk0|`J_m%4DE2e>ykm1@Z z|3qQ!8EiLk(O6Qf4PFG z^f+ZGces1hD8)coWnJqGd=u5kLSV>i#36%aGx|Oks#SCbJlVNmOlh%BOo`nju9XI5 zw+n{<@9L_P^VsD40%neq-x+;XC{}7a?Kg{x9 z5-S?09Jsnfu8ioLc=Qt9!I2B#_Q3n50nmhU$2clEPF!rmHGdT;$0ht=SStFfBxsNv zCibf76Y$^&@Efsi-0b%Wz{Nwo2jNH%wbw1O-3yVLKmc@#D+xuKSF&gF;lKERp=BP> zg35OgI1PmR%E11|Ef~L>yrh_qxc&tCn{|l+OM!?7+|%0_w?D}#^4APFt~*E?>s%mB z@1||bC(iY2A^IH68f-e>nnXLP!3{PaJ@-et6$_br)zt}LFrI7Yu58i@sl>&!g>pRV z!$=DnxkOeILzRGSk!%g>3fVUxHK#q2k-Z>GJ}1Wq8FjQu9a;E&#eHamHxeFXtC)!TOTju=o)!Rx{yb4}#-!ePgN8G(OMqZ0SFB+A>G z&ne|l(vSkuy@|x3zo%fJ)fO+VCGCxVEQuZ}ksvpJY-(o5E%&J*YLeB@%az+;FK+E5 zE~tYaEfb3~ruTA`214es#l*atE8w1ZT+T#O$IN_PA@66|1f5anB0B!qVEp-U|J`Cy zP*n|9GOzP$%)_xBD4RN^<;UldTX8KzTCYivvey|vdD1p3B?h+C&F>8*b9=MlNPZt3 zJ;=1H!vot4sbtOvE%zx4NC%yhinlLwql4>(IYSv^?}UUzFpUf^$4PQE2mZhu1d{We9zgjS1Z3e{5i zsajo?q%xitm5u~y{A+0%rZvGqRQVOGt>~!5kL#Owp3^{3R%*ozk)59xkxrEZp@UtD z``lT0T!c@WDBgS8+`xJhcCuz%i#g#_l)01p@a1(F=$XOkJno~Z86H_sdXI-lAU>rDpSU6R}CZK_y3?9z=(dx4HoQ<)jR8lXiNbVNn_LyLBLSDee=)-^#*D zb3}4#-oWwwHR-{Me*}+Rf9*n1oee_VU$=b?#u9$^J3?_|ET9n`uXEsu*m@f*1W%z1 zk3oUpK4^4lw&<%;5JTjn0qe^k3LkKVhIaQ<{F++VH@$cxE6B{gT_Ya>Z?&L7^!`A8 z(EFENvtjPOehqNA(MLO;NeU<61;1 zL>u{AY9Xcuf#ME@T&^5TrA0FVGf1uX5=(};_y?bQNi$v4iny%o{pdjykC&s*i+=`y zS+0wjGXtSFzmQ2dbvGPzpRou~6i@4$)^qXRoMkKd(>d+5@7M@tYdpqQBdjI75`~Y& z8AnOtk28JYd~@X*if56|P(S_5H8^leX)f;R1grhYqwGTA-Or$Q&Mq|eygQO{{(PX^ ziOaT9R3}Mq&Vg0ATw!`S@q}!t)J|It>mlk68rx(LzW7)yvFfJ!&v8Igj z%D_(h+Iw_9TFhs6{ZBrkdf zt`^U~C87@Fnog~#>nL?2B>}nLJ=w`ULIZunJ${O6KmVsWre1fkvFU!RMi9hAlptY5 z6M<1fz8dg~OTjtx=> zZhVQL-HGaHT`fB|)0~`CURw$Gd`_4mXjzQLG!#tO{Oelx^Ffzj_Gm3~OzCm?{FrFh zvzxAnBnWXs^Z~vhKO)g!vLprj)@rXQrQOCER zc#`b<#KB%B4LOj6i9kc`yu@ZC%&bsuT4uO&;>S^ToZ*MBhjdLdawE7N1Q5A|udGf~ z($RCZn*{Etsv8Rl+Q9H&^l-nZP*?bZ%m!zE7m?*b)WYNg(+a!ojL-X`wT-M5K<0NetdDgro z*WLtmHse0GCw7m?31r2@M;<5lr5T5{JWU#S@|ThB=U(M9Ha6|xniB`dbjZ4_?CuoX ztieEENw5j=rl&z73Z2N`G@_)Tb}S8vJG&c{#*FFI?5@seq>A5e*5jxl3De26;axnA z6<|75e17*XQ}=(PI{JmL0q>=gNM)=5+FdX5czcu(tXe$KrWjg+PvBXmy}B=>TBr`l@|0nCEbO;iYhe=RCmMWD?F}%gilj5V+Zt86_v84Z zxcF>5wkDa0K><19VN$T+&&>q78`sZ@SGx8Z3bxU08mE4D`^0zzN zAm3O@se@%N3)Y#T{GwZ|y#`B&8L#2=4q4GeuO_(jk9a(twr#M$Zjp6u14h3V+?mXB zhla-WRUb|Z6x-pVcv?SF2n~fEvkF2KEUa>cyG(KJgO?&BI0G#ob!ZC452X+A;rP`u zGCMeKfQIx^`wy{{G|9n4N8gd*u2NEIBq?HY1o-i=r#*Pt0La{G+I>kNOJTUl7yBPk`Zt4X_WFsq~5` z9z8HXZ8K0w^R}+F_l>6keN?Tw7t-h!7Zi6;jTt~wRfDKuy7AyO?$lxT)*fm$F81qe zA6+yK>mU19HZwO#?^igc!F8D$C|&=tnP>jjRIiBfy@-B-{+Y4rG;yWB)=&qPu#x`; zHK11=W7=e)ZcrB{8|x8GyS@xM4JJTlawHp=9~JZLKGx#hNK8RVQ+eTne>E_nBHRT6 z^qCWT3^d{Cv-{n0`@~05hjdtW-e=lz*6%xw&+=Ae7h}ccq88@{-qxG`>F<+f4@Pli zYqKF0J@{?)KWB>b{ip9zq*bAb-?6p{g=^iw8MA5U@ksVWpx$~|Owd0TFKLR16XW=i z_-2|CChp?^>kET~Kjjx7z!uRW!Q&y{A0qU5ym!0Jkyxj#!LN*60&PtL$^*fHdrJH> z)PoXh6n0i<8t_37Eb;k-$+#d59On_Fn&%P{XUPc(kWvn1i-xnTOUo7?pa#0Fk0Q!n z6Kh5Z-C1B6;3WIrNP(dlU&NIrmU{Gq+QKUb{CYWmMNp|;v^+-mKf#OK-K6oT107W7 zcaCh**6>#sWWGQKO6cU2H^v5>lJZN6%#aKjvv>0wsm*_5I)QIG`OEH_85af&55&yu zV%|t09Gw287r%pawk7W|bXz5N;?M+XA}& z)}2YTg?h2OZ40(GbXK`rGljpqYZPE(^FRZ2EFky4qp_|A+Z|zdLY+{WEQ*6dY?4NxFa=iut z|9@VZT*7H{#foG7JA?o0+DNJW57WD2rL4C2`~YHQk&aMe>#a z^Y7p}E{7*#XkuoXdC%?NAhMnPzvcq?kC!SELL9(j__)iu*aZFQzpJ-8PnN2|JUJIJ zCt^|`Il?MFG@K9@qW#pWEx00o)|s;C3+o<9 zF%0->TUe4Q--La|dZ`+SQwaanC1|5C$Wz#09*@fwKOXn)VBv7DC0+(}MtUiO0lMxczJ`L}&0v!OhHDvzc zA_9rWSm7$-Qm!ZDe0-r~eCs6K_2nke1C4^!zpV3=&PU^%EbL}QvL;j0AGu33K#}^OP9IUsnjk;n_jCa;%XWM>Jy~?g`sROLu9rZ zIrG;GQ%KZ&v_N=0HoJ5q7QeT7xCv_EhEKlc zT>lR}+HleI$Jz?k+Sj2{7_PpvE6hY$TMT}lhpZ%jv|`t0E+9ObXc+jcdBZ;;@+TW+ zUPjcBk`$-O4HFu+#+H1p5MMtNG&Ls99^FQLqKJWL!j!9gK^jGU6lRkIdr$l%L3Qw2 zq$r9S45jU@!uP!%M2|6|4Dii4c)ts| zRT#49yiuUQw#<{gXz;`FwqU7%E1gHcf78 zYk+gAKwFfH$i>GAwq7R`X_6C$u7dc(s=0f)Xt2RObA&F3k@yH!awk=ZNntmWvOALdorwpw=G#TEGWps zx!}LEu~+X!vav)~y*3!}K}LJjZg&V$*G-`(E=PSMMosQki1RNAfvKZfasC|XPvwp- zH3q?cRE}`B?CnhiK=m>4ucLa=Ydxp78N@!1%+C7C#4Jw36;7Qrzw3+{R=%?|D0%?l z(F!SK=tDPcEY*;dd7drb1=G<(MPx>o1m0rf5jdGNISr6P#FN_(>c3WIcB|+(_2)E- zM^}~4pHHoYZO7`K2nO=C{b*TtfmkCR{~!d{Kfr734uOGCmVoj#`qjHxEkwB)?O6!4 zcT<5GAGES~u~Wj@?evN^)%g5U#VwQEQa6`N5qv=K@vyYGc+ohz>f%ckE@3W6oj9bnzr2@|I$(`AGVI5yyWGYQ$%!0JwdE>0}CVHkMBf z?>j4FAwN#4R@b2TxXyP4`7}?zq`iT&$;{fkK1~Wg`ix%Ujh76(fr3ql zFHQO)wC>8Q3%kCp#tc$(^x7p?-bc!Cp{YtFH zYq8cOs8ZIYR+&c{g6c)_YFtNV1uOFTg|%MozcxgeHfv;v<7?!pqoStXQqlAFI5D^K zdbtfGE!n*hkSUB(aM8JOBV{L#q`2>`@GIP|#d7NY0QJm-4=*vZQ2KL~lQ|YE$06t~ zqC;QRG>hO$gF*8*N1OUZV#}S_jFZQ@S@py&Yv~gvsquX&DHA%-**Hd7BHk#Q3mRis zCfqF(qyq8OsiGVbLVInsI`Zk91wq3=`ff=r9@tz`@@BA0URD<-Bl^|Z|AKsTpf};F z9P#2u`${jR!ku`xs%&S8N`40y^!rr8XtzBZ5UA z1tFIzjs6qRIJ#1YwXIu*`6K!GgLko=3ZjI=#5O~EyiVF;4WRr zo*DUhBOr(@R!6~y&2)E3hgXuTX?XorlO?h4!C;5f)XGhcZQ1OI{79%G*?A zesfPDmUaq}JvsI#(?pAum~PU;3M)$<^TGbn`YB|!-9~bnaD|%rM~R3q*FKHkrzClR zTW=pbL2}jlQ|ep6-ZprV+hQU{*Rs}*HgaVaTKZEycF*AfV>LVr=VRz2@YncA-D4$m zrs;9+iu+z%Z~I%Rr>+x9%otoOS!A@3JtDM@>*FE~+WSZ&kj$L4k2W2k10QVygWoMp zgney>G2$_Y#6(n{AzCRgd6S{HIu^6#ioCknW{@_Jya>TBB`D)ro7&wVjWLqM%hsCJ z_Rqxm>ha+b=K1BuHoBO!n?V_y#?HaERG2#cR<=tSb8jG-wCG^1#rC940CjUGC}MJ0 z8hV`>^UVUrCaJyq&4m^E_CBA>znSuIkJ#r8jR|OVWus|~{Ea+p=3I)|Vtkp+U`V)c z-f=JOC=$%#s6ayY(1$McYMm@*ofsQI_ySvky7)o&ccRy+yH30__y^Wcap%5|VP675 z$LguEK21l*-Mz4<)}r>W_zX5S@LB`sG87Qk}cE?qUKSEtUPy#sr)*g!`GuHELW zpl13AN={HYhBe!=?<1cH_MMzEAb*Jm0Y$C8S^f-fD+ESEnR?Mg= zV_Cq21+(t6_Df0a7T7n&^Tv~Cof;FWv@VIs2{aj`nId>-W5+NRMqD%}%yBuNX+NXu zi$?HIGFkMX9|?4Sgw)KQhJSS{uo+@x7sx8hy_R$ikdep$z~6rs)|EFrL+Loa-^*&F z)~egJFwn5iy@asI1@#*OS#}26u^r1X5nF-d4aj`?iB#=}W^+)L-@aG8IqJhXTnrBq zv3?IphX}knib>~E{fCZEG|Pu`9;vQCqJGC3!0g7_hodr9uVhT*Nkx(IG;%O%sUumG zi4K2OeZbzcXVds>myo@ZT9trJjFwX=Qi6g)%}}_1o}9R0(oLdDIpTwwW6R$ja+cW0 z)0OJ5s0U3=a1{qmf=1*s#;m)t3+YDdeDTk-KE5s`Km3NQRlL``Y6YFevM?v)sQ+Zm z_9du_(An4*Tj9S|$nbNZ&juJl$DV7J`a<%~%=eGKoUek~^cxuShhc=&K^=)xnnbOx zTXX5`X z_xf*+Kls8BW^hM~F-ulm^%?}MWa(VO)7Tu4yJ*Lc9fiiU`3&lLN z64xIH59xrS=m;j;h2+4w`b|j8yBS-&NYsTPVS&0c3a5CIYf;((vI!y%Hq+q(K#E4!gn&+iU)xE1LZ!V2B+NDpwC( zoT=Qelud^e0I+!D#GCRXfV9U>fMQX4$;3ZCAk zb!QW#x2k%>P?Gl*Gb@(X+X8Cw-MWTaZ||ek)&c#0jXjx*g`<4Dc1_@ZVaX?|>9O>^ zgipO4Ac`ZtRERZi9)$E42}rGlrfOw^_}8q^m#O)|H`vj`bPRYAz&&f8lb=J?4vqx5 z3||~!D=JIV*}UD9&DRbDN^uCXY%I@dPxGlJ6CB})5#Zwz;Z+2i35eEWghy@GlnBh| zv|;~_>l?LOmY|S+fCew9Z>7Ty`b-TAu`bu1MH0GJFJ0B(MhcJL~anfF5 z?pW&~@=p2pF_FRYAPpF5d;(=k)B0Uf#d?M)v%Jc6Op74>gUHQR6_DQZuw<%P}Vi{SNp?2j_Xieoen zM~T}xLB$_#QiVo9Aim}bbnHzwQq}S&?q6m7!PQZa)3oky>@3~$h^-oR2 zE>RT*CvK{XXzM`;L<-**Y3DnNF(J8|0tTcjJ?XY9J=J#LQn&Yler`+^@w2Q+K%XSv!v!Ni^E&}rVC@8XSDqyoODXWMK0JIZljo!p2p zWDIsGQ3WL)O+TCp*L3KZ`{TzuX&b%eG#&ACCgK=9$Y6*5!nEVd1$$u=7lMHs|2Um=p2;P? zyuidn&LpGSQef6uUycwsH5;)_Ic)xQE<2|CrdpQ_rU?mb8J6!=7d6TUWxEBz*$JUK z0MdFbH*v*rrIdml>=2+Oo-10<-3}-o_XFroDhxeyT`kIQ?i@X>rCRL#b|mg#uoo67 zde=5~4q_1l9Yt8wpOAoByp+?|KRpEBDn70m+|iDJr;85?>5@VJq~y@LrIB$;#^}H( z5(2q=XFCf{WJx3u^AP4LJ@+_C7tps&(if6Gl8GB;OIDq^PLJU|w zlzMP;y8G54OZEVX6(&6XwmCfx)vi#V*Cm#0EoRv`<(=Ntj%`QwpWU)+$9mF9;1(NU z-#5YGg)hOZwx_3U*DHlNI{~b{&?o0QbV4Y8%(}iYM(LL%$FwnP=qg^+lXX ztrxMuswIEXnAnI{)OJv-Fh;aJ({^l4*97Op(cr^4f;po-6s)~1tz#4qQYb*D7Q%5E zrLjcK1Xlxh#x*Lvq)U~649%>#UUdp%E+04PBz6A@2@B4ylbH`uJCbyKh%!O2iUGd~ zl|o0v+u3f*K#fDiWdJRJEWC4g=$>#q)Q`?%5c`NO$W)IN?8_;3b}(nu3~gTADH?(- zx`-lp(yeOoi$fqQ2ilf!nR6wLGo?X7VwMx*!!cy(Q*GIRiU33h6{vAi+*q3qx z0`}Z~1~$K!Vjjk2Y3<|A6V`S2GTL{i6DCJk$7Ygujlm%r0VWO zXc0gA_YH>W>slqlvuDuG)gKyb{y9K;vpvGGrtT7nKKvK*`pESKpz*=D#el=*-E$>Xc41sjt@g<6VcsF)DX$><(k~&6%-)3m$5Lh=U zPsA`QRdrsc$v!B~Z0v@S`xDSCYFzThMvCmUBR?*W_k9>_a;nW+&)h69XuU&V=bPQD zb~wfXoeSjkrg7s|=b0<;AT%;e9XTLGarLNK>|lM}s#78$^?|T@{i}S_^k|+e;vtrr zrX-Y?)Ox$7qk^2K^W~irAwcgg733Q=LnX{Xyh~}74(2-yU(m@%QUfVnHF8U`n_(#M z0($WmZ%KyHv@fRs{3w{)7zUL=qNzd*?85UCX&@f$SCAwdV)xP9N8UOW?dqFe_eJo!$cTtAv9z4xg-XRtu}R z=4}Q9I~|_#F`cQ79Uns!#70V_m_%I!@K46$jIQ z*ID3IC1h2uUGmsr!$Aav5MV8}1mMz-2x2UGk3u^N_~BO!wCtjW`C8Snwcy>u+DqR+ z%B5%`dazWS<@Ha3g!RpR(r^4(EQK39XMkV{W+Kcl_Yiz;iqFMf7r-Nu?L2KWmJSi7*kYP^(!N z7XSG4u+IF@vPwH6vc%B=!Ps_P>-d&VncK5``_pcM71}QE_fM0TZ8?SZ+EV*SUf3Raj%ejjgTjar?zOmZ*#zGDmbrt~Z%xZ@d}Omh9p$f+d_WSiW>&Y1LT zem(A9say{7n3ebfo6lGJzM}9yzu7W4w7ZW9AH6Y~l1g|Z4g8>x2R-K|?INT@qv|;j ze~dhD$ehtn2tr>v6A`+^< z*xI0-c6<~4@yQtY@0lqZ1Orb%YyCqcB~kE%J*!D#c0T6_qKeva6U93E4mKF1+I&G6 z1(-F%W#7+@N(IWkP22OxWVO4qrpuNSCGV<+IBscrSb9gwcT3kEMRh}AS8x+_$B?|^ zmw8LtWiew&rk2!B%)@93msI4B8oYt<{i@_)S`A?mZS*D-08cG& zKnWbMC*P=k9`*T{tk*Nbpr^jZ512ax))BrgWY4#U%P}58Vs@JO*iz@D_+cxor4J-@ z%Hwlf?@tfb=UFIz1UHfO+JL9J+lI+1oah%X>S6#3#H%;QahUv)iG7TO&X&F_3MBge zkGIe{4~V0LUhUV#c)d>|d^k)O?+lIOMl%0*DURDWp?eusMhRnEK;Cj4_GDEIPY*Za zIk)9b5@VJvmU!tbn4!{2Cz@3U=)_i(Gm2U%c#Avx0Giz9E~y}PHscQ!p=A;9arx=H zfoS~*`ylW>B#%2{&2`VJU+gzA%Tuo}bL(=k>q)kukA{$m2tjGbvl4pK=&tfDJRb_kHn_~8=LIS6jCQC{?h$k~yvz;bEbpMlE z%?QByOGP^VjxW?)0_HcVdBxYGRdHBL?YKZoM}&M8D-%0gnIl%?Bbwkm-8j_{c_2Ne z8mJD+CF3rHj&QF?@u1K0av6Q_ubRD{>0<>MF^JBMD^+Plq!~IBCd0bA-pPH9oV%Z^ zNFB>EQH~G&1eTB@0AY-)ro!fHb;dzAvzTCTt#k#@I$)uwwWq`6nj$y)eoT zFo-b7m*`DMgXTqouay~I2RQA?se4(P?}@jH974A>!OPB%Q%RnrLrCgLiV6o}vNu}K=i5Gg#z|1*j&GGl`QZkaqB)!+2l%wo}|~DvLt2>K0gAN_=A)e zH%Rn}#~T;ZRX1_T5*3oVc9q`WMF62yrI9(mfBg zQ4qU>YC3bN&s`&Q`TMlrsQVV!ZQ8+$^*m{w6+~%kua~6>Z>#cZ;od=rXpmPfnb&7@ zZF1IxDM6-?g@fE^_CG)c)9Rt52~agw?Nw(^L)zwT9d_od6+O~+O;b5CS)XD*<+Ev+ z4>B8DMI*T2lm`xtM0B7Z9MO+v0e8J|_Pl?ay7hkJEfh1cwHrWSff+2>5q7EeE5F!& zZEZTt6sN4Egte|OJ+4Kzh{OLqg(+uB-3Zh-Dn{-$O`D9+EU~UqYgWpWQ3!RB;2C9s zuN}5x3DUC7i4PT(QU%9!qd-jE$wq7u`A1Sl-ExZPGu~c4k6LrI+})41=9~!E;#2rf z2Q~Z^4Cd_?>5ps6nyg`#V?G(mpIgvi-&qgIr{(uumh zfk@OEEi!wF)@J0QbVi{kXBvsfxJ~Ev7$gNuvq>Q!9(}zx=$sMxZ(kEe>8MXveW4%f zuO-D}bbqmYdQy-mybwVM`_6#j?50KMwJYDErf`H!u^%aGa`Jx6b6$*(u+q*9Kb}dd zMxSFHP7&mqp29CoKNZ^rf~IA?GP1lJ263P7Hs|tkcBIS3$F+-sZUGWma%C$I;PpKa zsxP571Lm@&qT?9mV|9*3uFnLCFHFySX1aCXwPnl~;D*P?A_PQ9SPv8wy&9o*7S(Q* z(dtuKF}9deY2HUMJD!PfVUOA$*|k|b2-z0tvx^iS_jI}nF?E!75?@*{CV$}9V{~nV zYJ^eK{I1mgl~)DM7Lal@a*xhfb?K-ereNUZa#roG-Y56(eLBKp(-9jECou?h+l@}L zMw%Mr!SJ-pjLb%6DS=oDwUsN6bj#5r!mh$ic@CrlI-NcmlHKY0I(eV?VPz9$ z^N5=@4ROjp{RS~$EhZ{F_|UAO3acQhUS_rfjn1yVpk{CyfXI`8yvSUC5~nKt;41zYqz$jMiiq}fIyVePe@eun;H>|dH> zpkEv8Fl`T92HmTU^)pcJ4rxw%6i5Ju(`4x<5L|U7NYPx}aYZIHwR^U~@gv#eamAo2 zJn?^bJAePgRx7p7q+S<6%p5?u(7LLGP$UP>Yf5w4{`5FwE>@?slu1WK#zC=Ynfe1q zlG*3wRZ}q`&P$1)vF2Y>T=9wNABE!B6*)a0$?rnYjr3;; zl6+s@Vsgff&5#7&6G0lkci8z$cltyna`nkx_-{*yXbYEEs$R@FV&q@wAUbSGG4n>&p@LJBYnF$xwiupnADlaJ-{&2qNu`(`El zild!g@WjNM4)qrYsmxj14<{3%RRg`{5Jdv*VxQvW@*OiCFq-FW;0ZBZLLoNm#M?K) z3{#wf?_Q^H^tZTgqm<2u3s@i`Xc}-)VlQcn-lAmNP+iHsf`D34xT*X@v%Vp1q2HP% zM$ZmCQ?bjo=i%Jhd753|E*dOhzJDJXHx5XJK0ni4PFj+{bG0_a1~FXdBpa$X4I9W1 zeej+5hwc+S*s<;uF5&J0d>Vds@DEs6lH0AD=E zFPZz-RgLIu=tR$NGyDUDuJr%fJBRj4xG2oVb}Gh=E4Gb_ZQHhO+qP}nNhPV+X2o`L zyZU+h2XyzOd+?p%9-iS|Yrk(IMlL0o9ZZIU334so(h^$Z_`02TYwg|R=p0HZJBG@y z6@{Zmf@czyrQBJ5@<;OePi~eZWa0}WGRFtEh`1tnea{N-FOV{MglCsUtCN5qpuYUM z1Y&~*j=!LCE6(ersWO^bK1fTQXCXJ{u5uNI2&DjupQFngnfuhW7sPgaa>cbD+HSGJ z+YOOadJ(5G6?|jZrz>BQIciIA6L~0fvY?4(u4R%EXS>AQ&?V6I*v`99$rypqm3Xbt z6b=|FDESX~neaTYm7U6ZMF}KFUk*@33ccyOE)PAkdR!%Ff87=L0=Qn|^crwu!+IPX zTu~zDwXNv8mch-Vun-6uvv8F!Ma*0CDW+f-x>-V{O)TF)iG-0yz&u~}R?Lp^&$w)| zax|r*?BKd<%(rBE%{dxNiQ_{c@)SJcG<{SZh<83BkFdVbyG4wCfXnW`mHLi(m#`t; zFD55Q!$$}}IUe|kS-7o)6|b*;Q2+uLNeh3)(8)Tlt<8L|Vl0-4qS*C6fiQ(j7g>{F zCV{`s0Qi-TMr}URZb-i_YLPMK@5L#y5s<#C4svV(eRnOCa#yVIb8zcM$1=$7oS&E( z(8lMJ+%vK4Fh3Ylp=%iGzJmh6K1KCzID_T_UCs*x5H>#&HbFR*o9v#4q|MZpZw@Wz<0;w-^wenv0XCNvO9mc@Sc3#FbDahMvY|DTR+oZ ze+2@2amBY8{jbC7YP0wZ9k6eEW7#+Iw@=gLOS#-b6ubk7A7Za3WV!lLHfSip6a1<7 zV4;8Utnp_)_IOYXi!C}de8jL!xLEO~#u+ruDp=f^r+@RqUO+@MftE|DCx7!kI*5< zU0=~%2wMa;S;r^4(x7YAwd;9*%+fZAaKgip!}_O1mres#zG1>7xa5~zh`S1fUSF`G zwtj&1M6lh+p2GEaaDcdCcNbrXnEH%!uu#Bh$EZF3nutXrN^z+b#Nn=o zW{gw^tZQiQoSLiT_W4gp#C~7=k$n zh`Ok-f|>#+QP;nPGo#u6Opr1FB?9Gw1p&b)!NerNL?eO$L3lg$U4KSijJJJbdSkf1 zndaN~cD@1D_4oUX0$Ez)dhfGN^k4iB{5|@2K6`=gz~ax{OY>QQ<=v;=QGadVzJK5g zDKL9w{N?78uuPx=*!WfRq5_Qn7{2j;H-F#V@DI{I0iM186&Um?eRJ}A{1gZ3082kg zudIPUiD!B}&ySmr8*rb&PrR4i&6|7w5C4_VYT)3GNTdJVJNy^;z5Ip1gx|h@=-1pw z;+Md;0OGe6u=3mdCHnK_vVKkfxPQU__qPr(;cM~T>r?*|c**naw)-OR4&3}e^v-?0 zdDY(+@aP}?F>0``|)G{yj9X80to*Q?bK{!+h5lD^=npa6CKK>r< z+)TDwgn@KYEm6DqAJ(T){8J0p8Ttpq4_+!<_ezYpTgslh-O_3mPFJL%v;vEN<}vVu z1yJ2?=`=eHI;RiDko!mldBH*H>+xW%TbcL0MJ1ED6E}U_MJi8>{6*?orqfljmV?aq z`qeMs&ikltXg;KMC-xo4BYHSB%xl#n?6lyXr|GKZH<@&~>p&QgEiO1oHpQ;LoiKnz zny+MLx=oJ1a-gOR=(`b6WwK^xrz?6IYPU*p7cN}NKf^dRiy~-u^!^>>2QdDAgD%aA zez#lPdlct8Fvgk-!&BUr3WGy_Cb(lNHN06{3~b0&eI=SF5jh=qcjk6b>XK#W&8*3~ zI&LCh0*U6=#7z2rnKhQK-kZCuhxj(pZS$yn2h7v^im$&P+IM)v{X9SB?$OhFZ~jcY zGd6F8yFolKU$CV}r6y8N!Sk0nT_&WhD8Th~bj`jKg(oLB{Tu=<= z*gPv|G;1XGX(K_Mo_zryC{iAdCS;4)G||oqM*V*Zdl!YB>6u@k8UD`k=Du{Th9s^$ z;sbqOaDB7F=Ieu2D&pZ20L@CWt5s3WsCC=d4g5BhHB`K@FT0)X7gk_|lhsq6{;d%u zQ4W)zwsF@rL7V-q@3&OUqhb;xw3<#6lAL%L>y)4N~W&5zNaA(tKhBtpQ3lrrS_k3M0; zY$dU>9dGz@oe|8wU}S5>0;7UMI>M`pyLM#Sq0fpq#=zM09;8!vpJ#eV95xtI+GDdy zfb@HaWwCDNL^N!oivxh_&f>GKz~P{{q>9K^1*o*5&gbE z$X`C0TP^z9%(;k#L9O(MEK+FbH5n0b8uKQY-};rrHf%%PzJZnAH%k#8e(IKN@6_!0 z9eCW9c2WESorc!?S=l9T#%jj2Uu`_d1McFb>}wN-y54Y0JbjLtG|cjtfd8wZ9|i7j z23^=+Ys>k!!2G#f-BYsux7tvYQsxUpJ1ad$O5INq3+3i&w4-zGbi|_Kk2ZB2KFsk1soqPeZD*aFSL8c`bm0$-&BAOeQ#)MddS}5gQ$zNfF_w~ z96I7#X5l=xhU-&aj)Qzs6J76)QkR3IN1kV2BqShO_FBeSw_#wj=zQ@ZSMlGcC;5el zxU;4dk=!jD$XKE~_rvzl2n7{KZ=k!2`HeN^qE*EG?RYsSBvT5sPk>bU-Y6SC z#VGpl@hSwKczTEb7x6kos^~!ueG+PuKb6+e-`OahbrSUFsrkW$y-z;H+G;j(kDVb$!+@4Ecy~X8SE7h zg%pYF*TZ~bc5ErB7LjeP$}1SWsAxA|@iRt#dvbum4nA*;?zV(7bNN0VEyFoK7YZ)M zgU@&av4gE-4W@6SgQlk=?_POe)3?I>u*FBK9Prz&Bb5?VofZ+E>BT=EI#2FU6;$v+<~}9X}F@K zN`wrnFcYgR&t~QQjUNk6bO&;~q&G$YkMgW#%Y@5sg$(LxAY39S0&#i1H2^qXbE_5E z$F84hA|b3(Yc({*vvX@i@=VxE!mh;q;Bc@lQ6Ax0tnp$`wlBcc>pJ&voDiX#`z8>{ zgc;9P;BMw`xy>4nC7tV(vMhmAk$U#~_rv7wJI3|>(7W6F!` zJK^6URvjSB{Xxrqwv>YZW9iw?KyoV!J%2qv^JbPFx^JT7KSAR^LE}F`<3B;;|B;|! zaIkrMc4haUpz)ue@t>gapP=!7BWSQvTXy~@X#AfE8l}h*S6E(X?Nuy4oi_Cy=nUr9 z@WQ{0Iw6aVqvir@mHFAC6t~%)NxRY;tz6ZDgPE?4S;Gj9jw%X2y48~CZI#@A zJModPVub?26!!zZ>tue&DuGDr_ zfLZBmI7thFwez#C!U(ZA?8R`pu$Wq?B=ujT{v zxF$k|Fi5y|P}WtyW&hbKc~nWpWL%X@xT`~e03D)5?rI%~bUVl=;|WxEa!nC$}vjBFgp(ry-nGDKUTYbJL5`OhGdKyv?N1=9bv z3WdBq>sX36vg_KH8tZv-dHwtcel5WsjZ0V!P#Tb!kGj{rhrvMNK_@(E^8`7$zs_k<$avDu|p>=VXcPQRdhJBk0%NOUH+ySfz z@XaN5DdIw}lUczqX->l05Tz-;59^U#uFvh`Un*_Hfqrgv%{}_+@(GO261TqvEOr`& zpE8LA*Sq^`F4B(k=gX&Zh&f?44lxT}Yfqx$=miC!=mb>tuqqXFbW%_E$o;gjb6%c$pK?A!YE00UPAW*aj`s zTFUw0LeKKhk^_}%*jYy|(5N1CisqzejrU%`%p8yw=FcXFY3K;q7LB)^u57Hmf;#A=oXcQ$Z%_yD z`<9{jVfe#b=M5%MBnY7*;F>ob%FI_dBKee6kY9Zx&@LorF{|E<)#$0(G_4^Mt+bv;eGe(8LB2ya6&8UZqpOCmg%y2# z85jK_vY9TYs}(DE``7o=>`KnyECxvSWdtm(CtKfi>u2Nnq># z=Qetz9SF0f)APaXM!ZFFR{n-JH*=gDu_@zX?MubQCIyyN} zqR#%wI4d_1fDO51)EBVXUd!==BFsI=_sKjSOg|cY0ETY#L=S7SU6x-(5N%vZ@&`R=O zZ$^AaG&Y3=&5BHIQ|N@0(h=BXV>YLbhqG)*DF!YkElu|_6`fo>CCZf(31-qpa=H;M zPQSZ9R=F3ZdgW0u^`S$rbAjU_((%;`pX4Y25vY^2=(i%Ld39@)(@rM>z{DxoG~{F)+2T=Go;T0t z3&(B~*0TJEYv5DHp)5x{Qj|;%ZNv$3l-MSmme+XZzr;vBc}bC?Ffr!vle+|_M;&+i z{vyx{P(9S&O|`35i}60OjLd(0$O@h2&|ppd=6&XN2GWb%4fj7KyFwM9f9(_$^W~US zOSzAz@trmC(^smA$O@KDiV8Qp!y=dRZ5ien%ZUbcBXqW~;jf}KApiCCkg>No+X>nB zd|(#&pvaOYc|vRER*SNdyqQC-4JuXJjDI^b+?nCXOu!0VV+g*a)+BU+b@(Erp@b!J zXc@Ip!1-9ng~j70LYJ${z#RSqcEsZnPbSw1wdSvUcoSVwuJ}h<06MZ`z#Ax4p^fre z&!Gi+5CkxM^jjC>snPmhDz>y3eIOH;#t*Xb7)Zs;R=wrQ6r#nyskgqa1!e_4;D2d% z#30D5x82=YO!x5TezT|#bB*HN7QaB8pD|)itglFi`KIeeZvBOZtXHNnrKY>A>`==> z+D^oCJ&H1k9wxkYJrtq(lBM}NazkWa*&u?Sme6rnlVmr99Uj!h^ZAUY_SSu^nMaDg zoogy{LMl|wH#|!*IQsYX;UpTT!qYL@s9N`frn074;2dBs)lOKlmv5v`){D;R)j3

Kf6+jsR)c%~a826P!X7ugu?H)+GF1T;8Iww;|DI*k+~suR`149a}{^&r%kpTsoF zl1ur=ZAs>GeprMXjG))wYw=l{i71?_%X3du?ZpA-s>IIOWJJjR62U?s8hH+mIVqkf zFtR-;;t}ri^J<)WVW1Qs6j5wz)T8{bm$@2&N$Y1W%|yy4j+PZuiFS0rW(@cmA`i!{ z0Nu$-R|%T{S{g$wHR&~ch(ttdIHlkBuchgK!xSa9UBNOxiDRy9WkYKeQi?60KF|huxy?jrLyc+F5CCKGNku9f zijm*T3}!+dc<-tHT%H_|~vi|EqX2>+xa>z4OpE(TE&j;>t5c>#AFffaSv5?uxXFs*E-@1eM#!M`ryaJO}W_ zveKU*K6%wcb?Pz|FDK8Ic0|+pXjs*%z zjNUCRt=?=lqbahx)SKvU&?jt4NHSc;=){sh-;|m=_R*@F4y=1V#S=6a+p+TQ8yR`_ zpS~f;!F%3W*V+&pWlsX)QaS8hRxn1t`O4>Bj7oEtsh(RWG99Pce311=oS*Wb-XzGn zP>c7A@;kLloxn8eZAQJ~PuB=8Y-C}Z*-s4#$dXn0%2;HPIYDE>>hTxqmn;1_eUBp%7-(P%B02qzLwRfA4W$s3!*cPGt@O^ocP$M zQei~Sq}e87yY8)V$);CdPL5hFT~o$3M|cHK^wBC(#&xI)HX;KLKBP878G;+WnM)rZ zmjU`u%=BkNF|_{$;&I>TyMl6+s8#_Fo|!6vBfT|9pX&PG*+{Tubm9i5uGXepGd5F$ ziFxf9c8cXyfev zPDK6`>(x|plvj-}K)LRh5D>%vo;7O_cSmql-56E@tzD7N({GQmXST&>H|czI-_cES z(*|QWz48*;yh-cN@&mEIPlK zGsk0jNP(d@9;ygwbs1VN#Ztxgny{B(gb_UQBn~ou75pIDdvS3s^vU?zrSNcMQn2en zvZi4V^TmPW(lwi)e;KvUZRFu6@vmQNTh5b{q|H%9Lb(x6x*!5!po;tF_#s(LGS&iP zKN}lsQmw#HkQ;fhdOUoRPWxVnZD zojS#1Y2bzHQpDDA$%6ohU#lQ(TY#mm?J#^7FG$`iYy&Z33joI}1LwmzGzOUM{`K+N z3%Q&W1bc=_@1{An8It%Tz0}{%?z{kpD~}R8j*d6vn(f zyVJFre70^jc8+TRC@KupW0n{|4pLi}Q`jiK(@T2p*)dpyQ=o)AZ2~txZzTl&#t&A9 zBS}OF%{k4g^Q3fslUC*KP2w0gxs~>R^KPyBE z6-DuEad``5l1nH;aaf)+-mPijoY6B`>o<${PGT&xRbu~9qq$lF`VFT%-GBHqA)Qw4 z3`wn6`b}J-q1gJp9B7gn-ACtHnT@T%%{3Y$ye=0I-!bd4<#!Nv5YUkzrp-Z!hck9&n)S7UcuqMbQVE^s{&5 zYZZM+pPmsKCr1G!Ib0+!Z5WrmyEzOqxZ&;)EXd*0vOhSekg7a(hh&cyRt*z$JCbhaD{(MkyAi&VuXcHsLY|%X82MbiS zel^`kpE|f)Kl-lNQNB({)TdV$sa~>6>6_R~ZbDa~g~psmulCp}S1Tk-3|^kDgX{dD zQ)S>NW1z)?=cw@5fv_}3*~|vrYpjh?%^_&Z10Ksu6)g-bTr)-JzsRMz;|MMCUZ&!+ zI^j+=4s@X0pVA*m=Y%%y+<8y#fjpaGE=2uvjg2J4J?L}xW-w!TWrC2kz384_{fk@!4~Z zv|fwT_y?PsY?P#m=8Ubyc4M~ZcS#J+W48=QsI?-Tu_4!E!Iy(fObR_u3a=?wWImcE zD>6Nn$!TcDk8K|W^1u>EIHp(&Ut15~1oD+}t=;Z17RqaddakWctA?|ig~VNvqd{M6 zA?K#<=w>U8{n4Eyo%)off(@JL@sxR|ItYBHDe&hEWOd_yG2(5HyKzOtNH~1l&hSo> zC83d%reikGA^wdWm*4kS@a|UyT;NdD))ScoqHy>$TPVM%SHK@N@Os1G{KQcqG8T4T zRb}UfnLN6&{fU{4+akv)dAdu}p*jR8Lz-~`H!xq=x~pre&idSIeUNmVC`aE6EjC2P z42PoSYh1tpQ{TROjR+LjL)B|Cc6xaj#aRgE^hza1A(8U%P{m5*$-3 zZ(5IHn)U7uk^x;NclpO(kfJ<&+tFS7y@H$q(N+|-$@Gq>gs+6sbSmCor1dsb8-C8Y+b>|eW;<#+5V z6xwv0t+2tTXBcUG$Iolh8tfO-_^RD4wDIctFX^=^S4>}5GxTqxg>DUS8Q>W?=czs| zR%`3dYv8`TZLODY6Gv(sP9#@qESkGvTIdwrwUv~#2ZY0SAJ7sgU!~EN5d{p=9M0Hc z%Y(9O65y&lPKo#SRVa}0qBo!*|}GXv#} zMFjxLdg`bq3Fp?0Tu{sFPtGoK1+C&aJFagCG7G`KkE)DK0QkbY$%IYq*^l^2GO{5_ zPHf69RURlXOqOmP!_Z9|6#rsRRL=%ojZ#4CbmhkfMzJ*GwtFsd3GuW(PVyXf)*5IN z2Cc^-hm`8Dztuhcp=Mxxu@HJ<>pf4qNI9{EKw!7RKoMo$TC|NFaT1h@e-d?e%iZx0 zI*oI9um-Wy_r42?s};!ZF1aIqaJUd{n;|%r4EqWZVe<0`hv+J;y%U~TWhG;_kB{6E zCeY8@=mC5&%jeAVgDJTW#kapK;T0kkoeCp1V7*8xjLMc4gL~9kp1Pd*K z4sw5Kzd&9BvJHohk2~8{{qE7iXX%6zYtArh!dW1C>^e8nv(P{cN|h$*ehp*Lo(8ly zNc3Px7KBNKxf)PR1*yjXp1fcM^A5{)iPqln1}|nTmf<1JtbF*P?v$H63dsnEU*


b!iw;o+77lqgun>PEk!YR8A_#m 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 literal 0 HcmV?d00001 diff --git a/tests/data/vp8-compat/2fa90d04ce17374a075194e72510319d4f12e5f1.vp8 b/tests/data/vp8-compat/2fa90d04ce17374a075194e72510319d4f12e5f1.vp8 new file mode 100644 index 0000000000000000000000000000000000000000..6a662c0843bd84d807def5814622f7e67ab54041 GIT binary patch literal 7092 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 literal 0 HcmV?d00001 diff --git a/tests/data/vp8-compat/41232e41e66c25526a68befb54c530f39385239e.vp8 b/tests/data/vp8-compat/41232e41e66c25526a68befb54c530f39385239e.vp8 new file mode 100644 index 0000000000000000000000000000000000000000..69e3bdd7cbfa236135b5b41c8c553bb709103f83 GIT binary patch literal 7092 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 SRm2g~sJGT?hqnM_;{FSTV8H7D literal 0 HcmV?d00001 diff --git a/tests/data/vp8-compat/45c6d1e69830bad6ca4e19014559805c7309a07a.vp8 b/tests/data/vp8-compat/45c6d1e69830bad6ca4e19014559805c7309a07a.vp8 new file mode 100644 index 0000000000000000000000000000000000000000..efb2353833cf6c6c00a8ee268f25f5066ba43b9a GIT binary patch literal 7092 zcmeH}cQD*t-^PE4kRU9#8p&!mR*l|+WYwt4ikcNIN+blgsH=t5g0)JbC0L@D=%PjM zv3l=OgJ5;`^}Nr#|Gv-P&rGhFGiR=ubFMkxKd$+lH+Dc85#J}^6QD>WB*PtZe_W*( zMZ%&aTEMs3jZa<6sXl*PP?Otn7hfGzz|A*!9;#Ti_quzxLowj;%O(0mvIFeE66@)C zPThr=Y1)|=_dM7i*K2x?jK2uH{7mq{pIpt#9FiY1?kcV$*^qCLYkq3lNHWBjV*8bj zZ`cC+Izbg_IiIRH>TiQ|KMNor2s}Tj&vxG;z9MS~EvE&C)ECuf)u%Cgij_@EO$V3G z=aQE#NBWnJv_aVZnbh3rpi>^4WEkHFS4n5fU&%oa=t|=FMg@PTnX$%&xAkz0YpGBa z=1xxTpWB^wmAsY(moo6i+I)ZY4VN^Xw{UjUzUc|jM&4emqVf>8qUt(n_#qa|2Cu=_ z(s5!EhEOXmuPMn$OqA*H4Jto$bHgf2w@W35e3F ze8M#Ub+=*Ug!hg{N=Mo{`o2u?PF7YhXPIZRvy9TQkgNHTLyAN9L~q}(&Cmq}nDQSn;4Mvs{W*bmFf|)vKenrQnDE9T*>q=lI{W(FrMB;~6;t?0*f&E|sC&y8Ngt33BzhD?h6#1wcW4o= zxJ!EDTZ(@xFmeoMipfbsQp~+^C?8C}tgDIR0;8fa$zOYfrtJOO0o4XFfE) z>)-2i;?)cJ!i4AI(kFxEmwxOwb~IzP{=MRR=33<6>I3pH4bDG-#Ydmseh5EdH#vKS zy;Z{EYw_MN&G7kWde>>)7U*GXBBg1Dl`3feyd?09=bMOXkC(>VH63N^pS(JJ=za?Y z$9b*d!JAO|VM)VdPns!Rzk4NzAI_;cA%qzQumq_CM!jDI{0$?}N%!}_1$zJUmjV6W z+Z38vdZ1f<%p`9Ll~xW)nb)bD+jx|Wnq^WDYWYd;{r~b6u19EUPT%}GEo@|JV8Nb# zsP5lHaCa>YW>}YX5fM-3Sr(y?AN5yR6c#Gb=(@s&d`;e#k|+0>Yc!831!jE+MBB}>1Z$=kO!SpeD)`R_9TkVIZ+MIcio6x{4~;a?>df57 zo?7F02Zi(Bw8h9PWE#`u*s+#ZlCQsww~lPts(^bahr!9ljqSAsixya`nQ$8`TKUK3+%OdGx+Ls} z43)Bpvy>#80kLs9phib%J-JA9 zK2hNRem94k11sN)U|JX8SW*F8#)Q1}E=zcjvsYFOgycDh*rb@8-Ex#>G~6vwNMCaU z+&DcNV$h%U;i^RAiQ>AuV0Keq`hlF5>$_WCON>8JRnwm}8Q-Q?pQ2J&k+H=^)tG%V zp-|NJh184s3;ilmfnWsa+oKBXc@}$kWJ`om+nr9Twg6|O)DQtuX$$>tp8HzkrK_8db5v!(o6&>Z`NEmPWN4y3w+zZNMY z0}bLo+?f&IApxHYWS*u{8oS9h^&&VpLd$krxTyow_ARFRJo-At=K7J#SCt;IuiC0C z*%-0z#ZlWUX-*yan^~}@yGM`O6i-IMoaLT9=Ahb#YA?3*8JiC5IQCz9>*%Il>;<37 zVz**mqVErvL!p2Yv?9u=^6H2iHKza38r{^blmtPvv83X=248!{bV@6pV@H@hF;$?3 z9J6-%i9q{zKK!de+i2x^I*`dj=Ms8f(MX~=uVE%o^m3AYjUq2qU2gv7WImaXyGvZ*lQzYeaqYEp|k%a+P=kuAOa8FVaOm&nIEDfhgyK#0R+E z8XDHzu*JG3M)gU@qsMU*uVly4 zNEA-w>l-(ynf(_FGnlgx-g~tDI;GMpkFy|uWB{BLr8&u-c_KXR!v2~j&dmjAv1B+u zp@@s-JWHwxf#1`H4g6zhhXbRz;M4Y$eB82-I#^}OB&8O^B{b492ZZ_ zkBgr&d9l^4C?rMLTIs=jyEl}0Hce(AkDsV2uDc&_03;E+NCv!f-m|bU@Ad5NDZCgg ztCqc7%wj3RNFP zN30v`ak*0ozAq!j*%E?ngP_)L!WAZ_O?9YtWGnv$7lD0>F|YoT2!SHa3jZxtJ z+~Asdpx%SHyTg_`Ai;uR{JtF|Z8^(8Iz+#T79wT0x&Bf+AEV%-Tcn53o!atVO1$%- z+JuVOCy48f8^gWYv8O|#Dg-{4}3KUyV^4JotygLBRaAb5*he_yc}w`FAE=coaO z6ygVWLL-h#?_E+aBeRzi;0D)YdwO)9{;Jf8Hqv};FU@ZjH9x`Zx|?0aIi&~DF2Nkp zH57|C+8rqZ*2Y8&OyyfC#K6qv#=5NQ9Uwh>))20!@H#1hao*CoKm>g zpxakq0(Qth62R6sxCR%~fNYH&qeVqwrkdIQ`fz89^eBmd=iOt@>A@jNCG*uf3Vk zFo%%*Of%W($BFUuy_=Sg5^dFj!z~w_BE$mcL2)q6Q@$td3w-{{4`OCJnb1B~Dc#1w zt-M%Psg9>Fsi)NY^%FRjBxl@&Knlwe07H;e+vT=n?PCx03!qo%*^N||wLQ5j71gmQ zD?UKpj}sgR8?6xi&SMILW*a6Q)f^3l*OfB1t5HL=iw=6SK_FN0`aK_6wZU5%(QX@A ztD&f_Rmkd>>-kXI=kFgDIljZKa=e#T{zvQn}h*S9oCH;1J(8N`=Ls6f$Cq`iux?rzts^W-B}$fC!&#J9Sb#0i6+VKskR@~0(# zTJoP-VwrEIl}2JzDY{i_`t*k%8pq)#`UPRW7r$Jnyj0u5|M*Jc1A~uyF>dTr9kRYF zFVfK8>7J?TMs5<5q#H9Go#FE|px?x|>j_m)N@BqFZfL z)~IOluEmCmt6Yn;GvmHaORz${c-#qe#i{~-~oVp0Pugwx&KP$ zQ6i(Mjao-;b7T@eTaKurvsMR})r}OTp0Q6TJ<#|{KWHhI zF~68{2G3sl<6{L7=RaEa#`l(qK9XBLf_65k*H*#LKLvZmHQlrM{4S!4cqP@6h2l^J zLdVYeB$V`aK4k4(ZM*fixe20Knk8wE;R*6^Rb{nvjo2le;LXh@Gz#UYOY#rXqi0 z@xF!clcV?1BSyQt15mBo!PQS6JFT+y5~itkbgxpMB&H61>^zBmm-N&kYk4y@+_(=a zm+*|$O2O;9w@N-Z#@l0O4L=Y?ZWf(7g`>~=_kC3;Vs9{-)XHOvm#-T zj!TxOmr)Xu;Ta0eAa#K&Cy4^8ud6wDF|CIC>Vjb1#G8Td_~Co=ofnzo<^Vx{b;6@R zoI~M^;rK=j?UCb6--wQkt3*WZ#bj+mq`KeI$Z#6zk6nbqxZxLdwx+Z8qfB`!q$Anvzw2bqHfIt_V(3_P8Y>M@ z4dBNyndAurEaWKZtM)X#ueD%7Zk6wsJBJ&bm93Owf?;LJa#JVew?)iLWOYk)y)7T| zwdDpqd-dIE%RKaWThgShB_r*t;0B0e?p(-CgXF+hso~zUz~iqp)IxcNN^Y%`ukdnw z&!~FXi?5oa(VE1vq#@{*pZcRu*kt_O3Hy{*45HkxR(houhsa%bN6#xZY6lG7sy})B WI_me}qc?W060RjD#HPt#yZ;9e9TRK- literal 0 HcmV?d00001 diff --git a/tests/data/vp8-compat/README.md b/tests/data/vp8-compat/README.md new file mode 100644 index 0000000..e17af44 --- /dev/null +++ b/tests/data/vp8-compat/README.md @@ -0,0 +1,30 @@ +# VP8 transform-width regressions + +These are the VP8 payloads of the eleven damaged lossy files found by the +2026-10-03 differential fuzz run. The payload of +`ef1d46488254374abe677fc99c4a1b29bc6c8f21` is identical to +`05386ead209a72d4411d4d0eadd0f9ed6235730c`, so ten fixtures cover eleven cases. +Names are the original libFuzzer corpus identifiers. The tests wrap each payload +in a fresh, valid simple WebP container to isolate transform arithmetic. + +The 160x160 files came from `segment01.webp` and `segment02.webp` in the public +[libwebp test data](https://chromium.googlesource.com/webm/libwebp-test-data/+/06ddd96/). +The three 640x480 files came from a synthetic image generated with ImageMagick +(`-seed 1 -size 640x480 plasma:fractal -attenuate 0.3 +noise Gaussian`) and +encoded by cwebp 1.6.0 at quality 75. None came from private images. + +`tests/vp8_compat.rs` pins FNV-1a hashes of visible Y, U and V bytes decoded +with libwebp 1.6.0's portable C decoder. To reproduce an oracle, wrap a payload +in a RIFF `WEBP` / `VP8` chunk and run +`dwebp -noasm -yuv input.webp -o output.yuv`. The tests do not require a native +decoder or downloaded corpus. + +[RFC 6386 sections 14.3 and 14.4](https://www.rfc-editor.org/rfc/rfc6386.html#section-14.3) +describe signed 16-bit intermediate buffers. Libwebp's C transforms keep the +first pass in 32-bit integers. Seven cases exceed the DCT first-pass width, one +exceeds the WHT first-pass width, and three exceed only the DCT second-pass +width. The latter three agree with default wpd and libwebp C; libwebp's native +x86 SSE2 path differs. Its DCT second-pass additions wrap to 16 bits before +shifting. Consequently a damaged file can have CPU-dependent pixels within +libwebp itself. The opt-in mode targets portable C arithmetic and does not claim +to match every libwebp CPU implementation. diff --git a/tests/data/vp8-compat/d4e4675457e402210e2e72ea9e61f93c40a81a68.vp8 b/tests/data/vp8-compat/d4e4675457e402210e2e72ea9e61f93c40a81a68.vp8 new file mode 100644 index 0000000000000000000000000000000000000000..a032e69ba050ecfdef082b186371199c42546270 GIT binary patch literal 7638 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 EUr*5{4gdfE literal 0 HcmV?d00001 diff --git a/tests/data/vp8-compat/fd4b9d6c836f6b8c5d6e6705e4e98261dfabb0be.vp8 b/tests/data/vp8-compat/fd4b9d6c836f6b8c5d6e6705e4e98261dfabb0be.vp8 new file mode 100644 index 0000000000000000000000000000000000000000..67f1c6aa00cdea4ddc44c5f0b2cc9eaa9c697a84 GIT binary patch literal 34980 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 vjAn||WqRokZL*e~V~H|lctC=kqZKaVfqP! literal 0 HcmV?d00001 diff --git a/tests/data/vp8-compat/fddf820775b3266dc39aeadc299d34df5c895299.vp8 b/tests/data/vp8-compat/fddf820775b3266dc39aeadc299d34df5c895299.vp8 new file mode 100644 index 0000000000000000000000000000000000000000..b623ca0c38d325e4ae9ef0dca7034657204a32ee GIT binary patch literal 34980 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 Xap^Nk#!{L^noc`0ga~0RzJdP_#40|G literal 0 HcmV?d00001 diff --git a/tests/vp8_compat.rs b/tests/vp8_compat.rs new file mode 100644 index 0000000..84e21da --- /dev/null +++ b/tests/vp8_compat.rs @@ -0,0 +1,158 @@ +use wpd::api::{Decoder, Options}; +use wpd::image::Format; + +const CASES: &[(&[u8], u64)] = &[ + ( + include_bytes!("data/vp8-compat/05386ead209a72d4411d4d0eadd0f9ed6235730c.vp8"), + 0xd5985d6a806cbc3e, + ), + ( + include_bytes!("data/vp8-compat/1106e5ea9977c4e83826977b05a664ba311fd396.vp8"), + 0x606a790c9aa52794, + ), + ( + include_bytes!("data/vp8-compat/2fa90d04ce17374a075194e72510319d4f12e5f1.vp8"), + 0xddf085515fdc19fc, + ), + ( + include_bytes!("data/vp8-compat/31ad5278df83871478b68166dc93f71a9d6ca96a.vp8"), + 0x662636aaaac3ffdb, + ), + ( + include_bytes!("data/vp8-compat/3d641847c558f1e8e204ae6b9ae7a39829df71e8.vp8"), + 0xaaf3d69cff46af8d, + ), + ( + include_bytes!("data/vp8-compat/41232e41e66c25526a68befb54c530f39385239e.vp8"), + 0xc9114c262ae54373, + ), + ( + include_bytes!("data/vp8-compat/45c6d1e69830bad6ca4e19014559805c7309a07a.vp8"), + 0x66bf1476e887b1f4, + ), + ( + include_bytes!("data/vp8-compat/d4e4675457e402210e2e72ea9e61f93c40a81a68.vp8"), + 0x3abd600787eaf9fc, + ), + // ef1d46488254374abe677fc99c4a1b29bc6c8f21 has the same VP8 chunk as 05386. + ( + include_bytes!("data/vp8-compat/05386ead209a72d4411d4d0eadd0f9ed6235730c.vp8"), + 0xd5985d6a806cbc3e, + ), + ( + include_bytes!("data/vp8-compat/fd4b9d6c836f6b8c5d6e6705e4e98261dfabb0be.vp8"), + 0xa09d93c6d9f17892, + ), + ( + include_bytes!("data/vp8-compat/fddf820775b3266dc39aeadc299d34df5c895299.vp8"), + 0x7c9c719b14554032, + ), +]; + +fn webp(vp8: &[u8]) -> Vec { + let mut bytes = b"RIFF".to_vec(); + let padded = vp8.len() + (vp8.len() & 1); + bytes.extend_from_slice(&(padded as u32 + 12).to_le_bytes()); + bytes.extend_from_slice(b"WEBPVP8 "); + bytes.extend_from_slice(&(vp8.len() as u32).to_le_bytes()); + bytes.extend_from_slice(vp8); + bytes.resize(20 + padded, 0); + bytes +} + +// FNV-1a over visible Y, U and V rows, in dwebp -yuv order. These values +// were measured with libwebp 1.6.0's portable C decoder (dwebp -noasm). +fn pixels(decoder: &mut Decoder<'_>) -> u64 { + let picture = decoder.next_frame().unwrap().unwrap(); + assert_eq!(picture.format(), Format::Yuv420p); + let mut hash = 0xcbf29ce484222325u64; + for plane in 0..3 { + for row in picture.rows_of(plane) { + for &value in row { + hash = (hash ^ u64::from(value)).wrapping_mul(0x100000001b3); + } + } + } + hash +} + +#[test] +fn compatibility_matches_libwebp_c_on_every_damaged_lossy_case() { + for &(vp8, expected) in CASES { + let bytes = webp(vp8); + for n_threads in [1, 2, 4] { + let mut decoder = Decoder::new(); + decoder.set_format(Format::Yuv420p).unwrap(); + decoder + .set_options(Options { + libwebp_compat: true, + n_threads, + ..Options::default() + }) + .unwrap(); + decoder.open(&bytes).unwrap(); + assert_eq!(pixels(&mut decoder), expected); + } + } +} + +#[test] +fn incremental_compatibility_uses_the_same_transform_arithmetic() { + for &(vp8, expected) in CASES { + let bytes = webp(vp8); + let mut decoder = Decoder::new(); + decoder.set_format(Format::Yuv420p).unwrap(); + decoder + .set_options(Options { + libwebp_compat: true, + ..Options::default() + }) + .unwrap(); + decoder.open_stream().unwrap(); + for part in bytes.chunks(97) { + decoder.append(part).unwrap(); + } + decoder.end_of_stream().unwrap(); + assert_eq!(pixels(&mut decoder), expected); + } +} + +#[test] +fn compatibility_preserves_the_valid_corpus_pixels() { + if cfg!(miri) { + return; + } + let corpus = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("wpd-test-data"); + for entry in std::fs::read_dir(corpus).unwrap() { + let path = entry.unwrap().path(); + if !path.extension().is_some_and(|ext| ext == "webp") { + continue; + } + let bytes = std::fs::read(&path).unwrap(); + let mut hashes = Vec::new(); + for compat in [false, true] { + let mut decoder = Decoder::new(); + decoder + .set_options(Options { + libwebp_compat: compat, + n_threads: 4, + ..Options::default() + }) + .unwrap(); + decoder.set_format(Format::Rgba).unwrap(); + decoder.open(&bytes).unwrap(); + let mut frames = Vec::new(); + while let Some(picture) = decoder.next_frame().unwrap() { + let mut hash = 0xcbf29ce484222325u64; + for row in picture.rows_of(0) { + for &value in row { + hash = (hash ^ u64::from(value)).wrapping_mul(0x100000001b3); + } + } + frames.push(hash); + } + hashes.push(frames); + } + assert_eq!(hashes[0], hashes[1], "{}", path.display()); + } +} diff --git a/tests/vp8_mode_reset.rs b/tests/vp8_mode_reset.rs new file mode 100644 index 0000000..32af5bd --- /dev/null +++ b/tests/vp8_mode_reset.rs @@ -0,0 +1,52 @@ +use wpd::vp8::Decoder; + +fn visible_planes(decoder: &Decoder) -> Vec> { + (0..3) + .map(|p| { + let plane = decoder.picture.planes[p]; + let bytes = decoder.picture.plane(p); + let (width, height) = if p == 0 { + (decoder.width as usize, decoder.height as usize) + } else { + ( + (decoder.width as usize).div_ceil(2), + (decoder.height as usize).div_ceil(2), + ) + }; + (0..height) + .flat_map(|y| { + let at = plane.origin + y * plane.stride; + bytes[at..at + width].iter().copied() + }) + .collect() + }) + .collect() +} + +// This separate test binary starts before any API decoder initializes CPU +// detection, making the raw scalar-to-assembly transition reproducible. +#[test] +fn compatibility_restores_the_original_raw_decoder_tables() { + let vp8 = + include_bytes!("data/vp8-compat/05386ead209a72d4411d4d0eadd0f9ed6235730c.vp8"); + let mut decoder = Decoder::new(); + decoder.decode_frame(vp8).unwrap(); + let strict = visible_planes(&decoder); + + // Another API decoder publishes detected CPU flags while this raw + // decoder retains the tables it selected at construction. + let _other = wpd::api::Decoder::new(); + for _ in 0..2 { + decoder.libwebp_compat = true; + decoder.decode_frame(vp8).unwrap(); + assert_ne!(strict, visible_planes(&decoder)); + + decoder.libwebp_compat = false; + decoder.decode_frame(vp8).unwrap(); + for (p, (expected, actual)) in + strict.iter().zip(visible_planes(&decoder)).enumerate() + { + assert!(expected == &actual, "visible plane {p} changed after reset"); + } + } +} From 00aa6462cac4c220ab403728004f12baff5b39a1 Mon Sep 17 00:00:00 2001 From: Mike Sulsenti Date: Sun, 4 Oct 2026 11:14:25 -0400 Subject: [PATCH 08/10] vp8l: match final alpha symbol EOF in compatibility mode libwebp accepts a paletted ALPH stream if the final entropy symbol crosses EOF but completes the alpha plane. wpd rejected it after all pixels had been written, including two mutations of the public odd_a_lossy.webp fixture that libwebp 1.6.0 decodes. Apply this malformed-input policy only with libwebp_compat. Keep the bounded ALPH input and all checks for incomplete planes and invalid backward references; default decoding remains strict. Add a synthetic entropy boundary regression and public-fixture alpha-only mutations with exact libwebp RGBA hashes. Verify threaded and incremental decoding, asm and fallback configurations, and stylecheck. --- src/driver/lossy.rs | 4 +++ src/vp8l/entropy.rs | 52 ++++++++++++++++++++++++++++++- src/vp8l/mod.rs | 9 +++++- tests/alpha_compat.rs | 71 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 tests/alpha_compat.rs diff --git a/src/driver/lossy.rs b/src/driver/lossy.rs index bab869c..bbe769e 100644 --- a/src/driver/lossy.rs +++ b/src/driver/lossy.rs @@ -62,6 +62,7 @@ struct Alpha<'p, 'i> { compression: i32, filter: i32, threads: usize, + libwebp_compat: bool, } fn decode_alpha(a: Alpha<'_, '_>) -> Result<()> { @@ -78,6 +79,7 @@ fn decode_alpha(a: Alpha<'_, '_>) -> Result<()> { compression, filter, threads, + libwebp_compat, } = a; let extent = width .checked_mul(height.max(0) as usize) @@ -98,6 +100,7 @@ fn decode_alpha(a: Alpha<'_, '_>) -> Result<()> { } else if compression == ALPHA_COMPRESSION_VP8L { vp8l.set_canvas(width as i32, height); vp8l.threads = threads; + vp8l.libwebp_compat = libwebp_compat; let rest = match filter { ALPHA_FILTER_HORIZONTAL => Some(fdsp.horizontal_unfilter), @@ -199,6 +202,7 @@ impl FrameSlot { compression: *alpha_compression, filter: *alpha_filter, threads: env.threads, + libwebp_compat: env.libwebp_compat, }, vp8.first_mut(), ) diff --git a/src/vp8l/entropy.rs b/src/vp8l/entropy.rs index 24cc72b..23c6f0a 100644 --- a/src/vp8l/entropy.rs +++ b/src/vp8l/entropy.rs @@ -734,6 +734,7 @@ fn run(args: Args<'_, '_>) -> Result { + pub allow_final_overrun: bool, pub gb: &'a mut BitReader, pub buf: &'a [u8], pub pixels: &'a mut [u8], @@ -745,6 +746,7 @@ pub struct AlphaArgs<'a, 'e> { pub fn decode_alpha_pixels(args: AlphaArgs<'_, '_>) -> Result<()> { let AlphaArgs { + allow_final_overrun, gb, buf, pixels, @@ -815,7 +817,10 @@ pub fn decode_alpha_pixels(args: AlphaArgs<'_, '_>) -> Result<()> { return Err(Error::InvalidData); } } - if gb.is_eos(buf) { + // libwebp's paletted-alpha decoder accepts EOF at the final symbol + // once every pixel is written. The checks inside the loop still refuse + // an overrun before the plane is complete and invalid references. + if gb.is_eos(buf) && !allow_final_overrun { crate::log::error("alpha data runs past the end of the chunk"); return Err(Error::InvalidData); } @@ -864,6 +869,51 @@ mod tests { } } + #[test] + fn compatibility_allows_only_a_final_alpha_symbol_at_eof() { + let mut arena = Vec::new(); + let mut plan = super::super::huffman::Plan::default(); + let lengths = [1u8, 1]; + let mut sorted = [0u16; 2]; + + super::super::huffman::count_lengths(&mut plan, &lengths); + let reader = + super::super::huffman::build(&mut arena, &mut plan, &lengths, &mut sorted) + .unwrap(); + let group = HTreeGroup { + trees: [reader; HUFFMAN_CODES_PER_META_CODE], + ..HTreeGroup::default() + }; + // Sixty-four one-bit literals fit; the sixty-fifth uses the + // zero-padded end of the bit window. A sixty-sixth is still invalid. + let buf = [0u8; 8]; + for (allow, count, expected) in [ + (false, 64, Ok(())), + (false, 65, Err(Error::InvalidData)), + (true, 65, Ok(())), + (true, 66, Err(Error::InvalidData)), + ] { + let mut gb = BitReader::new(&buf); + let mut pixels = vec![0xff; count]; + assert_eq!( + decode_alpha_pixels(AlphaArgs { + allow_final_overrun: allow, + gb: &mut gb, + buf: &buf, + pixels: &mut pixels, + width: count, + groups: &[group], + arena: &arena, + entropy: None, + }), + expected + ); + if expected.is_ok() { + assert!(pixels.iter().all(|&p| p == 0)); + } + } + } + #[test] fn overlapping_copies_match_the_pixel_at_a_time_definition() { check_overlaps(std::array::from_fn(|i| i as u8)); diff --git a/src/vp8l/mod.rs b/src/vp8l/mod.rs index f80ff55..1ccbb79 100644 --- a/src/vp8l/mod.rs +++ b/src/vp8l/mod.rs @@ -289,6 +289,8 @@ pub struct Decoder { /// The threads a decode may use, counting the calling one. pub threads: usize, + /// Allow libwebp's final paletted-alpha symbol to finish at EOF. + pub libwebp_compat: bool, } impl Decoder { @@ -925,7 +927,11 @@ impl Decoder { { let Decoder { - gb, image, indices, .. + gb, + image, + indices, + libwebp_compat, + .. } = self; let (head, tail) = image.split_at_mut(ROLE_ENTROPY); let ent = &tail[0]; @@ -934,6 +940,7 @@ impl Decoder { gb, buf, pixels: &mut indices[..total], + allow_final_overrun: *libwebp_compat, width, groups: &head[ROLE_ARGB].groups, arena: &head[ROLE_ARGB].arena, diff --git a/tests/alpha_compat.rs b/tests/alpha_compat.rs new file mode 100644 index 0000000..1213a2f --- /dev/null +++ b/tests/alpha_compat.rs @@ -0,0 +1,71 @@ +use wpd::api::{Decoder, Options}; +use wpd::image::Format; + +type AlphaCase = (&'static [(usize, u8, u8)], u64); + +fn pixel_hash(decoder: &mut Decoder<'_>) -> u64 { + let picture = decoder.next_frame().unwrap().unwrap(); + picture + .rows_of(0) + .flatten() + .fold(0xcbf29ce484222325u64, |hash, &value| { + (hash ^ u64::from(value)).wrapping_mul(0x100000001b3) + }) +} + +#[test] +fn compatibility_matches_libwebp_at_the_end_of_a_paletted_alpha_plane() { + if cfg!(miri) { + return; + } + // Public project testdata, mutated only in its compressed alpha stream. + // Native libwebp 1.6.0 accepts the final symbol at EOF. The unchanged + // colour stream and container isolate alpha decoding from other policies. + let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("wpd-test-data/odd_a_lossy.webp"); + let seed = std::fs::read(path).unwrap(); + let cases: &[AlphaCase] = &[ + (&[(231, 202, 200), (351, 128, 125)], 0x87ed788ad3cb8e12), + ( + &[(384, 53, 49), (385, 57, 56), (669, 191, 36)], + 0x01cdcded58bfb418, + ), + ]; + for &(patch, expected) in cases { + let mut bytes = seed.clone(); + for &(offset, old, new) in patch { + assert_eq!(bytes[offset], old); + bytes[offset] = new; + } + let mut strict = Decoder::new(); + strict.open(&bytes).unwrap(); + assert!(strict.next_frame().is_err()); + for n_threads in [1, 4] { + let mut decoder = Decoder::new(); + decoder + .set_options(Options { + libwebp_compat: true, + n_threads, + ..Options::default() + }) + .unwrap(); + decoder.set_format(Format::Rgba).unwrap(); + decoder.open(&bytes).unwrap(); + assert_eq!(pixel_hash(&mut decoder), expected); + } + let mut decoder = Decoder::new(); + decoder + .set_options(Options { + libwebp_compat: true, + ..Options::default() + }) + .unwrap(); + decoder.set_format(Format::Rgba).unwrap(); + decoder.open_stream().unwrap(); + for part in bytes.chunks(97) { + decoder.append(part).unwrap(); + } + decoder.end_of_stream().unwrap(); + assert_eq!(pixel_hash(&mut decoder), expected); + } +} From 4ad10964df07a6c0d41a4a6810d0e5c2acbe2927 Mon Sep 17 00:00:00 2001 From: Mike Sulsenti Date: Sun, 4 Oct 2026 10:58:57 -0400 Subject: [PATCH 09/10] fuzz: exercise strict and libwebp-compatible decoding Run every e2e input in both decoding modes, including serial/threaded comparison, C external buffers and incremental streams. Selecting a mode from container bytes would leave compatibility untested for many valid seeds; exercising both reaches the widened transforms and still-parser paths without changing the seed format. Validation: scripts/stylecheck.sh; all-target ASan build and seeded fuzz smoke run are part of final qualification. --- fuzz/fuzz_targets/e2e.rs | 17 ++++++++++++----- scripts/fuzz-smoke.sh | 11 +++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/fuzz/fuzz_targets/e2e.rs b/fuzz/fuzz_targets/e2e.rs index 07a2bd5..f600302 100644 --- a/fuzz/fuzz_targets/e2e.rs +++ b/fuzz/fuzz_targets/e2e.rs @@ -161,15 +161,14 @@ fn decode_external(data: &[u8], options: Options) { } } -fuzz_target!(|data: &[u8]| { - if !budget::fits(data) { - return; - } +fn exercise(data: &[u8], libwebp_compat: bool) { let Some(&first) = data.first() else { return; }; let format = FORMATS[first as usize % FORMATS.len()]; - let (options, subframe) = decode_options(data); + let (mut options, subframe) = decode_options(data); + + options.libwebp_compat = libwebp_compat; let mut serial = Decoder::new(); configure( @@ -251,4 +250,12 @@ fuzz_target!(|data: &[u8]| { break; } } +} + +fuzz_target!(|data: &[u8]| { + if !budget::fits(data) { + return; + } + exercise(data, false); + exercise(data, true); }); diff --git a/scripts/fuzz-smoke.sh b/scripts/fuzz-smoke.sh index 9ae87e1..de32d09 100755 --- a/scripts/fuzz-smoke.sh +++ b/scripts/fuzz-smoke.sh @@ -51,6 +51,17 @@ for path in files: (out / "vp8l" / f"{name}-{threads}").write_bytes( bytes((0, threads)) + payload) +# Exercise the damaged transform regressions through raw and driver targets. +for path in sorted(Path("tests/data/vp8-compat").glob("*.vp8")): + payload = path.read_bytes() + name = f"compat-{path.stem}" + (out / "vp8" / name).write_bytes(payload) + chunk = b"VP8 " + len(payload).to_bytes(4, "little") + payload + chunk += bytes(len(payload) & 1) + data = b"RIFF" + (len(chunk) + 4).to_bytes(4, "little") + b"WEBP" + chunk + for target in ("container", "e2e"): + (out / target / name).write_bytes(data) + for target in ("container", "e2e", "vp8", "vp8l"): if not any((out / target).iterdir()): sys.exit(f"no seeds prepared for {target}") From d638647ec2f68b612f4fab8992a6f69beaa752e5 Mon Sep 17 00:00:00 2001 From: Mike Sulsenti Date: Sun, 4 Oct 2026 10:43:24 -0400 Subject: [PATCH 10/10] cli: expose opt-in libwebp compatibility Wire --libwebp-compat through decoder creation, streaming reopens and muxer format probing. Keep strict decoding as the default. Regression coverage checks duplicate VP8X and damaged trailing chunks with text info, PAM and byte-at-a-time streaming. Document the streaming and portable-oracle contract and record the compatibility changes in the draft changelog. --- CHANGELOG.md | 18 ++++++++++ README.md | 32 ++++++++++++++++++ tools/tests/compat.rs | 79 +++++++++++++++++++++++++++++++++++++++++++ tools/wpd.rs | 25 +++++++++++++- 4 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 tools/tests/compat.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c83f2b6..99139ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,3 +11,21 @@ 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. + +### libwebp compatibility + +- Reject corrupted first-chunk FourCCs instead of silently treating an extended + container as a simple image. +- Add opt-in libwebp still compatibility through Rust, C and `--libwebp-compat`, + retaining strict decoding by default. Match duplicate VP8X chunks, shortened + image chunks, damaged trailing chunks and simple-image final padding. +- Retain the complete compatible still payload until streaming EOF and preserve + animation container validation. +- Match portable libwebp VP8 transforms on non-conforming lossy streams, with + damaged-stream regressions. Save and restore each decoder's strict DSP tables + when toggling the mode; native libwebp CPU output can still differ. +- Accept a final alpha entropy symbol crossing EOF only when compatibility mode + has already produced the complete alpha plane. +- Add a directory comparison script for libwebp 1.6.0 `dwebp` and `anim_dump`, + distinguishing visible differences from transparent RGB. +- Fuzz strict and compatible decoding with public and damaged regression seeds. diff --git a/README.md b/README.md index a789d16..a2b1cac 100644 --- a/README.md +++ b/README.md @@ -145,3 +145,35 @@ This project would not be possible without: - [dav1d](https://www.videolan.org/projects/dav1d.html), the fastest open-source AV1 decoder - [rav1d](https://github.com/memorysafety/rav1d), a Rust rewrite of dav1d + +## libwebp compatibility + +Strict decoding remains the default. `--libwebp-compat` accepts damaged still +containers that libwebp's still decoder accepts, including shortened image +chunks, duplicate VP8X chunks, and damaged trailing chunks. It also selects +libwebp's portable C VP8 transform behaviour for non-conforming lossy streams. +It applies to pixel output and text info. Animation containers retain their +validation. + +Set Rust's `Options.libwebp_compat` before opening input. C callers set +`WPDDecoderOptions.libwebp_compat` to 1 and initialize `struct_size` with +`sizeof(WPDDecoderOptions)`; older options structs retain strict decoding. + +Compatible still streams produce pixels after `end_of_stream`, retaining +physical input bytes past RIFF's declared end when the codec needs them. Default +still decoding and animation decoding remain incremental. Library callers should +bound incoming bytes; the CLI's `--max-input` and the normal frame and output +limits still apply. + +Compare a directory with libwebp 1.6.0's `dwebp` and `anim_dump`: + +```sh +python3 scripts/webpcompare.py path/to/corpus --wpd build/wpd \ + --libwebp-compat --noasm +``` + +`--noasm` selects portable C for stills through `dwebp`; `anim_dump` remains +native. Libwebp's CPU-specific code can render malformed lossy streams +differently from its portable code, so this mode does not promise identical +pixels from every native libwebp build on non-conforming input. The comparison +reports visible differences separately from RGB differences under zero alpha. diff --git a/tools/tests/compat.rs b/tools/tests/compat.rs new file mode 100644 index 0000000..bef0880 --- /dev/null +++ b/tools/tests/compat.rs @@ -0,0 +1,79 @@ +#![forbid(unsafe_code)] + +use std::io::Write; +use std::process::{Command, Output, Stdio}; + +/* A synthetic two-pixel lossless red/green image, encoded with cwebp 1.6.0 + * from P6\n2 1\n255\n followed by ff0000 00ff00. */ +const STILL: &[u8] = &[ + 0x52, 0x49, 0x46, 0x46, 0x1e, 0, 0, 0, 0x57, 0x45, 0x42, 0x50, 0x56, 0x50, 0x38, + 0x4c, 0x11, 0, 0, 0, 0x2f, 1, 0, 0, 0, 0x0f, 0xb0, 0xff, 0xf3, 0x1f, 0xf3, 0x1f, + 0x15, 0x32, 0xa2, 0xff, 1, 0, +]; + +fn run(args: &[&str], input: &[u8]) -> Output { + let mut child = Command::new(env!("CARGO_BIN_EXE_wpd")) + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + + child.stdin.take().unwrap().write_all(input).unwrap(); + child.wait_with_output().unwrap() +} + +fn chunk(tag: &[u8; 4], payload: &[u8]) -> Vec { + let mut out = tag.to_vec(); + + out.extend_from_slice(&(payload.len() as u32).to_le_bytes()); + out.extend_from_slice(payload); + if payload.len() & 1 != 0 { + out.push(0); + } + out +} + +fn riff(payload: &[u8]) -> Vec { + let mut out = b"RIFF".to_vec(); + + out.extend_from_slice(&(payload.len() as u32 + 4).to_le_bytes()); + out.extend_from_slice(b"WEBP"); + out.extend_from_slice(payload); + out +} + +#[test] +fn compatibility_applies_to_info_output_muxers_and_streaming() { + let mut payload = chunk(b"VP8X", &[0, 0, 0, 0, 1, 0, 0, 0, 0, 0]); + + payload.extend(chunk(b"VP8X", &[0, 0, 0, 0, 1, 0, 0, 0, 0, 0])); + payload.extend_from_slice(&STILL[12..]); + payload.extend_from_slice(b"JUNK\xff\xff\xff\xfftail"); + let data = riff(&payload); + let reference = run(&["--muxer", "pam", "-", "-"], STILL); + + assert!(reference.status.success()); + assert!(!run(&["--info", "-"], &data).status.success()); + assert!(run(&["--libwebp-compat", "--info", "-"], &data) + .status + .success()); + for stream in ["1", "997"] { + let output = run( + &[ + "--libwebp-compat", + "--stream", + stream, + "--muxer", + "pam", + "-", + "-", + ], + &data, + ); + + assert!(output.status.success()); + assert_eq!(output.stdout, reference.stdout); + } +} diff --git a/tools/wpd.rs b/tools/wpd.rs index d54e2c5..0ed3f8f 100644 --- a/tools/wpd.rs +++ b/tools/wpd.rs @@ -99,6 +99,9 @@ const USAGE_TAIL: &str = concat!( " --info\n", " print canvas, animation, the frame table and per-frame\n", " timing to stdout\n", + " --libwebp-compat\n", + " match libwebp's acceptance and lossy decoding of damaged\n", + " still images; default is strict decoding\n", " --stream u32\n", " decode incrementally, appending this many bytes at a time,\n", " instead of opening the file whole\n", @@ -280,6 +283,7 @@ struct Options { scale: Option<(i32, i32)>, frame_size_limit: u32, info: bool, + libwebp_compat: bool, subframe: bool, muxer: Option, verify: Option, @@ -303,6 +307,7 @@ const OPTIONS: &[(&str, Option, bool)] = &[ ("muxer", None, true), ("verify", None, true), ("info", None, false), + ("libwebp-compat", None, false), ("loops", None, true), ("cpumask", None, true), ("subframe", None, false), @@ -374,6 +379,7 @@ fn set(o: &mut Options, name: &str, value: String) -> Result<(), &'static str> { } "info" => o.info = true, "subframe" => o.subframe = true, + "libwebp-compat" => o.libwebp_compat = true, _ => return Err(MISSING), } Ok(()) @@ -657,6 +663,7 @@ fn new_decoder( n_threads: i32, scale: Option<(i32, i32)>, frame_size_limit: u32, + libwebp_compat: bool, ) -> Option> { let mut decoder = Decoder::new(); @@ -665,6 +672,7 @@ fn new_decoder( n_threads, scale, frame_size_limit, + libwebp_compat, ..api::Options::default() }) .is_err() @@ -854,7 +862,20 @@ fn run( let mut out_format = opts.out_format; if opened && output.muxer != Muxer::Raw { - let Ok(image) = api::info(&data) else { + let info = if opts.libwebp_compat { + let mut decoder = Decoder::new(); + + decoder + .set_options(api::Options { + libwebp_compat: true, + ..Default::default() + }) + .and_then(|()| decoder.open(&data)) + .and_then(|()| decoder.info()) + } else { + api::info(&data) + }; + let Ok(image) = info else { eprintln!("{}: cannot read image header", input_name.to_string_lossy()); return ExitCode::FAILURE; }; @@ -890,6 +911,7 @@ fn run( opts.n_threads, opts.scale, opts.frame_size_limit, + opts.libwebp_compat, ) else { return ExitCode::FAILURE; }; @@ -905,6 +927,7 @@ fn run( opts.n_threads, opts.scale, opts.frame_size_limit, + opts.libwebp_compat, ) else { return ExitCode::FAILURE; };