Skip to content

Commit 69168c2

Browse files
committed
NSDateComponents.hash: Fix arithmetic overflows
1 parent 707896f commit 69168c2

File tree

2 files changed

+100
-39
lines changed

2 files changed

+100
-39
lines changed

Foundation/NSCalendar.swift

+84-39
Original file line numberDiff line numberDiff line change
@@ -1276,49 +1276,94 @@ open class NSDateComponents : NSObject, NSCopying, NSSecureCoding {
12761276
public override init() {
12771277
super.init()
12781278
}
1279-
1279+
12801280
open override var hash: Int {
1281-
var calHash = 0
1282-
if let cal = calendar {
1283-
calHash = cal.hashValue
1284-
}
1285-
if let tz = timeZone {
1286-
calHash ^= tz.hashValue
1287-
}
1288-
var y = year
1289-
if NSDateComponentUndefined == y {
1290-
y = 0
1291-
}
1292-
var m = month
1293-
if NSDateComponentUndefined == m {
1294-
m = 0
1295-
}
1296-
var d = day
1297-
if NSDateComponentUndefined == d {
1298-
d = 0
1299-
}
1300-
var h = hour
1301-
if NSDateComponentUndefined == h {
1302-
h = 0
1303-
}
1304-
var mm = minute
1305-
if NSDateComponentUndefined == mm {
1306-
mm = 0
1307-
}
1308-
var s = second
1309-
if NSDateComponentUndefined == s {
1310-
s = 0
1311-
}
1312-
var yy = yearForWeekOfYear
1313-
if NSDateComponentUndefined == yy {
1314-
yy = 0
1315-
}
1316-
return calHash + (32832013 * (y + yy) + 2678437 * m + 86413 * d + 3607 * h + 61 * mm + s) + (41 * weekOfYear + 11 * weekOfMonth + 7 * weekday + 3 * weekdayOrdinal + quarter) * (1 << 5)
1281+
var hasher = Hasher()
1282+
var mask = 0
1283+
// The list of fields fed to the hasher here must be exactly
1284+
// the same as the ones compared in isEqual(_:) (modulo
1285+
// ordering).
1286+
//
1287+
// Given that NSDateComponents instances usually only have a
1288+
// few fields present, it makes sense to only hash those, as
1289+
// an optimization. We keep track of the fields hashed in the
1290+
// mask value, which we also feed to the hasher to make sure
1291+
// any two unequal values produce different hash encodings.
1292+
//
1293+
// FIXME: Why not just feed _values, calendar & timeZone to
1294+
// the hasher?
1295+
if let calendar = calendar {
1296+
hasher.combine(calendar)
1297+
mask |= 1 << 0
1298+
}
1299+
if let timeZone = timeZone {
1300+
hasher.combine(timeZone)
1301+
mask |= 1 << 1
1302+
}
1303+
if era != NSDateComponentUndefined {
1304+
hasher.combine(era)
1305+
mask |= 1 << 2
1306+
}
1307+
if year != NSDateComponentUndefined {
1308+
hasher.combine(year)
1309+
mask |= 1 << 3
1310+
}
1311+
if quarter != NSDateComponentUndefined {
1312+
hasher.combine(quarter)
1313+
mask |= 1 << 4
1314+
}
1315+
if month != NSDateComponentUndefined {
1316+
hasher.combine(month)
1317+
mask |= 1 << 5
1318+
}
1319+
if day != NSDateComponentUndefined {
1320+
hasher.combine(day)
1321+
mask |= 1 << 6
1322+
}
1323+
if hour != NSDateComponentUndefined {
1324+
hasher.combine(hour)
1325+
mask |= 1 << 7
1326+
}
1327+
if minute != NSDateComponentUndefined {
1328+
hasher.combine(minute)
1329+
mask |= 1 << 8
1330+
}
1331+
if second != NSDateComponentUndefined {
1332+
hasher.combine(second)
1333+
mask |= 1 << 9
1334+
}
1335+
if nanosecond != NSDateComponentUndefined {
1336+
hasher.combine(nanosecond)
1337+
mask |= 1 << 10
1338+
}
1339+
if weekOfYear != NSDateComponentUndefined {
1340+
hasher.combine(weekOfYear)
1341+
mask |= 1 << 11
1342+
}
1343+
if weekOfMonth != NSDateComponentUndefined {
1344+
hasher.combine(weekOfMonth)
1345+
mask |= 1 << 12
1346+
}
1347+
if yearForWeekOfYear != NSDateComponentUndefined {
1348+
hasher.combine(yearForWeekOfYear)
1349+
mask |= 1 << 13
1350+
}
1351+
if weekday != NSDateComponentUndefined {
1352+
hasher.combine(weekday)
1353+
mask |= 1 << 14
1354+
}
1355+
if weekdayOrdinal != NSDateComponentUndefined {
1356+
hasher.combine(weekdayOrdinal)
1357+
mask |= 1 << 15
1358+
}
1359+
hasher.combine(isLeapMonth)
1360+
hasher.combine(mask)
1361+
return hasher.finalize()
13171362
}
1318-
1363+
13191364
open override func isEqual(_ object: Any?) -> Bool {
13201365
guard let other = object as? NSDateComponents else { return false }
1321-
1366+
// FIXME: Why not just compare _values, calendar & timeZone?
13221367
return self === other
13231368
|| (era == other.era
13241369
&& year == other.year

TestFoundation/TestCalendar.swift

+16
Original file line numberDiff line numberDiff line change
@@ -198,10 +198,26 @@ class TestNSDateComponents: XCTestCase {
198198

199199
static var allTests: [(String, (TestNSDateComponents) -> () throws -> Void)] {
200200
return [
201+
("test_hash", test_hash),
201202
("test_copyNSDateComponents", test_copyNSDateComponents),
202203
]
203204
}
204205

206+
func test_hash() {
207+
let c1 = NSDateComponents()
208+
c1.year = 2018
209+
c1.month = 8
210+
c1.day = 1
211+
212+
let c2 = NSDateComponents()
213+
c2.year = 2018
214+
c2.month = 8
215+
c2.day = 1
216+
217+
XCTAssertEqual(c1, c2)
218+
XCTAssertEqual(c1.hash, c2.hash)
219+
}
220+
205221
func test_copyNSDateComponents() {
206222
let components = NSDateComponents()
207223
components.year = 1987

0 commit comments

Comments
 (0)