Skip to content

fix: guard getNativeAspectRatioValue against zero denominator#455

Open
shaun0927 wants to merge 3 commits intosiddharthvaddem:mainfrom
shaun0927:fix/native-aspect-ratio-zero-guard
Open

fix: guard getNativeAspectRatioValue against zero denominator#455
shaun0927 wants to merge 3 commits intosiddharthvaddem:mainfrom
shaun0927:fix/native-aspect-ratio-zero-guard

Conversation

@shaun0927
Copy link
Copy Markdown
Contributor

@shaun0927 shaun0927 commented Apr 16, 2026

Description

getNativeAspectRatioValue in src/utils/aspectRatioUtils.ts performed an unguarded division and could return Infinity, NaN, or negative values. The result flows into nine call sites in VideoEditor.tsx and VideoPlayback.tsx that use it for canvas sizing and CSS aspect-ratio, so a bad value collapses the preview to 0×0 or produces a broken export.

This PR clamps the denominator and falls back to 16 / 9 — the same default getAspectRatioValue already returns for the "native" case — whenever the computed ratio is not a finite positive number. A new vitest suite locks the behaviour in.

Motivation

Introduced in v1.3.0 when the "native" aspect ratio option was added. Two realistic paths hit the bug:

  1. Switching the aspect ratio dropdown to Native before the underlying <video> element has fired loadedmetadata (videoHeight === 0).
  2. Dragging a crop handle so the crop region height reaches 0 while Native is active.

Both cases reproduce with a one-liner against v1.3.0 source:

node -e 'const f=(w,h,c)=>{const cw=c?.width??1;const ch=c?.height??1;return (w*cw)/(h*ch);}; console.log(f(1920,0)); console.log(f(1920,1080,{x:0,y:0,width:0.5,height:0}))'
// Infinity
// Infinity

Type of Change

  • New Feature
  • Bug Fix
  • Refactor / Code Cleanup
  • Documentation Update
  • Other (please specify)

Related Issue(s)

Fixes #454

Testing

  • New test file src/utils/aspectRatioUtils.test.ts (8 cases) — covers the normal path, crop-applied path, videoHeight = 0, both-zero, cropHeight = 0, NaN inputs, negative dimensions, and a property-style check that no pathological input returns Infinity / NaN / non-positive.
  • npx vitest run src/utils/aspectRatioUtils.test.ts → 8/8 pass.

Manual repro steps for reviewers:

  1. Open a video, and while it is still buffering metadata, switch the aspect ratio dropdown to Native — preview no longer collapses.
  2. With Native active, drag the crop bottom edge up until the crop height reaches 0 — the layout stays on the 16/9 fallback instead of breaking.

Checklist

  • I have performed a self-review of my code.
  • I have added any necessary screenshots or videos.
  • I have linked related issue(s) and updated the changelog if applicable.

Summary by CodeRabbit

  • Bug Fixes

    • Improved aspect-ratio handling: calculations validate inputs and consistently fall back to a standard 16:9 ratio for invalid, missing, non-positive or non-finite dimensions, ensuring returned ratios are always finite and positive. "Native" aspect formatting now uses this fallback when no native value is available.
  • Tests

    • Added comprehensive tests covering normal, edge, and pathological aspect-ratio scenarios.

The helper performed an unguarded division, returning Infinity, NaN,
or negative values when videoHeight, the crop region height, or the
numerator was 0 / non-finite. The result is consumed by nine call
sites in VideoEditor/VideoPlayback that feed canvas sizing and CSS
aspect-ratio, producing 0x0 previews before video metadata loads or
when the crop height collapses.

Fall back to 16/9 (the same default getAspectRatioValue uses for
'native') whenever the computed ratio is not a finite positive number.
Covered by a new vitest suite exercising zero, NaN, negative, and
Infinity inputs.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 16, 2026

📝 Walkthrough

Walkthrough

A module-level fallback NATIVE_ASPECT_FALLBACK = 16/9 was added. getNativeAspectRatioValue() now computes numerator/denominator with crop scaling and returns the fallback when numerator/denominator are non-finite or denominator ≤ 0, or when the final ratio is non-finite or ≤ 0. A Vitest suite was added for normal, crop, and pathological inputs.

Changes

Cohort / File(s) Summary
Tests
src/utils/aspectRatioUtils.test.ts
Added a Vitest suite (+67 lines) asserting correct native ratio for standard dims, crop scaling behavior, and fallbacks for zero/NaN/Infinity/non-positive inputs; verifies returned ratio is finite and > 0.
Aspect ratio utils
src/utils/aspectRatioUtils.ts
Introduced NATIVE_ASPECT_FALLBACK = 16/9; refactored getNativeAspectRatioValue() to use numerator = videoWidth * cropW and denominator = videoHeight * cropH, added finiteness and denominator>0 guards, and made formatAspectRatioForCSS() use the constant when needed.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Suggested reviewers

  • siddharthvaddem
  • FabLrc

Poem

at 2am the math was kinda cursed,
infinities popped up and things got worse.
we planted sixteen over nine as a charm,
tests hush the chaos, nothing breaks the farm.
lowkey tidy, now the ratios behave.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% 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 Title accurately describes the main fix: guarding against zero denominator in getNativeAspectRatioValue. Clear, concise, and directly related to the core change.
Description check ✅ Passed Description is comprehensive: covers motivation, type of change, related issue, testing steps, manual reproduction, and checkbox completion. Follows template structure with all critical sections filled out.
Linked Issues check ✅ Passed Changes address the core requirement in #454: guards denominator against zero/non-finite values and falls back to 16/9. Adds comprehensive test coverage (8 test cases) matching the objectives.
Out of Scope Changes check ✅ Passed All changes are tightly scoped to the bug fix: refactored getNativeAspectRatioValue with denominator guards, introduced NATIVE_ASPECT_FALLBACK constant, and added comprehensive test suite. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/utils/aspectRatioUtils.ts (1)

44-45: Use the fallback constant everywhere (nit: cleaner).

good call introducing NATIVE_ASPECT_FALLBACK; lowkey risky to keep other 16/9 literals around in the same module because they can drift later. Reuse this constant in the "native" branches too.

Suggested diff
 export function getAspectRatioValue(aspectRatio: AspectRatio): number {
 	switch (aspectRatio) {
@@
 		case "native":
-			return 16 / 9;
+			return NATIVE_ASPECT_FALLBACK;
@@
 export function formatAspectRatioForCSS(aspectRatio: AspectRatio, nativeRatio?: number): string {
-	if (aspectRatio === "native") return String(nativeRatio ?? 16 / 9);
+	if (aspectRatio === "native") return String(nativeRatio ?? NATIVE_ASPECT_FALLBACK);
 	return aspectRatio.replace(":", "/");
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/aspectRatioUtils.ts` around lines 44 - 45, Some places still use
the literal 16/9 instead of the new constant; replace those literals with the
NATIVE_ASPECT_FALLBACK constant so the module uses one source of truth. Locate
the branches/handlers that check for the "native" mode (e.g., the "native" case
in any getAspectRatio or computeAspect* functions) and swap any 16/9 numeric
literals for NATIVE_ASPECT_FALLBACK, and also search the module for other 16/9
occurrences to replace them with the constant.
src/utils/aspectRatioUtils.test.ts (1)

39-52: Consider extending the pathological loop to include crop-region weirdness.

optional but useful: add cases with crop values like height: Number.NaN, width: Number.POSITIVE_INFINITY, width: 0, height: -1 so future edits don’t accidentally re-open that branch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/aspectRatioUtils.test.ts` around lines 39 - 52, Extend the
pathologicalInputs array used in the "never returns Infinity, NaN, or a
non-positive ratio" test for getNativeAspectRatioValue to include crop-region
edge cases (e.g., [width, height] entries with height: Number.NaN, width:
Number.POSITIVE_INFINITY, width: 0, height: -1) so the loop tests these
additional invalid inputs; update the array in
src/utils/aspectRatioUtils.test.ts where getNativeAspectRatioValue is invoked to
include those tuples and keep the same assertions (Number.isFinite(ratio) and
ratio > 0).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/utils/aspectRatioUtils.test.ts`:
- Around line 39-52: Extend the pathologicalInputs array used in the "never
returns Infinity, NaN, or a non-positive ratio" test for
getNativeAspectRatioValue to include crop-region edge cases (e.g., [width,
height] entries with height: Number.NaN, width: Number.POSITIVE_INFINITY, width:
0, height: -1) so the loop tests these additional invalid inputs; update the
array in src/utils/aspectRatioUtils.test.ts where getNativeAspectRatioValue is
invoked to include those tuples and keep the same assertions
(Number.isFinite(ratio) and ratio > 0).

In `@src/utils/aspectRatioUtils.ts`:
- Around line 44-45: Some places still use the literal 16/9 instead of the new
constant; replace those literals with the NATIVE_ASPECT_FALLBACK constant so the
module uses one source of truth. Locate the branches/handlers that check for the
"native" mode (e.g., the "native" case in any getAspectRatio or computeAspect*
functions) and swap any 16/9 numeric literals for NATIVE_ASPECT_FALLBACK, and
also search the module for other 16/9 occurrences to replace them with the
constant.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: acd95585-1039-43cc-8fc7-00d049b900a9

📥 Commits

Reviewing files that changed from the base of the PR and between a6ae0e6 and b8d05b0.

📒 Files selected for processing (2)
  • src/utils/aspectRatioUtils.test.ts
  • src/utils/aspectRatioUtils.ts

…nstant

Move the NATIVE_ASPECT_FALLBACK constant above its first usage and
replace all remaining 16/9 literals in getAspectRatioValue (native case)
and formatAspectRatioForCSS with the shared constant so the fallback
value has a single source of truth.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Qodo-Free-For-OSS
Copy link
Copy Markdown

Hi, The new crop-related test uses equal crop width/height (0.5/0.5), so the ratio is unchanged and the test would pass even if cropRegion were ignored. This leaves a regression hole around applying cropRegion in getNativeAspectRatioValue.

Severity: action required | Category: maintainability

How to fix: Use non-canceling crop values

Agent prompt to fix - you can give this to your LLM of choice:

Issue description

The test "applies the crop region when provided" does not actually verify that the crop is applied because the chosen crop dimensions cancel out in the ratio.

Issue Context

Current test uses width: 0.5, height: 0.5, which yields the same ratio as no crop.

Fix Focus Areas

  • src/utils/aspectRatioUtils.test.ts[11-14]

Suggested change

Change the crop to non-proportional values so the ratio changes, e.g.:

  • const crop = { x: 0, y: 0, width: 0.75, height: 0.5 };
  • Expect getNativeAspectRatioValue(1920, 1080, crop) to be close to (1920*0.75)/(1080*0.5) = 8/3

We noticed a couple of other issues in this PR as well - happy to share if helpful.


Found by Qodo code review

…eling values

The previous "applies the crop region when provided" case used
width/height = 0.5/0.5, which cancels out and returns the same ratio as
no crop — the test would still pass even if cropRegion were ignored.
Switch to non-proportional 0.75 × 0.5 (expected 8/3) so a future
regression in the crop branch actually fails the assertion.

Also broaden the pathological-input loop to drive the crop-region
branch directly: zero / NaN / Infinity / negative crop dimensions, all
of which must still resolve to the 16/9 fallback.
@shaun0927
Copy link
Copy Markdown
Contributor Author

@qodo-ai-reviewer Good catch — the 0.5 × 0.5 crop did cancel out and would've passed even if cropRegion were ignored. Switched to 0.75 × 0.5 (expected 8/3) so the assertion actually exercises the crop branch, and extended the pathological-input loop to drive zero / NaN / Infinity / negative crop dimensions through the same fallback path. Pushed in 5597651.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/utils/aspectRatioUtils.test.ts`:
- Around line 42-65: The test currently only asserts finiteness and positivity
for pathologicalInputs in the loop using getNativeAspectRatioValue, which lets
incorrect positive values slip through; change the assertions for these specific
cases to assert the exact fallback 16/9 behavior (e.g., replace the
Number.isFinite and >0 checks with an assertion that ratio equals the fallback
16/9, using a strict or numeric-precision matcher such as toBeCloseTo if needed)
so the pathological-input loop enforces the contract that
getNativeAspectRatioValue returns the 16/9 default for those inputs.
🪄 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: 0dfc1a49-b6c5-4625-92df-cea61f9a77d5

📥 Commits

Reviewing files that changed from the base of the PR and between 271268e and 5597651.

📒 Files selected for processing (1)
  • src/utils/aspectRatioUtils.test.ts

Comment on lines +42 to +65
it("never returns Infinity, NaN, or a non-positive ratio", () => {
const pathologicalInputs: Array<
[number, number, { x: number; y: number; width: number; height: number }?]
> = [
[0, 0],
[1920, 0],
[0, 1080],
[Number.POSITIVE_INFINITY, 1080],
[1920, Number.POSITIVE_INFINITY],
[Number.NaN, Number.NaN],
// Same idea, but exercising the crop-region branch so a future
// regression there can't slip past the dimension-only cases above.
[1920, 1080, { x: 0, y: 0, width: 0.5, height: 0 }],
[1920, 1080, { x: 0, y: 0, width: 0, height: 0.5 }],
[1920, 1080, { x: 0, y: 0, width: Number.NaN, height: 0.5 }],
[1920, 1080, { x: 0, y: 0, width: 0.5, height: Number.NaN }],
[1920, 1080, { x: 0, y: 0, width: Number.POSITIVE_INFINITY, height: 0.5 }],
[1920, 1080, { x: 0, y: 0, width: 0.5, height: -1 }],
];
for (const [w, h, crop] of pathologicalInputs) {
const ratio = getNativeAspectRatioValue(w, h, crop);
expect(Number.isFinite(ratio)).toBe(true);
expect(ratio).toBeGreaterThan(0);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

tighten the pathological-input assertion to enforce the actual contract.

right now (Line 63–64) you only assert “finite + positive”. that still allows incorrect positive values to pass. for these pathological cases, expected behavior is specifically fallback 16/9.

nit: stronger assertion (prevents sneaky regressions)
  for (const [w, h, crop] of pathologicalInputs) {
    const ratio = getNativeAspectRatioValue(w, h, crop);
    expect(Number.isFinite(ratio)).toBe(true);
    expect(ratio).toBeGreaterThan(0);
+   expect(ratio).toBe(FALLBACK);
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/aspectRatioUtils.test.ts` around lines 42 - 65, The test currently
only asserts finiteness and positivity for pathologicalInputs in the loop using
getNativeAspectRatioValue, which lets incorrect positive values slip through;
change the assertions for these specific cases to assert the exact fallback 16/9
behavior (e.g., replace the Number.isFinite and >0 checks with an assertion that
ratio equals the fallback 16/9, using a strict or numeric-precision matcher such
as toBeCloseTo if needed) so the pathological-input loop enforces the contract
that getNativeAspectRatioValue returns the 16/9 default for those inputs.

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.

[Bug]: getNativeAspectRatioValue returns Infinity/NaN before video metadata loads

2 participants