Skip to content

Commit 5874685

Browse files
dazzatronusDerk Zomer
andauthored
fix: reject invalid hex and merge tokens in color sanitize (#212)
The prior HEX_PATTERN treated 5-digit values like #00000 as valid, so they still reached Pixi Color and threw. Align with Pixi's 3/4/6/8 rule, cover unsubstituted {{ VAR }} tokens, and sanitize timeline background fills before Graphics fillStyle. Co-authored-by: Derk Zomer <derk@shotstack.io>
1 parent b762f4f commit 5874685

3 files changed

Lines changed: 21 additions & 5 deletions

File tree

‎src/components/canvas/shotstack-canvas.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { AlignmentGuides } from "@canvas/system/alignment-guides";
33
import { createWebGLErrorOverlay } from "@canvas/webgl-error-overlay";
44
import { Edit } from "@core/edit-session";
55
import { InternalEvent } from "@core/events/edit-events";
6+
import { sanitizeColor } from "@core/shared/color-utils";
67
import { ms } from "@core/timing/types";
78
import type { UIController } from "@core/ui/ui-controller";
89
import { checkWebGLSupport, WebGLUnsupportedError } from "@core/webgl-support";
@@ -110,7 +111,7 @@ export class Canvas {
110111
this.viewportContainer.sortableChildren = true;
111112

112113
this.editBackground = new pixi.Graphics();
113-
this.editBackground.fillStyle = { color: this.edit.getTimelineBackground() };
114+
this.editBackground.fillStyle = { color: sanitizeColor(this.edit.getTimelineBackground(), "#000000") };
114115
this.editBackground.rect(0, 0, this.edit.size.width, this.edit.size.height);
115116
this.editBackground.fill();
116117
this.viewportContainer.addChild(this.editBackground);
@@ -533,7 +534,7 @@ export class Canvas {
533534
public updateViewportForSize(width: number, height: number, backgroundColor: string): void {
534535
if (this.editBackground) {
535536
this.editBackground.clear();
536-
this.editBackground.fillStyle = { color: backgroundColor };
537+
this.editBackground.fillStyle = { color: sanitizeColor(backgroundColor, "#000000") };
537538
this.editBackground.rect(0, 0, width, height);
538539
this.editBackground.fill();
539540
}

‎src/core/shared/color-utils.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
* Color helpers for Pixi text/style construction.
33
*/
44

5-
/** Matches Pixi Color HEX_PATTERN (3/4/6/8 hex digits, optional # or 0x). */
6-
const HEX_PATTERN = /^(#|0x)?(([a-f0-9]{3}){1,2}([a-f0-9]{2})?)$/i;
5+
/** Matches Pixi Color HEX_PATTERN: exactly 3, 4, 6, or 8 hex digits (optional # or 0x). */
6+
const HEX_PATTERN = /^(#|0x)?([a-f0-9]{3}|[a-f0-9]{4}|[a-f0-9]{6}|[a-f0-9]{8})$/i;
77
const FUNCTIONAL_COLOR = /^(rgb|rgba|hsl|hsla)\(/i;
88
const CSS_COLOR_NAME = /^[a-z]+$/i;
99

1010
/**
1111
* Return a Pixi-safe color string, or `fallback` when the value is missing/invalid.
12-
* Incomplete hex (e.g. `#00`, `#0`) and empty strings fall back instead of throwing in TextStyle.
12+
* Incomplete hex (e.g. `#00`, `#00000`), unsubstituted merge tokens (e.g. `{{ BG_COLOR }}`),
13+
* and empty strings fall back instead of throwing in TextStyle / Graphics fillStyle.
1314
* @internal
1415
*/
1516
export function sanitizeColor(color: string | undefined | null, fallback = "#ffffff"): string {

‎tests/color-utils.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ describe("sanitizeColor", () => {
88
expect(sanitizeColor(" ")).toBe("#ffffff");
99
});
1010

11+
it("falls back for 5-digit hex that previously slipped past the sanitizer", () => {
12+
expect(sanitizeColor("#00000")).toBe("#ffffff");
13+
expect(sanitizeColor("#12345")).toBe("#ffffff");
14+
expect(sanitizeColor("0x00000")).toBe("#ffffff");
15+
});
16+
17+
it("falls back for unsubstituted merge-field tokens", () => {
18+
expect(sanitizeColor("{{ BG_COLOR }}")).toBe("#ffffff");
19+
expect(sanitizeColor("{{BG_COLOR}}")).toBe("#ffffff");
20+
expect(sanitizeColor("{{ FONT_COLOR }}", "#000000")).toBe("#000000");
21+
});
22+
1123
it("falls back for nullish and non-string values", () => {
1224
expect(sanitizeColor(undefined)).toBe("#ffffff");
1325
expect(sanitizeColor(null)).toBe("#ffffff");
@@ -16,12 +28,14 @@ describe("sanitizeColor", () => {
1628
it("preserves valid hex, named, and functional colors", () => {
1729
expect(sanitizeColor("#ffffff")).toBe("#ffffff");
1830
expect(sanitizeColor("#fff")).toBe("#fff");
31+
expect(sanitizeColor("#ffff")).toBe("#ffff");
1932
expect(sanitizeColor("#ff00ff00")).toBe("#ff00ff00");
2033
expect(sanitizeColor("red")).toBe("red");
2134
expect(sanitizeColor("rgb(255, 0, 0)")).toBe("rgb(255, 0, 0)");
2235
});
2336

2437
it("uses a custom fallback when provided", () => {
2538
expect(sanitizeColor("#00", "#000000")).toBe("#000000");
39+
expect(sanitizeColor("#00000", "#000000")).toBe("#000000");
2640
});
2741
});

0 commit comments

Comments
 (0)