grub-devel.gnu.org archive mirror
 help / color / mirror / Atom feed
* [PATCH v6 0/2] Support dates outside of 1901..2038 range
@ 2025-08-27 22:32 Andrew Hamilton
  2025-08-27 22:32 ` [PATCH v6 1/2] datetime: " Andrew Hamilton
                   ` (2 more replies)
  0 siblings, 3 replies; 4+ messages in thread
From: Andrew Hamilton @ 2025-08-27 22:32 UTC (permalink / raw)
  To: grub-devel
  Cc: daniel.kiper, masayuki.moriyama, andrea.biardi, phcoder,
	Andrew Hamilton

Support dates outside of 1901..2038.
Add tests for dates outside this range.
Vast majority of the work was done by Vladimir Serbinenko

Fixes: https://savannah.gnu.org/bugs/?63894
Fixes: https://savannah.gnu.org/bugs/?66301

V5 -> V6:
datetime.c:
 Move comment line "There are three 100-year periods worth of leap
  days (3*24)" down one line.
 Change order of terms in 'days' calculation to be more readable.
date_unit_test.c:
 Correct comment to "Leap year, after Feb" instead of "prior to Feb"
Add "reviewed by" from Daniel Kiper for patches with permission.

V4 -> V5:
datetime.c / h:
 Rename variables "is_bisextile" to "bisextile".
 Change some boolean assignments to use ternary operator.
 Compare booleans to true instead of checking "if(boolean)".
date_unit_test.c:
 Add additional commentary to the tests array for meaning of numbers.
 Rework test ranges to cover from years 0001 to 9999 and add specific
 min/max tests for min 0001 date and max 9999 date.

V3 -> V4: Add additional commentary for a few magic numbers
 and fix some coding standard issues.

V2 -> V3: Added additional commentary to datetime.c to describe
 the origin of some of the numbers used in calculations.

Andrew Hamilton (2):
  datetime: Support dates outside of 1901..2038 range
  date_unit_test: test dates outside of 32-bit unix range

 grub-core/lib/datetime.c | 65 +++++++++++++++++++++++++++-------
 include/grub/datetime.h  | 28 ++++++++++-----
 tests/date_unit_test.c   | 75 +++++++++++++++++++++++++++++++++++-----
 3 files changed, 139 insertions(+), 29 deletions(-)

-- 
2.39.5


_______________________________________________
Grub-devel mailing list
Grub-devel@gnu.org
https://lists.gnu.org/mailman/listinfo/grub-devel

^ permalink raw reply	[flat|nested] 4+ messages in thread

* [PATCH v6 1/2] datetime: Support dates outside of 1901..2038 range
  2025-08-27 22:32 [PATCH v6 0/2] Support dates outside of 1901..2038 range Andrew Hamilton
@ 2025-08-27 22:32 ` Andrew Hamilton
  2025-08-27 22:32 ` [PATCH v6 2/2] date_unit_test: test dates outside of 32-bit unix range Andrew Hamilton
  2025-08-28 14:01 ` [PATCH v6 0/2] Support dates outside of 1901..2038 range Daniel Kiper via Grub-devel
  2 siblings, 0 replies; 4+ messages in thread
From: Andrew Hamilton @ 2025-08-27 22:32 UTC (permalink / raw)
  To: grub-devel
  Cc: daniel.kiper, masayuki.moriyama, andrea.biardi, phcoder,
	Andrew Hamilton

Support dates outside of 1901..2038.

Fixes: https://savannah.gnu.org/bugs/?63894
Fixes: https://savannah.gnu.org/bugs/?66301

Signed-off-by: Vladimir Serbinenko <phcoder@gmail.com>
Signed-off-by: Andrew Hamilton <adhamilt@gmail.com>
Reviewed-by: Daniel Kiper <daniel.kiper@oracle.com>
---
 grub-core/lib/datetime.c | 65 ++++++++++++++++++++++++++++++++--------
 include/grub/datetime.h  | 28 ++++++++++++-----
 2 files changed, 73 insertions(+), 20 deletions(-)

diff --git a/grub-core/lib/datetime.c b/grub-core/lib/datetime.c
index 8f0922fb0..dbce17e03 100644
--- a/grub-core/lib/datetime.c
+++ b/grub-core/lib/datetime.c
@@ -64,7 +64,10 @@ grub_get_weekday_name (struct grub_datetime *datetime)
 #define SECPERDAY (24*SECPERHOUR)
 #define DAYSPERYEAR 365
 #define DAYSPER4YEARS (4*DAYSPERYEAR+1)
-
+/* 24 leap years in 100 years */
+#define DAYSPER100YEARS (100 * DAYSPERYEAR + 24)
+/* 97 leap years in 400 years */
+#define DAYSPER400YEARS (400 * DAYSPERYEAR + 97)
 
 void
 grub_unixtime2datetime (grub_int64_t nix, struct grub_datetime *datetime)
@@ -76,10 +79,12 @@ grub_unixtime2datetime (grub_int64_t nix, struct grub_datetime *datetime)
   /* Convenience: let's have 3 consecutive non-bissextile years
      at the beginning of the counting date. So count from 1901. */
   int days_epoch;
-  /* Number of days since 1st Januar, 1901.  */
+  /* Number of days since 1st January, 1 (proleptic). */
   unsigned days;
   /* Seconds into current day.  */
   unsigned secs_in_day;
+  /* Tracks whether this is a leap year. */
+  bool bisextile;
 
   /* Transform C divisions and modulos to mathematical ones */
   if (nix < 0)
@@ -92,27 +97,63 @@ grub_unixtime2datetime (grub_int64_t nix, struct grub_datetime *datetime)
     days_epoch = grub_divmod64 (nix, SECPERDAY, NULL);
 
   secs_in_day = nix - days_epoch * SECPERDAY;
-  days = days_epoch + 69 * DAYSPERYEAR + 17;
+  /*
+   * 1970 is Unix Epoch. Adjust to a year 1 epoch:
+   *  Leap year logic:
+   *   - Years evenly divisible by 400 are leap years
+   *   - Otherwise, if divisible by 100 are not leap years
+   *   - Otherwise, if divisible by 4 are leap years
+   *  There are four 400-year periods (1600 years worth of days with leap days)
+   *  There are 369 years in addition to the four 400 year periods
+   *  There are three 100-year periods worth of leap days (3*24)
+   *  There are 17 leap days in 69 years (beyond the three 100 year periods)
+   */
+  days = 4 * DAYSPER400YEARS + 369 * DAYSPERYEAR + 3 * 24 + 17 + days_epoch;
+
+  datetime->year = 1 + 400 * (days / DAYSPER400YEARS);
+  days %= DAYSPER400YEARS;
 
-  datetime->year = 1901 + 4 * (days / DAYSPER4YEARS);
+  /*
+   * On 31st December of bissextile (leap) years 365 days from the beginning
+   * of the year elapsed but year isn't finished yet - every 400 years
+   * 396 is 4 years less than 400 year leap cycle
+   * 96 is 1 day less than number of leap days in 400 years
+   */
+  if (days / DAYSPER100YEARS == 4)
+    {
+      datetime->year += 396;
+      days -= 396 * DAYSPERYEAR + 96;
+    }
+  else
+    {
+      datetime->year += 100 * (days / DAYSPER100YEARS);
+      days %= DAYSPER100YEARS;
+    }
+
+  datetime->year += 4 * (days / DAYSPER4YEARS);
   days %= DAYSPER4YEARS;
-  /* On 31st December of bissextile years 365 days from the beginning
-     of the year elapsed but year isn't finished yet */
+  /*
+   * On 31st December of bissextile (leap) years 365 days from the beginning
+   * of the year elapsed but year isn't finished yet - every 4 years
+   */
   if (days / DAYSPERYEAR == 4)
     {
       datetime->year += 3;
-      days -= 3*DAYSPERYEAR;
+      days -= 3 * DAYSPERYEAR;
     }
   else
     {
       datetime->year += days / DAYSPERYEAR;
       days %= DAYSPERYEAR;
     }
-  for (i = 0; i < 12
-	 && days >= (i==1 && datetime->year % 4 == 0
-		      ? 29 : months[i]); i++)
-    days -= (i==1 && datetime->year % 4 == 0
-			    ? 29 : months[i]);
+
+  bisextile = (datetime->year % 4 == 0
+               && (datetime->year % 100 != 0
+                   || datetime->year % 400 == 0)) ? true : false;
+  for (i = 0;
+       i < 12 && days >= ((i == 1 && bisextile == true) ? 29 : months[i]);
+       i++)
+    days -= ((i == 1 && bisextile == true) ? 29 : months[i]);
   datetime->month = i + 1;
   datetime->day = 1 + days;
   datetime->hour = (secs_in_day / SECPERHOUR);
diff --git a/include/grub/datetime.h b/include/grub/datetime.h
index bcec636f0..425c015fc 100644
--- a/include/grub/datetime.h
+++ b/include/grub/datetime.h
@@ -54,8 +54,9 @@ void grub_unixtime2datetime (grub_int64_t nix,
 static inline int
 grub_datetime2unixtime (const struct grub_datetime *datetime, grub_int64_t *nix)
 {
-  grub_int32_t ret;
+  grub_int64_t ret;
   int y4, ay;
+  bool bisextile;
   const grub_uint16_t monthssum[12]
     = { 0,
 	31,
@@ -75,15 +76,11 @@ grub_datetime2unixtime (const struct grub_datetime *datetime, grub_int64_t *nix)
   const int SECPERHOUR = 60 * SECPERMIN;
   const int SECPERDAY = 24 * SECPERHOUR;
   const int SECPERYEAR = 365 * SECPERDAY;
-  const int SECPER4YEARS = 4 * SECPERYEAR + SECPERDAY;
+  const grub_int64_t SECPER4YEARS = 4 * SECPERYEAR + SECPERDAY;
 
-  if (datetime->year > 2038 || datetime->year < 1901)
-    return 0;
   if (datetime->month > 12 || datetime->month < 1)
     return 0;
 
-  /* In the period of validity of unixtime all years divisible by 4
-     are bissextile*/
   /* Convenience: let's have 3 consecutive non-bissextile years
      at the beginning of the epoch. So count from 1973 instead of 1970 */
   ret = 3 * SECPERYEAR + SECPERDAY;
@@ -94,13 +91,28 @@ grub_datetime2unixtime (const struct grub_datetime *datetime, grub_int64_t *nix)
   ret += y4 * SECPER4YEARS;
   ret += ay * SECPERYEAR;
 
+  /*
+   * Correct above calculation (which assumes every 4 years is a leap year)
+   * to remove those "false leap years" that are divisible by 100 but not 400.
+   * Since this logic starts with seconds since 1973, 15 is used because:
+   *  - (1973 - 1) / 100 = 19 (floor due to integer math)
+   *  - (1973 - 1) / 400 = 4 (floor due to integer math)
+   *  - 19 - 4 - 15 = 0 (we want to start with no "false leap years" at time
+   *     zero of 1973)
+   */
+  ret -= ((datetime->year - 1) / 100 - (datetime->year - 1) / 400 - 15)
+         * SECPERDAY;
+
   ret += monthssum[datetime->month - 1] * SECPERDAY;
-  if (ay == 3 && datetime->month >= 3)
+  bisextile = (ay == 3
+               && (datetime->year % 100 != 0
+                   || datetime->year % 400 == 0)) ? true : false;
+  if (bisextile == true && datetime->month >= 3)
     ret += SECPERDAY;
 
   ret += (datetime->day - 1) * SECPERDAY;
   if ((datetime->day > months[datetime->month - 1]
-       && (!ay || datetime->month != 2 || datetime->day != 29))
+       && !(bisextile == true && datetime->month == 2 && datetime->day == 29))
       || datetime->day < 1)
     return 0;
 
-- 
2.39.5


_______________________________________________
Grub-devel mailing list
Grub-devel@gnu.org
https://lists.gnu.org/mailman/listinfo/grub-devel

^ permalink raw reply related	[flat|nested] 4+ messages in thread

* [PATCH v6 2/2] date_unit_test: test dates outside of 32-bit unix range
  2025-08-27 22:32 [PATCH v6 0/2] Support dates outside of 1901..2038 range Andrew Hamilton
  2025-08-27 22:32 ` [PATCH v6 1/2] datetime: " Andrew Hamilton
@ 2025-08-27 22:32 ` Andrew Hamilton
  2025-08-28 14:01 ` [PATCH v6 0/2] Support dates outside of 1901..2038 range Daniel Kiper via Grub-devel
  2 siblings, 0 replies; 4+ messages in thread
From: Andrew Hamilton @ 2025-08-27 22:32 UTC (permalink / raw)
  To: grub-devel
  Cc: daniel.kiper, masayuki.moriyama, andrea.biardi, phcoder,
	Andrew Hamilton

Add tests outside the date range possible with 32-bit time
calculation.

Signed-off-by: Vladimir Serbinenko <phcoder@gmail.com>
Signed-off-by: Andrew Hamilton <adhamilt@gmail.com>
Reviewed-by: Daniel Kiper <daniel.kiper@oracle.com>
---
 tests/date_unit_test.c | 75 +++++++++++++++++++++++++++++++++++++-----
 1 file changed, 66 insertions(+), 9 deletions(-)

diff --git a/tests/date_unit_test.c b/tests/date_unit_test.c
index 99774f199..08edc54df 100644
--- a/tests/date_unit_test.c
+++ b/tests/date_unit_test.c
@@ -25,12 +25,13 @@
 #include <grub/test.h>
 
 static void
-date_test (grub_int32_t v)
+date_test (grub_int64_t v)
 {
   struct grub_datetime dt;
   time_t t = v;
   struct tm *g;
   int w;
+  grub_int64_t back = 0;
 
   g = gmtime (&t);
 
@@ -38,28 +39,56 @@ date_test (grub_int32_t v)
 
   w = grub_get_weekday (&dt);
 
-  grub_test_assert (g->tm_sec == dt.second, "time %d bad second: %d vs %d", v,
+  grub_datetime2unixtime (&dt, &back);
+
+  grub_test_assert (g->tm_sec == dt.second, "time %lld bad second: %d vs %d", (long long) v,
 		    g->tm_sec, dt.second);
-  grub_test_assert (g->tm_min == dt.minute, "time %d bad minute: %d vs %d", v,
+  grub_test_assert (g->tm_min == dt.minute, "time %lld bad minute: %d vs %d", (long long) v,
 		    g->tm_min, dt.minute);
-  grub_test_assert (g->tm_hour == dt.hour, "time %d bad hour: %d vs %d", v,
+  grub_test_assert (g->tm_hour == dt.hour, "time %lld bad hour: %d vs %d", (long long) v,
 		    g->tm_hour, dt.hour);
-  grub_test_assert (g->tm_mday == dt.day, "time %d bad day: %d vs %d", v,
+  grub_test_assert (g->tm_mday == dt.day, "time %lld bad day: %d vs %d", (long long) v,
 		    g->tm_mday, dt.day);
-  grub_test_assert (g->tm_mon + 1 == dt.month, "time %d bad month: %d vs %d", v,
+  grub_test_assert (g->tm_mon + 1 == dt.month, "time %lld bad month: %d vs %d",(long long) v,
 		    g->tm_mon + 1, dt.month);
   grub_test_assert (g->tm_year + 1900 == dt.year,
-		    "time %d bad year: %d vs %d", v,
+                   "time %lld bad year: %d vs %d", (long long) v,
 		    g->tm_year + 1900, dt.year);
-  grub_test_assert (g->tm_wday == w, "time %d bad week day: %d vs %d", v,
+  grub_test_assert (g->tm_wday == w, "time %lld bad week day: %d vs %d", (long long) v,
 		    g->tm_wday, w);
+  grub_test_assert (back == v, "time %lld bad back transform: %lld", (long long) v,
+                   (long long) back);
 }
 
 static void
 date_test_iter (void)
 {
-  grub_int32_t tests[] = { -1, 0, +1, -2133156255, GRUB_INT32_MIN,
+  /*
+   * Test several interesting UNIX timestamps in 32-bit time:
+   *   1.             -1: 1969-12-31 23:59:59 - Just before EPOCH
+   *   2.              0: 1970-01-01 00:00:00 - EPOCH
+   *   3.             +1: 1970-01-01 00:00:01 - Just after EPOCH
+   *   4.      978224552: 2000-12-31 01:02:32 - Leap year, after Feb
+   *   5.    -2133156255: 1902-05-28 16:35:45 - Nominal value
+   *   6.    -2110094321: 1903-02-19 14:41:19 - Nominal value
+   *   7. GRUB_INT32_MIN: 1901-12-13 20:45:52 - 32-bit Min value
+   *   8. GRUB_INT32_MAX: 2038-01-19 03:14:07 - 32-bit Max value
+   */
+  grub_int32_t tests[] = { -1, 0, +1, 978224552, -2133156255, -2110094321, GRUB_INT32_MIN,
 			   GRUB_INT32_MAX };
+  /*
+   * Test several known UNIX timestamps outside 32-bit time:
+   *   1. 5774965200:   2152-12-31 21:00:00  - Leap year
+   *   2. 4108700725:   2100-03-14 09:45:25  - Not a leap year
+   *   3. -5029179792:  1810-08-19 21:36:48  - Not a leap year
+   *   4. -62135596799: 0001-01-01 00:00:00  - Minimum AD
+   *   5. 253402300799: 9999-12-31 23:59:59  - Maximum 4 digit year
+   */
+  grub_int64_t tests64[] = { (grub_int64_t) 5774965200,
+                             (grub_int64_t) 4108700725,
+                             (grub_int64_t) -5029179792,
+                             (grub_int64_t) -62135596799,
+                             (grub_int64_t) 253402300799 };
   unsigned i;
 
   for (i = 0; i < ARRAY_SIZE (tests); i++)
@@ -71,6 +100,34 @@ date_test_iter (void)
       date_test (x);
       date_test (-x);
     }
+
+  if (sizeof (time_t) > 4)
+    {
+      for (i = 0; i < ARRAY_SIZE (tests64); i++)
+        date_test (tests64[i]);
+      for (i = 0; i < 5000000; i++)
+        {
+          /*
+           * Test some pseudo-random dates/times between 1970 and 9999
+           * "117" is used to scale the random 32-bit int from range
+           * 0..2147483648 to 0..251255586816. This is reasonably
+           * close to max 9999 date represented by 253402300799.
+           */
+          grub_int64_t x = (grub_int64_t) rand () * (grub_int64_t) 117;
+          date_test (x);
+        }
+      for (i = 0; i < 5000000; i++)
+        {
+          /*
+           * Test some pseudo-random dates/times between 0001 and 1969
+           * "-28" is used to scale the random 32-bit int from range
+           * 0..2147483648 to -60129542144..0. This is reasonably
+           * close to min 0001 date represented by -62135596799.
+           */
+          grub_int64_t x = (grub_int64_t) rand () * (grub_int64_t) -28;
+          date_test (x);
+        }
+    }
 }
 
 GRUB_UNIT_TEST ("date_unit_test", date_test_iter);
-- 
2.39.5


_______________________________________________
Grub-devel mailing list
Grub-devel@gnu.org
https://lists.gnu.org/mailman/listinfo/grub-devel

^ permalink raw reply related	[flat|nested] 4+ messages in thread

* Re: [PATCH v6 0/2] Support dates outside of 1901..2038 range
  2025-08-27 22:32 [PATCH v6 0/2] Support dates outside of 1901..2038 range Andrew Hamilton
  2025-08-27 22:32 ` [PATCH v6 1/2] datetime: " Andrew Hamilton
  2025-08-27 22:32 ` [PATCH v6 2/2] date_unit_test: test dates outside of 32-bit unix range Andrew Hamilton
@ 2025-08-28 14:01 ` Daniel Kiper via Grub-devel
  2 siblings, 0 replies; 4+ messages in thread
From: Daniel Kiper via Grub-devel @ 2025-08-28 14:01 UTC (permalink / raw)
  To: Andrew Hamilton
  Cc: Daniel Kiper, grub-devel, masayuki.moriyama, andrea.biardi,
	phcoder

On Wed, Aug 27, 2025 at 05:32:12PM -0500, Andrew Hamilton wrote:
> Support dates outside of 1901..2038.
> Add tests for dates outside this range.
> Vast majority of the work was done by Vladimir Serbinenko
>
> Fixes: https://savannah.gnu.org/bugs/?63894
> Fixes: https://savannah.gnu.org/bugs/?66301
>
> V5 -> V6:
> datetime.c:
>  Move comment line "There are three 100-year periods worth of leap
>   days (3*24)" down one line.
>  Change order of terms in 'days' calculation to be more readable.
> date_unit_test.c:
>  Correct comment to "Leap year, after Feb" instead of "prior to Feb"
> Add "reviewed by" from Daniel Kiper for patches with permission.
>
> V4 -> V5:
> datetime.c / h:
>  Rename variables "is_bisextile" to "bisextile".
>  Change some boolean assignments to use ternary operator.
>  Compare booleans to true instead of checking "if(boolean)".
> date_unit_test.c:
>  Add additional commentary to the tests array for meaning of numbers.
>  Rework test ranges to cover from years 0001 to 9999 and add specific
>  min/max tests for min 0001 date and max 9999 date.
>
> V3 -> V4: Add additional commentary for a few magic numbers
>  and fix some coding standard issues.
>
> V2 -> V3: Added additional commentary to datetime.c to describe
>  the origin of some of the numbers used in calculations.
>
> Andrew Hamilton (2):
>   datetime: Support dates outside of 1901..2038 range
>   date_unit_test: test dates outside of 32-bit unix range

LGTM...

Thank you for taking a stab at it!

I will push this together with other patches next week...

Daniel

_______________________________________________
Grub-devel mailing list
Grub-devel@gnu.org
https://lists.gnu.org/mailman/listinfo/grub-devel

^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2025-08-28 14:03 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-08-27 22:32 [PATCH v6 0/2] Support dates outside of 1901..2038 range Andrew Hamilton
2025-08-27 22:32 ` [PATCH v6 1/2] datetime: " Andrew Hamilton
2025-08-27 22:32 ` [PATCH v6 2/2] date_unit_test: test dates outside of 32-bit unix range Andrew Hamilton
2025-08-28 14:01 ` [PATCH v6 0/2] Support dates outside of 1901..2038 range Daniel Kiper via Grub-devel

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).