-
Notifications
You must be signed in to change notification settings - Fork 594
Fix DateTime out-of-range panics #1048
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
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
DateTimeis near the end of the range ofNaiveDateTimeand 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 theDebugimplementation 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_YEARandMAX_YEARsmaller, 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::MINandNaiveDate::MAXare derived from these. There is a very helpfultest_date_boundsto 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_YEARwas 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
AddorSubimplementation ofNaiveDateTimewithFixedOffset, 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_offsetandNaiveTime::overflowing_sub_offsetNaiveDateTime::checked_add_offsetandNaiveDateTime::checked_sub_offsetNaiveDateTime::unchecked_add_offset(in a later commit)The
AddandSubimplementations ofFixedOffsetwithNaiveTime,NaiveDateTimeandDateTimeare reimplemented using of these methods. This fixes a code comment:The best place to put the
AddandSubimplementations forDateTimewhere 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
NaiveTimeshould be different.I extracted the part that operates on the
NaiveDatethat could be shared into anadd_daysmethods. PreviouslyNaiveDate::checked_add_dayswould convert the days to aDurationin 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
DateTimeinTimeZone::from_local_datetimeshould usechecked_sub_offset, and returnLocalResult::Noneon overflow.The implementation of
Localhad 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_localis not public, and can be used to create an out-of-boundsNaiveDateTimefor use as an intermediate value.Most of the problematic methods on
DateTimesimply work when converted to useoverflowing_naive_local. If they don't return aDateTime,NaiveDateTimeorNaiveDatethere is also no worry the intermediate value may be exposed outside chrono.The
DateTime::with_*methods that are part of theDateliketrait have an interesting property: if the localNaiveDateTimewould 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-rangeNaiveDateTimeall these methods short-circuit when possible.The only two methods on
NaiveDate(note: notDateTime) that had to change arechecked_add_monthsandchecked_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_*_monthsdidn't check whether the result would be valid.Not fixed: parsing
All the relevant methods on
Parsedare public.Methods in there like
Parsed::to_naive_date,Parsed::to_naive_datetime_with_offsetandNaiveDateTime::from_timestamp_optcan'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_datetimesis in my opinion the interesting one, testing all methods that would previously panic.