| # coding: utf-8 |
| |
| """ |
| Miscellaneous data helpers, including functions for converting integers to and |
| from bytes and UTC timezone. Exports the following items: |
| |
| - OrderedDict() |
| - int_from_bytes() |
| - int_to_bytes() |
| - timezone.utc |
| - inet_ntop() |
| - inet_pton() |
| - uri_to_iri() |
| - iri_to_uri() |
| """ |
| |
| from __future__ import unicode_literals, division, absolute_import, print_function |
| |
| import math |
| import sys |
| from datetime import datetime, date, time |
| |
| from ._errors import unwrap |
| from ._iri import iri_to_uri, uri_to_iri # noqa |
| from ._ordereddict import OrderedDict # noqa |
| from ._types import type_name |
| |
| if sys.platform == 'win32': |
| from ._inet import inet_ntop, inet_pton |
| else: |
| from socket import inet_ntop, inet_pton # noqa |
| |
| |
| # Python 2 |
| if sys.version_info <= (3,): |
| |
| from datetime import timedelta, tzinfo |
| |
| py2 = True |
| |
| def int_to_bytes(value, signed=False, width=None): |
| """ |
| Converts an integer to a byte string |
| |
| :param value: |
| The integer to convert |
| |
| :param signed: |
| If the byte string should be encoded using two's complement |
| |
| :param width: |
| None == auto, otherwise an integer of the byte width for the return |
| value |
| |
| :return: |
| A byte string |
| """ |
| |
| # Handle negatives in two's complement |
| is_neg = False |
| if signed and value < 0: |
| is_neg = True |
| bits = int(math.ceil(len('%x' % abs(value)) / 2.0) * 8) |
| value = (value + (1 << bits)) % (1 << bits) |
| |
| hex_str = '%x' % value |
| if len(hex_str) & 1: |
| hex_str = '0' + hex_str |
| |
| output = hex_str.decode('hex') |
| |
| if signed and not is_neg and ord(output[0:1]) & 0x80: |
| output = b'\x00' + output |
| |
| if width is not None: |
| if is_neg: |
| pad_char = b'\xFF' |
| else: |
| pad_char = b'\x00' |
| output = (pad_char * (width - len(output))) + output |
| elif is_neg and ord(output[0:1]) & 0x80 == 0: |
| output = b'\xFF' + output |
| |
| return output |
| |
| def int_from_bytes(value, signed=False): |
| """ |
| Converts a byte string to an integer |
| |
| :param value: |
| The byte string to convert |
| |
| :param signed: |
| If the byte string should be interpreted using two's complement |
| |
| :return: |
| An integer |
| """ |
| |
| if value == b'': |
| return 0 |
| |
| num = long(value.encode("hex"), 16) # noqa |
| |
| if not signed: |
| return num |
| |
| # Check for sign bit and handle two's complement |
| if ord(value[0:1]) & 0x80: |
| bit_len = len(value) * 8 |
| return num - (1 << bit_len) |
| |
| return num |
| |
| class utc(tzinfo): # noqa |
| |
| def tzname(self, _): |
| return b'UTC+00:00' |
| |
| def utcoffset(self, _): |
| return timedelta(0) |
| |
| def dst(self, _): |
| return timedelta(0) |
| |
| class timezone(): # noqa |
| |
| utc = utc() |
| |
| |
| # Python 3 |
| else: |
| |
| from datetime import timezone # noqa |
| |
| py2 = False |
| |
| def int_to_bytes(value, signed=False, width=None): |
| """ |
| Converts an integer to a byte string |
| |
| :param value: |
| The integer to convert |
| |
| :param signed: |
| If the byte string should be encoded using two's complement |
| |
| :param width: |
| None == auto, otherwise an integer of the byte width for the return |
| value |
| |
| :return: |
| A byte string |
| """ |
| |
| if width is None: |
| if signed: |
| if value < 0: |
| bits_required = abs(value + 1).bit_length() |
| else: |
| bits_required = value.bit_length() |
| if bits_required % 8 == 0: |
| bits_required += 1 |
| else: |
| bits_required = value.bit_length() |
| width = math.ceil(bits_required / 8) or 1 |
| return value.to_bytes(width, byteorder='big', signed=signed) |
| |
| def int_from_bytes(value, signed=False): |
| """ |
| Converts a byte string to an integer |
| |
| :param value: |
| The byte string to convert |
| |
| :param signed: |
| If the byte string should be interpreted using two's complement |
| |
| :return: |
| An integer |
| """ |
| |
| return int.from_bytes(value, 'big', signed=signed) |
| |
| |
| _DAYS_PER_MONTH_YEAR_0 = { |
| 1: 31, |
| 2: 29, # Year 0 was a leap year |
| 3: 31, |
| 4: 30, |
| 5: 31, |
| 6: 30, |
| 7: 31, |
| 8: 31, |
| 9: 30, |
| 10: 31, |
| 11: 30, |
| 12: 31 |
| } |
| |
| |
| class extended_date(object): |
| """ |
| A datetime.date-like object that can represent the year 0. This is just |
| to handle 0000-01-01 found in some certificates. |
| """ |
| |
| year = None |
| month = None |
| day = None |
| |
| def __init__(self, year, month, day): |
| """ |
| :param year: |
| The integer 0 |
| |
| :param month: |
| An integer from 1 to 12 |
| |
| :param day: |
| An integer from 1 to 31 |
| """ |
| |
| if year != 0: |
| raise ValueError('year must be 0') |
| |
| if month < 1 or month > 12: |
| raise ValueError('month is out of range') |
| |
| if day < 0 or day > _DAYS_PER_MONTH_YEAR_0[month]: |
| raise ValueError('day is out of range') |
| |
| self.year = year |
| self.month = month |
| self.day = day |
| |
| def _format(self, format): |
| """ |
| Performs strftime(), always returning a unicode string |
| |
| :param format: |
| A strftime() format string |
| |
| :return: |
| A unicode string of the formatted date |
| """ |
| |
| format = format.replace('%Y', '0000') |
| # Year 0 is 1BC and a leap year. Leap years repeat themselves |
| # every 28 years. Because of adjustments and the proleptic gregorian |
| # calendar, the simplest way to format is to substitute year 2000. |
| temp = date(2000, self.month, self.day) |
| if '%c' in format: |
| c_out = temp.strftime('%c') |
| # Handle full years |
| c_out = c_out.replace('2000', '0000') |
| c_out = c_out.replace('%', '%%') |
| format = format.replace('%c', c_out) |
| if '%x' in format: |
| x_out = temp.strftime('%x') |
| # Handle formats such as 08/16/2000 or 16.08.2000 |
| x_out = x_out.replace('2000', '0000') |
| x_out = x_out.replace('%', '%%') |
| format = format.replace('%x', x_out) |
| return temp.strftime(format) |
| |
| def isoformat(self): |
| """ |
| Formats the date as %Y-%m-%d |
| |
| :return: |
| The date formatted to %Y-%m-%d as a unicode string in Python 3 |
| and a byte string in Python 2 |
| """ |
| |
| return self.strftime('0000-%m-%d') |
| |
| def strftime(self, format): |
| """ |
| Formats the date using strftime() |
| |
| :param format: |
| The strftime() format string |
| |
| :return: |
| The formatted date as a unicode string in Python 3 and a byte |
| string in Python 2 |
| """ |
| |
| output = self._format(format) |
| if py2: |
| return output.encode('utf-8') |
| return output |
| |
| def replace(self, year=None, month=None, day=None): |
| """ |
| Returns a new datetime.date or asn1crypto.util.extended_date |
| object with the specified components replaced |
| |
| :return: |
| A datetime.date or asn1crypto.util.extended_date object |
| """ |
| |
| if year is None: |
| year = self.year |
| if month is None: |
| month = self.month |
| if day is None: |
| day = self.day |
| |
| if year > 0: |
| cls = date |
| else: |
| cls = extended_date |
| |
| return cls( |
| year, |
| month, |
| day |
| ) |
| |
| def __str__(self): |
| if py2: |
| return self.__bytes__() |
| else: |
| return self.__unicode__() |
| |
| def __bytes__(self): |
| return self.__unicode__().encode('utf-8') |
| |
| def __unicode__(self): |
| return self._format('%Y-%m-%d') |
| |
| def __eq__(self, other): |
| if not isinstance(other, self.__class__): |
| return False |
| return self.__cmp__(other) == 0 |
| |
| def __ne__(self, other): |
| return not self.__eq__(other) |
| |
| def _comparison_error(self, other): |
| raise TypeError(unwrap( |
| ''' |
| An asn1crypto.util.extended_date object can only be compared to |
| an asn1crypto.util.extended_date or datetime.date object, not %s |
| ''', |
| type_name(other) |
| )) |
| |
| def __cmp__(self, other): |
| if isinstance(other, date): |
| return -1 |
| |
| if not isinstance(other, self.__class__): |
| self._comparison_error(other) |
| |
| st = ( |
| self.year, |
| self.month, |
| self.day |
| ) |
| ot = ( |
| other.year, |
| other.month, |
| other.day |
| ) |
| |
| if st < ot: |
| return -1 |
| if st > ot: |
| return 1 |
| return 0 |
| |
| def __lt__(self, other): |
| return self.__cmp__(other) < 0 |
| |
| def __le__(self, other): |
| return self.__cmp__(other) <= 0 |
| |
| def __gt__(self, other): |
| return self.__cmp__(other) > 0 |
| |
| def __ge__(self, other): |
| return self.__cmp__(other) >= 0 |
| |
| |
| class extended_datetime(object): |
| """ |
| A datetime.datetime-like object that can represent the year 0. This is just |
| to handle 0000-01-01 found in some certificates. |
| """ |
| |
| year = None |
| month = None |
| day = None |
| hour = None |
| minute = None |
| second = None |
| microsecond = None |
| tzinfo = None |
| |
| def __init__(self, year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None): |
| """ |
| :param year: |
| The integer 0 |
| |
| :param month: |
| An integer from 1 to 12 |
| |
| :param day: |
| An integer from 1 to 31 |
| |
| :param hour: |
| An integer from 0 to 23 |
| |
| :param minute: |
| An integer from 0 to 59 |
| |
| :param second: |
| An integer from 0 to 59 |
| |
| :param microsecond: |
| An integer from 0 to 999999 |
| """ |
| |
| if year != 0: |
| raise ValueError('year must be 0') |
| |
| if month < 1 or month > 12: |
| raise ValueError('month is out of range') |
| |
| if day < 0 or day > _DAYS_PER_MONTH_YEAR_0[month]: |
| raise ValueError('day is out of range') |
| |
| if hour < 0 or hour > 23: |
| raise ValueError('hour is out of range') |
| |
| if minute < 0 or minute > 59: |
| raise ValueError('minute is out of range') |
| |
| if second < 0 or second > 59: |
| raise ValueError('second is out of range') |
| |
| if microsecond < 0 or microsecond > 999999: |
| raise ValueError('microsecond is out of range') |
| |
| self.year = year |
| self.month = month |
| self.day = day |
| self.hour = hour |
| self.minute = minute |
| self.second = second |
| self.microsecond = microsecond |
| self.tzinfo = tzinfo |
| |
| def date(self): |
| """ |
| :return: |
| An asn1crypto.util.extended_date of the date |
| """ |
| |
| return extended_date(self.year, self.month, self.day) |
| |
| def time(self): |
| """ |
| :return: |
| A datetime.time object of the time |
| """ |
| |
| return time(self.hour, self.minute, self.second, self.microsecond, self.tzinfo) |
| |
| def utcoffset(self): |
| """ |
| :return: |
| None or a datetime.timedelta() of the offset from UTC |
| """ |
| |
| if self.tzinfo is None: |
| return None |
| return self.tzinfo.utcoffset(self.replace(year=2000)) |
| |
| def dst(self): |
| """ |
| :return: |
| None or a datetime.timedelta() of the daylight savings time offset |
| """ |
| |
| if self.tzinfo is None: |
| return None |
| return self.tzinfo.dst(self.replace(year=2000)) |
| |
| def tzname(self): |
| """ |
| :return: |
| None or the name of the timezone as a unicode string in Python 3 |
| and a byte string in Python 2 |
| """ |
| |
| if self.tzinfo is None: |
| return None |
| return self.tzinfo.tzname(self.replace(year=2000)) |
| |
| def _format(self, format): |
| """ |
| Performs strftime(), always returning a unicode string |
| |
| :param format: |
| A strftime() format string |
| |
| :return: |
| A unicode string of the formatted datetime |
| """ |
| |
| format = format.replace('%Y', '0000') |
| # Year 0 is 1BC and a leap year. Leap years repeat themselves |
| # every 28 years. Because of adjustments and the proleptic gregorian |
| # calendar, the simplest way to format is to substitute year 2000. |
| temp = datetime( |
| 2000, |
| self.month, |
| self.day, |
| self.hour, |
| self.minute, |
| self.second, |
| self.microsecond, |
| self.tzinfo |
| ) |
| if '%c' in format: |
| c_out = temp.strftime('%c') |
| # Handle full years |
| c_out = c_out.replace('2000', '0000') |
| c_out = c_out.replace('%', '%%') |
| format = format.replace('%c', c_out) |
| if '%x' in format: |
| x_out = temp.strftime('%x') |
| # Handle formats such as 08/16/2000 or 16.08.2000 |
| x_out = x_out.replace('2000', '0000') |
| x_out = x_out.replace('%', '%%') |
| format = format.replace('%x', x_out) |
| return temp.strftime(format) |
| |
| def isoformat(self, sep='T'): |
| """ |
| Formats the date as "%Y-%m-%d %H:%M:%S" with the sep param between the |
| date and time portions |
| |
| :param set: |
| A single character of the separator to place between the date and |
| time |
| |
| :return: |
| The formatted datetime as a unicode string in Python 3 and a byte |
| string in Python 2 |
| """ |
| |
| if self.microsecond == 0: |
| return self.strftime('0000-%%m-%%d%s%%H:%%M:%%S' % sep) |
| return self.strftime('0000-%%m-%%d%s%%H:%%M:%%S.%%f' % sep) |
| |
| def strftime(self, format): |
| """ |
| Formats the date using strftime() |
| |
| :param format: |
| The strftime() format string |
| |
| :return: |
| The formatted date as a unicode string in Python 3 and a byte |
| string in Python 2 |
| """ |
| |
| output = self._format(format) |
| if py2: |
| return output.encode('utf-8') |
| return output |
| |
| def replace(self, year=None, month=None, day=None, hour=None, minute=None, |
| second=None, microsecond=None, tzinfo=None): |
| """ |
| Returns a new datetime.datetime or asn1crypto.util.extended_datetime |
| object with the specified components replaced |
| |
| :return: |
| A datetime.datetime or asn1crypto.util.extended_datetime object |
| """ |
| |
| if year is None: |
| year = self.year |
| if month is None: |
| month = self.month |
| if day is None: |
| day = self.day |
| if hour is None: |
| hour = self.hour |
| if minute is None: |
| minute = self.minute |
| if second is None: |
| second = self.second |
| if microsecond is None: |
| microsecond = self.microsecond |
| if tzinfo is None: |
| tzinfo = self.tzinfo |
| |
| if year > 0: |
| cls = datetime |
| else: |
| cls = extended_datetime |
| |
| return cls( |
| year, |
| month, |
| day, |
| hour, |
| minute, |
| second, |
| microsecond, |
| tzinfo |
| ) |
| |
| def __str__(self): |
| if py2: |
| return self.__bytes__() |
| else: |
| return self.__unicode__() |
| |
| def __bytes__(self): |
| return self.__unicode__().encode('utf-8') |
| |
| def __unicode__(self): |
| format = '%Y-%m-%d %H:%M:%S' |
| if self.microsecond != 0: |
| format += '.%f' |
| return self._format(format) |
| |
| def __eq__(self, other): |
| if not isinstance(other, self.__class__): |
| return False |
| return self.__cmp__(other) == 0 |
| |
| def __ne__(self, other): |
| return not self.__eq__(other) |
| |
| def _comparison_error(self, other): |
| """ |
| Raises a TypeError about the other object not being suitable for |
| comparison |
| |
| :param other: |
| The object being compared to |
| """ |
| |
| raise TypeError(unwrap( |
| ''' |
| An asn1crypto.util.extended_datetime object can only be compared to |
| an asn1crypto.util.extended_datetime or datetime.datetime object, |
| not %s |
| ''', |
| type_name(other) |
| )) |
| |
| def __cmp__(self, other): |
| so = self.utcoffset() |
| oo = other.utcoffset() |
| |
| if (so is not None and oo is None) or (so is None and oo is not None): |
| raise TypeError("can't compare offset-naive and offset-aware datetimes") |
| |
| if isinstance(other, datetime): |
| return -1 |
| |
| if not isinstance(other, self.__class__): |
| self._comparison_error(other) |
| |
| st = ( |
| self.year, |
| self.month, |
| self.day, |
| self.hour, |
| self.minute, |
| self.second, |
| self.microsecond, |
| so |
| ) |
| ot = ( |
| other.year, |
| other.month, |
| other.day, |
| other.hour, |
| other.minute, |
| other.second, |
| other.microsecond, |
| oo |
| ) |
| |
| if st < ot: |
| return -1 |
| if st > ot: |
| return 1 |
| return 0 |
| |
| def __lt__(self, other): |
| return self.__cmp__(other) < 0 |
| |
| def __le__(self, other): |
| return self.__cmp__(other) <= 0 |
| |
| def __gt__(self, other): |
| return self.__cmp__(other) > 0 |
| |
| def __ge__(self, other): |
| return self.__cmp__(other) >= 0 |