[m-rev.] for review: overflow checks for string conversions in the calendar module
Julien Fischer
jfischer at opturion.com
Sat Aug 15 01:36:07 AEST 2026
For review by anyone.
------------------------------
Overflow checks for string conversions in calendar module.
The predicates date_time_from_string/2 and duration_from_string/2 do not
currently do any overflow checking on their components; add the missing
overflow checks.
library/calendar.m:
As above.
tests/hard_coded/calendar_date_time_conv.{m,exp}:
tests/hard_coded/calendar_duration.{m,exp}:
Extend these test to check for component overflow.
Julien.
diff --git a/library/calendar.m b/library/calendar.m
index bfedaec53..dc813ec4f 100644
--- a/library/calendar.m
+++ b/library/calendar.m
@@ -216,8 +216,11 @@
% The microseconds component (.mmmmmm) is optional. If present,
% it may have between one and six digits.
%
- % This predicate fails if the string does not conform to the above format,
- % or if any date or time component is outside its valid range.
+ % This predicate fails if
+ % - the string does not conform to the above format;
+ % - any date or time component is outside its valid range;
+ % - the absolute value of the year cannot be represented by Mercury's
+ % int type.
%
:- pred date_time_from_string(string::in, date_time::out) is semidet.
@@ -460,8 +463,11 @@
% This fraction component cannot include more than six digits, since
% the maximum resolution of a duration is a microsecond.
%
- % Fail if the string does not conform to the above format, or if the
- % fractional part of the seconds component has more than six digits.
+ % This predicates fails if
+ % - the string does not conform to the above format;
+ % - the fractional part of the seconds component has more than six digits;
+ % - any of the duration components cannot be represented by Mercury's
+ % int type.
%
% For example, the duration 1 year, 18 months, 100 days, 10 hours, 15
% minutes, 90 seconds and 300 microseconds can be written as:
@@ -1498,25 +1504,45 @@ foldl3_days(Pred, !.Curr, End, !Acc1, !Acc2, !Acc3) :-
read_next_char(Char, [Char | Rest], Rest).
+ % read_int_and_return_num_digits(Int, NumDigits, !Chars):
+ %
+ % Int is a non-negative integer of NumDigits digits at the start
+ % of the given character list.
+ % Int and NumDigits are both zero if the start of the character list does
+ % not contain a digit.
+ % Fails if the integer is outside the bounds of what can be represented by
+ % Mercury's int type.
+ %
:- pred read_int_and_return_num_digits(int::out, int::out,
- list(char)::in, list(char)::out) is det.
+ list(char)::in, list(char)::out) is semidet.
read_int_and_return_num_digits(Int, NumDigits, !Chars) :-
- read_int_and_return_num_digits_loop(0, Int, 0, NumDigits, !Chars).
-
-:- pred read_int_and_return_num_digits_loop(int::in, int::out,
- int::in, int::out, list(char)::in, list(char)::out) is det.
-
-read_int_and_return_num_digits_loop(!Int, !NumDigits, !Chars) :-
+ % See the comment for string.do_base_string_to_positive_int_loop/8
+ % for an explanation of how we check for overflow here.
+ CutOff = max_int `unchecked_quotient` 10,
+ CutLimit = max_int `unchecked_rem` 10,
+ read_int_and_return_num_digits_loop(CutOff, CutLimit, 0, Int,
+ 0, NumDigits, !Chars).
+
+:- pred read_int_and_return_num_digits_loop(int::in, int::in,
+ int::in, int::out, int::in, int::out, list(char)::in, list(char)::out)
+ is semidet.
+
+read_int_and_return_num_digits_loop(CutOff, CutLimit, Acc0, Acc,
+ !NumDigits, !Chars) :-
( if
!.Chars = [Char | !:Chars],
decimal_digit_to_int(Char, Digit)
then
- !:Int = !.Int * 10 + Digit,
- read_int_and_return_num_digits_loop(!Int, !.NumDigits + 1, !:NumDigits,
- !Chars)
+ % Fail on overflow.
+ ( Acc0 < CutOff
+ ; Acc0 = CutOff, Digit =< CutLimit
+ ),
+ NextAcc = (10 * Acc0) + Digit,
+ read_int_and_return_num_digits_loop(CutOff, CutLimit, NextAcc, Acc,
+ !.NumDigits + 1, !:NumDigits, !Chars)
else
- true
+ Acc = Acc0
).
:- pred read_microseconds(microseconds::out, list(char)::in, list(char)::out)
diff --git a/tests/hard_coded/calendar_date_time_conv.exp
b/tests/hard_coded/calendar_date_time_conv.exp
index 01258fd13..f565cf7ef 100644
--- a/tests/hard_coded/calendar_date_time_conv.exp
+++ b/tests/hard_coded/calendar_date_time_conv.exp
@@ -129,6 +129,9 @@ PASS date_time(1970, 1, 1, 0, 0, 0, 0)
"1582-10-15 00:00:00": first day of the Gregorian calendar
PASS date_time(1582, 10, 15, 0, 0, 0, 0)
+"2147483647-01-01 00:00:00": year on 32-bit integer boundary
+PASS date_time(2147483647, 1, 1, 0, 0, 0, 0)
+
"2024-01-01 00:00:00.10": one fractional trailing zero
PASS date_time(2024, 1, 1, 0, 0, 0, 100000)
TEST 2024-01-01 00:00:00.10
@@ -366,3 +369,9 @@ PASS: ERROR DETECTED
"2024-01-01 00:00:": trailing colon with no second digits
PASS: ERROR DETECTED
+
+"9223372036854775808-01-01 00:00:00": integer overflow in year component
+PASS: ERROR DETECTED
+
+"23058430092136939520-01-01 00:00:00": bad overflow check
+PASS: ERROR DETECTED
diff --git a/tests/hard_coded/calendar_date_time_conv.m
b/tests/hard_coded/calendar_date_time_conv.m
index 994f5d788..a180d333f 100644
--- a/tests/hard_coded/calendar_date_time_conv.m
+++ b/tests/hard_coded/calendar_date_time_conv.m
@@ -181,7 +181,11 @@ valid_date_times = [
dt_conv_test("all maximum", "2024-12-31 23:59:60.999999"),
dt_conv_test("Unix epoch", "1970-01-01 00:00:00"),
dt_conv_test("first day of the Gregorian calendar",
- "1582-10-15 00:00:00")
+ "1582-10-15 00:00:00"),
+
+ % Valid in all grades.
+ dt_conv_test("year on 32-bit integer boundary",
+ "2147483647-01-01 00:00:00")
].
% These cannot be round-tripped through date_time_to_string/1.
@@ -294,7 +298,15 @@ invalid_date_times = [
dt_conv_test("missing day", "2024-01- 00:00:00"),
dt_conv_test("missing hour", "2024-01-01 :00:00"),
dt_conv_test("missing minute", "2024-01-01 00::00"),
- dt_conv_test("trailing colon with no second digits", "2024-01-01 00:00:")
+ dt_conv_test("trailing colon with no second digits", "2024-01-01 00:00:"),
+
+ % One past max_int for 64-bit integers.
+ dt_conv_test("integer overflow in year component",
+ "9223372036854775808-01-01 00:00:00"),
+
+ % A test case intended to defeat incorrect overflow checks.
+ dt_conv_test("bad overflow check",
+ "23058430092136939520-01-01 00:00:00")
].
%---------------------------------------------------------------------------%
diff --git a/tests/hard_coded/calendar_duration_conv.exp
b/tests/hard_coded/calendar_duration_conv.exp
index be64d8e8e..befb93b69 100644
--- a/tests/hard_coded/calendar_duration_conv.exp
+++ b/tests/hard_coded/calendar_duration_conv.exp
@@ -333,3 +333,21 @@ PASS: ERROR DETECTED
"PTS": S with no second digits
PASS: ERROR DETECTED
+
+"P9223372036854775808Y": integer overflow in years component
+PASS: ERROR DETECTED
+
+"P9223372036854775808M": integer overflow in months component
+PASS: ERROR DETECTED
+
+"P9223372036854775808D": integer overflow in days component
+PASS: ERROR DETECTED
+
+"PT9223372036854775808H": integer overflow in hours component
+PASS: ERROR DETECTED
+
+"PT9223372036854775808M": integer overflow in minutes component
+PASS: ERROR DETECTED
+
+"PT9223372036854775808S": integer overflow in seconds component
+PASS: ERROR DETECTED
diff --git a/tests/hard_coded/calendar_duration_conv.m
b/tests/hard_coded/calendar_duration_conv.m
index b47410d13..f853b36ad 100644
--- a/tests/hard_coded/calendar_duration_conv.m
+++ b/tests/hard_coded/calendar_duration_conv.m
@@ -289,7 +289,21 @@ invalid_durations = [
duration_test("D with no day digits", "P5YD"),
duration_test("H with no hour digits", "PTH"),
duration_test("M with no minute digits", "PT5HM"),
- duration_test("S with no second digits", "PTS")
+ duration_test("S with no second digits", "PTS"),
+
+ % Integer overflow in components.
+ duration_test("integer overflow in years component",
+ "P9223372036854775808Y"),
+ duration_test("integer overflow in months component",
+ "P9223372036854775808M"),
+ duration_test("integer overflow in days component",
+ "P9223372036854775808D"),
+ duration_test("integer overflow in hours component",
+ "PT9223372036854775808H"),
+ duration_test("integer overflow in minutes component",
+ "PT9223372036854775808M"),
+ duration_test("integer overflow in seconds component",
+ "PT9223372036854775808S")
].
%---------------------------------------------------------------------------%
More information about the reviews
mailing list