datetime: add validation for date/time parameters

Added validation of lxw_datetime fields to ensure that they are
within Excel's allowable ranges.

Closes #491
This commit is contained in:
John McNamara 2025-10-31 08:37:11 +00:00
parent 9a48ca8996
commit f0647157ba
6 changed files with 213 additions and 0 deletions

View file

@ -110,6 +110,9 @@ typedef enum lxw_error {
/** Function string parameter is empty. */
LXW_ERROR_PARAMETER_IS_EMPTY,
/** A #lxw_datetime parameter has a validation error. */
LXW_ERROR_DATETIME_VALIDATION,
/** Worksheet name exceeds Excel's limit of 31 characters. */
LXW_ERROR_SHEETNAME_LENGTH_EXCEEDED,

View file

@ -212,6 +212,30 @@ double lxw_datetime_to_excel_datetime(lxw_datetime *datetime);
double lxw_datetime_to_excel_date_with_epoch(lxw_datetime *datetime,
uint8_t use_1904_epoch);
/**
* @brief Validate a #lxw_datetime struct.
*
* Validates a #lxw_datetime struct to ensure its fields are within acceptable
* ranges for Excel dates and times.
*
* The members of the #lxw_datetime struct and the range of their values are:
*
* Member | Value
* -------- | -----------
* year | 1900 - 9999
* month | 1 - 12
* day | 1 - 31
* hour | 0 - 23
* min | 0 - 59
* sec | 0 - 59.999
*
* @param datetime A pointer to a #lxw_datetime struct.
*
* @return A #lxw_error code. Either #LXW_NO_ERROR or
* #LXW_ERROR_DATETIME_VALIDATION if a field is out of range.
*/
lxw_error lxw_datetime_validate(lxw_datetime *datetime);
/**
* @brief Converts a unix datetime to an Excel datetime number.
*

View file

@ -41,6 +41,7 @@ char *error_strings[LXW_MAX_ERRNO + 1] = {
"NULL function parameter ignored.",
"Function parameter validation error.",
"Function string parameter is empty.",
"Datetime struct parameter has an invalid field value.",
"Worksheet name exceeds Excel's limit of 31 characters.",
"Worksheet name cannot contain invalid characters: '[ ] : * ? / \\'",
"Worksheet name cannot start or end with an apostrophe.",
@ -332,6 +333,69 @@ lxw_name_to_col_2(const char *col_str)
return 0;
}
lxw_error
lxw_datetime_validate(lxw_datetime *datetime)
{
if (!datetime)
return LXW_ERROR_DATETIME_VALIDATION;
/*
* Excel uses the year 1900 as the default epoch but it uses 1899-12-31 as
* the 0 date and internally we use the 0-0-0 date for time only values.
*/
if (datetime->year < 1900 &&
!(datetime->year == 0 &&
datetime->month == 0 && datetime->day == 0) &&
!(datetime->year == 1899 &&
datetime->month == 12 && datetime->day == 31)) {
LXW_WARN_FORMAT1("lxw_datetime_validate(): invalid year: %d. "
"Valid range is 1900-9999.", datetime->year);
return LXW_ERROR_DATETIME_VALIDATION;
}
if (datetime->year > 9999) {
LXW_WARN_FORMAT1("lxw_datetime_validate(): invalid year: %d. "
"Valid range is 1900-9999.", datetime->year);
return LXW_ERROR_DATETIME_VALIDATION;
}
if (datetime->year != 0) {
if (datetime->month < 1 || datetime->month > 12) {
LXW_WARN_FORMAT1("lxw_datetime_validate(): invalid month: %d. "
"Valid range is 1-12.", datetime->month);
return LXW_ERROR_DATETIME_VALIDATION;
}
if (datetime->day < 1 || datetime->day > 31) {
LXW_WARN_FORMAT1("lxw_datetime_validate(): invalid day: %d. "
"Valid range is 1-31.", datetime->day);
return LXW_ERROR_DATETIME_VALIDATION;
}
}
if (datetime->hour < 0 || datetime->hour > 23) {
LXW_WARN_FORMAT1("lxw_datetime_validate(): invalid hour: %d. "
"Valid range is 0-23.", datetime->hour);
return LXW_ERROR_DATETIME_VALIDATION;
}
if (datetime->min < 0 || datetime->min > 59) {
LXW_WARN_FORMAT1("lxw_datetime_validate(): invalid minute: %d. "
"Valid range is 0-59.", datetime->min);
return LXW_ERROR_DATETIME_VALIDATION;
}
if (datetime->sec < 0.0 || datetime->sec >= 60.0) {
LXW_WARN_FORMAT1("lxw_datetime_validate(): invalid seconds: %.3f. "
"Valid range is 0.0-59.999.", datetime->sec);
return LXW_ERROR_DATETIME_VALIDATION;
}
return LXW_NO_ERROR;
}
/*
* Convert a lxw_datetime struct to an Excel serial date, with a 1900
* or 1904 epoch.
@ -357,6 +421,9 @@ lxw_datetime_to_excel_date_with_epoch(lxw_datetime *datetime,
int days = 0;
int i;
if (lxw_datetime_validate(datetime) != LXW_NO_ERROR)
return 0.0;
/* For times without dates set the default date for the epoch. */
if (!year) {
if (!use_1904_epoch) {

View file

@ -13,6 +13,7 @@
#include "xlsxwriter/utility.h"
#include "xlsxwriter/packager.h"
#include "xlsxwriter/hash_table.h"
#include "xlsxwriter/hash_table.h"
STATIC int _worksheet_name_cmp(lxw_worksheet_name *name1,
lxw_worksheet_name *name2);
@ -2659,6 +2660,10 @@ workbook_set_custom_property_datetime(lxw_workbook *self, const char *name,
return LXW_ERROR_NULL_PARAMETER_IGNORED;
}
if (lxw_datetime_validate(datetime) != LXW_NO_ERROR) {
return LXW_ERROR_DATETIME_VALIDATION;
}
/* Create a struct to hold the custom property. */
custom_property = calloc(1, sizeof(struct lxw_custom_property));
RETURN_ON_MEM_ERROR(custom_property, LXW_ERROR_MEMORY_MALLOC_FAILED);

View file

@ -8334,6 +8334,10 @@ worksheet_write_datetime(lxw_worksheet *self,
if (err)
return err;
err = lxw_datetime_validate(datetime);
if (err)
return err;
excel_date =
lxw_datetime_to_excel_date_with_epoch(datetime, self->use_1904_epoch);

View file

@ -0,0 +1,110 @@
/*
* Tests for the libxlsxwriter library.
*
* SPDX-License-Identifier: BSD-2-Clause
* Copyright 2014-2025, John McNamara, jmcnamara@cpan.org.
*
*/
#include "../ctest.h"
#include "../helper.h"
#include "../../../include/xlsxwriter/utility.h"
// Test valid datetime.
CTEST(utility, test_datetime_validate01) {
lxw_datetime datetime = {2025, 10, 30, 21, 07, 0.0};
lxw_error exp = LXW_NO_ERROR;
lxw_error got = lxw_datetime_validate(&datetime);
ASSERT_EQUAL(exp, got);
}
// Test valid datetime (time only).
CTEST(utility, test_datetime_validate02) {
lxw_datetime datetime = {0, 0, 0, 21, 07, 0.0};
lxw_error exp = LXW_NO_ERROR;
lxw_error got = lxw_datetime_validate(&datetime);
ASSERT_EQUAL(exp, got);
}
// Test valid datetime (1900 epoch).
CTEST(utility, test_datetime_validate03) {
lxw_datetime datetime = {1899, 12, 31, 21, 07, 0.0};
lxw_error exp = LXW_NO_ERROR;
lxw_error got = lxw_datetime_validate(&datetime);
ASSERT_EQUAL(exp, got);
}
// Test invalid year.
CTEST(utility, test_datetime_validate04) {
lxw_datetime datetime = {1800, 10, 30, 21, 07, 0.0};
lxw_error exp = LXW_ERROR_DATETIME_VALIDATION;
lxw_error got = lxw_datetime_validate(&datetime);
ASSERT_EQUAL(exp, got);
}
// Test invalid month.
CTEST(utility, test_datetime_validate05) {
lxw_datetime datetime = {1900, 13, 30, 21, 07, 0.0};
lxw_error exp = LXW_ERROR_DATETIME_VALIDATION;
lxw_error got = lxw_datetime_validate(&datetime);
ASSERT_EQUAL(exp, got);
}
// Test invalid day.
CTEST(utility, test_datetime_validate06) {
lxw_datetime datetime = {1900, 10, 32, 21, 07, 0.0};
lxw_error exp = LXW_ERROR_DATETIME_VALIDATION;
lxw_error got = lxw_datetime_validate(&datetime);
ASSERT_EQUAL(exp, got);
}
// Test invalid hour.
CTEST(utility, test_datetime_validate07) {
lxw_datetime datetime = {1900, 1, 1, 24, 07, 0.0};
lxw_error exp = LXW_ERROR_DATETIME_VALIDATION;
lxw_error got = lxw_datetime_validate(&datetime);
ASSERT_EQUAL(exp, got);
}
// Test invalid minute.
CTEST(utility, test_datetime_validate08) {
lxw_datetime datetime = {1900, 1, 1, 21, 60, 0.0};
lxw_error exp = LXW_ERROR_DATETIME_VALIDATION;
lxw_error got = lxw_datetime_validate(&datetime);
ASSERT_EQUAL(exp, got);
}
// Test invalid second.
CTEST(utility, test_datetime_validate09) {
lxw_datetime datetime = {1900, 1, 1, 21, 07, 60.0};
lxw_error exp = LXW_ERROR_DATETIME_VALIDATION;
lxw_error got = lxw_datetime_validate(&datetime);
ASSERT_EQUAL(exp, got);
}