-
Notifications
You must be signed in to change notification settings - Fork 533
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix DateTime out-of-range panics #1048
Conversation
89029fe
to
4074ba9
Compare
Found another somewhat related panic.
This was easily fixable in the implementation of let date_min = NaiveDate::MIN;
assert!(date_min.week(Weekday::Mon).last_day() >= date_min); |
7b11303
to
c02648d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this PR is too large for me to digest. If you have to preface with a bunch of prose in the PR description to make it all make sense, you've already lost me. Please split this in smaller chunks that I can easily review (here's some early feedback from the first couple of commits).
src/naive/date.rs
Outdated
- (MIN_YEAR + 400_000) / 100 | ||
+ (MIN_YEAR + 400_000) / 400 | ||
- 146_097_000; | ||
const MIN_DAYS_FROM_YEAR_0: i32 = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd like this commit to be split in multiple parts that do one thing each. This seems to do refactoring (divide by 1000) which is distracting from the more material change of the value, making it harder to review.
Haha. That's fair. I'll see what I can do to split it up. |
c02648d
to
0dd37c2
Compare
f71b3bd
to
1a82a4a
Compare
b3d7ac0
to
0601202
Compare
Okay, I have done my best to split this up into smaller commits. |
b0109c1
to
86a980f
Compare
9473718
to
2d317cc
Compare
2d317cc
to
c1a213e
Compare
1134d91
to
243314d
Compare
243314d
to
36a2622
Compare
99f1679
to
9113d91
Compare
9113d91
to
94ec7da
Compare
Codecov Report
@@ Coverage Diff @@
## 0.4.x #1048 +/- ##
==========================================
+ Coverage 91.50% 91.55% +0.04%
==========================================
Files 38 38
Lines 17314 17385 +71
==========================================
+ Hits 15844 15916 +72
+ Misses 1470 1469 -1
📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
94ec7da
to
d904534
Compare
FWIW, would help me to start the PR description out with other PRs that it depends on. |
4bb6c09
to
bea69bb
Compare
80de015
to
69c434f
Compare
40b0150
to
2ab9ff1
Compare
261c543
to
0dfb0c2
Compare
0dfb0c2
to
ffee507
Compare
If the timestamp of a
DateTime
is near the end of the range ofNaiveDateTime
and the offset pushes the timestamp beyond that range, all kinds of methods currently panic. About 40 methods that are part of the public API. With theDebug
implementation panicking being the most fun. See #1047.Most of these methods fall in two categories:
year()
,hour()
.None
. Examples arechecked_add_days
,with_month
.A somewhat easy fix is to slightly restrict the range of
NaiveDate
, so that we have some buffer space to represent the out-of-range datetimes. Everything that needs just the intermediate value will be fixed by this. But care should be taken to never let an invalid intermediate value escape to the library user.This PR grew larger than hoped. I'll describe the various commits to hopefully help review.
Adjust MIN_YEAR and MAX_YEAR
I made
MIN_YEAR
andMAX_YEAR
smaller, so that we have 1 year of buffer space. 1 day would have been enough, but having the minimum date be January 2 and the maximum date December 30 is just strange.NaiveDate::MIN
andNaiveDate::MAX
are derived from these. There is a very helpfultest_date_bounds
to confirm the flags and oridinal are correct.There is something subtly wrong in the calculation of
MIN_DAYS_FROM_YEAR_0
, which is only used in tests. It was only correct ifMIN_YEAR
was a leap year. I changed it to a correct formula that tries to be less smart, but couldn't figure out the problem in the derivation yet. Added a comment.checked_add_offset and unchecked_add_offset
Instead of panicking in the
Add
orSub
implementation ofNaiveDateTime
withFixedOffset
, we need a way to be informed of out-of-range result. Or in other cases we need to be able to construct a value in the buffer space for intermediate use.I added the following methods (not public for now):
NaiveTime::overflowing_add_offset
andNaiveTime::overflowing_sub_offset
NaiveDateTime::checked_add_offset
andNaiveDateTime::checked_sub_offset
NaiveDateTime::unchecked_add_offset
(in a later commit)The
Add
andSub
implementations ofFixedOffset
withNaiveTime
,NaiveDateTime
andDateTime
are reimplemented using of these methods. This fixes a code comment:The best place to put the
Add
andSub
implementations forDateTime
where in the module ofDateTime
, because there they have access to its private fields. I have moved all implementations to the module of their output type for consistency.Simplify implementation
Adding an offset works differently from adding a duration. Adding a duration tries to do something smart—but still rudimentary—with leap seconds. Adding an offset should not touch the leap seconds at all. So the methods that operate on
NaiveTime
should be different.I extracted the part that operates on the
NaiveDate
that could be shared into anadd_days
methods. PreviouslyNaiveDate::checked_add_days
would convert the days to aDuration
in seconds, and later devide them to get the value in days again. This now works in a less roundabout way. Might also help with the const implementation later.Fix creating a DateTime with NaiveDateTime::{MIN, MAX}
The creation of a
DateTime
inTimeZone::from_local_datetime
should usechecked_sub_offset
, and returnLocalResult::None
on overflow.The implementation of
Local
had grown into a mess (sorry, that is the best word for it). With the last commit in #1017 they should just use the provided implementation and pick up this fix.The actual fixes
The new method
DateTime::overflowing_naive_local
is not public, and can be used to create an out-of-boundsNaiveDateTime
for use as an intermediate value.Most of the problematic methods on
DateTime
simply work when converted to useoverflowing_naive_local
. If they don't return aDateTime
,NaiveDateTime
orNaiveDate
there is also no worry the intermediate value may be exposed outside chrono.The
DateTime::with_*
methods that are part of theDatelike
trait have an interesting property: if the localNaiveDateTime
would be out-of-range, there is only exactly one year/month/day/ordinal they can be set to that would result in a validDateTime
: the one that is already there. This is thanks to the restriction that offset is always less then 24h. To prevent creating an out-of-rangeNaiveDateTime
all these methods short-circuit when possible.The only two methods on
NaiveDate
(note: notDateTime
) that had to change arechecked_add_months
andchecked_sub_months
. Both had a short-circuiting behaviour that with the changes in this PR could return invalid dates. When the input is out of range andmonths == 0
,checked_*_months
didn't check whether the result would be valid.Not fixed: parsing
All the relevant methods on
Parsed
are public.Methods in there like
Parsed::to_naive_date
,Parsed::to_naive_datetime_with_offset
andNaiveDateTime::from_timestamp_opt
can't be converted to return an intermediate out-of-range value.I have left it as is. Parsing doesn't panic, it just can't round-trip some
DateTime
's.Tests
About 275 lines in this PR are tests, so its size may not be as bad as it looks. @jtmoon79 I tried to behave 😇
test_min_max_datetimes
is in my opinion the interesting one, testing all methods that would previously panic.