Skip to content

Fix verified correctness issues from #197 - #214

Draft
kixelated wants to merge 1 commit into
mainfrom
fix/issue-197
Draft

Fix verified correctness issues from #197#214
kixelated wants to merge 1 commit into
mainfrom
fix/issue-197

Conversation

@kixelated

Copy link
Copy Markdown
Owner

Addresses the low-severity, verified correctness items tracked in #197 (overflow/underflow edges, flag-fidelity round-trips, encode truncations).

Per the issue author's guidance, this prefers spec-correctness over back-compat: some previously-"successful" but malformed encodes now return Err instead of silently truncating. This is a 0.x crate.

Core / types

  • u24/u48 TryFrom now reject out-of-range values (new Error::OutOfRange) instead of silently truncating via to_be_bytes()[1..]; adds u48::MAX.
  • FixedPoint's Debug prints the true fixed-point value (int + dec/2^bits) rather than int / dec.
  • Header::encode uses checked arithmetic (no overflow panic/wrap) and a 32-bit-safe largesize branch; decode paths (sync, ReadFrom, tokio) use usize::try_from so a 64-bit largesize no longer truncates on 32-bit targets.
  • Atoms > 4 GiB now re-encode using the 64-bit largesize form instead of Error::TooLarge (the existing TODO). This adds a new BufMut::insert_slice method (implemented for Vec, &mut T, BytesMut) plus a shared write_box_size helper — a breaking change for external BufMut implementors.
  • Mdhd gets a manual Default with language: "und" so decode(encode(default)) == default (an empty string isn't representable in the packed 15-bit form).

Codecs

  • avcC — extension block gated on profile_idc (the ISO 14496-15 High-profile set, incl. 244) instead of leftover bytes, so trailing padding on a Baseline record no longer misparses as the chroma/bit-depth extension; Avcc::new emits the ext for High profiles; SPS (≤31) / PPS / NAL count & length overflow checks.
  • hvcC — array / NALU count & length overflow checks.
  • av01 — fixes initial_presentation_delay - 1 underflow and adds range checks (seq_profile, seq_level_idx_0, chroma_sample_position).
  • colrfull_range_flag decoded as byte & 0x80 != 0 (was == 0x80).
  • FLAC — opaque metadata blocks (Padding/Application/SeekTable/CueSheet/Picture/Reserved/Forbidden) retain their payload and round-trip instead of being dropped, and the last-metadata-block flag is always emitted. Variant shapes changed (they now carry Vec<u8>).
  • eac3 — reject > 13-bit data_rate and empty/oversized substream lists.
  • uncv — returns Err instead of panicking on a missing component_type_uri.
  • subs — size version derived from all subsamples (errors on mixed U16/U32); codec_specific_parameters length enforced to 4.

moov / moof

  • prft — nonconformant real_time without consistent_offset now errors instead of misparsing as an unrelated ReferenceTime.
  • irot — masks angle & 0x03 on encode to match decode.
  • mfraMfro.parent_size recomputed on encode; tfra drops the .expect() in favor of ?.

Not included / deviations

  • The issue listed avcC High 4:4:4 as profile 144; the real value is 244 (144 isn't a profile). Both are included in the gate set for safety; 244 is what the avcc_ext_2 fixture actually uses.
  • Hmhd (hint media header) — already present on main, so no change here.
  • String::decode missing-NUL — left lenient: many end-of-box string fields (hdlr name, url location, auxc, cprt) legitimately omit the trailing NUL in real files; strict decode would reject valid input, and re-encode already appends the NUL (box sizes recomputed, so benign).
  • Unknown FullBox flag bits — not preserved (would require an "unknown flags" field on every FullBox for reserved bits that are ~always 0).
  • minf "exactly one media header" — not enforced (risks rejecting real files; left as a separate decision).

Testing

cargo test --all-features (240 pass, incl. new tests for the largesize header format, FLAC padding round-trip, and Mdhd::default round-trip), cargo clippy --all-targets --all-features -- -D warnings, and cargo fmt --check all clean.

Closes #197

🤖 Generated with Claude Code

Addresses the low-severity, verified correctness items tracked in #197.
Prefers spec-correctness over back-compat (some previously-"successful"
but malformed encodes now return Err instead of silently truncating).

Core / types:
- u24/u48 TryFrom now reject out-of-range values (new Error::OutOfRange)
  instead of silently truncating; adds u48::MAX.
- FixedPoint Debug prints the true value (int + dec/2^bits), not int/dec.
- Header::encode uses checked arithmetic and a 32-bit-safe largesize branch;
  decode paths (sync/ReadFrom/tokio) use usize::try_from so a 64-bit
  largesize no longer truncates on 32-bit targets.
- Atoms >4 GiB now re-encode via the 64-bit largesize form instead of
  Error::TooLarge (new BufMut::insert_slice + shared write_box_size helper).
- Mdhd gets a manual Default of language "und" so it round-trips (empty
  string isn't representable in the packed form).

Codecs:
- avcC: extension block gated on profile_idc (High-profile set incl. 244)
  instead of leftover bytes; Avcc::new emits the ext for High profiles;
  SPS(<=31)/PPS/NAL count & length overflow checks.
- hvcC: array/NALU count & length overflow checks.
- av01: fix initial_presentation_delay underflow and add range checks.
- colr: full_range_flag decoded as (byte & 0x80 != 0).
- FLAC: opaque metadata blocks retain their payload and round-trip, and the
  last-metadata-block flag is always emitted (variant shapes changed).
- eac3: reject >13-bit data_rate and empty/oversized substream lists.
- uncv: return Err instead of panicking on a missing component_type_uri.
- subs: size version derived from all subsamples (errors on mixed widths);
  codec_specific_parameters length enforced to 4.

moov / moof:
- prft: nonconformant real_time without consistent_offset now errors.
- mfra: Mfro.parent_size recomputed on encode; tfra drops an .expect().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The changes add shared 32-bit and 64-bit box-size backfilling, buffer insertion support, and checked largesize decoding. They introduce explicit range errors for fixed-width values and strengthen validation across AV1, AVC, HEVC, E-AC-3, subsample, and component encoders. FLAC metadata payloads are preserved for round-tripping. Additional updates recalculate fragment sizes, define Mdhd defaults, correct fixed-point formatting, mask reserved bits, and reject invalid flag combinations.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.17% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the PR’s main purpose: fixing verified correctness issues from #197.
Description check ✅ Passed The description is directly about the same correctness fixes and behavior changes implemented in the patch.
Linked Issues check ✅ Passed The PR addresses the tracked correctness issues across types, headers, codecs, and round-trips, and the noted omissions are explicitly intentional.
Out of Scope Changes check ✅ Passed No clear unrelated changes stand out; the new helpers and API adjustments support the same correctness fixes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-197

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

❤️ Share

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

@bradh

bradh commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

@CodeRabbit: review

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

@bradh: I’ll review the changes, with particular attention to the new overflow checks, wire-format round trips, and intentional compatibility changes.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/moov/trak/mdia/minf/stbl/stsd/eac3.rs (1)

137-155: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate bit-packed substream fields to prevent adjacent field corruption.

While the outer list and data_rate are properly validated, the bit-packed fields within each substream are left unchecked. If an caller provides an out-of-range value (e.g., acmod > 0b111 or bsid > 0b11111), the bitwise left-shifts (<<) will overflow into neighboring fields within the encoded byte. This silently corrupts the container format, defeating the PR's objective of rejecting invalid codec fields.

Enforce boundary checks on the bit-packed properties before packing them.

🛡️ Proposed validation
         header.encode(buf)?;
         for substream in &self.substreams {
+            if substream.fscod > 0b11
+                || substream.bsid > 0b11111
+                || substream.bsmod > 0b111
+                || substream.acmod > 0b111
+                || substream.num_dep_sub > 0b1111
+                || substream.chan_loc.unwrap_or(0) > 0x1FF
+            {
+                return Err(Error::OutOfRange);
+            }
             // low bit is reserved = 0
             let b = (substream.fscod << 6) | (substream.bsid << 1);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/moov/trak/mdia/minf/stbl/stsd/eac3.rs` around lines 137 - 155, Validate
each substream’s bit-packed fields before encoding in the substream loop: ensure
fscod, bsid, bsmod, acmod, and num_dep_sub fit their allocated bit widths, and
ensure chan_loc fits the 9-bit dependent-channel field when applicable. Return
the established validation error for any out-of-range value before performing
the shifts, while preserving the existing packing for valid substreams.
🧹 Nitpick comments (1)
src/buf.rs (1)

145-149: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid double-reallocation when extending the buffer.

Because split_off shrinks the capacity of self to exactly pos, the subsequent extend_from_slice(val) call will trigger a reallocation. Furthermore, because self was reallocated, it is no longer contiguous with tail, causing unsplit(tail) to degenerate into extend_from_slice(tail.as_ref()) which triggers a second reallocation.

You can explicitly reserve enough capacity upfront to fit both val and tail, reducing this to a single reallocation and preventing the double-copy of the existing bytes.

⚡ Proposed optimization
     fn insert_slice(&mut self, pos: usize, val: &[u8]) {
         let tail = self.split_off(pos);
+        self.reserve(val.len() + tail.len());
         self.extend_from_slice(val);
         self.unsplit(tail);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/buf.rs` around lines 145 - 149, Update Buf::insert_slice to reserve
capacity for the final buffer size before splitting or extending, accounting for
both val.len() and the existing tail. Preserve the current insertion order and
unsplit behavior while ensuring extend_from_slice and unsplit do not cause
separate reallocations.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/mfra/tfra.rs`:
- Around line 86-91: Update the integer-width encoding match so the 0 and 1
branches use fallible conversions from value to u8 and u16, respectively,
matching the existing try_into behavior in the u24 branch and propagating
conversion errors instead of silently truncating.

In `@src/moov/trak/mdia/minf/stbl/stsd/flac.rs`:
- Around line 87-89: Replace the `as u32` cast in the payload-length calculation
with a checked `u32::try_from(data.len())`, mapping conversion failure to
`Error::TooLarge(Dfla::KIND)` before the existing `u24` conversion. Preserve the
subsequent `u24` bounds validation so lengths exceeding either limit fail
explicitly.

---

Outside diff comments:
In `@src/moov/trak/mdia/minf/stbl/stsd/eac3.rs`:
- Around line 137-155: Validate each substream’s bit-packed fields before
encoding in the substream loop: ensure fscod, bsid, bsmod, acmod, and
num_dep_sub fit their allocated bit widths, and ensure chan_loc fits the 9-bit
dependent-channel field when applicable. Return the established validation error
for any out-of-range value before performing the shifts, while preserving the
existing packing for valid substreams.

---

Nitpick comments:
In `@src/buf.rs`:
- Around line 145-149: Update Buf::insert_slice to reserve capacity for the
final buffer size before splitting or extending, accounting for both val.len()
and the existing tail. Preserve the current insertion order and unsplit behavior
while ensuring extend_from_slice and unsplit do not cause separate
reallocations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 97fc2c5f-88f4-4529-a5e2-f98836fa95a2

📥 Commits

Reviewing files that changed from the base of the PR and between ac798db and 30f4c67.

📒 Files selected for processing (20)
  • src/any.rs
  • src/atom.rs
  • src/buf.rs
  • src/error.rs
  • src/header.rs
  • src/meta/properties/irot.rs
  • src/mfra/mod.rs
  • src/mfra/tfra.rs
  • src/moov/trak/mdia/mdhd.rs
  • src/moov/trak/mdia/minf/stbl/stsd/av01.rs
  • src/moov/trak/mdia/minf/stbl/stsd/colr.rs
  • src/moov/trak/mdia/minf/stbl/stsd/eac3.rs
  • src/moov/trak/mdia/minf/stbl/stsd/flac.rs
  • src/moov/trak/mdia/minf/stbl/stsd/h264/avcc.rs
  • src/moov/trak/mdia/minf/stbl/stsd/hevc/hvcc.rs
  • src/moov/trak/mdia/minf/stbl/stsd/uncv.rs
  • src/moov/trak/mdia/minf/stbl/subs.rs
  • src/prft.rs
  • src/tokio/header.rs
  • src/types.rs

Comment thread src/mfra/tfra.rs
Comment on lines 86 to 91
0 => (value as u8).encode(buf),
1 => (value as u16).encode(buf),
2 => {
let v: u24 = value.try_into().expect("should have already been checked");
let v: u24 = value.try_into()?;
v.encode(buf)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use fallible conversions for all integer widths.

The as u8 and as u16 casts silently truncate value if it exceeds their respective maximums, which goes against the PR's objective of preventing silent truncation. Since you already converted the u24 case to use a fallible conversion, apply the same protection to the u8 and u16 cases.

🛡️ Proposed protection
     match num_bits_minus_one {
-        0 => (value as u8).encode(buf),
-        1 => (value as u16).encode(buf),
+        0 => u8::try_from(value).map_err(|_| Error::OutOfRange)?.encode(buf),
+        1 => u16::try_from(value).map_err(|_| Error::OutOfRange)?.encode(buf),
         2 => {
             let v: u24 = value.try_into()?;
             v.encode(buf)
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
0 => (value as u8).encode(buf),
1 => (value as u16).encode(buf),
2 => {
let v: u24 = value.try_into().expect("should have already been checked");
let v: u24 = value.try_into()?;
v.encode(buf)
}
0 => u8::try_from(value).map_err(|_| Error::OutOfRange)?.encode(buf),
1 => u16::try_from(value).map_err(|_| Error::OutOfRange)?.encode(buf),
2 => {
let v: u24 = value.try_into()?;
v.encode(buf)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mfra/tfra.rs` around lines 86 - 91, Update the integer-width encoding
match so the 0 and 1 branches use fallible conversions from value to u8 and u16,
respectively, matching the existing try_into behavior in the u24 branch and
propagating conversion errors instead of silently truncating.

Comment on lines +87 to +89
let length: u24 = (data.len() as u32)
.try_into()
.map_err(|_| Error::TooLarge(Dfla::KIND))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Prevent silent truncation when determining payload length.

Casting data.len() from usize to u32 via as u32 silently truncates lengths that exceed u32::MAX (e.g., on 64-bit platforms). For example, a slice of 4GB+1 byte would truncate to 1, which successfully passes the subsequent .try_into() bounds check for u24. This results in writing a length of 1 but emitting 4GB of payload, corrupting the bitstream.

To align with the PR's objective of preventing silent truncation, use u32::try_from() to strictly bounds-check the usize length before checking it against u24. Based on learnings, operations evaluating payload lengths should fail explicitly with an error instead of silently truncating.

🐛 Proposed fix
-        let length: u24 = (data.len() as u32)
+        let length: u24 = u32::try_from(data.len())
+            .map_err(|_| Error::TooLarge(Dfla::KIND))?
             .try_into()
             .map_err(|_| Error::TooLarge(Dfla::KIND))?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let length: u24 = (data.len() as u32)
.try_into()
.map_err(|_| Error::TooLarge(Dfla::KIND))?;
let length: u24 = u32::try_from(data.len())
.map_err(|_| Error::TooLarge(Dfla::KIND))?
.try_into()
.map_err(|_| Error::TooLarge(Dfla::KIND))?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/moov/trak/mdia/minf/stbl/stsd/flac.rs` around lines 87 - 89, Replace the
`as u32` cast in the payload-length calculation with a checked
`u32::try_from(data.len())`, mapping conversion failure to
`Error::TooLarge(Dfla::KIND)` before the existing `u24` conversion. Preserve the
subsequent `u24` bounds validation so lengths exceeding either limit fail
explicitly.

Source: Learnings

@kixelated
kixelated marked this pull request as draft July 18, 2026 11:27
@kixelated

Copy link
Copy Markdown
Owner Author

@bradh lemme know if you want this split up into a bunch of PRs...

@bradh

bradh commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Would probably be easier (aka sooner) to review, but only if your agent can do it without much fuss.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tracking: minor verified correctness issues (overflow edges, flag fidelity, encode truncations)

2 participants