From f0647157ba70d3b9372e5eef05ed489c73107b62 Mon Sep 17 00:00:00 2001 From: John McNamara Date: Fri, 31 Oct 2025 08:37:11 +0000 Subject: [PATCH] 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 --- include/xlsxwriter/common.h | 3 + include/xlsxwriter/utility.h | 24 +++++ src/utility.c | 67 +++++++++++++ src/workbook.c | 5 + src/worksheet.c | 4 + test/unit/utility/test_datetime_validate.c | 110 +++++++++++++++++++++ 6 files changed, 213 insertions(+) create mode 100644 test/unit/utility/test_datetime_validate.c diff --git a/include/xlsxwriter/common.h b/include/xlsxwriter/common.h index 2b793223..f7b281f6 100644 --- a/include/xlsxwriter/common.h +++ b/include/xlsxwriter/common.h @@ -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, diff --git a/include/xlsxwriter/utility.h b/include/xlsxwriter/utility.h index 4aaec1c6..075bdcff 100644 --- a/include/xlsxwriter/utility.h +++ b/include/xlsxwriter/utility.h @@ -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. * diff --git a/src/utility.c b/src/utility.c index bd49e9bc..012474e0 100644 --- a/src/utility.c +++ b/src/utility.c @@ -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) { diff --git a/src/workbook.c b/src/workbook.c index f680a1a2..d31b1bf9 100644 --- a/src/workbook.c +++ b/src/workbook.c @@ -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); diff --git a/src/worksheet.c b/src/worksheet.c index 49e2c798..7fe438b4 100644 --- a/src/worksheet.c +++ b/src/worksheet.c @@ -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); diff --git a/test/unit/utility/test_datetime_validate.c b/test/unit/utility/test_datetime_validate.c new file mode 100644 index 00000000..633ad525 --- /dev/null +++ b/test/unit/utility/test_datetime_validate.c @@ -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); +} \ No newline at end of file