analogous(results) and monochromatic(results) decrement their loop counter and test it for truthiness, so a counter that never lands exactly on 0 never terminates. Each iteration pushes a tinycolor instance, so the process exhausts its heap rather than hanging quietly.
// tinycolor.js:659 analogous
for (hsl.h = (hsl.h - (part * results >> 1) + 720) % 360; --results;) { ... }
// tinycolor.js:673 monochromatic
while (results--) { ... }
From results = -1 the sequence is -2, -3, -4, …. From results = 1.5 it is 0.5, -0.5, -1.5, …. Neither reaches 0.
Reproduction
Node 20, current main (npm/cjs/tinycolor.js), heap capped so it fails fast:
$ node --max-old-space-size=256 -e "require('./tinycolor.js')('red').analogous(-1)"
<--- Last few GCs --->
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
$ echo $?
134
Confirmed for both functions at -1, 1.5 and 0.5 — six cases, all exit 134. 0, 2 and null behave normally (0 and null fall back to the default of 6).
Why this seems worth guarding
polyad() already validates the same shape of input:
if (isNaN(number) || number <= 0) {
throw new Error("Argument to polyad must be a positive number");
}
So the hazard is recognised in one of the three combination functions but not the other two. Any caller passing a user-supplied count through to analogous() or monochromatic() — a palette-size field in a colour tool, say — has an unauthenticated way to exhaust the process heap.
Suggested fix
Either apply the guard polyad() uses, or coerce with Math.max(1, Math.floor(results)). The first is consistent with existing behaviour; the second avoids breaking any caller currently passing a fractional value that happens to work today.
Found while building a Rust port of TinyColor and differentially fuzzing it against the original — the port returns a finite list for these inputs, and the disagreement surfaced the loop. Happy to open a PR if a preferred approach is indicated.
analogous(results)andmonochromatic(results)decrement their loop counter and test it for truthiness, so a counter that never lands exactly on0never terminates. Each iteration pushes atinycolorinstance, so the process exhausts its heap rather than hanging quietly.From
results = -1the sequence is-2, -3, -4, …. Fromresults = 1.5it is0.5, -0.5, -1.5, …. Neither reaches0.Reproduction
Node 20, current
main(npm/cjs/tinycolor.js), heap capped so it fails fast:Confirmed for both functions at
-1,1.5and0.5— six cases, all exit 134.0,2andnullbehave normally (0andnullfall back to the default of 6).Why this seems worth guarding
polyad()already validates the same shape of input:So the hazard is recognised in one of the three combination functions but not the other two. Any caller passing a user-supplied count through to
analogous()ormonochromatic()— a palette-size field in a colour tool, say — has an unauthenticated way to exhaust the process heap.Suggested fix
Either apply the guard
polyad()uses, or coerce withMath.max(1, Math.floor(results)). The first is consistent with existing behaviour; the second avoids breaking any caller currently passing a fractional value that happens to work today.Found while building a Rust port of TinyColor and differentially fuzzing it against the original — the port returns a finite list for these inputs, and the disagreement surfaced the loop. Happy to open a PR if a preferred approach is indicated.