712 lines
18 KiB
Python
712 lines
18 KiB
Python
# 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
|