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