#include "Python.h" #include "structmember.h" #include #include #include #include "datetime.h" // Imports static PyObject *io_open = NULL; static PyObject *_tzpath_find_tzfile = NULL; static PyObject *_common_mod = NULL; typedef struct TransitionRuleType TransitionRuleType; typedef struct StrongCacheNode StrongCacheNode; typedef struct { PyObject *utcoff; PyObject *dstoff; PyObject *tzname; long utcoff_seconds; } _ttinfo; typedef struct { _ttinfo std; _ttinfo dst; int dst_diff; TransitionRuleType *start; TransitionRuleType *end; unsigned char std_only; } _tzrule; typedef struct { PyDateTime_TZInfo base; PyObject *key; PyObject *file_repr; PyObject *weakreflist; size_t num_transitions; size_t num_ttinfos; int64_t *trans_list_utc; int64_t *trans_list_wall[2]; _ttinfo **trans_ttinfos; // References to the ttinfo for each transition _ttinfo *ttinfo_before; _tzrule tzrule_after; _ttinfo *_ttinfos; // Unique array of ttinfos for ease of deallocation unsigned char fixed_offset; unsigned char source; } PyZoneInfo_ZoneInfo; struct TransitionRuleType { int64_t (*year_to_timestamp)(TransitionRuleType *, int); }; typedef struct { TransitionRuleType base; uint8_t month; uint8_t week; uint8_t day; int8_t hour; int8_t minute; int8_t second; } CalendarRule; typedef struct { TransitionRuleType base; uint8_t julian; unsigned int day; int8_t hour; int8_t minute; int8_t second; } DayRule; struct StrongCacheNode { StrongCacheNode *next; StrongCacheNode *prev; PyObject *key; PyObject *zone; }; static PyTypeObject PyZoneInfo_ZoneInfoType; // Globals static PyObject *TIMEDELTA_CACHE = NULL; static PyObject *ZONEINFO_WEAK_CACHE = NULL; static StrongCacheNode *ZONEINFO_STRONG_CACHE = NULL; static size_t ZONEINFO_STRONG_CACHE_MAX_SIZE = 8; static _ttinfo NO_TTINFO = {NULL, NULL, NULL, 0}; // Constants static const int EPOCHORDINAL = 719163; static int DAYS_IN_MONTH[] = { -1, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, }; static int DAYS_BEFORE_MONTH[] = { -1, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, }; static const int SOURCE_NOCACHE = 0; static const int SOURCE_CACHE = 1; static const int SOURCE_FILE = 2; // Forward declarations static int load_data(PyZoneInfo_ZoneInfo *self, PyObject *file_obj); static void utcoff_to_dstoff(size_t *trans_idx, long *utcoffs, long *dstoffs, unsigned char *isdsts, size_t num_transitions, size_t num_ttinfos); static int ts_to_local(size_t *trans_idx, int64_t *trans_utc, long *utcoff, int64_t *trans_local[2], size_t num_ttinfos, size_t num_transitions); static int parse_tz_str(PyObject *tz_str_obj, _tzrule *out); static Py_ssize_t parse_abbr(const char *const p, PyObject **abbr); static Py_ssize_t parse_tz_delta(const char *const p, long *total_seconds); static Py_ssize_t parse_transition_time(const char *const p, int8_t *hour, int8_t *minute, int8_t *second); static Py_ssize_t parse_transition_rule(const char *const p, TransitionRuleType **out); static _ttinfo * find_tzrule_ttinfo(_tzrule *rule, int64_t ts, unsigned char fold, int year); static _ttinfo * find_tzrule_ttinfo_fromutc(_tzrule *rule, int64_t ts, int year, unsigned char *fold); static int build_ttinfo(long utcoffset, long dstoffset, PyObject *tzname, _ttinfo *out); static void xdecref_ttinfo(_ttinfo *ttinfo); static int ttinfo_eq(const _ttinfo *const tti0, const _ttinfo *const tti1); static int build_tzrule(PyObject *std_abbr, PyObject *dst_abbr, long std_offset, long dst_offset, TransitionRuleType *start, TransitionRuleType *end, _tzrule *out); static void free_tzrule(_tzrule *tzrule); static PyObject * load_timedelta(long seconds); static int get_local_timestamp(PyObject *dt, int64_t *local_ts); static _ttinfo * find_ttinfo(PyZoneInfo_ZoneInfo *self, PyObject *dt); static int ymd_to_ord(int y, int m, int d); static int is_leap_year(int year); static size_t _bisect(const int64_t value, const int64_t *arr, size_t size); static int eject_from_strong_cache(const PyTypeObject *const type, PyObject *key); static void clear_strong_cache(const PyTypeObject *const type); static void update_strong_cache(const PyTypeObject *const type, PyObject *key, PyObject *zone); static PyObject * zone_from_strong_cache(const PyTypeObject *const type, PyObject *const key); static PyObject * zoneinfo_new_instance(PyTypeObject *type, PyObject *key) { PyObject *file_obj = NULL; PyObject *file_path = NULL; file_path = PyObject_CallFunctionObjArgs(_tzpath_find_tzfile, key, NULL); if (file_path == NULL) { return NULL; } else if (file_path == Py_None) { file_obj = PyObject_CallMethod(_common_mod, "load_tzdata", "O", key); if (file_obj == NULL) { Py_DECREF(file_path); return NULL; } } PyObject *self = (PyObject *)(type->tp_alloc(type, 0)); if (self == NULL) { goto error; } if (file_obj == NULL) { file_obj = PyObject_CallFunction(io_open, "Os", file_path, "rb"); if (file_obj == NULL) { goto error; } } if (load_data((PyZoneInfo_ZoneInfo *)self, file_obj)) { goto error; } PyObject *rv = PyObject_CallMethod(file_obj, "close", NULL); Py_DECREF(file_obj); file_obj = NULL; if (rv == NULL) { goto error; } Py_DECREF(rv); ((PyZoneInfo_ZoneInfo *)self)->key = key; Py_INCREF(key); goto cleanup; error: Py_XDECREF(self); self = NULL; cleanup: if (file_obj != NULL) { PyObject *exc, *val, *tb; PyErr_Fetch(&exc, &val, &tb); PyObject *tmp = PyObject_CallMethod(file_obj, "close", NULL); _PyErr_ChainExceptions(exc, val, tb); if (tmp == NULL) { Py_CLEAR(self); } Py_XDECREF(tmp); Py_DECREF(file_obj); } Py_DECREF(file_path); return self; } static PyObject * get_weak_cache(PyTypeObject *type) { if (type == &PyZoneInfo_ZoneInfoType) { return ZONEINFO_WEAK_CACHE; } else { PyObject *cache = PyObject_GetAttrString((PyObject *)type, "_weak_cache"); // We are assuming that the type lives at least as long as the function // that calls get_weak_cache, and that it holds a reference to the // cache, so we'll return a "borrowed reference". Py_XDECREF(cache); return cache; } } static PyObject * zoneinfo_new(PyTypeObject *type, PyObject *args, PyObject *kw) { PyObject *key = NULL; static char *kwlist[] = {"key", NULL}; if (PyArg_ParseTupleAndKeywords(args, kw, "O", kwlist, &key) == 0) { return NULL; } PyObject *instance = zone_from_strong_cache(type, key); if (instance != NULL || PyErr_Occurred()) { return instance; } PyObject *weak_cache = get_weak_cache(type); instance = PyObject_CallMethod(weak_cache, "get", "O", key, Py_None); if (instance == NULL) { return NULL; } if (instance == Py_None) { Py_DECREF(instance); PyObject *tmp = zoneinfo_new_instance(type, key); if (tmp == NULL) { return NULL; } instance = PyObject_CallMethod(weak_cache, "setdefault", "OO", key, tmp); Py_DECREF(tmp); if (instance == NULL) { return NULL; } ((PyZoneInfo_ZoneInfo *)instance)->source = SOURCE_CACHE; } update_strong_cache(type, key, instance); return instance; } static void zoneinfo_dealloc(PyObject *obj_self) { PyZoneInfo_ZoneInfo *self = (PyZoneInfo_ZoneInfo *)obj_self; if (self->weakreflist != NULL) { PyObject_ClearWeakRefs(obj_self); } if (self->trans_list_utc != NULL) { PyMem_Free(self->trans_list_utc); } for (size_t i = 0; i < 2; i++) { if (self->trans_list_wall[i] != NULL) { PyMem_Free(self->trans_list_wall[i]); } } if (self->_ttinfos != NULL) { for (size_t i = 0; i < self->num_ttinfos; ++i) { xdecref_ttinfo(&(self->_ttinfos[i])); } PyMem_Free(self->_ttinfos); } if (self->trans_ttinfos != NULL) { PyMem_Free(self->trans_ttinfos); } free_tzrule(&(self->tzrule_after)); Py_XDECREF(self->key); Py_XDECREF(self->file_repr); Py_TYPE(self)->tp_free((PyObject *)self); } static PyObject * zoneinfo_from_file(PyTypeObject *type, PyObject *args, PyObject *kwargs) { PyObject *file_obj = NULL; PyObject *file_repr = NULL; PyObject *key = Py_None; PyZoneInfo_ZoneInfo *self = NULL; static char *kwlist[] = {"", "key", NULL}; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|O", kwlist, &file_obj, &key)) { return NULL; } PyObject *obj_self = (PyObject *)(type->tp_alloc(type, 0)); self = (PyZoneInfo_ZoneInfo *)obj_self; if (self == NULL) { return NULL; } file_repr = PyUnicode_FromFormat("%R", file_obj); if (file_repr == NULL) { goto error; } if (load_data(self, file_obj)) { goto error; } self->source = SOURCE_FILE; self->file_repr = file_repr; self->key = key; Py_INCREF(key); return obj_self; error: Py_XDECREF(file_repr); Py_XDECREF(self); return NULL; } static PyObject * zoneinfo_no_cache(PyTypeObject *cls, PyObject *args, PyObject *kwargs) { static char *kwlist[] = {"key", NULL}; PyObject *key = NULL; if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", kwlist, &key)) { return NULL; } PyObject *out = zoneinfo_new_instance(cls, key); if (out != NULL) { ((PyZoneInfo_ZoneInfo *)out)->source = SOURCE_NOCACHE; } return out; } static PyObject * zoneinfo_clear_cache(PyObject *cls, PyObject *args, PyObject *kwargs) { PyObject *only_keys = NULL; static char *kwlist[] = {"only_keys", NULL}; if (!(PyArg_ParseTupleAndKeywords(args, kwargs, "|$O", kwlist, &only_keys))) { return NULL; } PyTypeObject *type = (PyTypeObject *)cls; PyObject *weak_cache = get_weak_cache(type); if (only_keys == NULL || only_keys == Py_None) { PyObject *rv = PyObject_CallMethod(weak_cache, "clear", NULL); if (rv != NULL) { Py_DECREF(rv); } clear_strong_cache(type); } else { PyObject *item = NULL; PyObject *pop = PyUnicode_FromString("pop"); if (pop == NULL) { return NULL; } PyObject *iter = PyObject_GetIter(only_keys); if (iter == NULL) { Py_DECREF(pop); return NULL; } while ((item = PyIter_Next(iter))) { // Remove from strong cache if (eject_from_strong_cache(type, item) < 0) { Py_DECREF(item); break; } // Remove from weak cache PyObject *tmp = PyObject_CallMethodObjArgs(weak_cache, pop, item, Py_None, NULL); Py_DECREF(item); if (tmp == NULL) { break; } Py_DECREF(tmp); } Py_DECREF(iter); Py_DECREF(pop); } if (PyErr_Occurred()) { return NULL; } Py_RETURN_NONE; } static PyObject * zoneinfo_utcoffset(PyObject *self, PyObject *dt) { _ttinfo *tti = find_ttinfo((PyZoneInfo_ZoneInfo *)self, dt); if (tti == NULL) { return NULL; } Py_INCREF(tti->utcoff); return tti->utcoff; } static PyObject * zoneinfo_dst(PyObject *self, PyObject *dt) { _ttinfo *tti = find_ttinfo((PyZoneInfo_ZoneInfo *)self, dt); if (tti == NULL) { return NULL; } Py_INCREF(tti->dstoff); return tti->dstoff; } static PyObject * zoneinfo_tzname(PyObject *self, PyObject *dt) { _ttinfo *tti = find_ttinfo((PyZoneInfo_ZoneInfo *)self, dt); if (tti == NULL) { return NULL; } Py_INCREF(tti->tzname); return tti->tzname; } #define HASTZINFO(p) (((_PyDateTime_BaseTZInfo *)(p))->hastzinfo) #define GET_DT_TZINFO(p) \ (HASTZINFO(p) ? ((PyDateTime_DateTime *)(p))->tzinfo : Py_None) static PyObject * zoneinfo_fromutc(PyObject *obj_self, PyObject *dt) { if (!PyDateTime_Check(dt)) { PyErr_SetString(PyExc_TypeError, "fromutc: argument must be a datetime"); return NULL; } if (GET_DT_TZINFO(dt) != obj_self) { PyErr_SetString(PyExc_ValueError, "fromutc: dt.tzinfo " "is not self"); return NULL; } PyZoneInfo_ZoneInfo *self = (PyZoneInfo_ZoneInfo *)obj_self; int64_t timestamp; if (get_local_timestamp(dt, ×tamp)) { return NULL; } size_t num_trans = self->num_transitions; _ttinfo *tti = NULL; unsigned char fold = 0; if (num_trans >= 1 && timestamp < self->trans_list_utc[0]) { tti = self->ttinfo_before; } else if (num_trans == 0 || timestamp > self->trans_list_utc[num_trans - 1]) { tti = find_tzrule_ttinfo_fromutc(&(self->tzrule_after), timestamp, PyDateTime_GET_YEAR(dt), &fold); // Immediately after the last manual transition, the fold/gap is // between self->trans_ttinfos[num_transitions - 1] and whatever // ttinfo applies immediately after the last transition, not between // the STD and DST rules in the tzrule_after, so we may need to // adjust the fold value. if (num_trans) { _ttinfo *tti_prev = NULL; if (num_trans == 1) { tti_prev = self->ttinfo_before; } else { tti_prev = self->trans_ttinfos[num_trans - 2]; } int64_t diff = tti_prev->utcoff_seconds - tti->utcoff_seconds; if (diff > 0 && timestamp < (self->trans_list_utc[num_trans - 1] + diff)) { fold = 1; } } } else { size_t idx = _bisect(timestamp, self->trans_list_utc, num_trans); _ttinfo *tti_prev = NULL; if (idx >= 2) { tti_prev = self->trans_ttinfos[idx - 2]; tti = self->trans_ttinfos[idx - 1]; } else { tti_prev = self->ttinfo_before; tti = self->trans_ttinfos[0]; } // Detect fold int64_t shift = (int64_t)(tti_prev->utcoff_seconds - tti->utcoff_seconds); if (shift > (timestamp - self->trans_list_utc[idx - 1])) { fold = 1; } } PyObject *tmp = PyNumber_Add(dt, tti->utcoff); if (tmp == NULL) { return NULL; } if (fold) { if (PyDateTime_CheckExact(tmp)) { ((PyDateTime_DateTime *)tmp)->fold = 1; dt = tmp; } else { PyObject *replace = PyObject_GetAttrString(tmp, "replace"); PyObject *args = PyTuple_New(0); PyObject *kwargs = PyDict_New(); Py_DECREF(tmp); if (args == NULL || kwargs == NULL || replace == NULL) { Py_XDECREF(args); Py_XDECREF(kwargs); Py_XDECREF(replace); return NULL; } dt = NULL; if (!PyDict_SetItemString(kwargs, "fold", _PyLong_One)) { dt = PyObject_Call(replace, args, kwargs); } Py_DECREF(args); Py_DECREF(kwargs); Py_DECREF(replace); if (dt == NULL) { return NULL; } } } else { dt = tmp; } return dt; } static PyObject * zoneinfo_repr(PyZoneInfo_ZoneInfo *self) { PyObject *rv = NULL; const char *type_name = Py_TYPE((PyObject *)self)->tp_name; if (!(self->key == Py_None)) { rv = PyUnicode_FromFormat("%s(key=%R)", type_name, self->key); } else { assert(PyUnicode_Check(self->file_repr)); rv = PyUnicode_FromFormat("%s.from_file(%U)", type_name, self->file_repr); } return rv; } static PyObject * zoneinfo_str(PyZoneInfo_ZoneInfo *self) { if (!(self->key == Py_None)) { Py_INCREF(self->key); return self->key; } else { return zoneinfo_repr(self); } } /* Pickles the ZoneInfo object by key and source. * * ZoneInfo objects are pickled by reference to the TZif file that they came * from, which means that the exact transitions may be different or the file * may not un-pickle if the data has changed on disk in the interim. * * It is necessary to include a bit indicating whether or not the object * was constructed from the cache, because from-cache objects will hit the * unpickling process's cache, whereas no-cache objects will bypass it. * * Objects constructed from ZoneInfo.from_file cannot be pickled. */ static PyObject * zoneinfo_reduce(PyObject *obj_self, PyObject *unused) { PyZoneInfo_ZoneInfo *self = (PyZoneInfo_ZoneInfo *)obj_self; if (self->source == SOURCE_FILE) { // Objects constructed from files cannot be pickled. PyObject *pickle = PyImport_ImportModule("pickle"); if (pickle == NULL) { return NULL; } PyObject *pickle_error = PyObject_GetAttrString(pickle, "PicklingError"); Py_DECREF(pickle); if (pickle_error == NULL) { return NULL; } PyErr_Format(pickle_error, "Cannot pickle a ZoneInfo file from a file stream."); Py_DECREF(pickle_error); return NULL; } unsigned char from_cache = self->source == SOURCE_CACHE ? 1 : 0; PyObject *constructor = PyObject_GetAttrString(obj_self, "_unpickle"); if (constructor == NULL) { return NULL; } PyObject *rv = Py_BuildValue("O(OB)", constructor, self->key, from_cache); Py_DECREF(constructor); return rv; } static PyObject * zoneinfo__unpickle(PyTypeObject *cls, PyObject *args) { PyObject *key; unsigned char from_cache; if (!PyArg_ParseTuple(args, "OB", &key, &from_cache)) { return NULL; } if (from_cache) { PyObject *val_args = Py_BuildValue("(O)", key); if (val_args == NULL) { return NULL; } PyObject *rv = zoneinfo_new(cls, val_args, NULL); Py_DECREF(val_args); return rv; } else { return zoneinfo_new_instance(cls, key); } } /* It is relatively expensive to construct new timedelta objects, and in most * cases we're looking at a relatively small number of timedeltas, such as * integer number of hours, etc. We will keep a cache so that we construct * a minimal number of these. * * Possibly this should be replaced with an LRU cache so that it's not possible * for the memory usage to explode from this, but in order for this to be a * serious problem, one would need to deliberately craft a malicious time zone * file with many distinct offsets. As of tzdb 2019c, loading every single zone * fills the cache with ~450 timedeltas for a total size of ~12kB. * * This returns a new reference to the timedelta. */ static PyObject * load_timedelta(long seconds) { PyObject *rv = NULL; PyObject *pyoffset = PyLong_FromLong(seconds); if (pyoffset == NULL) { return NULL; } int contains = PyDict_Contains(TIMEDELTA_CACHE, pyoffset); if (contains == -1) { goto error; } if (!contains) { PyObject *tmp = PyDateTimeAPI->Delta_FromDelta( 0, seconds, 0, 1, PyDateTimeAPI->DeltaType); if (tmp == NULL) { goto error; } rv = PyDict_SetDefault(TIMEDELTA_CACHE, pyoffset, tmp); Py_DECREF(tmp); } else { rv = PyDict_GetItem(TIMEDELTA_CACHE, pyoffset); } Py_DECREF(pyoffset); Py_INCREF(rv); return rv; error: Py_DECREF(pyoffset); return NULL; } /* Constructor for _ttinfo object - this starts by initializing the _ttinfo * to { NULL, NULL, NULL }, so that Py_XDECREF will work on partially * initialized _ttinfo objects. */ static int build_ttinfo(long utcoffset, long dstoffset, PyObject *tzname, _ttinfo *out) { out->utcoff = NULL; out->dstoff = NULL; out->tzname = NULL; out->utcoff_seconds = utcoffset; out->utcoff = load_timedelta(utcoffset); if (out->utcoff == NULL) { return -1; } out->dstoff = load_timedelta(dstoffset); if (out->dstoff == NULL) { return -1; } out->tzname = tzname; Py_INCREF(tzname); return 0; } /* Decrease reference count on any non-NULL members of a _ttinfo */ static void xdecref_ttinfo(_ttinfo *ttinfo) { if (ttinfo != NULL) { Py_XDECREF(ttinfo->utcoff); Py_XDECREF(ttinfo->dstoff); Py_XDECREF(ttinfo->tzname); } } /* Equality function for _ttinfo. */ static int ttinfo_eq(const _ttinfo *const tti0, const _ttinfo *const tti1) { int rv; if ((rv = PyObject_RichCompareBool(tti0->utcoff, tti1->utcoff, Py_EQ)) < 1) { goto end; } if ((rv = PyObject_RichCompareBool(tti0->dstoff, tti1->dstoff, Py_EQ)) < 1) { goto end; } if ((rv = PyObject_RichCompareBool(tti0->tzname, tti1->tzname, Py_EQ)) < 1) { goto end; } end: return rv; } /* Given a file-like object, this populates a ZoneInfo object * * The current version calls into a Python function to read the data from * file into Python objects, and this translates those Python objects into * C values and calculates derived values (e.g. dstoff) in C. * * This returns 0 on success and -1 on failure. * * The function will never return while `self` is partially initialized — * the object only needs to be freed / deallocated if this succeeds. */ static int load_data(PyZoneInfo_ZoneInfo *self, PyObject *file_obj) { PyObject *data_tuple = NULL; long *utcoff = NULL; long *dstoff = NULL; size_t *trans_idx = NULL; unsigned char *isdst = NULL; self->trans_list_utc = NULL; self->trans_list_wall[0] = NULL; self->trans_list_wall[1] = NULL; self->trans_ttinfos = NULL; self->_ttinfos = NULL; self->file_repr = NULL; size_t ttinfos_allocated = 0; data_tuple = PyObject_CallMethod(_common_mod, "load_data", "O", file_obj); if (data_tuple == NULL) { goto error; } if (!PyTuple_CheckExact(data_tuple)) { PyErr_Format(PyExc_TypeError, "Invalid data result type: %r", data_tuple); goto error; } // Unpack the data tuple PyObject *trans_idx_list = PyTuple_GetItem(data_tuple, 0); if (trans_idx_list == NULL) { goto error; } PyObject *trans_utc = PyTuple_GetItem(data_tuple, 1); if (trans_utc == NULL) { goto error; } PyObject *utcoff_list = PyTuple_GetItem(data_tuple, 2); if (utcoff_list == NULL) { goto error; } PyObject *isdst_list = PyTuple_GetItem(data_tuple, 3); if (isdst_list == NULL) { goto error; } PyObject *abbr = PyTuple_GetItem(data_tuple, 4); if (abbr == NULL) { goto error; } PyObject *tz_str = PyTuple_GetItem(data_tuple, 5); if (tz_str == NULL) { goto error; } // Load the relevant sizes Py_ssize_t num_transitions = PyTuple_Size(trans_utc); if (num_transitions < 0) { goto error; } Py_ssize_t num_ttinfos = PyTuple_Size(utcoff_list); if (num_ttinfos < 0) { goto error; } self->num_transitions = (size_t)num_transitions; self->num_ttinfos = (size_t)num_ttinfos; // Load the transition indices and list self->trans_list_utc = PyMem_Malloc(self->num_transitions * sizeof(int64_t)); if (self->trans_list_utc == NULL) { goto error; } trans_idx = PyMem_Malloc(self->num_transitions * sizeof(Py_ssize_t)); if (trans_idx == NULL) { goto error; } for (size_t i = 0; i < self->num_transitions; ++i) { PyObject *num = PyTuple_GetItem(trans_utc, i); if (num == NULL) { goto error; } self->trans_list_utc[i] = PyLong_AsLongLong(num); if (self->trans_list_utc[i] == -1 && PyErr_Occurred()) { goto error; } num = PyTuple_GetItem(trans_idx_list, i); if (num == NULL) { goto error; } Py_ssize_t cur_trans_idx = PyLong_AsSsize_t(num); if (cur_trans_idx == -1) { goto error; } trans_idx[i] = (size_t)cur_trans_idx; if (trans_idx[i] > self->num_ttinfos) { PyErr_Format( PyExc_ValueError, "Invalid transition index found while reading TZif: %zd", cur_trans_idx); goto error; } } // Load UTC offsets and isdst (size num_ttinfos) utcoff = PyMem_Malloc(self->num_ttinfos * sizeof(long)); isdst = PyMem_Malloc(self->num_ttinfos * sizeof(unsigned char)); if (utcoff == NULL || isdst == NULL) { goto error; } for (size_t i = 0; i < self->num_ttinfos; ++i) { PyObject *num = PyTuple_GetItem(utcoff_list, i); if (num == NULL) { goto error; } utcoff[i] = PyLong_AsLong(num); if (utcoff[i] == -1 && PyErr_Occurred()) { goto error; } num = PyTuple_GetItem(isdst_list, i); if (num == NULL) { goto error; } int isdst_with_error = PyObject_IsTrue(num); if (isdst_with_error == -1) { goto error; } else { isdst[i] = (unsigned char)isdst_with_error; } } dstoff = PyMem_Calloc(self->num_ttinfos, sizeof(long)); if (dstoff == NULL) { goto error; } // Derive dstoff and trans_list_wall from the information we've loaded utcoff_to_dstoff(trans_idx, utcoff, dstoff, isdst, self->num_transitions, self->num_ttinfos); if (ts_to_local(trans_idx, self->trans_list_utc, utcoff, self->trans_list_wall, self->num_ttinfos, self->num_transitions)) { goto error; } // Build _ttinfo objects from utcoff, dstoff and abbr self->_ttinfos = PyMem_Malloc(self->num_ttinfos * sizeof(_ttinfo)); if (self->_ttinfos == NULL) { goto error; } for (size_t i = 0; i < self->num_ttinfos; ++i) { PyObject *tzname = PyTuple_GetItem(abbr, i); if (tzname == NULL) { goto error; } ttinfos_allocated++; if (build_ttinfo(utcoff[i], dstoff[i], tzname, &(self->_ttinfos[i]))) { goto error; } } // Build our mapping from transition to the ttinfo that applies self->trans_ttinfos = PyMem_Calloc(self->num_transitions, sizeof(_ttinfo *)); if (self->trans_ttinfos == NULL) { goto error; } for (size_t i = 0; i < self->num_transitions; ++i) { size_t ttinfo_idx = trans_idx[i]; assert(ttinfo_idx < self->num_ttinfos); self->trans_ttinfos[i] = &(self->_ttinfos[ttinfo_idx]); } // Set ttinfo_before to the first non-DST transition for (size_t i = 0; i < self->num_ttinfos; ++i) { if (!isdst[i]) { self->ttinfo_before = &(self->_ttinfos[i]); break; } } // If there are only DST ttinfos, pick the first one, if there are no // ttinfos at all, set ttinfo_before to NULL if (self->ttinfo_before == NULL && self->num_ttinfos > 0) { self->ttinfo_before = &(self->_ttinfos[0]); } if (tz_str != Py_None && PyObject_IsTrue(tz_str)) { if (parse_tz_str(tz_str, &(self->tzrule_after))) { goto error; } } else { if (!self->num_ttinfos) { PyErr_Format(PyExc_ValueError, "No time zone information found."); goto error; } size_t idx; if (!self->num_transitions) { idx = self->num_ttinfos - 1; } else { idx = trans_idx[self->num_transitions - 1]; } _ttinfo *tti = &(self->_ttinfos[idx]); build_tzrule(tti->tzname, NULL, tti->utcoff_seconds, 0, NULL, NULL, &(self->tzrule_after)); // We've abused the build_tzrule constructor to construct an STD-only // rule mimicking whatever ttinfo we've picked up, but it's possible // that the one we've picked up is a DST zone, so we need to make sure // that the dstoff is set correctly in that case. if (PyObject_IsTrue(tti->dstoff)) { _ttinfo *tti_after = &(self->tzrule_after.std); Py_DECREF(tti_after->dstoff); tti_after->dstoff = tti->dstoff; Py_INCREF(tti_after->dstoff); } } // Determine if this is a "fixed offset" zone, meaning that the output of // the utcoffset, dst and tzname functions does not depend on the specific // datetime passed. // // We make three simplifying assumptions here: // // 1. If tzrule_after is not std_only, it has transitions that might occur // (it is possible to construct TZ strings that specify STD and DST but // no transitions ever occur, such as AAA0BBB,0/0,J365/25). // 2. If self->_ttinfos contains more than one _ttinfo object, the objects // represent different offsets. // 3. self->ttinfos contains no unused _ttinfos (in which case an otherwise // fixed-offset zone with extra _ttinfos defined may appear to *not* be // a fixed offset zone). // // Violations to these assumptions would be fairly exotic, and exotic // zones should almost certainly not be used with datetime.time (the // only thing that would be affected by this). if (self->num_ttinfos > 1 || !self->tzrule_after.std_only) { self->fixed_offset = 0; } else if (self->num_ttinfos == 0) { self->fixed_offset = 1; } else { int constant_offset = ttinfo_eq(&(self->_ttinfos[0]), &self->tzrule_after.std); if (constant_offset < 0) { goto error; } else { self->fixed_offset = constant_offset; } } int rv = 0; goto cleanup; error: // These resources only need to be freed if we have failed, if we succeed // in initializing a PyZoneInfo_ZoneInfo object, we can rely on its dealloc // method to free the relevant resources. if (self->trans_list_utc != NULL) { PyMem_Free(self->trans_list_utc); self->trans_list_utc = NULL; } for (size_t i = 0; i < 2; ++i) { if (self->trans_list_wall[i] != NULL) { PyMem_Free(self->trans_list_wall[i]); self->trans_list_wall[i] = NULL; } } if (self->_ttinfos != NULL) { for (size_t i = 0; i < ttinfos_allocated; ++i) { xdecref_ttinfo(&(self->_ttinfos[i])); } PyMem_Free(self->_ttinfos); self->_ttinfos = NULL; } if (self->trans_ttinfos != NULL) { PyMem_Free(self->trans_ttinfos); self->trans_ttinfos = NULL; } rv = -1; cleanup: Py_XDECREF(data_tuple); if (utcoff != NULL) { PyMem_Free(utcoff); } if (dstoff != NULL) { PyMem_Free(dstoff); } if (isdst != NULL) { PyMem_Free(isdst); } if (trans_idx != NULL) { PyMem_Free(trans_idx); } return rv; } /* Function to calculate the local timestamp of a transition from the year. */ int64_t calendarrule_year_to_timestamp(TransitionRuleType *base_self, int year) { CalendarRule *self = (CalendarRule *)base_self; // We want (year, month, day of month); we have year and month, but we // need to turn (week, day-of-week) into day-of-month // // Week 1 is the first week in which day `day` (where 0 = Sunday) appears. // Week 5 represents the last occurrence of day `day`, so we need to know // the first weekday of the month and the number of days in the month. int8_t first_day = (ymd_to_ord(year, self->month, 1) + 6) % 7; uint8_t days_in_month = DAYS_IN_MONTH[self->month]; if (self->month == 2 && is_leap_year(year)) { days_in_month += 1; } // This equation seems magical, so I'll break it down: // 1. calendar says 0 = Monday, POSIX says 0 = Sunday so we need first_day // + 1 to get 1 = Monday -> 7 = Sunday, which is still equivalent // because this math is mod 7 // 2. Get first day - desired day mod 7 (adjusting by 7 for negative // numbers so that -1 % 7 = 6). // 3. Add 1 because month days are a 1-based index. int8_t month_day = ((int8_t)(self->day) - (first_day + 1)) % 7; if (month_day < 0) { month_day += 7; } month_day += 1; // Now use a 0-based index version of `week` to calculate the w-th // occurrence of `day` month_day += ((int8_t)(self->week) - 1) * 7; // month_day will only be > days_in_month if w was 5, and `w` means "last // occurrence of `d`", so now we just check if we over-shot the end of the // month and if so knock off 1 week. if (month_day > days_in_month) { month_day -= 7; } int64_t ordinal = ymd_to_ord(year, self->month, month_day) - EPOCHORDINAL; return ((ordinal * 86400) + (int64_t)(self->hour * 3600) + (int64_t)(self->minute * 60) + (int64_t)(self->second)); } /* Constructor for CalendarRule. */ int calendarrule_new(uint8_t month, uint8_t week, uint8_t day, int8_t hour, int8_t minute, int8_t second, CalendarRule *out) { // These bounds come from the POSIX standard, which describes an Mm.n.d // rule as: // // The d'th day (0 <= d <= 6) of week n of month m of the year (1 <= n <= // 5, 1 <= m <= 12, where week 5 means "the last d day in month m" which // may occur in either the fourth or the fifth week). Week 1 is the first // week in which the d'th day occurs. Day zero is Sunday. if (month <= 0 || month > 12) { PyErr_Format(PyExc_ValueError, "Month must be in (0, 12]"); return -1; } if (week <= 0 || week > 5) { PyErr_Format(PyExc_ValueError, "Week must be in (0, 5]"); return -1; } // If the 'day' parameter type is changed to a signed type, // "day < 0" check must be added. if (/* day < 0 || */ day > 6) { PyErr_Format(PyExc_ValueError, "Day must be in [0, 6]"); return -1; } TransitionRuleType base = {&calendarrule_year_to_timestamp}; CalendarRule new_offset = { .base = base, .month = month, .week = week, .day = day, .hour = hour, .minute = minute, .second = second, }; *out = new_offset; return 0; } /* Function to calculate the local timestamp of a transition from the year. * * This translates the day of the year into a local timestamp — either a * 1-based Julian day, not including leap days, or the 0-based year-day, * including leap days. * */ int64_t dayrule_year_to_timestamp(TransitionRuleType *base_self, int year) { // The function signature requires a TransitionRuleType pointer, but this // function is only applicable to DayRule* objects. DayRule *self = (DayRule *)base_self; // ymd_to_ord calculates the number of days since 0001-01-01, but we want // to know the number of days since 1970-01-01, so we must subtract off // the equivalent of ymd_to_ord(1970, 1, 1). // // We subtract off an additional 1 day to account for January 1st (we want // the number of full days *before* the date of the transition - partial // days are accounted for in the hour, minute and second portions. int64_t days_before_year = ymd_to_ord(year, 1, 1) - EPOCHORDINAL - 1; // The Julian day specification skips over February 29th in leap years, // from the POSIX standard: // // Leap days shall not be counted. That is, in all years-including leap // years-February 28 is day 59 and March 1 is day 60. It is impossible to // refer explicitly to the occasional February 29. // // This is actually more useful than you'd think — if you want a rule that // always transitions on a given calendar day (other than February 29th), // you would use a Julian day, e.g. J91 always refers to April 1st and J365 // always refers to December 31st. unsigned int day = self->day; if (self->julian && day >= 59 && is_leap_year(year)) { day += 1; } return ((days_before_year + day) * 86400) + (self->hour * 3600) + (self->minute * 60) + self->second; } /* Constructor for DayRule. */ static int dayrule_new(uint8_t julian, unsigned int day, int8_t hour, int8_t minute, int8_t second, DayRule *out) { // The POSIX standard specifies that Julian days must be in the range (1 <= // n <= 365) and that non-Julian (they call it "0-based Julian") days must // be in the range (0 <= n <= 365). if (day < julian || day > 365) { PyErr_Format(PyExc_ValueError, "day must be in [%u, 365], not: %u", julian, day); return -1; } TransitionRuleType base = { &dayrule_year_to_timestamp, }; DayRule tmp = { .base = base, .julian = julian, .day = day, .hour = hour, .minute = minute, .second = second, }; *out = tmp; return 0; } /* Calculate the start and end rules for a _tzrule in the given year. */ static void tzrule_transitions(_tzrule *rule, int year, int64_t *start, int64_t *end) { assert(rule->start != NULL); assert(rule->end != NULL); *start = rule->start->year_to_timestamp(rule->start, year); *end = rule->end->year_to_timestamp(rule->end, year); } /* Calculate the _ttinfo that applies at a given local time from a _tzrule. * * This takes a local timestamp and fold for disambiguation purposes; the year * could technically be calculated from the timestamp, but given that the * callers of this function already have the year information accessible from * the datetime struct, it is taken as an additional parameter to reduce * unnecessary calculation. * */ static _ttinfo * find_tzrule_ttinfo(_tzrule *rule, int64_t ts, unsigned char fold, int year) { if (rule->std_only) { return &(rule->std); } int64_t start, end; uint8_t isdst; tzrule_transitions(rule, year, &start, &end); // With fold = 0, the period (denominated in local time) with the smaller // offset starts at the end of the gap and ends at the end of the fold; // with fold = 1, it runs from the start of the gap to the beginning of the // fold. // // So in order to determine the DST boundaries we need to know both the // fold and whether DST is positive or negative (rare), and it turns out // that this boils down to fold XOR is_positive. if (fold == (rule->dst_diff >= 0)) { end -= rule->dst_diff; } else { start += rule->dst_diff; } if (start < end) { isdst = (ts >= start) && (ts < end); } else { isdst = (ts < end) || (ts >= start); } if (isdst) { return &(rule->dst); } else { return &(rule->std); } } /* Calculate the ttinfo and fold that applies for a _tzrule at an epoch time. * * This function can determine the _ttinfo that applies at a given epoch time, * (analogous to trans_list_utc), and whether or not the datetime is in a fold. * This is to be used in the .fromutc() function. * * The year is technically a redundant parameter, because it can be calculated * from the timestamp, but all callers of this function should have the year * in the datetime struct anyway, so taking it as a parameter saves unnecessary * calculation. **/ static _ttinfo * find_tzrule_ttinfo_fromutc(_tzrule *rule, int64_t ts, int year, unsigned char *fold) { if (rule->std_only) { *fold = 0; return &(rule->std); } int64_t start, end; uint8_t isdst; tzrule_transitions(rule, year, &start, &end); start -= rule->std.utcoff_seconds; end -= rule->dst.utcoff_seconds; if (start < end) { isdst = (ts >= start) && (ts < end); } else { isdst = (ts < end) || (ts >= start); } // For positive DST, the ambiguous period is one dst_diff after the end of // DST; for negative DST, the ambiguous period is one dst_diff before the // start of DST. int64_t ambig_start, ambig_end; if (rule->dst_diff > 0) { ambig_start = end; ambig_end = end + rule->dst_diff; } else { ambig_start = start; ambig_end = start - rule->dst_diff; } *fold = (ts >= ambig_start) && (ts < ambig_end); if (isdst) { return &(rule->dst); } else { return &(rule->std); } } /* Parse a TZ string in the format specified by the POSIX standard: * * std offset[dst[offset],start[/time],end[/time]] * * std and dst must be 3 or more characters long and must not contain a * leading colon, embedded digits, commas, nor a plus or minus signs; The * spaces between "std" and "offset" are only for display and are not actually * present in the string. * * The format of the offset is ``[+|-]hh[:mm[:ss]]`` * * See the POSIX.1 spec: IEE Std 1003.1-2018 §8.3: * * https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html */ static int parse_tz_str(PyObject *tz_str_obj, _tzrule *out) { PyObject *std_abbr = NULL; PyObject *dst_abbr = NULL; TransitionRuleType *start = NULL; TransitionRuleType *end = NULL; // Initialize offsets to invalid value (> 24 hours) long std_offset = 1 << 20; long dst_offset = 1 << 20; char *tz_str = PyBytes_AsString(tz_str_obj); if (tz_str == NULL) { return -1; } char *p = tz_str; // Read the `std` abbreviation, which must be at least 3 characters long. Py_ssize_t num_chars = parse_abbr(p, &std_abbr); if (num_chars < 1) { PyErr_Format(PyExc_ValueError, "Invalid STD format in %R", tz_str_obj); goto error; } p += num_chars; // Now read the STD offset, which is required num_chars = parse_tz_delta(p, &std_offset); if (num_chars < 0) { PyErr_Format(PyExc_ValueError, "Invalid STD offset in %R", tz_str_obj); goto error; } p += num_chars; // If the string ends here, there is no DST, otherwise we must parse the // DST abbreviation and start and end dates and times. if (*p == '\0') { goto complete; } num_chars = parse_abbr(p, &dst_abbr); if (num_chars < 1) { PyErr_Format(PyExc_ValueError, "Invalid DST format in %R", tz_str_obj); goto error; } p += num_chars; if (*p == ',') { // From the POSIX standard: // // If no offset follows dst, the alternative time is assumed to be one // hour ahead of standard time. dst_offset = std_offset + 3600; } else { num_chars = parse_tz_delta(p, &dst_offset); if (num_chars < 0) { PyErr_Format(PyExc_ValueError, "Invalid DST offset in %R", tz_str_obj); goto error; } p += num_chars; } TransitionRuleType **transitions[2] = {&start, &end}; for (size_t i = 0; i < 2; ++i) { if (*p != ',') { PyErr_Format(PyExc_ValueError, "Missing transition rules in TZ string: %R", tz_str_obj); goto error; } p++; num_chars = parse_transition_rule(p, transitions[i]); if (num_chars < 0) { PyErr_Format(PyExc_ValueError, "Malformed transition rule in TZ string: %R", tz_str_obj); goto error; } p += num_chars; } if (*p != '\0') { PyErr_Format(PyExc_ValueError, "Extraneous characters at end of TZ string: %R", tz_str_obj); goto error; } complete: build_tzrule(std_abbr, dst_abbr, std_offset, dst_offset, start, end, out); Py_DECREF(std_abbr); Py_XDECREF(dst_abbr); return 0; error: Py_XDECREF(std_abbr); if (dst_abbr != NULL && dst_abbr != Py_None) { Py_DECREF(dst_abbr); } if (start != NULL) { PyMem_Free(start); } if (end != NULL) { PyMem_Free(end); } return -1; } static int parse_uint(const char *const p, uint8_t *value) { if (!isdigit(*p)) { return -1; } *value = (*p) - '0'; return 0; } /* Parse the STD and DST abbreviations from a TZ string. */ static Py_ssize_t parse_abbr(const char *const p, PyObject **abbr) { const char *ptr = p; char buff = *ptr; const char *str_start; const char *str_end; if (*ptr == '<') { ptr++; str_start = ptr; while ((buff = *ptr) != '>') { // From the POSIX standard: // // In the quoted form, the first character shall be the less-than // ( '<' ) character and the last character shall be the // greater-than ( '>' ) character. All characters between these // quoting characters shall be alphanumeric characters from the // portable character set in the current locale, the plus-sign ( // '+' ) character, or the minus-sign ( '-' ) character. The std // and dst fields in this case shall not include the quoting // characters. if (!isalpha(buff) && !isdigit(buff) && buff != '+' && buff != '-') { return -1; } ptr++; } str_end = ptr; ptr++; } else { str_start = p; // From the POSIX standard: // // In the unquoted form, all characters in these fields shall be // alphabetic characters from the portable character set in the // current locale. while (isalpha(*ptr)) { ptr++; } str_end = ptr; } *abbr = PyUnicode_FromStringAndSize(str_start, str_end - str_start); if (*abbr == NULL) { return -1; } return ptr - p; } /* Parse a UTC offset from a TZ str. */ static Py_ssize_t parse_tz_delta(const char *const p, long *total_seconds) { // From the POSIX spec: // // Indicates the value added to the local time to arrive at Coordinated // Universal Time. The offset has the form: // // hh[:mm[:ss]] // // One or more digits may be used; the value is always interpreted as a // decimal number. // // The POSIX spec says that the values for `hour` must be between 0 and 24 // hours, but RFC 8536 §3.3.1 specifies that the hours part of the // transition times may be signed and range from -167 to 167. long sign = -1; long hours = 0; long minutes = 0; long seconds = 0; const char *ptr = p; char buff = *ptr; if (buff == '-' || buff == '+') { // Negative numbers correspond to *positive* offsets, from the spec: // // If preceded by a '-', the timezone shall be east of the Prime // Meridian; otherwise, it shall be west (which may be indicated by // an optional preceding '+' ). if (buff == '-') { sign = 1; } ptr++; } // The hour can be 1 or 2 numeric characters for (size_t i = 0; i < 2; ++i) { buff = *ptr; if (!isdigit(buff)) { if (i == 0) { return -1; } else { break; } } hours *= 10; hours += buff - '0'; ptr++; } if (hours > 24 || hours < 0) { return -1; } // Minutes and seconds always of the format ":dd" long *outputs[2] = {&minutes, &seconds}; for (size_t i = 0; i < 2; ++i) { if (*ptr != ':') { goto complete; } ptr++; for (size_t j = 0; j < 2; ++j) { buff = *ptr; if (!isdigit(buff)) { return -1; } *(outputs[i]) *= 10; *(outputs[i]) += buff - '0'; ptr++; } } complete: *total_seconds = sign * ((hours * 3600) + (minutes * 60) + seconds); return ptr - p; } /* Parse the date portion of a transition rule. */ static Py_ssize_t parse_transition_rule(const char *const p, TransitionRuleType **out) { // The full transition rule indicates when to change back and forth between // STD and DST, and has the form: // // date[/time],date[/time] // // This function parses an individual date[/time] section, and returns // the number of characters that contributed to the transition rule. This // does not include the ',' at the end of the first rule. // // The POSIX spec states that if *time* is not given, the default is 02:00. const char *ptr = p; int8_t hour = 2; int8_t minute = 0; int8_t second = 0; // Rules come in one of three flavors: // // 1. Jn: Julian day n, with no leap days. // 2. n: Day of year (0-based, with leap days) // 3. Mm.n.d: Specifying by month, week and day-of-week. if (*ptr == 'M') { uint8_t month, week, day; ptr++; if (parse_uint(ptr, &month)) { return -1; } ptr++; if (*ptr != '.') { uint8_t tmp; if (parse_uint(ptr, &tmp)) { return -1; } month *= 10; month += tmp; ptr++; } uint8_t *values[2] = {&week, &day}; for (size_t i = 0; i < 2; ++i) { if (*ptr != '.') { return -1; } ptr++; if (parse_uint(ptr, values[i])) { return -1; } ptr++; } if (*ptr == '/') { ptr++; Py_ssize_t num_chars = parse_transition_time(ptr, &hour, &minute, &second); if (num_chars < 0) { return -1; } ptr += num_chars; } CalendarRule *rv = PyMem_Calloc(1, sizeof(CalendarRule)); if (rv == NULL) { return -1; } if (calendarrule_new(month, week, day, hour, minute, second, rv)) { PyMem_Free(rv); return -1; } *out = (TransitionRuleType *)rv; } else { uint8_t julian = 0; unsigned int day = 0; if (*ptr == 'J') { julian = 1; ptr++; } for (size_t i = 0; i < 3; ++i) { if (!isdigit(*ptr)) { if (i == 0) { return -1; } break; } day *= 10; day += (*ptr) - '0'; ptr++; } if (*ptr == '/') { ptr++; Py_ssize_t num_chars = parse_transition_time(ptr, &hour, &minute, &second); if (num_chars < 0) { return -1; } ptr += num_chars; } DayRule *rv = PyMem_Calloc(1, sizeof(DayRule)); if (rv == NULL) { return -1; } if (dayrule_new(julian, day, hour, minute, second, rv)) { PyMem_Free(rv); return -1; } *out = (TransitionRuleType *)rv; } return ptr - p; } /* Parse the time portion of a transition rule (e.g. following an /) */ static Py_ssize_t parse_transition_time(const char *const p, int8_t *hour, int8_t *minute, int8_t *second) { // From the spec: // // The time has the same format as offset except that no leading sign // ( '-' or '+' ) is allowed. // // The format for the offset is: // // h[h][:mm[:ss]] // // RFC 8536 also allows transition times to be signed and to range from // -167 to +167, but the current version only supports [0, 99]. // // TODO: Support the full range of transition hours. int8_t *components[3] = {hour, minute, second}; const char *ptr = p; int8_t sign = 1; if (*ptr == '-' || *ptr == '+') { if (*ptr == '-') { sign = -1; } ptr++; } for (size_t i = 0; i < 3; ++i) { if (i > 0) { if (*ptr != ':') { break; } ptr++; } uint8_t buff = 0; for (size_t j = 0; j < 2; j++) { if (!isdigit(*ptr)) { if (i == 0 && j > 0) { break; } return -1; } buff *= 10; buff += (*ptr) - '0'; ptr++; } *(components[i]) = sign * buff; } return ptr - p; } /* Constructor for a _tzrule. * * If `dst_abbr` is NULL, this will construct an "STD-only" _tzrule, in which * case `dst_offset` will be ignored and `start` and `end` are expected to be * NULL as well. * * Returns 0 on success. */ static int build_tzrule(PyObject *std_abbr, PyObject *dst_abbr, long std_offset, long dst_offset, TransitionRuleType *start, TransitionRuleType *end, _tzrule *out) { _tzrule rv = {{0}}; rv.start = start; rv.end = end; if (build_ttinfo(std_offset, 0, std_abbr, &rv.std)) { goto error; } if (dst_abbr != NULL) { rv.dst_diff = dst_offset - std_offset; if (build_ttinfo(dst_offset, rv.dst_diff, dst_abbr, &rv.dst)) { goto error; } } else { rv.std_only = 1; } *out = rv; return 0; error: xdecref_ttinfo(&rv.std); xdecref_ttinfo(&rv.dst); return -1; } /* Destructor for _tzrule. */ static void free_tzrule(_tzrule *tzrule) { xdecref_ttinfo(&(tzrule->std)); if (!tzrule->std_only) { xdecref_ttinfo(&(tzrule->dst)); } if (tzrule->start != NULL) { PyMem_Free(tzrule->start); } if (tzrule->end != NULL) { PyMem_Free(tzrule->end); } } /* Calculate DST offsets from transitions and UTC offsets * * This is necessary because each C `ttinfo` only contains the UTC offset, * time zone abbreviation and an isdst boolean - it does not include the * amount of the DST offset, but we need the amount for the dst() function. * * Thus function uses heuristics to infer what the offset should be, so it * is not guaranteed that this will work for all zones. If we cannot assign * a value for a given DST offset, we'll assume it's 1H rather than 0H, so * bool(dt.dst()) will always match ttinfo.isdst. */ static void utcoff_to_dstoff(size_t *trans_idx, long *utcoffs, long *dstoffs, unsigned char *isdsts, size_t num_transitions, size_t num_ttinfos) { size_t dst_count = 0; size_t dst_found = 0; for (size_t i = 0; i < num_ttinfos; ++i) { dst_count++; } for (size_t i = 1; i < num_transitions; ++i) { if (dst_count == dst_found) { break; } size_t idx = trans_idx[i]; size_t comp_idx = trans_idx[i - 1]; // Only look at DST offsets that have nto been assigned already if (!isdsts[idx] || dstoffs[idx] != 0) { continue; } long dstoff = 0; long utcoff = utcoffs[idx]; if (!isdsts[comp_idx]) { dstoff = utcoff - utcoffs[comp_idx]; } if (!dstoff && idx < (num_ttinfos - 1)) { comp_idx = trans_idx[i + 1]; // If the following transition is also DST and we couldn't find // the DST offset by this point, we're going to have to skip it // and hope this transition gets assigned later if (isdsts[comp_idx]) { continue; } dstoff = utcoff - utcoffs[comp_idx]; } if (dstoff) { dst_found++; dstoffs[idx] = dstoff; } } if (dst_found < dst_count) { // If there are time zones we didn't find a value for, we'll end up // with dstoff = 0 for something where isdst=1. This is obviously // wrong — one hour will be a much better guess than 0. for (size_t idx = 0; idx < num_ttinfos; ++idx) { if (isdsts[idx] && !dstoffs[idx]) { dstoffs[idx] = 3600; } } } } #define _swap(x, y, buffer) \ buffer = x; \ x = y; \ y = buffer; /* Calculate transitions in local time from UTC time and offsets. * * We want to know when each transition occurs, denominated in the number of * nominal wall-time seconds between 1970-01-01T00:00:00 and the transition in * *local time* (note: this is *not* equivalent to the output of * datetime.timestamp, which is the total number of seconds actual elapsed * since 1970-01-01T00:00:00Z in UTC). * * This is an ambiguous question because "local time" can be ambiguous — but it * is disambiguated by the `fold` parameter, so we allocate two arrays: * * trans_local[0]: The wall-time transitions for fold=0 * trans_local[1]: The wall-time transitions for fold=1 * * This returns 0 on success and a negative number of failure. The trans_local * arrays must be freed if they are not NULL. */ static int ts_to_local(size_t *trans_idx, int64_t *trans_utc, long *utcoff, int64_t *trans_local[2], size_t num_ttinfos, size_t num_transitions) { if (num_transitions == 0) { return 0; } // Copy the UTC transitions into each array to be modified in place later for (size_t i = 0; i < 2; ++i) { trans_local[i] = PyMem_Malloc(num_transitions * sizeof(int64_t)); if (trans_local[i] == NULL) { return -1; } memcpy(trans_local[i], trans_utc, num_transitions * sizeof(int64_t)); } int64_t offset_0, offset_1, buff; if (num_ttinfos > 1) { offset_0 = utcoff[0]; offset_1 = utcoff[trans_idx[0]]; if (offset_1 > offset_0) { _swap(offset_0, offset_1, buff); } } else { offset_0 = utcoff[0]; offset_1 = utcoff[0]; } trans_local[0][0] += offset_0; trans_local[1][0] += offset_1; for (size_t i = 1; i < num_transitions; ++i) { offset_0 = utcoff[trans_idx[i - 1]]; offset_1 = utcoff[trans_idx[i]]; if (offset_1 > offset_0) { _swap(offset_1, offset_0, buff); } trans_local[0][i] += offset_0; trans_local[1][i] += offset_1; } return 0; } /* Simple bisect_right binary search implementation */ static size_t _bisect(const int64_t value, const int64_t *arr, size_t size) { size_t lo = 0; size_t hi = size; size_t m; while (lo < hi) { m = (lo + hi) / 2; if (arr[m] > value) { hi = m; } else { lo = m + 1; } } return hi; } /* Find the ttinfo rules that apply at a given local datetime. */ static _ttinfo * find_ttinfo(PyZoneInfo_ZoneInfo *self, PyObject *dt) { // datetime.time has a .tzinfo attribute that passes None as the dt // argument; it only really has meaning for fixed-offset zones. if (dt == Py_None) { if (self->fixed_offset) { return &(self->tzrule_after.std); } else { return &NO_TTINFO; } } int64_t ts; if (get_local_timestamp(dt, &ts)) { return NULL; } unsigned char fold = PyDateTime_DATE_GET_FOLD(dt); assert(fold < 2); int64_t *local_transitions = self->trans_list_wall[fold]; size_t num_trans = self->num_transitions; if (num_trans && ts < local_transitions[0]) { return self->ttinfo_before; } else if (!num_trans || ts > local_transitions[self->num_transitions - 1]) { return find_tzrule_ttinfo(&(self->tzrule_after), ts, fold, PyDateTime_GET_YEAR(dt)); } else { size_t idx = _bisect(ts, local_transitions, self->num_transitions) - 1; assert(idx < self->num_transitions); return self->trans_ttinfos[idx]; } } static int is_leap_year(int year) { const unsigned int ayear = (unsigned int)year; return ayear % 4 == 0 && (ayear % 100 != 0 || ayear % 400 == 0); } /* Calculates ordinal datetime from year, month and day. */ static int ymd_to_ord(int y, int m, int d) { y -= 1; int days_before_year = (y * 365) + (y / 4) - (y / 100) + (y / 400); int yearday = DAYS_BEFORE_MONTH[m]; if (m > 2 && is_leap_year(y + 1)) { yearday += 1; } return days_before_year + yearday + d; } /* Calculate the number of seconds since 1970-01-01 in local time. * * This gets a datetime in the same "units" as self->trans_list_wall so that we * can easily determine which transitions a datetime falls between. See the * comment above ts_to_local for more information. * */ static int get_local_timestamp(PyObject *dt, int64_t *local_ts) { assert(local_ts != NULL); int hour, minute, second; int ord; if (PyDateTime_CheckExact(dt)) { int y = PyDateTime_GET_YEAR(dt); int m = PyDateTime_GET_MONTH(dt); int d = PyDateTime_GET_DAY(dt); hour = PyDateTime_DATE_GET_HOUR(dt); minute = PyDateTime_DATE_GET_MINUTE(dt); second = PyDateTime_DATE_GET_SECOND(dt); ord = ymd_to_ord(y, m, d); } else { PyObject *num = PyObject_CallMethod(dt, "toordinal", NULL); if (num == NULL) { return -1; } ord = PyLong_AsLong(num); Py_DECREF(num); if (ord == -1 && PyErr_Occurred()) { return -1; } num = PyObject_GetAttrString(dt, "hour"); if (num == NULL) { return -1; } hour = PyLong_AsLong(num); Py_DECREF(num); if (hour == -1) { return -1; } num = PyObject_GetAttrString(dt, "minute"); if (num == NULL) { return -1; } minute = PyLong_AsLong(num); Py_DECREF(num); if (minute == -1) { return -1; } num = PyObject_GetAttrString(dt, "second"); if (num == NULL) { return -1; } second = PyLong_AsLong(num); Py_DECREF(num); if (second == -1) { return -1; } } *local_ts = (int64_t)(ord - EPOCHORDINAL) * 86400 + (int64_t)(hour * 3600 + minute * 60 + second); return 0; } ///// // Functions for cache handling /* Constructor for StrongCacheNode */ static StrongCacheNode * strong_cache_node_new(PyObject *key, PyObject *zone) { StrongCacheNode *node = PyMem_Malloc(sizeof(StrongCacheNode)); if (node == NULL) { return NULL; } Py_INCREF(key); Py_INCREF(zone); node->next = NULL; node->prev = NULL; node->key = key; node->zone = zone; return node; } /* Destructor for StrongCacheNode */ void strong_cache_node_free(StrongCacheNode *node) { Py_XDECREF(node->key); Py_XDECREF(node->zone); PyMem_Free(node); } /* Frees all nodes at or after a specified root in the strong cache. * * This can be used on the root node to free the entire cache or it can be used * to clear all nodes that have been expired (which, if everything is going * right, will actually only be 1 node at a time). */ void strong_cache_free(StrongCacheNode *root) { StrongCacheNode *node = root; StrongCacheNode *next_node; while (node != NULL) { next_node = node->next; strong_cache_node_free(node); node = next_node; } } /* Removes a node from the cache and update its neighbors. * * This is used both when ejecting a node from the cache and when moving it to * the front of the cache. */ static void remove_from_strong_cache(StrongCacheNode *node) { if (ZONEINFO_STRONG_CACHE == node) { ZONEINFO_STRONG_CACHE = node->next; } if (node->prev != NULL) { node->prev->next = node->next; } if (node->next != NULL) { node->next->prev = node->prev; } node->next = NULL; node->prev = NULL; } /* Retrieves the node associated with a key, if it exists. * * This traverses the strong cache until it finds a matching key and returns a * pointer to the relevant node if found. Returns NULL if no node is found. * * root may be NULL, indicating an empty cache. */ static StrongCacheNode * find_in_strong_cache(const StrongCacheNode *const root, PyObject *const key) { const StrongCacheNode *node = root; while (node != NULL) { int rv = PyObject_RichCompareBool(key, node->key, Py_EQ); if (rv < 0) { return NULL; } if (rv) { return (StrongCacheNode *)node; } node = node->next; } return NULL; } /* Ejects a given key from the class's strong cache, if applicable. * * This function is used to enable the per-key functionality in clear_cache. */ static int eject_from_strong_cache(const PyTypeObject *const type, PyObject *key) { if (type != &PyZoneInfo_ZoneInfoType) { return 0; } StrongCacheNode *node = find_in_strong_cache(ZONEINFO_STRONG_CACHE, key); if (node != NULL) { remove_from_strong_cache(node); strong_cache_node_free(node); } else if (PyErr_Occurred()) { return -1; } return 0; } /* Moves a node to the front of the LRU cache. * * The strong cache is an LRU cache, so whenever a given node is accessed, if * it is not at the front of the cache, it needs to be moved there. */ static void move_strong_cache_node_to_front(StrongCacheNode **root, StrongCacheNode *node) { StrongCacheNode *root_p = *root; if (root_p == node) { return; } remove_from_strong_cache(node); node->prev = NULL; node->next = root_p; if (root_p != NULL) { root_p->prev = node; } *root = node; } /* Retrieves a ZoneInfo from the strong cache if it's present. * * This function finds the ZoneInfo by key and if found will move the node to * the front of the LRU cache and return a new reference to it. It returns NULL * if the key is not in the cache. * * The strong cache is currently only implemented for the base class, so this * always returns a cache miss for subclasses. */ static PyObject * zone_from_strong_cache(const PyTypeObject *const type, PyObject *const key) { if (type != &PyZoneInfo_ZoneInfoType) { return NULL; // Strong cache currently only implemented for base class } StrongCacheNode *node = find_in_strong_cache(ZONEINFO_STRONG_CACHE, key); if (node != NULL) { move_strong_cache_node_to_front(&ZONEINFO_STRONG_CACHE, node); Py_INCREF(node->zone); return node->zone; } return NULL; // Cache miss } /* Inserts a new key into the strong LRU cache. * * This function is only to be used after a cache miss — it creates a new node * at the front of the cache and ejects any stale entries (keeping the size of * the cache to at most ZONEINFO_STRONG_CACHE_MAX_SIZE). */ static void update_strong_cache(const PyTypeObject *const type, PyObject *key, PyObject *zone) { if (type != &PyZoneInfo_ZoneInfoType) { return; } StrongCacheNode *new_node = strong_cache_node_new(key, zone); move_strong_cache_node_to_front(&ZONEINFO_STRONG_CACHE, new_node); StrongCacheNode *node = new_node->next; for (size_t i = 1; i < ZONEINFO_STRONG_CACHE_MAX_SIZE; ++i) { if (node == NULL) { return; } node = node->next; } // Everything beyond this point needs to be freed if (node != NULL) { if (node->prev != NULL) { node->prev->next = NULL; } strong_cache_free(node); } } /* Clears all entries into a type's strong cache. * * Because the strong cache is not implemented for subclasses, this is a no-op * for everything except the base class. */ void clear_strong_cache(const PyTypeObject *const type) { if (type != &PyZoneInfo_ZoneInfoType) { return; } strong_cache_free(ZONEINFO_STRONG_CACHE); ZONEINFO_STRONG_CACHE = NULL; } static PyObject * new_weak_cache(void) { PyObject *weakref_module = PyImport_ImportModule("weakref"); if (weakref_module == NULL) { return NULL; } PyObject *weak_cache = PyObject_CallMethod(weakref_module, "WeakValueDictionary", ""); Py_DECREF(weakref_module); return weak_cache; } static int initialize_caches(void) { // TODO: Move to a PyModule_GetState / PEP 573 based caching system. if (TIMEDELTA_CACHE == NULL) { TIMEDELTA_CACHE = PyDict_New(); } else { Py_INCREF(TIMEDELTA_CACHE); } if (TIMEDELTA_CACHE == NULL) { return -1; } if (ZONEINFO_WEAK_CACHE == NULL) { ZONEINFO_WEAK_CACHE = new_weak_cache(); } else { Py_INCREF(ZONEINFO_WEAK_CACHE); } if (ZONEINFO_WEAK_CACHE == NULL) { return -1; } return 0; } static PyObject * zoneinfo_init_subclass(PyTypeObject *cls, PyObject *args, PyObject **kwargs) { PyObject *weak_cache = new_weak_cache(); if (weak_cache == NULL) { return NULL; } if (PyObject_SetAttrString((PyObject *)cls, "_weak_cache", weak_cache) < 0) { Py_DECREF(weak_cache); return NULL; } Py_DECREF(weak_cache); Py_RETURN_NONE; } ///// // Specify the ZoneInfo type static PyMethodDef zoneinfo_methods[] = { {"clear_cache", (PyCFunction)(void (*)(void))zoneinfo_clear_cache, METH_VARARGS | METH_KEYWORDS | METH_CLASS, PyDoc_STR("Clear the ZoneInfo cache.")}, {"no_cache", (PyCFunction)(void (*)(void))zoneinfo_no_cache, METH_VARARGS | METH_KEYWORDS | METH_CLASS, PyDoc_STR("Get a new instance of ZoneInfo, bypassing the cache.")}, {"from_file", (PyCFunction)(void (*)(void))zoneinfo_from_file, METH_VARARGS | METH_KEYWORDS | METH_CLASS, PyDoc_STR("Create a ZoneInfo file from a file object.")}, {"utcoffset", (PyCFunction)zoneinfo_utcoffset, METH_O, PyDoc_STR("Retrieve a timedelta representing the UTC offset in a zone at " "the given datetime.")}, {"dst", (PyCFunction)zoneinfo_dst, METH_O, PyDoc_STR("Retrieve a timedelta representing the amount of DST applied " "in a zone at the given datetime.")}, {"tzname", (PyCFunction)zoneinfo_tzname, METH_O, PyDoc_STR("Retrieve a string containing the abbreviation for the time " "zone that applies in a zone at a given datetime.")}, {"fromutc", (PyCFunction)zoneinfo_fromutc, METH_O, PyDoc_STR("Given a datetime with local time in UTC, retrieve an adjusted " "datetime in local time.")}, {"__reduce__", (PyCFunction)zoneinfo_reduce, METH_NOARGS, PyDoc_STR("Function for serialization with the pickle protocol.")}, {"_unpickle", (PyCFunction)zoneinfo__unpickle, METH_VARARGS | METH_CLASS, PyDoc_STR("Private method used in unpickling.")}, {"__init_subclass__", (PyCFunction)(void (*)(void))zoneinfo_init_subclass, METH_VARARGS | METH_KEYWORDS | METH_CLASS, PyDoc_STR("Function to initialize subclasses.")}, {NULL} /* Sentinel */ }; static PyMemberDef zoneinfo_members[] = { {.name = "key", .offset = offsetof(PyZoneInfo_ZoneInfo, key), .type = T_OBJECT_EX, .flags = READONLY, .doc = NULL}, {NULL}, /* Sentinel */ }; static PyTypeObject PyZoneInfo_ZoneInfoType = { PyVarObject_HEAD_INIT(NULL, 0) // .tp_name = "zoneinfo.ZoneInfo", .tp_basicsize = sizeof(PyZoneInfo_ZoneInfo), .tp_weaklistoffset = offsetof(PyZoneInfo_ZoneInfo, weakreflist), .tp_repr = (reprfunc)zoneinfo_repr, .tp_str = (reprfunc)zoneinfo_str, .tp_getattro = PyObject_GenericGetAttr, .tp_flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE), /* .tp_doc = zoneinfo_doc, */ .tp_methods = zoneinfo_methods, .tp_members = zoneinfo_members, .tp_new = zoneinfo_new, .tp_dealloc = zoneinfo_dealloc, }; ///// // Specify the _zoneinfo module static PyMethodDef module_methods[] = {{NULL, NULL}}; static void module_free(void *m) { Py_XDECREF(_tzpath_find_tzfile); _tzpath_find_tzfile = NULL; Py_XDECREF(_common_mod); _common_mod = NULL; Py_XDECREF(io_open); io_open = NULL; xdecref_ttinfo(&NO_TTINFO); if (TIMEDELTA_CACHE != NULL && Py_REFCNT(TIMEDELTA_CACHE) > 1) { Py_DECREF(TIMEDELTA_CACHE); } else { Py_CLEAR(TIMEDELTA_CACHE); } if (ZONEINFO_WEAK_CACHE != NULL && Py_REFCNT(ZONEINFO_WEAK_CACHE) > 1) { Py_DECREF(ZONEINFO_WEAK_CACHE); } else { Py_CLEAR(ZONEINFO_WEAK_CACHE); } clear_strong_cache(&PyZoneInfo_ZoneInfoType); } static int zoneinfomodule_exec(PyObject *m) { PyDateTime_IMPORT; if (PyDateTimeAPI == NULL) { goto error; } PyZoneInfo_ZoneInfoType.tp_base = PyDateTimeAPI->TZInfoType; if (PyType_Ready(&PyZoneInfo_ZoneInfoType) < 0) { goto error; } Py_INCREF(&PyZoneInfo_ZoneInfoType); PyModule_AddObject(m, "ZoneInfo", (PyObject *)&PyZoneInfo_ZoneInfoType); /* Populate imports */ PyObject *_tzpath_module = PyImport_ImportModule("zoneinfo._tzpath"); if (_tzpath_module == NULL) { goto error; } _tzpath_find_tzfile = PyObject_GetAttrString(_tzpath_module, "find_tzfile"); Py_DECREF(_tzpath_module); if (_tzpath_find_tzfile == NULL) { goto error; } PyObject *io_module = PyImport_ImportModule("io"); if (io_module == NULL) { goto error; } io_open = PyObject_GetAttrString(io_module, "open"); Py_DECREF(io_module); if (io_open == NULL) { goto error; } _common_mod = PyImport_ImportModule("zoneinfo._common"); if (_common_mod == NULL) { goto error; } if (NO_TTINFO.utcoff == NULL) { NO_TTINFO.utcoff = Py_None; NO_TTINFO.dstoff = Py_None; NO_TTINFO.tzname = Py_None; for (size_t i = 0; i < 3; ++i) { Py_INCREF(Py_None); } } if (initialize_caches()) { goto error; } return 0; error: return -1; } static PyModuleDef_Slot zoneinfomodule_slots[] = { {Py_mod_exec, zoneinfomodule_exec}, {0, NULL}}; static struct PyModuleDef zoneinfomodule = { PyModuleDef_HEAD_INIT, .m_name = "_zoneinfo", .m_doc = "C implementation of the zoneinfo module", .m_size = 0, .m_methods = module_methods, .m_slots = zoneinfomodule_slots, .m_free = (freefunc)module_free}; PyMODINIT_FUNC PyInit__zoneinfo(void) { return PyModuleDef_Init(&zoneinfomodule); }