1# coding: utf-8
2
3"""
4Miscellaneous data helpers, including functions for converting integers to and
5from bytes and UTC timezone. Exports the following items:
6
7 - OrderedDict()
8 - int_from_bytes()
9 - int_to_bytes()
10 - timezone.utc
11 - utc_with_dst
12 - create_timezone()
13 - inet_ntop()
14 - inet_pton()
15 - uri_to_iri()
16 - iri_to_uri()
17"""
18
19from __future__ import unicode_literals, division, absolute_import, print_function
20
21import math
22import sys
23from datetime import datetime, date, timedelta, tzinfo
24
25from ._errors import unwrap
26from ._iri import iri_to_uri, uri_to_iri  # noqa
27from ._ordereddict import OrderedDict  # noqa
28from ._types import type_name
29
30if sys.platform == 'win32':
31    from ._inet import inet_ntop, inet_pton
32else:
33    from socket import inet_ntop, inet_pton  # noqa
34
35
36# Python 2
37if sys.version_info <= (3,):
38
39    def int_to_bytes(value, signed=False, width=None):
40        """
41        Converts an integer to a byte string
42
43        :param value:
44            The integer to convert
45
46        :param signed:
47            If the byte string should be encoded using two's complement
48
49        :param width:
50            If None, the minimal possible size (but at least 1),
51            otherwise an integer of the byte width for the return value
52
53        :return:
54            A byte string
55        """
56
57        if value == 0 and width == 0:
58            return b''
59
60        # Handle negatives in two's complement
61        is_neg = False
62        if signed and value < 0:
63            is_neg = True
64            bits = int(math.ceil(len('%x' % abs(value)) / 2.0) * 8)
65            value = (value + (1 << bits)) % (1 << bits)
66
67        hex_str = '%x' % value
68        if len(hex_str) & 1:
69            hex_str = '0' + hex_str
70
71        output = hex_str.decode('hex')
72
73        if signed and not is_neg and ord(output[0:1]) & 0x80:
74            output = b'\x00' + output
75
76        if width is not None:
77            if len(output) > width:
78                raise OverflowError('int too big to convert')
79            if is_neg:
80                pad_char = b'\xFF'
81            else:
82                pad_char = b'\x00'
83            output = (pad_char * (width - len(output))) + output
84        elif is_neg and ord(output[0:1]) & 0x80 == 0:
85            output = b'\xFF' + output
86
87        return output
88
89    def int_from_bytes(value, signed=False):
90        """
91        Converts a byte string to an integer
92
93        :param value:
94            The byte string to convert
95
96        :param signed:
97            If the byte string should be interpreted using two's complement
98
99        :return:
100            An integer
101        """
102
103        if value == b'':
104            return 0
105
106        num = long(value.encode("hex"), 16)  # noqa
107
108        if not signed:
109            return num
110
111        # Check for sign bit and handle two's complement
112        if ord(value[0:1]) & 0x80:
113            bit_len = len(value) * 8
114            return num - (1 << bit_len)
115
116        return num
117
118    class timezone(tzinfo):  # noqa
119        """
120        Implements datetime.timezone for py2.
121        Only full minute offsets are supported.
122        DST is not supported.
123        """
124
125        def __init__(self, offset, name=None):
126            """
127            :param offset:
128                A timedelta with this timezone's offset from UTC
129
130            :param name:
131                Name of the timezone; if None, generate one.
132            """
133
134            if not timedelta(hours=-24) < offset < timedelta(hours=24):
135                raise ValueError('Offset must be in [-23:59, 23:59]')
136
137            if offset.seconds % 60 or offset.microseconds:
138                raise ValueError('Offset must be full minutes')
139
140            self._offset = offset
141
142            if name is not None:
143                self._name = name
144            elif not offset:
145                self._name = 'UTC'
146            else:
147                self._name = 'UTC' + _format_offset(offset)
148
149        def __eq__(self, other):
150            """
151            Compare two timezones
152
153            :param other:
154                The other timezone to compare to
155
156            :return:
157                A boolean
158            """
159
160            if type(other) != timezone:
161                return False
162            return self._offset == other._offset
163
164        def __getinitargs__(self):
165            """
166            Called by tzinfo.__reduce__ to support pickle and copy.
167
168            :return:
169                offset and name, to be used for __init__
170            """
171
172            return self._offset, self._name
173
174        def tzname(self, dt):
175            """
176            :param dt:
177                A datetime object; ignored.
178
179            :return:
180                Name of this timezone
181            """
182
183            return self._name
184
185        def utcoffset(self, dt):
186            """
187            :param dt:
188                A datetime object; ignored.
189
190            :return:
191                A timedelta object with the offset from UTC
192            """
193
194            return self._offset
195
196        def dst(self, dt):
197            """
198            :param dt:
199                A datetime object; ignored.
200
201            :return:
202                Zero timedelta
203            """
204
205            return timedelta(0)
206
207    timezone.utc = timezone(timedelta(0))
208
209# Python 3
210else:
211
212    from datetime import timezone  # noqa
213
214    def int_to_bytes(value, signed=False, width=None):
215        """
216        Converts an integer to a byte string
217
218        :param value:
219            The integer to convert
220
221        :param signed:
222            If the byte string should be encoded using two's complement
223
224        :param width:
225            If None, the minimal possible size (but at least 1),
226            otherwise an integer of the byte width for the return value
227
228        :return:
229            A byte string
230        """
231
232        if width is None:
233            if signed:
234                if value < 0:
235                    bits_required = abs(value + 1).bit_length()
236                else:
237                    bits_required = value.bit_length()
238                if bits_required % 8 == 0:
239                    bits_required += 1
240            else:
241                bits_required = value.bit_length()
242            width = math.ceil(bits_required / 8) or 1
243        return value.to_bytes(width, byteorder='big', signed=signed)
244
245    def int_from_bytes(value, signed=False):
246        """
247        Converts a byte string to an integer
248
249        :param value:
250            The byte string to convert
251
252        :param signed:
253            If the byte string should be interpreted using two's complement
254
255        :return:
256            An integer
257        """
258
259        return int.from_bytes(value, 'big', signed=signed)
260
261
262def _format_offset(off):
263    """
264    Format a timedelta into "[+-]HH:MM" format or "" for None
265    """
266
267    if off is None:
268        return ''
269    mins = off.days * 24 * 60 + off.seconds // 60
270    sign = '-' if mins < 0 else '+'
271    return sign + '%02d:%02d' % divmod(abs(mins), 60)
272
273
274class _UtcWithDst(tzinfo):
275    """
276    Utc class where dst does not return None; required for astimezone
277    """
278
279    def tzname(self, dt):
280        return 'UTC'
281
282    def utcoffset(self, dt):
283        return timedelta(0)
284
285    def dst(self, dt):
286        return timedelta(0)
287
288
289utc_with_dst = _UtcWithDst()
290
291_timezone_cache = {}
292
293
294def create_timezone(offset):
295    """
296    Returns a new datetime.timezone object with the given offset.
297    Uses cached objects if possible.
298
299    :param offset:
300        A datetime.timedelta object; It needs to be in full minutes and between -23:59 and +23:59.
301
302    :return:
303        A datetime.timezone object
304    """
305
306    try:
307        tz = _timezone_cache[offset]
308    except KeyError:
309        tz = _timezone_cache[offset] = timezone(offset)
310    return tz
311
312
313class extended_date(object):
314    """
315    A datetime.datetime-like object that represents the year 0. This is just
316    to handle 0000-01-01 found in some certificates. Python's datetime does
317    not support year 0.
318
319    The proleptic gregorian calendar repeats itself every 400 years. Therefore,
320    the simplest way to format is to substitute year 2000.
321    """
322
323    def __init__(self, year, month, day):
324        """
325        :param year:
326            The integer 0
327
328        :param month:
329            An integer from 1 to 12
330
331        :param day:
332            An integer from 1 to 31
333        """
334
335        if year != 0:
336            raise ValueError('year must be 0')
337
338        self._y2k = date(2000, month, day)
339
340    @property
341    def year(self):
342        """
343        :return:
344            The integer 0
345        """
346
347        return 0
348
349    @property
350    def month(self):
351        """
352        :return:
353            An integer from 1 to 12
354        """
355
356        return self._y2k.month
357
358    @property
359    def day(self):
360        """
361        :return:
362            An integer from 1 to 31
363        """
364
365        return self._y2k.day
366
367    def strftime(self, format):
368        """
369        Formats the date using strftime()
370
371        :param format:
372            A strftime() format string
373
374        :return:
375            A str, the formatted date as a unicode string
376            in Python 3 and a byte string in Python 2
377        """
378
379        # Format the date twice, once with year 2000, once with year 4000.
380        # The only differences in the result will be in the millennium. Find them and replace by zeros.
381        y2k = self._y2k.strftime(format)
382        y4k = self._y2k.replace(year=4000).strftime(format)
383        return ''.join('0' if (c2, c4) == ('2', '4') else c2 for c2, c4 in zip(y2k, y4k))
384
385    def isoformat(self):
386        """
387        Formats the date as %Y-%m-%d
388
389        :return:
390            The date formatted to %Y-%m-%d as a unicode string in Python 3
391            and a byte string in Python 2
392        """
393
394        return self.strftime('0000-%m-%d')
395
396    def replace(self, year=None, month=None, day=None):
397        """
398        Returns a new datetime.date or asn1crypto.util.extended_date
399        object with the specified components replaced
400
401        :return:
402            A datetime.date or asn1crypto.util.extended_date object
403        """
404
405        if year is None:
406            year = self.year
407        if month is None:
408            month = self.month
409        if day is None:
410            day = self.day
411
412        if year > 0:
413            cls = date
414        else:
415            cls = extended_date
416
417        return cls(
418            year,
419            month,
420            day
421        )
422
423    def __str__(self):
424        """
425        :return:
426            A str representing this extended_date, e.g. "0000-01-01"
427        """
428
429        return self.strftime('%Y-%m-%d')
430
431    def __eq__(self, other):
432        """
433        Compare two extended_date objects
434
435        :param other:
436            The other extended_date to compare to
437
438        :return:
439            A boolean
440        """
441
442        # datetime.date object wouldn't compare equal because it can't be year 0
443        if not isinstance(other, self.__class__):
444            return False
445        return self.__cmp__(other) == 0
446
447    def __ne__(self, other):
448        """
449        Compare two extended_date objects
450
451        :param other:
452            The other extended_date to compare to
453
454        :return:
455            A boolean
456        """
457
458        return not self.__eq__(other)
459
460    def _comparison_error(self, other):
461        raise TypeError(unwrap(
462            '''
463            An asn1crypto.util.extended_date object can only be compared to
464            an asn1crypto.util.extended_date or datetime.date object, not %s
465            ''',
466            type_name(other)
467        ))
468
469    def __cmp__(self, other):
470        """
471        Compare two extended_date or datetime.date objects
472
473        :param other:
474            The other extended_date object to compare to
475
476        :return:
477            An integer smaller than, equal to, or larger than 0
478        """
479
480        # self is year 0, other is >= year 1
481        if isinstance(other, date):
482            return -1
483
484        if not isinstance(other, self.__class__):
485            self._comparison_error(other)
486
487        if self._y2k < other._y2k:
488            return -1
489        if self._y2k > other._y2k:
490            return 1
491        return 0
492
493    def __lt__(self, other):
494        return self.__cmp__(other) < 0
495
496    def __le__(self, other):
497        return self.__cmp__(other) <= 0
498
499    def __gt__(self, other):
500        return self.__cmp__(other) > 0
501
502    def __ge__(self, other):
503        return self.__cmp__(other) >= 0
504
505
506class extended_datetime(object):
507    """
508    A datetime.datetime-like object that represents the year 0. This is just
509    to handle 0000-01-01 found in some certificates. Python's datetime does
510    not support year 0.
511
512    The proleptic gregorian calendar repeats itself every 400 years. Therefore,
513    the simplest way to format is to substitute year 2000.
514    """
515
516    # There are 97 leap days during 400 years.
517    DAYS_IN_400_YEARS = 400 * 365 + 97
518    DAYS_IN_2000_YEARS = 5 * DAYS_IN_400_YEARS
519
520    def __init__(self, year, *args, **kwargs):
521        """
522        :param year:
523            The integer 0
524
525        :param args:
526            Other positional arguments; see datetime.datetime.
527
528        :param kwargs:
529            Other keyword arguments; see datetime.datetime.
530        """
531
532        if year != 0:
533            raise ValueError('year must be 0')
534
535        self._y2k = datetime(2000, *args, **kwargs)
536
537    @property
538    def year(self):
539        """
540        :return:
541            The integer 0
542        """
543
544        return 0
545
546    @property
547    def month(self):
548        """
549        :return:
550            An integer from 1 to 12
551        """
552
553        return self._y2k.month
554
555    @property
556    def day(self):
557        """
558        :return:
559            An integer from 1 to 31
560        """
561
562        return self._y2k.day
563
564    @property
565    def hour(self):
566        """
567        :return:
568            An integer from 1 to 24
569        """
570
571        return self._y2k.hour
572
573    @property
574    def minute(self):
575        """
576        :return:
577            An integer from 1 to 60
578        """
579
580        return self._y2k.minute
581
582    @property
583    def second(self):
584        """
585        :return:
586            An integer from 1 to 60
587        """
588
589        return self._y2k.second
590
591    @property
592    def microsecond(self):
593        """
594        :return:
595            An integer from 0 to 999999
596        """
597
598        return self._y2k.microsecond
599
600    @property
601    def tzinfo(self):
602        """
603        :return:
604            If object is timezone aware, a datetime.tzinfo object, else None.
605        """
606
607        return self._y2k.tzinfo
608
609    def utcoffset(self):
610        """
611        :return:
612            If object is timezone aware, a datetime.timedelta object, else None.
613        """
614
615        return self._y2k.utcoffset()
616
617    def time(self):
618        """
619        :return:
620            A datetime.time object
621        """
622
623        return self._y2k.time()
624
625    def date(self):
626        """
627        :return:
628            An asn1crypto.util.extended_date of the date
629        """
630
631        return extended_date(0, self.month, self.day)
632
633    def strftime(self, format):
634        """
635        Performs strftime(), always returning a str
636
637        :param format:
638            A strftime() format string
639
640        :return:
641            A str of the formatted datetime
642        """
643
644        # Format the datetime twice, once with year 2000, once with year 4000.
645        # The only differences in the result will be in the millennium. Find them and replace by zeros.
646        y2k = self._y2k.strftime(format)
647        y4k = self._y2k.replace(year=4000).strftime(format)
648        return ''.join('0' if (c2, c4) == ('2', '4') else c2 for c2, c4 in zip(y2k, y4k))
649
650    def isoformat(self, sep='T'):
651        """
652        Formats the date as "%Y-%m-%d %H:%M:%S" with the sep param between the
653        date and time portions
654
655        :param set:
656            A single character of the separator to place between the date and
657            time
658
659        :return:
660            The formatted datetime as a unicode string in Python 3 and a byte
661            string in Python 2
662        """
663
664        s = '0000-%02d-%02d%c%02d:%02d:%02d' % (self.month, self.day, sep, self.hour, self.minute, self.second)
665        if self.microsecond:
666            s += '.%06d' % self.microsecond
667        return s + _format_offset(self.utcoffset())
668
669    def replace(self, year=None, *args, **kwargs):
670        """
671        Returns a new datetime.datetime or asn1crypto.util.extended_datetime
672        object with the specified components replaced
673
674        :param year:
675            The new year to substitute. None to keep it.
676
677        :param args:
678            Other positional arguments; see datetime.datetime.replace.
679
680        :param kwargs:
681            Other keyword arguments; see datetime.datetime.replace.
682
683        :return:
684            A datetime.datetime or asn1crypto.util.extended_datetime object
685        """
686
687        if year:
688            return self._y2k.replace(year, *args, **kwargs)
689
690        return extended_datetime.from_y2k(self._y2k.replace(2000, *args, **kwargs))
691
692    def astimezone(self, tz):
693        """
694        Convert this extended_datetime to another timezone.
695
696        :param tz:
697            A datetime.tzinfo object.
698
699        :return:
700            A new extended_datetime or datetime.datetime object
701        """
702
703        return extended_datetime.from_y2k(self._y2k.astimezone(tz))
704
705    def timestamp(self):
706        """
707        Return POSIX timestamp. Only supported in python >= 3.3
708
709        :return:
710            A float representing the seconds since 1970-01-01 UTC. This will be a negative value.
711        """
712
713        return self._y2k.timestamp() - self.DAYS_IN_2000_YEARS * 86400
714
715    def __str__(self):
716        """
717        :return:
718            A str representing this extended_datetime, e.g. "0000-01-01 00:00:00.000001-10:00"
719        """
720
721        return self.isoformat(sep=' ')
722
723    def __eq__(self, other):
724        """
725        Compare two extended_datetime objects
726
727        :param other:
728            The other extended_datetime to compare to
729
730        :return:
731            A boolean
732        """
733
734        # Only compare against other datetime or extended_datetime objects
735        if not isinstance(other, (self.__class__, datetime)):
736            return False
737
738        # Offset-naive and offset-aware datetimes are never the same
739        if (self.tzinfo is None) != (other.tzinfo is None):
740            return False
741
742        return self.__cmp__(other) == 0
743
744    def __ne__(self, other):
745        """
746        Compare two extended_datetime objects
747
748        :param other:
749            The other extended_datetime to compare to
750
751        :return:
752            A boolean
753        """
754
755        return not self.__eq__(other)
756
757    def _comparison_error(self, other):
758        """
759        Raises a TypeError about the other object not being suitable for
760        comparison
761
762        :param other:
763            The object being compared to
764        """
765
766        raise TypeError(unwrap(
767            '''
768            An asn1crypto.util.extended_datetime object can only be compared to
769            an asn1crypto.util.extended_datetime or datetime.datetime object,
770            not %s
771            ''',
772            type_name(other)
773        ))
774
775    def __cmp__(self, other):
776        """
777        Compare two extended_datetime or datetime.datetime objects
778
779        :param other:
780            The other extended_datetime or datetime.datetime object to compare to
781
782        :return:
783            An integer smaller than, equal to, or larger than 0
784        """
785
786        if not isinstance(other, (self.__class__, datetime)):
787            self._comparison_error(other)
788
789        if (self.tzinfo is None) != (other.tzinfo is None):
790            raise TypeError("can't compare offset-naive and offset-aware datetimes")
791
792        diff = self - other
793        zero = timedelta(0)
794        if diff < zero:
795            return -1
796        if diff > zero:
797            return 1
798        return 0
799
800    def __lt__(self, other):
801        return self.__cmp__(other) < 0
802
803    def __le__(self, other):
804        return self.__cmp__(other) <= 0
805
806    def __gt__(self, other):
807        return self.__cmp__(other) > 0
808
809    def __ge__(self, other):
810        return self.__cmp__(other) >= 0
811
812    def __add__(self, other):
813        """
814        Adds a timedelta
815
816        :param other:
817            A datetime.timedelta object to add.
818
819        :return:
820            A new extended_datetime or datetime.datetime object.
821        """
822
823        return extended_datetime.from_y2k(self._y2k + other)
824
825    def __sub__(self, other):
826        """
827        Subtracts a timedelta or another datetime.
828
829        :param other:
830            A datetime.timedelta or datetime.datetime or extended_datetime object to subtract.
831
832        :return:
833            If a timedelta is passed, a new extended_datetime or datetime.datetime object.
834            Else a datetime.timedelta object.
835        """
836
837        if isinstance(other, timedelta):
838            return extended_datetime.from_y2k(self._y2k - other)
839
840        if isinstance(other, extended_datetime):
841            return self._y2k - other._y2k
842
843        if isinstance(other, datetime):
844            return self._y2k - other - timedelta(days=self.DAYS_IN_2000_YEARS)
845
846        return NotImplemented
847
848    def __rsub__(self, other):
849        return -(self - other)
850
851    @classmethod
852    def from_y2k(cls, value):
853        """
854        Revert substitution of year 2000.
855
856        :param value:
857            A datetime.datetime object which is 2000 years in the future.
858        :return:
859            A new extended_datetime or datetime.datetime object.
860        """
861
862        year = value.year - 2000
863
864        if year > 0:
865            new_cls = datetime
866        else:
867            new_cls = cls
868
869        return new_cls(
870            year,
871            value.month,
872            value.day,
873            value.hour,
874            value.minute,
875            value.second,
876            value.microsecond,
877            value.tzinfo
878        )
879