5496 : fixed UI crash from excessive tick generation with invalid dates/intervals

on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
This commit is contained in:
omkarht
2025-11-26 13:39:22 +05:30
parent 2346801c30
commit 6025ec663c
2 changed files with 105 additions and 46 deletions

View File

@@ -300,6 +300,16 @@ const getStartDate = function (prevTime, dateFormat, str) {
} else {
log.debug('Invalid date:' + str);
log.debug('With date format:' + dateFormat.trim());
// Only allow fallback for formats that are simple timestamps (x, X)
// which represent Unix timestamps. For all other formats, if dayjs
// strict parsing fails - throws an error.
const isTimestampFormat = dateFormat.trim() === 'x' || dateFormat.trim() === 'X';
if (!isTimestampFormat) {
throw new Error(`Invalid date: "${str}" does not match format "${dateFormat.trim()}".`);
}
const d = new Date(str);
if (
d === undefined ||

View File

@@ -78,6 +78,7 @@ const getMaxIntersections = (tasks, orderOffset) => {
};
let w;
const MAX_TICK_COUNT = 10000;
export const draw = function (text, id, version, diagObj) {
const conf = getConfig().gantt;
@@ -602,6 +603,30 @@ export const draw = function (text, id, version, diagObj) {
.attr('class', 'exclude-range');
}
/**
* Calculates the estimated number of ticks based on the time domain and tick interval.
* Returns the count or Infinity if there would be too many ticks.
* @param {Date} minTime - The minimum time in the domain
* @param {Date} maxTime - The maximum time in the domain
* @param {number} every - The interval count (e.g., 1 for "1second")
* @param {string} interval - The interval unit (e.g., "second", "day")
* @returns {number} The estimated number of ticks
*/
function getEstimatedTickCount(minTime, maxTime, every, interval) {
const timeDiffMs = maxTime - minTime;
const msPerUnit = {
millisecond: 1,
second: 1000,
minute: 60 * 1000,
hour: 60 * 60 * 1000,
day: 24 * 60 * 60 * 1000,
week: 7 * 24 * 60 * 60 * 1000,
month: 30 * 24 * 60 * 60 * 1000, // Approximate
};
const intervalMs = (msPerUnit[interval] || msPerUnit.day) * every;
return Math.ceil(timeDiffMs / intervalMs);
}
/**
* @param theSidePad
* @param theTopPad
@@ -630,10 +655,24 @@ export const draw = function (text, id, version, diagObj) {
);
if (resultTickInterval !== null) {
const every = resultTickInterval[1];
const every = parseInt(resultTickInterval[1], 10);
const interval = resultTickInterval[2];
const weekday = diagObj.db.getWeekday() || conf.weekday;
// Get the time domain to check tick count
const domain = timeScale.domain();
const minTime = domain[0];
const maxTime = domain[1];
const estimatedTicks = getEstimatedTickCount(minTime, maxTime, every, interval);
if (estimatedTicks > MAX_TICK_COUNT) {
log.warn(
`The tick interval "${every}${interval}" would generate ${estimatedTicks} ticks, ` +
`which exceeds the maximum allowed (${MAX_TICK_COUNT}). ` +
`This may indicate an invalid date or time range. Skipping custom tick interval.`
);
// D3 using its default automatic tick generation
} else {
switch (interval) {
case 'millisecond':
bottomXAxis.ticks(timeMillisecond.every(every));
@@ -658,6 +697,7 @@ export const draw = function (text, id, version, diagObj) {
break;
}
}
}
svg
.append('g')
@@ -677,10 +717,18 @@ export const draw = function (text, id, version, diagObj) {
.tickFormat(timeFormat(axisFormat));
if (resultTickInterval !== null) {
const every = resultTickInterval[1];
const every = parseInt(resultTickInterval[1], 10);
const interval = resultTickInterval[2];
const weekday = diagObj.db.getWeekday() || conf.weekday;
// Get the time domain to check tick count
const domain = timeScale.domain();
const minTime = domain[0];
const maxTime = domain[1];
const estimatedTicks = getEstimatedTickCount(minTime, maxTime, every, interval);
// Only apply custom ticks if the count is reasonable
if (estimatedTicks <= MAX_TICK_COUNT) {
switch (interval) {
case 'millisecond':
topXAxis.ticks(timeMillisecond.every(every));
@@ -705,6 +753,7 @@ export const draw = function (text, id, version, diagObj) {
break;
}
}
}
svg
.append('g')