From 77904ae5e909ad86d49717009a175fd11cb47d9a Mon Sep 17 00:00:00 2001 From: Danish Ali Siddiqui Date: Tue, 18 Aug 2026 15:02:26 +0530 Subject: [PATCH] Guard analogous() and monochromatic() against counts that never terminate Both functions decrement their loop counter and test it for truthiness: for (hsl.h = ...; --results; ) // analogous while (results--) // monochromatic A counter that never lands exactly on 0 never stops. From -1 the sequence is -2, -3, -4...; from 1.5 it is 0.5, -0.5, -1.5... Each pass pushes another colour, so the process exhausts its heap and aborts with exit 134. $ node --max-old-space-size=256 -e "require('tinycolor2')('red').analogous(-1)" FATAL ERROR: Reached heap limit Allocation failed $ echo $? 134 Six cases reproduce: both functions at -1, 1.5 and 0.5. The reason this survived is that `results = results || 6` looks like a default but acts as an accidental guard. 0, null, undefined, NaN and false are falsy, so every value someone would casually test gets replaced by 6. -1, 1.5 and 0.5 are truthy and pass straight through. polyad() already validates the same shape of argument, so this brings the other two combination functions in line with it. The guard additionally rejects non-integers, which polyad does not need: its loop is `for (i = 1; i < number; i++)` and terminates on fractional counts, whereas these two cannot. Falsy inputs still fall through to the default of 6, unchanged. Fixes #280 --- mod.js | 8 ++++++++ npm/cjs/tinycolor.js | 8 ++++++++ npm/esm/tinycolor.js | 8 ++++++++ test.js | 20 ++++++++++++++++++++ tinycolor.js | 8 ++++++++ 5 files changed, 52 insertions(+) diff --git a/mod.js b/mod.js index be6b3825..a584ee19 100644 --- a/mod.js +++ b/mod.js @@ -745,6 +745,10 @@ function analogous(color, results, slices) { results = results || 6; slices = slices || 30; + if (isNaN(results) || results <= 0 || results % 1 !== 0) { + throw new Error("Argument to analogous must be a positive integer"); + } + var hsl = tinycolor(color).toHsl(); var part = 360 / slices; var ret = [tinycolor(color)]; @@ -758,6 +762,10 @@ function analogous(color, results, slices) { function monochromatic(color, results) { results = results || 6; + + if (isNaN(results) || results <= 0 || results % 1 !== 0) { + throw new Error("Argument to monochromatic must be a positive integer"); + } var hsv = tinycolor(color).toHsv(); var h = hsv.h, s = hsv.s, diff --git a/npm/cjs/tinycolor.js b/npm/cjs/tinycolor.js index 4f584cab..8ef99ea5 100644 --- a/npm/cjs/tinycolor.js +++ b/npm/cjs/tinycolor.js @@ -653,6 +653,10 @@ function _analogous(color, results, slices) { results = results || 6; slices = slices || 30; + + if (isNaN(results) || results <= 0 || results % 1 !== 0) { + throw new Error("Argument to analogous must be a positive integer"); + } var hsl = tinycolor(color).toHsl(); var part = 360 / slices; var ret = [tinycolor(color)]; @@ -664,6 +668,10 @@ } function _monochromatic(color, results) { results = results || 6; + + if (isNaN(results) || results <= 0 || results % 1 !== 0) { + throw new Error("Argument to monochromatic must be a positive integer"); + } var hsv = tinycolor(color).toHsv(); var h = hsv.h, s = hsv.s, diff --git a/npm/esm/tinycolor.js b/npm/esm/tinycolor.js index 374f5ea9..d00a9e41 100644 --- a/npm/esm/tinycolor.js +++ b/npm/esm/tinycolor.js @@ -647,6 +647,10 @@ function _splitcomplement(color) { function _analogous(color, results, slices) { results = results || 6; slices = slices || 30; + + if (isNaN(results) || results <= 0 || results % 1 !== 0) { + throw new Error("Argument to analogous must be a positive integer"); + } var hsl = tinycolor(color).toHsl(); var part = 360 / slices; var ret = [tinycolor(color)]; @@ -658,6 +662,10 @@ function _analogous(color, results, slices) { } function _monochromatic(color, results) { results = results || 6; + + if (isNaN(results) || results <= 0 || results % 1 !== 0) { + throw new Error("Argument to monochromatic must be a positive integer"); + } var hsv = tinycolor(color).toHsv(); var h = hsv.h, s = hsv.s, diff --git a/test.js b/test.js index d6003428..5c6e7cd0 100644 --- a/test.js +++ b/test.js @@ -2138,6 +2138,26 @@ Deno.test("tetrad", function () { ); }); +Deno.test("analogous and monochromatic reject counts that never terminate", function () { + // `--results` / `results--` test for truthiness, so a negative or fractional + // count never lands on 0 and the loop allocates until the heap is gone. + // `polyad` already guards the same shape of input. + for (const bad of [-1, 1.5, 0.5, -0.5]) { + assertThrows(() => { + tinycolor("red").analogous(bad); + }); + assertThrows(() => { + tinycolor("red").monochromatic(bad); + }); + } + + // Falsy values still fall through to the default of 6, unchanged. + assertEquals(tinycolor("red").analogous().length, 6); + assertEquals(tinycolor("red").analogous(0).length, 6); + assertEquals(tinycolor("red").monochromatic(null).length, 6); + assertEquals(tinycolor("red").analogous(3).length, 3); +}); + Deno.test({ name: "polyad", // Disabled until https://github.com/bgrins/TinyColor/issues/254 diff --git a/tinycolor.js b/tinycolor.js index e52a3d55..25daf20a 100644 --- a/tinycolor.js +++ b/tinycolor.js @@ -652,6 +652,10 @@ function _analogous(color, results, slices) { results = results || 6; slices = slices || 30; + + if (isNaN(results) || results <= 0 || results % 1 !== 0) { + throw new Error("Argument to analogous must be a positive integer"); + } var hsl = tinycolor(color).toHsl(); var part = 360 / slices; var ret = [tinycolor(color)]; @@ -663,6 +667,10 @@ } function _monochromatic(color, results) { results = results || 6; + + if (isNaN(results) || results <= 0 || results % 1 !== 0) { + throw new Error("Argument to monochromatic must be a positive integer"); + } var hsv = tinycolor(color).toHsv(); var h = hsv.h, s = hsv.s,