Refactor CPTimeZone to use dynamic Intl validation and host-native IANA enumeration

* Fully remove the static, hand-maintained knownTimeZoneNames array and knownTimeZoneNamesSet dictionary from +initialize.

* Implement runtime dynamic population via Intl.supportedValuesOf("timeZone") with strict fallback handling for GMT and UTC.

* Introduce a robust _isValidTimeZoneName runtime check utilizing Intl.DateTimeFormat validation to reject unrecognized zone names dynamically rather than relying on a stale static list.
 * Update initWithName: and _initWithName:abbreviation: to leverage the new engine-native validation bridge.
This commit is contained in:
David Richardson
2026-09-11 18:53:33 -06:00
parent 6adb410cc6
commit 6f1f56205e
+40 -77
View File
@@ -48,7 +48,6 @@ CPSystemTimeZoneDidChangeNotification = @"CPSystemTimeZoneDidChangeNotification"
*/
var abbreviationDictionary,
knownTimeZoneNames,
knownTimeZoneNamesSet,
defaultTimeZone,
localTimeZone,
systemTimeZone,
@@ -63,6 +62,24 @@ var _stdDstOffsetCache = {};
// +_timeZoneFromString:style:locale: and built lazily on first lookup.
var _nameToZoneCache = {};
/*!
Validates a given time zone name by attempting to construct a lightweight
Intl.DateTimeFormat object. The engine throws a RangeError if the timeZone
is not recognized.
*/
function _isValidTimeZoneName(tzName)
{
try
{
new Intl.DateTimeFormat('en-US', { timeZone: tzName });
return YES;
}
catch (e)
{
return NO;
}
}
/*!
Live UTC offset, in minutes, for an IANA zone name at a given date.
DST-aware by construction, since it asks Intl to render the actual wall
@@ -266,8 +283,7 @@ function _localizedNameForZone(tzName, style, locale, fixedOffsetSecondsOrNil)
/*!
Resolves the runtime's own current zone as a CPTimeZone: prefer the
IANA name Intl resolves to (DST-correct, works for any of the 400+
zones in knownTimeZoneNames), and fall back to a fixed-offset zone
IANA name Intl resolves to (DST-correct), and fall back to a fixed-offset zone
built from the JS Date offset if Intl isn't available at all. Always
succeeds.
*/
@@ -279,7 +295,7 @@ function _systemTimeZoneFromRuntime()
{
var ianaName = new Intl.DateTimeFormat().resolvedOptions().timeZone;
if (ianaName && knownTimeZoneNamesSet[ianaName])
if (ianaName && _isValidTimeZoneName(ianaName))
{
var zone = [CPTimeZone timeZoneWithName:ianaName];
@@ -320,47 +336,9 @@ function _systemTimeZoneFromRuntime()
if (self !== [CPTimeZone class])
return;
knownTimeZoneNames = [
@"Africa/Addis_Ababa",
@"Africa/Harare",
@"Africa/Lagos",
@"America/Argentina/Buenos_Aires",
@"America/Bogota",
@"America/Chicago",
@"America/Denver",
@"America/Halifax",
@"America/Juneau",
@"America/Lima",
@"America/Los_Angeles",
@"America/New_York",
@"America/Santiago",
@"America/Sao_Paulo",
@"Asia/Bangkok",
@"Asia/Calcutta",
@"Asia/Dhaka",
@"Asia/Dubai",
@"Asia/Hong_Kong",
@"Asia/Jakarta",
@"Asia/Karachi",
@"Asia/Manila",
@"Asia/Seoul",
@"Asia/Singapore",
@"Asia/Tehran",
@"Asia/Tokyo",
@"Europe/Istanbul",
@"Europe/Lisbon",
@"Europe/London",
@"Europe/Moscow",
@"Europe/Paris",
@"GMT",
@"Pacific/Auckland",
@"Pacific/Honolulu",
@"UTC"
];
knownTimeZoneNames = [];
// Prefer the runtime's own IANA database, when it exposes one, over the
// hardcoded 35-city list above: it's the full current set, not a
// snapshot that will silently drift the way a hand-maintained list does.
// Prefer the runtime's own IANA database when it exposes one.
if (typeof Intl !== "undefined" && typeof Intl.supportedValuesOf === "function")
{
try
@@ -369,51 +347,36 @@ function _systemTimeZoneFromRuntime()
if (supportedZones && supportedZones.length > 0)
{
var zones = [];
var hasGMT = false;
var hasUTC = false;
var count = supportedZones.length;
// Iterate using primitive property access.
// The array returned by Intl across the runtime bridge may lack
// standard Array prototypes (e.g., slice, indexOf). A standard loop
// ensures safe data extraction into a local array without triggering
// prototype resolution exceptions or relying on CPArray.
for (var i = 0; i < count; i++)
{
var zone = supportedZones[i];
zones[i] = zone;
if (zone === @"GMT")
hasGMT = true;
else if (zone === @"UTC")
hasUTC = true;
knownTimeZoneNames[i] = supportedZones[i];
}
// Explicitly restore legacy aliases if the host engine omits them.
// Engines adhering strictly to canonical IANA identifiers omit "GMT"
// and "UTC", but code in this class treats both as always-known.
if (!hasGMT)
zones[zones.length] = @"GMT";
if (!hasUTC)
zones[zones.length] = @"UTC";
knownTimeZoneNames = zones;
}
}
catch (e)
{
// Fall through, keep the hardcoded list above.
// Fall through.
}
}
// O(1) membership testing for -initWithName: and the runtime zone
// resolver, instead of a linear scan over several hundred names.
knownTimeZoneNamesSet = {};
var hasGMT = false;
var hasUTC = false;
for (var i = 0, count = knownTimeZoneNames.length; i < count; i++)
knownTimeZoneNamesSet[knownTimeZoneNames[i]] = true;
{
if (knownTimeZoneNames[i] === @"GMT")
hasGMT = true;
else if (knownTimeZoneNames[i] === @"UTC")
hasUTC = true;
}
if (!hasGMT)
knownTimeZoneNames[knownTimeZoneNames.length] = @"GMT";
if (!hasUTC)
knownTimeZoneNames[knownTimeZoneNames.length] = @"UTC";
// A curated abbreviation -> canonical name lookup for +timeZoneWithAbbreviation:.
// This stays a fixed, hand-maintained set deliberately: abbreviations are
@@ -676,7 +639,7 @@ function _systemTimeZoneFromRuntime()
if (!tzName)
[CPException raise:CPInvalidArgumentException reason:"Invalid value provided for tzName"];
if (!knownTimeZoneNamesSet[tzName] || !abbreviation)
if (!_isValidTimeZoneName(tzName) || !abbreviation)
return nil;
if (self = [super init])
@@ -715,7 +678,7 @@ function _systemTimeZoneFromRuntime()
if (!tzName)
[CPException raise:CPInvalidArgumentException reason:"Invalid value provided for tzName"];
if (!knownTimeZoneNamesSet[tzName])
if (!_isValidTimeZoneName(tzName))
return nil;
if (self = [super init])