v0.0.6
Other
- RFC 4587 §6.2.1 offer/answer negotiation helper
- fix truncated
1?TCOEFF prefix panic (daily-fuzz finding) - criterion suite for transform / encode / decode hot paths
- scrub decorative external-implementation attribution
- second cargo-fuzz target for RTCP compound parser
- cargo-fuzz decoder harness + daily workflow
Added
-
SDP offer/answer negotiation helper (RFC 4587 §6.2.1). The
sdpmodule gains the free functionnegotiate_answer(offer, our_capability) -> Result<H261FmtpParams, SdpError>that computes
the §6.2.1 answer parameters from a received offer and our
local capability:- Picture-size intersection. Only sizes both peers advertise
survive into the answer; a disjoint pair (e.g. CIF-only offer vs
QCIF-only capability) errors withSdpError::NoPictureSize,
matching §6.2.1's "SHALL specify at least one supported picture
size". - MPI per shared size. §6.1.1's MPI is the minimum picture
interval, so29.97 / MPIis the upper bound on frame rate.
The answer carriesMPI = max(offer.MPI, our.MPI)per shared
size, i.e. the more restrictive bound binds. - Annex D (
D). §6.2.1: "This option MUST NOT appear unless
the sender of this SDP message is able to decode this option."
The answer'sD=1requires bothoffer.d == Some(true)AND
our_capability.d == Some(true); otherwiseDis omitted from
the answer (matching §6.1.1's "SHOULD NOT be used … if not
supported"). - RFC 2032 fallback. §6.2.1: "If the receiver does not specify
the picture size/MPI parameter … assume that such a receiver is
able to support reception of QCIF resolution with MPI=1." The
helper applies that fallback automatically (equivalent to
H261FmtpParams::rfc2032_fallback()) when the offer carries no
picture-size parameter. The fallback is not applied to
our_capability— that side is local and should be supplied
explicitly.
The companion method
H261FmtpParams::preferred_picture_size()
returns the preferred receiver mode per §6.2.1 ("Parameters offered
first are the most preferred") —Some(SourceFormat::Cif)when CIF
is advertised (matchingformat_value's CIF-before-QCIF emission
order from the §6.2.1 worked example),Some(SourceFormat::Qcif)
when only QCIF is, elseNone. Eight new tests cover the
intersection / MPI-max / disjoint-sizes / Annex-D / RFC-2032-
fallback / format round-trip / max-frame-rate / validate-passes
paths; the negotiation example also runs as a doctest. - Picture-size intersection. Only sizes both peers advertise
Fixed
- Decoder panic on a truncated
1?TCOEFF prefix (round 175,
surfaced by the scheduled dailydecode_h261fuzz harness).
decode_tcoeff(.., is_first = false)saw a bit-reader where exactly
one bit remained and that bit was1. The function took the
b0 == 1branch and then peeked two bits from
peek >> (avail - 2), whereavail = 1caused an unsigned
underflow →attempt to subtract with overflowpanic under debug
/ ASAN builds. The two-bit peek is now gated behindavail >= 2
and the call returnsError::invalid("h261 tcoeff: truncated1?prefix")on the malformed input, restoring the public-surface
contract from the fuzz harness: every call returns — no panic, no
abort, no out-of-bounds. New regression test
tcoeff_truncated_one_bit_does_not_paniccovers it on stable Rust.
Added
-
Criterion benchmark suite (
benches/transform,benches/encode,
benches/decode). Round 175 (depth-mode) wires upcriterion = "0.5"
as a dev-dependency and registers threeharness = falsebench
binaries so future optimisation rounds have a recorded baseline to
A/B against:transformtimes the 8×8 inverse / forward DCT block hot path —
fdct_intra+fdct_signed(encoder forward pass) and
idct_intra+idct_signed(decoder inverse pass). One block per
iteration; throughput reported in samples so per-sample cycle-
equivalents land naturally.encodetimes whole-picture encode through the production
encode_intra_picture/H261Encoder::encode_framepaths in four
scenarios: QCIF intra-only (no ME), single-P from a pre-built I
reference, I + 3 P chain (full rate-controller carryover), and CIF
intra (the 4× area test).decodetimes whole-picture decode throughH261Decoder::send_packetreceive_frame, mirroring the encode scenarios. Each decode
bench runs the in-crate encoder once during setup to produce a
real elementary stream, so the timed loop measures the decoder
alone.
Every benchmark synthesises its YUV source inline from a
deterministic striped pattern plus low-amplitude xorshift noise —
no on-disk fixtures, no third-party CLI, nodocs/files read at
bench time.cargo bench -p oxideav-h261 --no-rundoubles as a
compile-only CI regression guard via the existing matrix.
-
Second
cargo-fuzztarget — RTCP compound parser. New
parse_rtcp_compoundfuzz target drives arbitrary fuzz-supplied bytes
through the public RTCP parser surface (parse_compound,
parse_report,parse_sdes,parse_bye,parse_app) so the §6.1
compound walk (16-bit-length advance), the SR/RR fixed header + RC
block walk, SDES chunk + item walk (including the PRIV inner 8-bit
length), BYE reason-string length-prefix, and APPname/data32-bit
alignment are all exercised against bytes whose shape the fuzzer
dictates. Same contract as the existingdecode_h261target: every
call must return — no panic, no abort, no integer overflow (in debug
/ ASAN builds), no out-of-bounds index, no allocator OOM. The seed
corpus underfuzz/corpus/parse_rtcp_compound/contains nine valid
datagrams (empty RR, SR with no blocks, SR with one block, RR with
two blocks, SDES CNAME, BYE with reason, APP with PING payload, and
two compound packets).tests/fuzz_seed_corpus_rtcp.rsdrives the
same logic on stable Rust against the corpus plus several adversarial
in-line buffers (lying header length, zero-length advance, truncated
compound, SDES PRIV length overflow, BYE reason overflow, APP at the
5-bit subtype maximum, unknown PT=205) so a regression in the public
parser surface trips an existing CI lane rather than waiting for the
daily fuzz run.