codext 1.16.4__py3-none-any.whl → 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. codext/VERSION.txt +1 -1
  2. codext/__common__.py +181 -86
  3. codext/__init__.py +0 -227
  4. codext/__main__.py +236 -0
  5. codext/base/__init__.py +5 -0
  6. codext/base/_base.py +14 -4
  7. codext/base/_base2n.py +4 -0
  8. codext/base/base100.py +7 -1
  9. codext/base/base1024.py +177 -0
  10. codext/base/base122.py +8 -0
  11. codext/base/base38.py +62 -0
  12. codext/base/base45.py +7 -0
  13. codext/base/base65536.py +158 -0
  14. codext/base/base85.py +13 -1
  15. codext/base/base91.py +7 -0
  16. codext/base/baseN.py +15 -0
  17. codext/base/bech32.py +119 -0
  18. codext/base/leb128.py +73 -0
  19. codext/binary/__init__.py +3 -0
  20. codext/binary/_universal.py +57 -0
  21. codext/binary/baudot.py +12 -1
  22. codext/binary/bcd.py +7 -0
  23. codext/binary/chucknorris.py +59 -0
  24. codext/binary/elias.py +78 -0
  25. codext/binary/excess3.py +7 -0
  26. codext/binary/fibonacci.py +65 -0
  27. codext/binary/gray.py +6 -0
  28. codext/binary/manchester.py +8 -0
  29. codext/binary/rotate.py +11 -7
  30. codext/checksums/adler.py +5 -1
  31. codext/checksums/crc.py +11 -1
  32. codext/checksums/luhn.py +8 -1
  33. codext/common/a1z26.py +7 -0
  34. codext/common/cases.py +6 -1
  35. codext/common/dummy.py +6 -1
  36. codext/common/octal.py +3 -0
  37. codext/common/ordinal.py +3 -0
  38. codext/compressions/__init__.py +1 -0
  39. codext/compressions/gzipp.py +6 -0
  40. codext/compressions/lz77.py +7 -0
  41. codext/compressions/lz78.py +7 -0
  42. codext/compressions/pkzip.py +6 -0
  43. codext/compressions/rle.py +116 -0
  44. codext/crypto/__init__.py +8 -0
  45. codext/crypto/adfgx.py +124 -0
  46. codext/crypto/affine.py +5 -0
  47. codext/crypto/atbash.py +5 -0
  48. codext/crypto/bacon.py +5 -0
  49. codext/crypto/barbie.py +4 -2
  50. codext/crypto/bazeries.py +132 -0
  51. codext/crypto/bifid.py +87 -0
  52. codext/crypto/chaocipher.py +77 -0
  53. codext/crypto/citrix.py +4 -2
  54. codext/crypto/columnar.py +80 -0
  55. codext/crypto/hill.py +110 -0
  56. codext/crypto/nihilist.py +86 -0
  57. codext/crypto/playfair.py +30 -16
  58. codext/crypto/polybius.py +6 -0
  59. codext/crypto/railfence.py +6 -0
  60. codext/crypto/rot.py +8 -0
  61. codext/crypto/scytale.py +6 -0
  62. codext/crypto/shift.py +3 -0
  63. codext/crypto/skip.py +59 -0
  64. codext/crypto/vic.py +9 -0
  65. codext/crypto/vigenere.py +54 -3
  66. codext/crypto/xor.py +3 -0
  67. codext/hashing/blake.py +7 -1
  68. codext/hashing/crypt.py +7 -1
  69. codext/hashing/md.py +8 -1
  70. codext/hashing/mmh3.py +6 -1
  71. codext/hashing/sha.py +7 -1
  72. codext/hashing/shake.py +5 -1
  73. codext/languages/braille.py +6 -0
  74. codext/languages/galactic.py +6 -0
  75. codext/languages/ipsum.py +8 -1
  76. codext/languages/leetspeak.py +6 -0
  77. codext/languages/morse.py +7 -0
  78. codext/languages/navajo.py +7 -0
  79. codext/languages/radio.py +6 -0
  80. codext/languages/southpark.py +7 -0
  81. codext/languages/tap.py +7 -0
  82. codext/languages/tomtom.py +6 -0
  83. codext/others/__init__.py +3 -0
  84. codext/others/binhex.py +125 -0
  85. codext/others/dna.py +4 -2
  86. codext/others/kbshift.py +5 -0
  87. codext/others/letters.py +5 -0
  88. codext/others/markdown.py +6 -0
  89. codext/others/uuencode.py +8 -0
  90. codext/others/xxencode.py +83 -0
  91. codext/others/yenc.py +121 -0
  92. codext/stegano/__init__.py +2 -0
  93. codext/stegano/hexagram.py +7 -0
  94. codext/stegano/klopf.py +6 -0
  95. codext/stegano/resistor.py +6 -0
  96. codext/stegano/rick.py +4 -0
  97. codext/stegano/sms.py +6 -0
  98. codext/stegano/tags.py +62 -0
  99. codext/stegano/whitespace.py +8 -0
  100. codext/stegano/zerowidth.py +112 -0
  101. codext/web/html.py +8 -0
  102. codext/web/url.py +6 -0
  103. {codext-1.16.4.dist-info → codext-2.0.0.dist-info}/METADATA +45 -18
  104. codext-2.0.0.dist-info/RECORD +116 -0
  105. {codext-1.16.4.dist-info → codext-2.0.0.dist-info}/WHEEL +1 -1
  106. {codext-1.16.4.dist-info → codext-2.0.0.dist-info}/entry_points.txt +6 -1
  107. codext-1.16.4.dist-info/RECORD +0 -92
  108. {codext-1.16.4.dist-info → codext-2.0.0.dist-info}/licenses/LICENSE +0 -0
  109. {codext-1.16.4.dist-info → codext-2.0.0.dist-info}/top_level.txt +0 -0
codext/VERSION.txt CHANGED
@@ -1 +1 @@
1
- 1.16.4
1
+ 2.0.0
codext/__common__.py CHANGED
@@ -11,12 +11,13 @@ import sys
11
11
  from encodings.aliases import aliases as ALIASES
12
12
  from functools import reduce, update_wrapper, wraps
13
13
  from importlib import import_module
14
+ from importlib.util import find_spec
14
15
  from inspect import currentframe
16
+ from collections import Counter
15
17
  from io import BytesIO
16
18
  from itertools import chain, product
17
19
  from locale import getlocale
18
20
  from math import log
19
- from pkgutil import iter_modules
20
21
  from platform import system
21
22
  from random import randint
22
23
  from string import *
@@ -41,6 +42,7 @@ CODECS_REGISTRY = None
41
42
  CODECS_OVERWRITTEN = []
42
43
  CODECS_CATEGORIES = ["native", "custom"]
43
44
  CODECS_CACHE = {}
45
+ PRINTABLES = frozenset(printable)
44
46
  LANG = getlocale()
45
47
  if LANG:
46
48
  LANG = (LANG[0] or "")[:2].lower()
@@ -57,6 +59,10 @@ MASKS = {
57
59
  }
58
60
 
59
61
  __codecs_registry = []
62
+ # caches depending on the state of the registry (cleared whenever it changes)
63
+ __guess_lists = {}
64
+ __names_table = []
65
+ __lookup_cache = {}
60
66
 
61
67
  MACROS = {}
62
68
  PERS_MACROS = {}
@@ -67,7 +73,12 @@ LINUX = system() == "Linux"
67
73
  UNIX = DARWIN or LINUX
68
74
  WINDOWS = system() == "Windows"
69
75
 
70
- entropy = lambda s: -sum([p * log(p, 2) for p in [float(s.count(c)) / len(s) for c in set(s)]])
76
+
77
+ def entropy(s):
78
+ """ Shannon entropy of the given string or bytes, counting the characters in a single pass. """
79
+ n, l = Counter(s), len(s)
80
+ return -sum([p * log(p, 2) for p in [n[c] / l for c in set(s)]])
81
+
71
82
 
72
83
  isb = lambda s: isinstance(s, bytes)
73
84
  iss = lambda s: isinstance(s, str)
@@ -178,14 +189,10 @@ def __stdin_pipe():
178
189
 
179
190
  def _input(infile):
180
191
  # handle input file or stdin
181
- c = b("")
182
192
  if infile:
183
193
  with open(infile, 'rb') as f:
184
- c = f.read()
185
- else:
186
- for line in __stdin_pipe():
187
- c += line
188
- return c
194
+ return f.read()
195
+ return b("").join(__stdin_pipe())
189
196
 
190
197
 
191
198
  def _set_exc(name, etype="ValueError"):
@@ -238,15 +245,23 @@ def add(ename, encode=None, decode=None, pattern=None, text=True, add_to_codecs=
238
245
  for exc in kwargs.get('extra_exceptions', []):
239
246
  _set_exc(exc) # create additional custom exceptions as builtins
240
247
  glob = currentframe().f_back.f_globals
248
+ # the pattern is compiled once, as it is matched against every encoding name looked up
249
+ rx = re.compile(pattern) if pattern else None
250
+ # category of the codec, from the folder of the module adding it
251
+ cat = glob.get('__file__', os.path.join("custom", "_")).split(os.path.sep)[-2].rstrip("s")
252
+ category = kwargs.get('category', cat)
253
+ for c in [cat, category]:
254
+ if c not in CODECS_CATEGORIES:
255
+ CODECS_CATEGORIES.append(c)
241
256
  # search function for the new encoding
242
257
  @_with_repr(ename)
243
258
  def getregentry(encoding):
244
- if encoding != ename and not (pattern and re.match(pattern, encoding)):
259
+ if encoding != ename and not (rx and rx.match(encoding)):
245
260
  return
246
261
  fenc, fdec, name = encode, decode, encoding
247
262
  # prepare CodecInfo input arguments
248
263
  if pattern:
249
- m, args, i = re.match(pattern, encoding), [], 1
264
+ m, args, i = rx.match(encoding), [], 1
250
265
  try:
251
266
  while True:
252
267
  try:
@@ -275,12 +290,12 @@ def add(ename, encode=None, decode=None, pattern=None, text=True, add_to_codecs=
275
290
  if fenc:
276
291
  fenc = fix_inout_formats(fenc)
277
292
  if fdec:
293
+ sl, sc = getattr(fdec, '__strip__', (False, False))
278
294
  fdec = fix_inout_formats(fdec)
279
- sl, sc = kwargs.pop('strip_lines', False), kwargs.pop('strip_crlf', False)
280
295
  if sl or sc:
281
296
  def _striplines(f):
282
297
  def __wrapper(input, *a, **kw):
283
- return f(_stripl(input, sc, sl), *a, **kw)
298
+ return f(_stripl(input, sl, sc), *a, **kw)
284
299
  return __wrapper
285
300
  # this fixes issues with wrapped encoded inputs
286
301
  fdec = _striplines(fdec)
@@ -329,11 +344,7 @@ def add(ename, encode=None, decode=None, pattern=None, text=True, add_to_codecs=
329
344
  ci.parameters['add_to_codecs'] = add_to_codecs
330
345
  ci.parameters['pattern'] = pattern
331
346
  ci.parameters['text'] = text
332
- f = glob.get('__file__', os.path.join("custom", "_"))
333
- cat = f.split(os.path.sep)[-2].rstrip("s")
334
- if cat not in CODECS_CATEGORIES:
335
- CODECS_CATEGORIES.append(cat)
336
- ci.parameters['category'] = kwargs.get('category', cat)
347
+ ci.parameters['category'] = category
337
348
  ci.parameters['examples'] = kwargs.get('examples', glob.get('__examples__'))
338
349
  ci.parameters['guess'] = kwargs.get('guess', glob.get('__guess__', [ename])) or []
339
350
  ci.parameters['module'] = kwargs.get('module', glob.get('__name__'))
@@ -349,6 +360,7 @@ def add(ename, encode=None, decode=None, pattern=None, text=True, add_to_codecs=
349
360
  if kwargs.get('aliases'):
350
361
  getregentry.__aliases__ = list(map(lambda n: re.sub(r"[\s\-]", "_", n), kwargs['aliases']))
351
362
  getregentry.__pattern__ = pattern
363
+ getregentry.__category__ = category
352
364
  register(getregentry, add_to_codecs)
353
365
  return getregentry
354
366
 
@@ -495,6 +507,7 @@ def add_map(ename, encmap, repl_char="?", sep="", ignore_case=None, no_error=Fal
495
507
  smapdict = {getattr(k, case_e)() if i in ["both", "encode"] else k: \
496
508
  ([getattr(x, case_d)() for x in v] if isinstance(v, list) else getattr(v, case_d)()) \
497
509
  if i in ["both", "decode"] else v for k, v in smapdict.items()}
510
+ strip = (False, False)
498
511
  if decode:
499
512
  tmp = {}
500
513
  # this has a meaning for encoding maps that could have clashes in encoded chars (e.g. Bacon's cipher ;
@@ -506,7 +519,10 @@ def add_map(ename, encmap, repl_char="?", sep="", ignore_case=None, no_error=Fal
506
519
  if x not in tmp.keys():
507
520
  tmp[x] = k
508
521
  smapdict, cs = tmp, reduce(lambda acc, x: acc + x, tmp.keys())
509
- kwargs['strip_lines'], kwargs['strip_crlf'] = "\n" not in set(cs), "\r\n" not in cs
522
+ # line breaks that are not in the decoding map are stripped from the input (wrapped encoded text),
523
+ # except for codecs triggering no error, as they keep the unknown characters (thus, the lines)
524
+ if not no_error:
525
+ strip = ("\n" not in set(cs), "\r\n" not in cs)
510
526
  # this allows to avoid an error with Python2 in the "for i, c in enumerate(parts)" loop
511
527
  if '' not in smapdict.keys():
512
528
  smapdict[''] = ""
@@ -517,14 +533,14 @@ def add_map(ename, encmap, repl_char="?", sep="", ignore_case=None, no_error=Fal
517
533
  for x in smapdict.values():
518
534
  getattr(l, ["append", "extend"][isinstance(x, list)])(x)
519
535
  rminlen = max(1, min(map(len, set(l) - {''})))
536
+ # case handling, separator and kind of token do not depend on the text, hence they are determined once
537
+ icase = ignore_case in ["both", "decode" if decode else "encode"]
538
+ case = getattr(str, case_d if decode else case_e) if icase else None
539
+ lsep = "" if decode else sep if len(sep) <= 1 else sep[0]
540
+ kind = ["character", "token"][tmaxlen > 1]
520
541
 
521
542
  # generic encoding/decoding function for map encodings
522
543
  def code(text, errors="strict"):
523
- icase = ignore_case == "both" or \
524
- decode and ignore_case == "decode" or \
525
- not decode and ignore_case == "encode"
526
- if icase:
527
- case = case_d if decode else case_e
528
544
  if no_error:
529
545
  errors = "leave"
530
546
  text = ensure_str(text)
@@ -533,9 +549,7 @@ def add_map(ename, encmap, repl_char="?", sep="", ignore_case=None, no_error=Fal
533
549
  text = "".join(f"{bin(ord(c))[2:]:0>8}" for c in text)
534
550
  elif intype == "ord":
535
551
  text = "".join(str(ord(c)).zfill(3) for c in text)
536
- r = ""
537
- lsep = "" if decode else sep if len(sep) <= 1 else sep[0]
538
- kind = ["character", "token"][tmaxlen > 1]
552
+ r = []
539
553
  error_func = handle_error(ename, errors, lsep, repl_char, rminlen, decode, kind)
540
554
 
541
555
  # get the value from the mapping dictionary, trying the token with its inverted case if relevant
@@ -544,8 +558,7 @@ def add_map(ename, encmap, repl_char="?", sep="", ignore_case=None, no_error=Fal
544
558
  result = smapdict[token]
545
559
  except KeyError:
546
560
  if icase and not case_changed:
547
- token_inv_case = getattr(token, case)()
548
- return __get_value(token_inv_case, position, True)
561
+ return __get_value(case(token), position, True)
549
562
  return error_func(token, position)
550
563
  if isinstance(result, list):
551
564
  result = result[0]
@@ -554,7 +567,7 @@ def add_map(ename, encmap, repl_char="?", sep="", ignore_case=None, no_error=Fal
554
567
  # if a separator is defined, rely on it by splitting the input text
555
568
  if decode and len(sep) > 0:
556
569
  for i, c in enumerate(re.split("[" + sep + "]", text)):
557
- r += __get_value(c, i)
570
+ r.append(__get_value(c, i))
558
571
  # otherwise, move through the text using a cursor for tokenizing it ; this allows defining more complex
559
572
  # encodings with variable token lengths
560
573
  else:
@@ -563,8 +576,8 @@ def add_map(ename, encmap, repl_char="?", sep="", ignore_case=None, no_error=Fal
563
576
  token = text[cursor:cursor+1]
564
577
  for l in range(tminlen, tmaxlen + 1):
565
578
  token = text[cursor:cursor+l]
566
- if token in smapdict.keys() or icase and getattr(token, case)() in smapdict.keys():
567
- r += __get_value(token, cursor)
579
+ if token in smapdict or icase and case(token) in smapdict:
580
+ r.append(__get_value(token, cursor))
568
581
  cursor += l
569
582
  break
570
583
  else:
@@ -574,21 +587,24 @@ def add_map(ename, encmap, repl_char="?", sep="", ignore_case=None, no_error=Fal
574
587
  # if the number of bad chars is the minimum token length, consume it and start a new buffer
575
588
  if len(bad) == tminlen or errors == "leave":
576
589
  posn = cursor - len(bad)
577
- r += error_func(bad, posn)
590
+ r.append(error_func(bad, posn))
578
591
  bad = ""
592
+ r = "".join(r)
579
593
  if decode:
580
594
  if outype in ["bin", "ord"]:
581
- tmp, r = "", r.replace(lsep, "")
595
+ tmp, r = [], r.replace(lsep, "")
582
596
  step = [3, 8][outype == "bin"]
583
597
  for i in range(0, len(r), step):
584
598
  s = r[i:i+step]
585
599
  try:
586
- tmp += chr(int(s, 2) if outype == "bin" else int(s))
600
+ tmp.append(chr(int(s, 2) if outype == "bin" else int(s)))
587
601
  except ValueError:
588
602
  if len(s) > 0:
589
- tmp += "[" + s + "]"
590
- r = tmp + lsep
603
+ tmp.append("[" + s + "]")
604
+ r = "".join(tmp) + lsep
591
605
  return r[:len(r)-len(lsep)], len(b(text))
606
+ # the stripping flags are carried by the function, as it is built for each looked up encoding name
607
+ code.__strip__ = strip
592
608
  return code
593
609
  if re.search(r"\([^(?:)]", kwargs.get('pattern', "")) is None:
594
610
  # in this case, there is no capturing group for parametrization
@@ -623,6 +639,7 @@ def clear():
623
639
  """ Clear codext's local registry of search functions. """
624
640
  global __codecs_registry, MACROS, PERS_MACROS # noqa: F824
625
641
  __codecs_registry, MACROS, PERS_MACROS = [], {}, {}
642
+ __registry_changed()
626
643
  codecs.clear = clear
627
644
 
628
645
 
@@ -664,14 +681,13 @@ def is_native(encoding):
664
681
 
665
682
  def list_categories():
666
683
  """ Get a list of all codec categories. """
667
- c = CODECS_CATEGORIES
668
684
  root = os.path.dirname(__file__)
669
- for d in os.listdir(root):
670
- if os.path.isdir(os.path.join(root, d)) and not d.startswith("__"):
671
- c.append(d.rstrip("s"))
672
- # particular category, hardcoded from base/_base.py
673
- c += ["base-generic"]
674
- return list(set(c))
685
+ # categories from the folders of the codecs, plus a particular category, hardcoded from base/_base.py
686
+ for c in [d.rstrip("s") for d in os.listdir(root) if os.path.isdir(os.path.join(root, d)) and
687
+ not d.startswith("__")] + ["base-generic"]:
688
+ if c not in CODECS_CATEGORIES:
689
+ CODECS_CATEGORIES.append(c)
690
+ return CODECS_CATEGORIES[:]
675
691
  list_categories()
676
692
 
677
693
 
@@ -705,9 +721,11 @@ def list_encodings(*categories):
705
721
  enc.append(ci.name)
706
722
  for search_function in CODECS_OVERWRITTEN + __codecs_registry:
707
723
  name = search_function.__name__.replace("_", "-")
708
- p = search_function.__pattern__
709
- ci = search_function(name) if p is None else search_function(generate_string_from_regex(p))
710
- c = "other" if ci is None else ci.parameters['category']
724
+ c = getattr(search_function, "__category__", None)
725
+ if c is None:
726
+ p = getattr(search_function, "__pattern__", None)
727
+ ci = search_function(name) if p is None else search_function(_sample_name(search_function))
728
+ c = "other" if ci is None else ci.parameters['category']
711
729
  if (len(categories) == 0 or c in categories) and c not in exclude:
712
730
  enc.append(name)
713
731
  for category in categories:
@@ -731,6 +749,7 @@ def remove(name):
731
749
  tbr.append(search_function)
732
750
  for search_function in tbr:
733
751
  __codecs_registry.remove(search_function)
752
+ __registry_changed()
734
753
  try:
735
754
  del MACROS[name]
736
755
  except KeyError:
@@ -769,6 +788,7 @@ def reset():
769
788
  # restore codext's registry
770
789
  else:
771
790
  __codecs_registry = CODECS_REGISTRY[:]
791
+ __registry_changed()
772
792
  # restore codext's embedded set of macros
773
793
  with open(os.path.join(os.path.dirname(__file__), "macros.json")) as f:
774
794
  MACROS = json.load(f)
@@ -897,12 +917,15 @@ def decode(obj, encoding='utf-8', errors='strict'):
897
917
  codecs.decode = decode
898
918
 
899
919
 
920
+ _REPEAT = re.compile(r"\[(\d+)\]$")
921
+
922
+
900
923
  def encode(obj, encoding='utf-8', errors='strict'):
901
924
  """ Custom encode function relying on the hooked lookup function. """
902
- n, m = 1, re.search(r"\[(\d+)\]$", encoding)
925
+ n, m = 1, _REPEAT.search(encoding)
903
926
  if m:
904
927
  n = int(m.group(1))
905
- encoding = re.sub(r"\[(\d+)\]$", "", encoding)
928
+ encoding = encoding[:m.start()]
906
929
  ci = lookup(encoding)
907
930
  for i in range(n):
908
931
  try:
@@ -919,6 +942,26 @@ codecs.encode = encode
919
942
 
920
943
  def lookup(encoding, macro=True):
921
944
  """ Hooked lookup function for searching first for codecs in the local registry of this module. """
945
+ # the results are cached per encoding name, as searching through the registry is costly ; a macro is only returned
946
+ # when allowed, and failures are not cached (a codec or a macro may be added afterwards)
947
+ try:
948
+ ci = __lookup_cache[encoding]
949
+ if macro or not isinstance(ci, CodecMacro):
950
+ return ci
951
+ except (KeyError, TypeError):
952
+ pass
953
+ ci = __lookup(encoding, macro)
954
+ try:
955
+ __lookup_cache[encoding] = ci
956
+ except TypeError:
957
+ pass
958
+ return ci
959
+ codecs.lookup = lookup
960
+
961
+
962
+ def __lookup(encoding, macro=True):
963
+ """ Search for the given encoding, first in the local registry of this module, then with the original lookup
964
+ function and finally among the macros. """
922
965
  # first, try to match the given encoding with codecs' search functions
923
966
  for search_function in CODECS_OVERWRITTEN + __codecs_registry:
924
967
  codecinfo = search_function(encoding)
@@ -928,7 +971,7 @@ def lookup(encoding, macro=True):
928
971
  for search_function in CODECS_OVERWRITTEN + __codecs_registry:
929
972
  if search_function.__name__.replace("_", "-") == encoding or \
930
973
  encoding in getattr(search_function, "__aliases__", []):
931
- codecinfo = search_function(generate_string_from_regex(search_function.__pattern__))
974
+ codecinfo = search_function(_sample_name(search_function))
932
975
  if codecinfo is not None:
933
976
  return codecinfo
934
977
  # finally, get a CodecInfo with the original lookup function and refine it with a dictionary of parameters
@@ -945,7 +988,6 @@ def lookup(encoding, macro=True):
945
988
  e = LookupError(f"unknown encoding: {encoding}")
946
989
  e.__cause__ = e # stop exception chaining
947
990
  raise e
948
- codecs.lookup = lookup
949
991
 
950
992
 
951
993
  def register(search_function, add_to_codecs=False):
@@ -964,6 +1006,7 @@ def register(search_function, add_to_codecs=False):
964
1006
  except LookupError:
965
1007
  l = __codecs_registry
966
1008
  l.append(search_function)
1009
+ __registry_changed()
967
1010
  if add_to_codecs:
968
1011
  __orig_register(search_function)
969
1012
 
@@ -974,37 +1017,69 @@ def __register(search_function):
974
1017
  codecs.register = __register
975
1018
 
976
1019
 
1020
+ def __names():
1021
+ """ Get the table of names used for searching encodings, as tuples (names to be matched, resulting name, search
1022
+ function), built once per state of the registry. """
1023
+ if len(__names_table) == 0:
1024
+ for search_function in CODECS_OVERWRITTEN + __codecs_registry:
1025
+ n = search_function.__name__
1026
+ __names_table.append(((n, n.replace("_", "-")), n.replace("_", "-"), search_function))
1027
+ for alias, name in ALIASES.items():
1028
+ __names_table.append(((alias, name), name, None))
1029
+ return __names_table
1030
+
1031
+
1032
+ def __registry_changed():
1033
+ """ Clear the caches depending on the state of the registry. """
1034
+ __guess_lists.clear()
1035
+ __lookup_cache.clear()
1036
+ __names_table.clear()
1037
+
1038
+
1039
+ def _sample_name(search_function):
1040
+ """ Get the encoding name generated from the pattern of the given search function (used when looking up a codec by
1041
+ its name) ; it is cached on the search function, as generating strings from a regex is costly. """
1042
+ try:
1043
+ return search_function.__sample__
1044
+ except AttributeError:
1045
+ search_function.__sample__ = s = generate_string_from_regex(getattr(search_function, "__pattern__", None))
1046
+ return s
1047
+
1048
+
1049
+ def _sample_rounds(search_function):
1050
+ """ Get 5 sets of encoding names generated from the pattern of the given search function, for the extended search ;
1051
+ they are cached on the search function, as generating strings from a regex is costly. """
1052
+ try:
1053
+ return search_function.__samples__
1054
+ except AttributeError:
1055
+ p = getattr(search_function, "__pattern__", None)
1056
+ search_function.__samples__ = r = [list(generate_strings_from_regex(p)) for _ in range(5)]
1057
+ return r
1058
+
1059
+
977
1060
  def search(encoding_regex, extended=True):
978
1061
  """ Function similar to lookup but allows to search for an encoding based on a regex instead. It searches this way
979
1062
  into the local registry but also tries a simple lookup with the original lookup function. """
980
- matches = []
981
- for search_function in CODECS_OVERWRITTEN + __codecs_registry:
982
- n = search_function.__name__
983
- for name in [n, n.replace("_", "-")]:
984
- if re.search(encoding_regex, name):
985
- matches.append(n.replace("_", "-"))
986
- continue
987
- if extended:
1063
+ matches, rx = [], re.compile(encoding_regex)
1064
+ for names, name, search_function in __names():
1065
+ if any(rx.search(n) for n in names):
1066
+ matches.append(name)
1067
+ if extended and search_function is not None:
988
1068
  # in some cases, encoding_regex can match a generated string that uses a particular portion of its
989
1069
  # generating pattern ; e.g. we expect encoding_regex="uu_" to find "uu" and "uu_codec" while it can also
990
1070
  # find "morse" or "atbash" very rarely because of their dynamic patterns and the limited number of randomly
991
1071
  # generated strings
992
1072
  # so, we can use a qualified majority voting to ensure we do not get a "junk" encoding in the list of
993
- # matches ; executing 5 times the string generation for a given codec but adding the codec to the list of
1073
+ # matches ; using 5 sets of generated strings for a given codec but adding the codec to the list of
994
1074
  # matches only if we get at least 3 matches ensures that we consider up to 2 failures that could be
995
1075
  # stochastic, therefore drastically decreasing the probability to get a "junk" encoding in the matches list
996
1076
  c = 0
997
- for i in range(5):
998
- for s in generate_strings_from_regex(search_function.__pattern__):
999
- if re.search(encoding_regex, s):
1000
- c += 1
1077
+ for strings in _sample_rounds(search_function):
1078
+ if any(rx.search(s) for s in strings):
1079
+ c += 1
1080
+ if c >= 3:
1081
+ matches.append(search_function.__name__)
1001
1082
  break
1002
- if c >= 3:
1003
- matches.append(n)
1004
- break
1005
- for s, n in ALIASES.items():
1006
- if re.search(encoding_regex, s) or re.search(encoding_regex, n):
1007
- matches.append(n)
1008
1083
  return sorted(list(set(matches)), key=_human_keys)
1009
1084
  codecs.search = search
1010
1085
 
@@ -1136,7 +1211,14 @@ def generate_strings_from_regex(regex, star_plus_max=STAR_PLUS_MAX, repeat_max=R
1136
1211
 
1137
1212
 
1138
1213
  # guess feature objects
1139
- __module_exists = lambda n: n in [x[1] for x in iter_modules()]
1214
+ def __module_exists(name):
1215
+ """ Check if a top-level module is installed, without importing it. """
1216
+ try:
1217
+ return find_spec(name) is not None
1218
+ except (ImportError, ValueError):
1219
+ return False
1220
+
1221
+
1140
1222
  stopfunc = ModuleType("stopfunc", """
1141
1223
  Predefined stop functions
1142
1224
  ~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1149,7 +1231,7 @@ stopfunc = ModuleType("stopfunc", """
1149
1231
  - `regex`: takes one argument, the regular expression, for checking a string against the given pattern
1150
1232
  - `text`: checks for printables and an entropy less than 4.6 (empirically determined)
1151
1233
  """)
1152
- stopfunc.printables = lambda s: all(c in printable for c in ensure_str(s))
1234
+ stopfunc.printables = lambda s: PRINTABLES.issuperset(ensure_str(s))
1153
1235
  stopfunc.printables.__name__ = stopfunc.printables.__qualname__ = "printables"
1154
1236
  stopfunc.regex = lambda p: lambda s: re.search(p, ensure_str(s)) is not None
1155
1237
  stopfunc.regex.__name__ = stopfunc.regex.__qualname__ = "regex"
@@ -1270,6 +1352,20 @@ def __guess(prev_input, input, stop_func, depth, max_depth, min_depth, encodings
1270
1352
  stop, show, scoring_heuristic, extended, debug)
1271
1353
 
1272
1354
 
1355
+ def __guess_list(encoding):
1356
+ """ Get the list of encoding names to be tried when guessing for the given encoding, cached per state of the
1357
+ registry. """
1358
+ try:
1359
+ return __guess_lists[encoding]
1360
+ except KeyError:
1361
+ try:
1362
+ l = lookup(encoding, False).parameters['guess']
1363
+ except Exception:
1364
+ l = []
1365
+ __guess_lists[encoding] = l
1366
+ return l
1367
+
1368
+
1273
1369
  def __make_encodings_dict(include, exclude):
1274
1370
  """ Process encodings inclusion and exclusion lists, listing categories and developping codecs' lists of possible
1275
1371
  encoding names. It also creates a cache with the CodecInfo objects for improving performance. """
@@ -1281,11 +1377,7 @@ def __make_encodings_dict(include, exclude):
1281
1377
  for enc in ((list_encodings(*cc) if (len(cc) > 0 or keep) and len(sc) == 0 else []) + sc):
1282
1378
  g = []
1283
1379
  for e in (search(enc, False) or [enc]):
1284
- try:
1285
- ci = lookup(e, False)
1286
- g.extend(ci.parameters['guess'])
1287
- except:
1288
- pass
1380
+ g.extend(__guess_list(e))
1289
1381
  if enc in g: # e.g. "rot-1" => ["rot-1", "rot-2", ...] ; only "rot-1" is to be selected
1290
1382
  l.append(enc)
1291
1383
  else: # e.g. "rot" => ["rot-1", "rot-2", ...] ; all the "rot-N" shall be selected
@@ -1298,7 +1390,8 @@ def __make_encodings_dict(include, exclude):
1298
1390
 
1299
1391
  def __rank(prev_input, input, prev_encoding, encodings, heuristic=False, extended=False, yield_score=False):
1300
1392
  """ Filter valid encodings and rank them by relevance. """
1301
- ranking = {}
1393
+ # characteristics of the input text, computed once per padding character (see __score)
1394
+ ranking, texts = {}, {}
1302
1395
  for e in encodings:
1303
1396
  try:
1304
1397
  codec = CODECS_CACHE[e]
@@ -1307,7 +1400,7 @@ def __rank(prev_input, input, prev_encoding, encodings, heuristic=False, extende
1307
1400
  CODECS_CACHE[e] = codec = lookup(e, False)
1308
1401
  except LookupError:
1309
1402
  continue
1310
- t = __score(prev_input, input, prev_encoding, e, codec, heuristic, extended)
1403
+ t = __score(prev_input, input, prev_encoding, e, codec, heuristic, extended, texts)
1311
1404
  if t:
1312
1405
  ranking[e] = t
1313
1406
  for encoding, result in sorted(ranking.items(), key=lambda x: (-x[1][0], x[0])):
@@ -1326,13 +1419,12 @@ class _Text(object):
1326
1419
  text = text.rstrip(b(pad_char) if isinstance(text, bytes) else pad_char)
1327
1420
  self.len = len(self.text)
1328
1421
  self.lcharset = len(set(self.text))
1329
- self.printables = float(len([c for c in self.text if c in printable])) / self.len
1422
+ self.printables = float(sum(map(PRINTABLES.__contains__, self.text))) / self.len
1330
1423
  self.entropy = entropy(self.text)
1331
1424
 
1332
1425
 
1333
- def __score(prev_input, input, prev_encoding, encoding, codec, heuristic=False, extended=False):
1426
+ def __score(prev_input, input, prev_encoding, encoding, codec, heuristic=False, extended=False, texts=None):
1334
1427
  """ Score relevant encodings given an input. """
1335
- obj = None
1336
1428
  sc = codec.parameters.get('scoring', {})
1337
1429
  no_error, transitive = codec.parameters.get('no_error', False), sc.get('transitive', False)
1338
1430
  # ignore encodings that fail to decode with their default errors handling value
@@ -1349,10 +1441,12 @@ def __score(prev_input, input, prev_encoding, encoding, codec, heuristic=False,
1349
1441
  ci_prev = lookup(prev_encoding, False)
1350
1442
  if ci_prev.parameters['name'] == codec.parameters['name']:
1351
1443
  return
1352
- # compute input's characteristics only once and only if the control flow reaches this point
1353
- pad = sc.get('padding_char')
1354
- if obj is None:
1355
- obj = _Text(input, pad)
1444
+ # compute input's characteristics only once per padding character and only if the control flow reaches this point
1445
+ pad, texts = sc.get('padding_char'), {} if texts is None else texts
1446
+ try:
1447
+ obj = texts[pad]
1448
+ except KeyError:
1449
+ obj = texts[pad] = _Text(input, pad)
1356
1450
  if heuristic:
1357
1451
  # from here, the goal (e.g. if the input is Base32) is to rank candidate encodings (e.g. multiple base
1358
1452
  # codecs) so that we can put the right one as early as possible and eventually exclude bad candidates
@@ -1488,7 +1582,7 @@ def guess(input, stop_func=stopfunc.default, min_depth=0, max_depth=5, include=N
1488
1582
  break
1489
1583
  except KeyboardInterrupt:
1490
1584
  pass
1491
- CODECS_CACHE = {}
1585
+ CODECS_CACHE.clear()
1492
1586
  return result
1493
1587
  codecs.guess = guess
1494
1588
 
@@ -1505,6 +1599,7 @@ def rank(input, extended=False, limit=-1, include=None, exclude=None):
1505
1599
  encodings = __make_encodings_dict(include if isinstance(include, dict) else {-1: include or CODECS_CATEGORIES},
1506
1600
  exclude if isinstance(exclude, dict) else {-1: exclude or []})
1507
1601
  r = list(__rank(None, input, "", encodings[-1], True, extended, True))
1602
+ CODECS_CACHE.clear()
1508
1603
  return r[:limit] if len(r) > 1 else r
1509
1604
  codecs.rank = rank
1510
1605