codext 2.0.0__py3-none-any.whl → 2.0.1__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 (152) hide show
  1. codext/VERSION.txt +1 -1
  2. codext/__common__.py +87 -399
  3. codext/__init__.py +2 -0
  4. codext/__main__.py +37 -1
  5. codext/barcodes/__init__.py +6 -0
  6. codext/barcodes/aztec.py +726 -0
  7. codext/barcodes/datamatrix.py +669 -0
  8. codext/barcodes/fakeqr.py +216 -0
  9. codext/barcodes/pdf417.py +865 -0
  10. codext/barcodes/qr.py +671 -0
  11. codext/base/__init__.py +4 -0
  12. codext/base/_base.py +68 -14
  13. codext/base/_base2n.py +12 -2
  14. codext/base/base128.py +80 -0
  15. codext/base/base131072.py +219 -0
  16. codext/base/base256.py +52 -0
  17. codext/base/base45.py +4 -1
  18. codext/base/base69.py +73 -0
  19. codext/binary/__init__.py +2 -0
  20. codext/binary/hamming.py +87 -0
  21. codext/binary/manchester.py +51 -1
  22. codext/binary/nrzi.py +52 -0
  23. codext/checksums/__init__.py +1 -0
  24. codext/checksums/fletcher.py +39 -0
  25. codext/{crypto → ciphers}/__init__.py +10 -1
  26. codext/ciphers/_jsfloat.py +104 -0
  27. codext/ciphers/_square.py +67 -0
  28. codext/ciphers/aer256.py +35 -0
  29. codext/ciphers/armon64.py +37 -0
  30. codext/{crypto → ciphers}/bazeries.py +7 -18
  31. codext/ciphers/enigma.py +150 -0
  32. codext/ciphers/ezip64.py +35 -0
  33. codext/ciphers/foursquare.py +74 -0
  34. codext/ciphers/fractionatedmorse.py +91 -0
  35. codext/ciphers/okto3.py +35 -0
  36. codext/{crypto → ciphers}/playfair.py +13 -32
  37. codext/ciphers/twosquare.py +82 -0
  38. codext/ciphers/vernam.py +39 -0
  39. codext/ciphers/zara128.py +33 -0
  40. codext/compressions/__init__.py +2 -0
  41. codext/compressions/huffman.py +137 -0
  42. codext/compressions/zstd.py +60 -0
  43. codext/esoteric/__init__.py +10 -0
  44. codext/esoteric/_brainfuck.py +89 -0
  45. codext/esoteric/befunge.py +249 -0
  46. codext/esoteric/brainfuck.py +67 -0
  47. codext/esoteric/church.py +228 -0
  48. codext/esoteric/cow.py +160 -0
  49. codext/esoteric/lolcode.py +709 -0
  50. codext/esoteric/malbolge.py +198 -0
  51. codext/esoteric/ook.py +103 -0
  52. codext/esoteric/piet.py +430 -0
  53. codext/esoteric/pikalang.py +66 -0
  54. codext/frequency/__init__.py +4 -0
  55. codext/frequency/_ita2.py +40 -0
  56. codext/frequency/coquelet.py +98 -0
  57. codext/frequency/dtmf.py +182 -0
  58. codext/frequency/piccolo.py +106 -0
  59. codext/guess.py +428 -0
  60. codext/hashing/__init__.py +21 -0
  61. codext/hashing/_descrypt.py +50 -0
  62. codext/hashing/_h64.py +52 -0
  63. codext/hashing/argon2.py +42 -0
  64. codext/hashing/bcrypt.py +107 -0
  65. codext/hashing/cisco.py +41 -0
  66. codext/hashing/cryptvariants.py +136 -0
  67. codext/hashing/ctph.py +92 -0
  68. codext/hashing/fnv.py +59 -0
  69. codext/hashing/gost.py +137 -0
  70. codext/hashing/hmac.py +31 -0
  71. codext/hashing/keccak.py +87 -0
  72. codext/hashing/ldap.py +32 -0
  73. codext/hashing/md.py +93 -5
  74. codext/hashing/mscash.py +40 -0
  75. codext/hashing/mssql.py +35 -0
  76. codext/hashing/mysql.py +35 -0
  77. codext/hashing/oracle.py +49 -0
  78. codext/hashing/phpass.py +38 -0
  79. codext/hashing/postgres.py +25 -0
  80. codext/hashing/ripemd.py +73 -0
  81. codext/hashing/scrypt.py +44 -0
  82. codext/hashing/sha.py +32 -5
  83. codext/hashing/snefru.py +268 -0
  84. codext/hashing/whirlpool.py +87 -0
  85. codext/hashing/windows.py +101 -0
  86. codext/languages/__init__.py +2 -0
  87. codext/languages/fancy.py +119 -0
  88. codext/languages/galactic.py +90 -14
  89. codext/languages/tap.py +103 -45
  90. codext/languages/upsidedown.py +58 -0
  91. codext/obfuscation/__init__.py +17 -0
  92. codext/obfuscation/_jsbase64.py +79 -0
  93. codext/obfuscation/_jsuri.py +55 -0
  94. codext/obfuscation/arabica2rs.py +77 -0
  95. codext/obfuscation/atom128.py +30 -0
  96. codext/obfuscation/chinzo72c.py +78 -0
  97. codext/obfuscation/cisco.py +65 -0
  98. codext/obfuscation/esab46.py +30 -0
  99. codext/obfuscation/feron74.py +30 -0
  100. codext/obfuscation/gila7.py +30 -0
  101. codext/obfuscation/hazz15.py +31 -0
  102. codext/obfuscation/hindia4x.py +86 -0
  103. codext/obfuscation/japooc2s.py +83 -0
  104. codext/obfuscation/korex3s.py +70 -0
  105. codext/obfuscation/megan35.py +31 -0
  106. codext/obfuscation/tigo3fx.py +31 -0
  107. codext/obfuscation/tripo5.py +31 -0
  108. codext/obfuscation/zong22.py +31 -0
  109. codext/others/__init__.py +6 -0
  110. codext/others/asn1.py +482 -0
  111. codext/others/dvorak.py +45 -0
  112. codext/others/letters.py +3 -1
  113. codext/others/ns3.py +93 -0
  114. codext/others/soundex.py +482 -0
  115. codext/others/telegraph.py +278 -0
  116. codext/others/xy.py +30 -0
  117. codext/stegano/sms.py +2 -1
  118. codext/stegano/whitespace.py +30 -9
  119. codext/strength.py +164 -0
  120. codext/web/__init__.py +3 -0
  121. codext/web/_javascript.py +59 -0
  122. codext/web/aaencode.py +103 -0
  123. codext/web/jjencode.py +135 -0
  124. codext/web/jsfuck.py +635 -0
  125. {codext-2.0.0.dist-info → codext-2.0.1.dist-info}/METADATA +143 -32
  126. codext-2.0.1.dist-info/RECORD +214 -0
  127. {codext-2.0.0.dist-info → codext-2.0.1.dist-info}/entry_points.txt +4 -0
  128. codext-2.0.0.dist-info/RECORD +0 -116
  129. codext/{crypto → ciphers}/adfgx.py +0 -0
  130. codext/{crypto → ciphers}/affine.py +0 -0
  131. codext/{crypto → ciphers}/atbash.py +0 -0
  132. codext/{crypto → ciphers}/bacon.py +0 -0
  133. codext/{crypto → ciphers}/barbie.py +0 -0
  134. codext/{crypto → ciphers}/bifid.py +0 -0
  135. codext/{crypto → ciphers}/chaocipher.py +0 -0
  136. codext/{crypto → ciphers}/columnar.py +0 -0
  137. codext/{crypto → ciphers}/hill.py +0 -0
  138. codext/{crypto → ciphers}/nihilist.py +0 -0
  139. codext/{crypto → ciphers}/phillips.py +0 -0
  140. codext/{crypto → ciphers}/polybius.py +0 -0
  141. codext/{crypto → ciphers}/railfence.py +0 -0
  142. codext/{crypto → ciphers}/rot.py +0 -0
  143. codext/{crypto → ciphers}/scytale.py +0 -0
  144. codext/{crypto → ciphers}/shift.py +0 -0
  145. codext/{crypto → ciphers}/skip.py +0 -0
  146. codext/{crypto → ciphers}/vic.py +0 -0
  147. codext/{crypto → ciphers}/vigenere.py +14 -14
  148. /codext/{crypto → ciphers}/xor.py +0 -0
  149. /codext/{crypto → obfuscation}/citrix.py +0 -0
  150. {codext-2.0.0.dist-info → codext-2.0.1.dist-info}/WHEEL +0 -0
  151. {codext-2.0.0.dist-info → codext-2.0.1.dist-info}/licenses/LICENSE +0 -0
  152. {codext-2.0.0.dist-info → codext-2.0.1.dist-info}/top_level.txt +0 -0
codext/VERSION.txt CHANGED
@@ -1 +1 @@
1
- 2.0.0
1
+ 2.0.1
codext/__common__.py CHANGED
@@ -8,6 +8,8 @@ import os
8
8
  import random
9
9
  import re
10
10
  import sys
11
+ import unicodedata
12
+ import warnings
11
13
  from encodings.aliases import aliases as ALIASES
12
14
  from functools import reduce, update_wrapper, wraps
13
15
  from importlib import import_module
@@ -33,16 +35,27 @@ re.sre_parse = sre_parse
33
35
  maketrans = str.maketrans
34
36
 
35
37
 
36
- __all__ = ["add", "add_macro", "add_map", "b", "clear", "codecs", "decode", "encode", "ensure_str", "examples", "guess",
38
+ __all__ = ["add", "add_macro", "add_map", "b", "clear", "codecs", "decode", "encode", "ensure_str", "examples",
37
39
  "isb", "generate_strings_from_regex", "get_alphabet_from_mask", "handle_error", "hashlib", "i2s",
38
- "is_native", "list_categories", "list_encodings", "list_macros", "lookup", "maketrans", "os", "rank", "re",
39
- "register", "remove", "reset", "s2i", "search", "stopfunc", "BytesIO", "_input", "_stripl", "CodecMacro",
40
+ "is_native", "list_categories", "list_encodings", "list_macros", "lookup", "maketrans", "os", "re",
41
+ "register", "remove", "reset", "s2i", "search", "BytesIO", "_input", "_stripl", "CodecMacro",
40
42
  "DARWIN", "LANG", "LINUX", "MASKS", "UNIX", "WINDOWS"]
41
43
  CODECS_REGISTRY = None
42
44
  CODECS_OVERWRITTEN = []
43
45
  CODECS_CATEGORIES = ["native", "custom"]
46
+ # categories that were renamed or split, with the current categories of their codecs (still accepted, with a warning)
47
+ DEPRECATED_CATEGORIES = {'crypto': ["cipher", "obfuscation"]}
44
48
  CODECS_CACHE = {}
45
49
  PRINTABLES = frozenset(printable)
50
+ # scoring of the decoded outputs: textness above which an output is considered as a text, and weight of its bonus
51
+ TEXTNESS_THRESHOLD, TEXTNESS_WEIGHT = .5, 4
52
+ # most common bigrams of English texts (a large part being common to other Latin-script languages)
53
+ BIGRAMS = frozenset("th he in er an re on at en nd ti es or te of ed is it al ar st to nt ng se ha as ou io le ve co "
54
+ "me de hi ri ro ic ne ea ra ce li ch ll be ma si om ur ca el ta la ns di fo ho pe ec pr no ct us "
55
+ "ac ot il tr ly nc et ut ss so rs un lo wa ge ie wh ee wi em ad ol rt po we na ul ni ts mo ow pa "
56
+ "im mi ai sh".split())
57
+ # scripts of the letters (cache) and scripts whose words are not separated by spaces
58
+ SCRIPTS, SCRIPTS_WITHOUT_SPACES = {}, {"CJK", "HIRAGANA", "KATAKANA", "THAI", "LAO", "KHMER", "MYANMAR", "TIBETAN"}
46
59
  LANG = getlocale()
47
60
  if LANG:
48
61
  LANG = (LANG[0] or "")[:2].lower()
@@ -80,6 +93,61 @@ def entropy(s):
80
93
  return -sum([p * log(p, 2) for p in [n[c] / l for c in set(s)]])
81
94
 
82
95
 
96
+ def _script(c):
97
+ """ Get the script of a letter (e.g. "LATIN", "CYRILLIC" or "CJK"), from its Unicode name. """
98
+ try:
99
+ return SCRIPTS[c]
100
+ except KeyError:
101
+ SCRIPTS[c] = r = unicodedata.name(c, "?").split()[0]
102
+ return r
103
+
104
+
105
+ def _textness(s, any_script=True):
106
+ """ Rate in [0, 1] how much the given string looks like a natural language text.
107
+
108
+ For a text mostly made of Latin letters, this is mainly the rate of common bigrams among the pairs of successive
109
+ letters (around 70% for English texts, against around 15% for random letters), weighted by the rates of printable
110
+ characters and of letters and spaces, and by the number of distinct letters (a text uses many letters, unlike e.g.
111
+ digits shifted to letters) ; this distinguishes a text from a substitution of its letters (e.g. ROT13).
112
+ For a text in another script (e.g. Cyrillic, Greek or CJK), this only relies on language-independent features: the
113
+ rate of letters of the dominant script among the letters, the rate of letters, spaces and punctuation, the number
114
+ of distinct letters and the length of the words (for the scripts that separate words with spaces) ; as these
115
+ features cannot distinguish a text from a substitution of its characters, this can be disabled
116
+ (any_script=False), then rating 0. """
117
+ # a decoded output may hold the UTF-8 bytes of a text as Latin-1 characters
118
+ s = ensure_str(b(s))
119
+ if (n := len(s)) < 2:
120
+ return 0.
121
+ letters = [c for c in s if c.isalpha()]
122
+ if len(letters) == 0:
123
+ return 0.
124
+ scripts = Counter(map(_script, letters))
125
+ script, count = scripts.most_common(1)[0]
126
+ if script != "LATIN":
127
+ if not any_script:
128
+ return 0.
129
+ printables = sum(c.isprintable() or c.isspace() for c in s) / n
130
+ rate = sum(c.isalpha() or c.isspace() or unicodedata.category(c)[0] == "P" for c in s) / n
131
+ distinct = min(1., len(set(letters)) / min(10., len(letters) / 2))
132
+ r = printables * (count / len(letters)) ** 2 * rate * distinct
133
+ if script not in SCRIPTS_WITHOUT_SPACES:
134
+ # the mean length of the words is around 5 letters
135
+ words = s.split()
136
+ r *= 1. if 2 <= len(letters) / len(words) <= 12 else .3
137
+ return r
138
+ t = s.lower()
139
+ pairs = [t[i:i+2] for i in range(n - 1) if t[i].isalpha() and t[i+1].isalpha()]
140
+ if len(pairs) == 0:
141
+ return 0.
142
+ bigrams = min(1., sum(map(BIGRAMS.__contains__, pairs)) / len(pairs) / .7)
143
+ printables, rate = sum(map(PRINTABLES.__contains__, s)) / n, sum(c.isalpha() or c == " " for c in s) / n
144
+ # a text holds at least 10 distinct letters (or half of its letters if shorter), unlike e.g. digits shifted to
145
+ # letters
146
+ distinct = set(c for c in t if c.isalpha())
147
+ expected = min(10., len(letters) / 2)
148
+ return printables * bigrams * (.5 + .5 * rate) * min(1., len(distinct) / expected)
149
+
150
+
83
151
  isb = lambda s: isinstance(s, bytes)
84
152
  iss = lambda s: isinstance(s, str)
85
153
  fix = lambda x, ref: b(x) if isb(ref) else ensure_str(x) if iss(ref) else x
@@ -691,10 +759,25 @@ def list_categories():
691
759
  list_categories()
692
760
 
693
761
 
762
+ def _categories(names):
763
+ """ Replace the deprecated categories (possibly negated with "~") by the current categories of their codecs. """
764
+ r = []
765
+ for name in names:
766
+ neg = "~" if isinstance(name, str) and name.startswith("~") else ""
767
+ new = DEPRECATED_CATEGORIES.get(name[len(neg):]) if isinstance(name, str) else None
768
+ if new is None:
769
+ r.append(name)
770
+ continue
771
+ warnings.warn(f"Category '{name[len(neg):]}' is deprecated, use {' and '.join(map(repr, new))} instead",
772
+ DeprecationWarning, stacklevel=3)
773
+ r.extend(neg + c for c in new if neg + c not in r)
774
+ return r
775
+
776
+
694
777
  def list_encodings(*categories):
695
778
  """ Get a list of all codecs. """
696
779
  # if "non-native" is in the input list, extend the list with the whole categories but "native"
697
- categories, exclude = list(categories), []
780
+ categories, exclude = _categories(categories), []
698
781
  for c in categories[:]:
699
782
  if c == "non-native":
700
783
  for c in CODECS_CATEGORIES:
@@ -1208,398 +1291,3 @@ def generate_strings_from_regex(regex, star_plus_max=STAR_PLUS_MAX, repeat_max=R
1208
1291
  """ Utility function to generate strings from a regex pattern. """
1209
1292
  for r in __gen_str_from_re(regex, star_plus_max, repeat_max, yield_max):
1210
1293
  yield r
1211
-
1212
-
1213
- # guess feature objects
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
-
1222
- stopfunc = ModuleType("stopfunc", """
1223
- Predefined stop functions
1224
- ~~~~~~~~~~~~~~~~~~~~~~~~~
1225
-
1226
- This submodule contains stop functions for the guess feature of codext.
1227
-
1228
- - `flag`: searches for the pattern "[Ff][Ll1][Aa4@][Gg9]" (either UTF-8 or UTF-16)
1229
- - `lang_**`: checks if the given lang (any from the PROFILES_DIRECTORY of the langdetect module) is detected
1230
- - `printables`: checks that every output character is in the set of printables
1231
- - `regex`: takes one argument, the regular expression, for checking a string against the given pattern
1232
- - `text`: checks for printables and an entropy less than 4.6 (empirically determined)
1233
- """)
1234
- stopfunc.printables = lambda s: PRINTABLES.issuperset(ensure_str(s))
1235
- stopfunc.printables.__name__ = stopfunc.printables.__qualname__ = "printables"
1236
- stopfunc.regex = lambda p: lambda s: re.search(p, ensure_str(s)) is not None
1237
- stopfunc.regex.__name__ = stopfunc.regex.__qualname__ = "regex"
1238
- stopfunc.text = lambda s: stopfunc.printables(s) and entropy(s) < 4.6
1239
- stopfunc.text.__name__ = stopfunc.text.__qualname__ = "text"
1240
- stopfunc.flag = lambda x: re.search(r"[Ff][Ll1][Aa4@][Gg96]", ensure_str(x)) is not None
1241
- stopfunc.flag.__name__ = stopfunc.flag.__qualname__ = "flag"
1242
- stopfunc.default = stopfunc.text
1243
-
1244
- stopfunc.LANG_BACKEND = None
1245
- stopfunc.LANG_BACKENDS = [n for n in ["pycld2", "langdetect", "langid", "cld3", "textblob"] if __module_exists(n)]
1246
- if len(stopfunc.LANG_BACKENDS) > 0:
1247
- stopfunc.LANG_BACKEND = stopfunc.LANG_BACKENDS[0]
1248
- if "cld3" in stopfunc.LANG_BACKENDS:
1249
- stopfunc.CLD3_LANGUAGES = "af|am|ar|bg|bn|bs|ca|ce|co|cs|cy|da|de|el|en|eo|es|et|eu|fa|fi|fr|fy|ga|gd|gl|gu|ha|" \
1250
- "hi|hm|hr|ht|hu|hy|id|ig|is|it|iw|ja|jv|ka|kk|km|kn|ko|ku|ky|la|lb|lo|lt|lv|mg|mi|mk|" \
1251
- "ml|mn|mr|ms|mt|my|ne|nl|no|ny|pa|pl|ps|pt|ro|ru|sd|si|sk|sl|sm|sn|so|sq|sr|st|su|sv|" \
1252
- "sw|ta|te|tg|th|tr|uk|ur|uz|vi|xh|yi|yo|zh|zu".split("|")
1253
- if "textblob" in stopfunc.LANG_BACKENDS:
1254
- stopfunc.TEXTBLOB_LANGUAGES = "af|ar|az|be|bg|bn|ca|cs|cy|da|de|el|en|eo|es|et|eu|fa|fi|fr|ga|gl|gu|hi|hr|ht|hu|" \
1255
- "id|is|it|iw|ja|ka|kn|ko|la|lt|lv|mk|ms|mt|nl|no|pl|pt|ro|ru|sk|sl|sq|sr|sv|sw|ta|" \
1256
- "te|th|tl|tr|uk|ur|vi|yi|zh".split("|")
1257
-
1258
-
1259
- def _detect(text):
1260
- _lb, t = stopfunc.LANG_BACKEND, ensure_str(text)
1261
- if _lb is None:
1262
- raise ValueError("No language backend %s" % ["selected", "installed"][len(stopfunc.LANG_BACKENDS) == 0])
1263
- return langid.classify(t)[0] if _lb == "langid" else \
1264
- langdetect.detect(t) if _lb == "langdetect" else \
1265
- pycld2.detect(t)[2][0][1] if _lb == "pycld2" else \
1266
- cld3.get_language(t).language[:2] if _lb == "cld3" else \
1267
- textblob.TextBlob(t).detect_language()[:2]
1268
-
1269
-
1270
- def _lang(lang):
1271
- def _test(s):
1272
- if not stopfunc.text(s):
1273
- return False
1274
- try:
1275
- return _detect(ensure_str(s))[:2] == lang
1276
- except:
1277
- return False
1278
- return _test
1279
-
1280
-
1281
- def _load_lang_backend(backend=None):
1282
- # import the requested backend library if not imported yet
1283
- if backend is None or backend in stopfunc.LANG_BACKENDS:
1284
- stopfunc.LANG_BACKEND = backend
1285
- if backend:
1286
- globals()[backend] = __import__(backend)
1287
- else:
1288
- raise ValueError("Unsupported language detection backend")
1289
- # remove language-related stop functions
1290
- for attr in dir(stopfunc):
1291
- if attr.startswith("_") or not isinstance(getattr(stopfunc, attr), FunctionType):
1292
- continue
1293
- if re.match(r"lang_[a-z]{2}$", attr):
1294
- delattr(stopfunc, attr)
1295
- # rebind applicable language-related stop functions
1296
- if stopfunc.LANG_BACKEND:
1297
- _lb = stopfunc.LANG_BACKEND
1298
- if _lb == "langid":
1299
- langid.langid.load_model()
1300
- for lang in (
1301
- langid.langid.identifier.nb_classes if _lb == "langid" else \
1302
- list(set(p[:2] for p in os.listdir(langdetect.PROFILES_DIRECTORY))) if _lb == "langdetect" else \
1303
- list(set(x[1][:2] for x in pycld2.LANGUAGES if x[0] in pycld2.DETECTED_LANGUAGES)) if _lb == "pycld2" else \
1304
- stopfunc.CLD3_LANGUAGES if _lb == "cld3" else \
1305
- stopfunc.TEXTBLOB_LANGUAGES if _lb == "textblob" else \
1306
- []):
1307
- n = f"lang_{lang}"
1308
- setattr(stopfunc, n, _lang(lang))
1309
- getattr(stopfunc, n).__name__ = getattr(stopfunc, n).__qualname__ = n
1310
- if LANG:
1311
- flng = f"lang_{LANG}"
1312
- if getattr(stopfunc, flng, None):
1313
- stopfunc.default = getattr(stopfunc, flng)
1314
- stopfunc._reload_lang = _load_lang_backend
1315
-
1316
-
1317
- def _validate(stop_function, lang_backend="none"):
1318
- s, lb = stop_function, lang_backend
1319
- if isinstance(s, str):
1320
- if re.match(r"lang_[a-z]{2}$", s) and lb != "none" and \
1321
- all(re.match(r"lang_[a-z]{2}$", x) is None for x in dir(stopfunc)):
1322
- stopfunc._reload_lang(lb)
1323
- f = getattr(stopfunc, s, None)
1324
- if f:
1325
- return f
1326
- elif not isinstance(s, FunctionType):
1327
- raise ValueError("Bad stop function")
1328
- return s
1329
- stopfunc._validate = _validate
1330
-
1331
-
1332
- def __guess(prev_input, input, stop_func, depth, max_depth, min_depth, encodings, result, found=(),
1333
- stop=True, show=False, scoring_heuristic=False, extended=False, debug=False):
1334
- """ Perform a breadth-first tree search using a ranking logic to select and prune the list of codecs. """
1335
- if depth > min_depth and stop_func(input):
1336
- if not stop and (show or debug) and found not in result:
1337
- s = repr(input)
1338
- s = s[2:-1] if s.startswith("b'") and s.endswith("'") else s
1339
- s = f"[+] {', '.join(found)}: {s}"
1340
- print(s if len(s) <= 80 else f"{s[:77]}...")
1341
- result[found] = input
1342
- if depth >= max_depth or len(result) > 0 and stop:
1343
- return
1344
- prev_enc = found[-1] if len(found) > 0 else ""
1345
- e = encodings.get(depth, encodings.get(-1, []))
1346
- for new_input, encoding in __rank(prev_input, input, prev_enc, e, scoring_heuristic, extended):
1347
- if len(result) > 0 and stop:
1348
- return
1349
- if debug:
1350
- print(f"[*] Depth {depth+1:0{len(str(max_depth))}}/{max_depth}: {encoding}")
1351
- __guess(input, new_input, stop_func, depth+1, max_depth, min_depth, encodings, result, found + (encoding, ),
1352
- stop, show, scoring_heuristic, extended, debug)
1353
-
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
-
1369
- def __make_encodings_dict(include, exclude):
1370
- """ Process encodings inclusion and exclusion lists, listing categories and developping codecs' lists of possible
1371
- encoding names. It also creates a cache with the CodecInfo objects for improving performance. """
1372
- def _develop(d, keep=True):
1373
- d = d or {}
1374
- for k, v in d.items():
1375
- l, cc, sc = [], [e for e in v if e in CODECS_CATEGORIES], [e for e in v if e not in CODECS_CATEGORIES]
1376
- # list from in-scope categories and then everything that is not a category
1377
- for enc in ((list_encodings(*cc) if (len(cc) > 0 or keep) and len(sc) == 0 else []) + sc):
1378
- g = []
1379
- for e in (search(enc, False) or [enc]):
1380
- g.extend(__guess_list(e))
1381
- if enc in g: # e.g. "rot-1" => ["rot-1", "rot-2", ...] ; only "rot-1" is to be selected
1382
- l.append(enc)
1383
- else: # e.g. "rot" => ["rot-1", "rot-2", ...] ; all the "rot-N" shall be selected
1384
- l.extend(g)
1385
- d[k] = list(set(l))
1386
- return d
1387
- _excl, _incl = _develop(exclude, False), _develop(include)
1388
- return {k: [x for x in v if x not in _excl.get(k, [])] for k, v in _incl.items()}
1389
-
1390
-
1391
- def __rank(prev_input, input, prev_encoding, encodings, heuristic=False, extended=False, yield_score=False):
1392
- """ Filter valid encodings and rank them by relevance. """
1393
- # characteristics of the input text, computed once per padding character (see __score)
1394
- ranking, texts = {}, {}
1395
- for e in encodings:
1396
- try:
1397
- codec = CODECS_CACHE[e]
1398
- except KeyError:
1399
- try:
1400
- CODECS_CACHE[e] = codec = lookup(e, False)
1401
- except LookupError:
1402
- continue
1403
- t = __score(prev_input, input, prev_encoding, e, codec, heuristic, extended, texts)
1404
- if t:
1405
- ranking[e] = t
1406
- for encoding, result in sorted(ranking.items(), key=lambda x: (-x[1][0], x[0])):
1407
- yield result if yield_score else result[1], encoding
1408
-
1409
-
1410
- class _Text(object):
1411
- __slots__ = ["entropy", "lcharset", "len", "padding", "printables", "text"]
1412
-
1413
- def __init__(self, text, pad_char=None):
1414
- self.text = ensure_str(text)
1415
- c = self.text[-1]
1416
- pad_char, last_char = (chr(pad_char), chr(c)) if isinstance(c, int) else (pad_char, c)
1417
- self.padding = pad_char is not None and last_char == pad_char
1418
- if self.padding:
1419
- text = text.rstrip(b(pad_char) if isinstance(text, bytes) else pad_char)
1420
- self.len = len(self.text)
1421
- self.lcharset = len(set(self.text))
1422
- self.printables = float(sum(map(PRINTABLES.__contains__, self.text))) / self.len
1423
- self.entropy = entropy(self.text)
1424
-
1425
-
1426
- def __score(prev_input, input, prev_encoding, encoding, codec, heuristic=False, extended=False, texts=None):
1427
- """ Score relevant encodings given an input. """
1428
- sc = codec.parameters.get('scoring', {})
1429
- no_error, transitive = codec.parameters.get('no_error', False), sc.get('transitive', False)
1430
- # ignore encodings that fail to decode with their default errors handling value
1431
- try:
1432
- new_input = codec.decode(input)[0]
1433
- except:
1434
- return
1435
- # ignore encodings that give an output identical to the input (identity transformation) or to the previous input
1436
- if len(new_input) == 0 or prev_input is not None and b(input) == b(new_input) or b(prev_input) == b(new_input):
1437
- return
1438
- # ignore encodings that transitively give the same output (identity transformation by chaining twice a same
1439
- # codec (e.g. rot-15 is equivalent to rot-3 and rot-12 or rot-6 and rot-9)
1440
- if transitive and prev_encoding:
1441
- ci_prev = lookup(prev_encoding, False)
1442
- if ci_prev.parameters['name'] == codec.parameters['name']:
1443
- return
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)
1450
- if heuristic:
1451
- # from here, the goal (e.g. if the input is Base32) is to rank candidate encodings (e.g. multiple base
1452
- # codecs) so that we can put the right one as early as possible and eventually exclude bad candidates
1453
- s = -sc.get('penalty', .0)
1454
- # first, apply a bonus if the length of input text's charset is exactly the same as encoding's charset ;
1455
- # on the contrary, if the length of input text's charset is strictly greater, give a penalty
1456
- lcs = sc.get('len_charset', 256)
1457
- if isinstance(lcs, type(lambda: None)):
1458
- lcs = int(lcs(encoding))
1459
- if (pad and obj.padding and lcs + 1 >= obj.lcharset) or lcs >= obj.lcharset:
1460
- s += max(.0, round(.6 * (.99 ** (lcs - obj.lcharset)), 5) - .1)
1461
- elif (pad and obj.padding and lcs + 1 < obj.lcharset) or lcs < obj.lcharset:
1462
- s -= .2 # this can occur for encodings with no_error set to True
1463
- # then, take padding into account, giving a bonus if padding is to be encountered and effectively present,
1464
- # or a penalty when it should not be encountered but it is present
1465
- if pad and obj.padding:
1466
- s += .2 # when padding is encountered while it is legitimate, it could be a good indication => bonus
1467
- elif not pad and obj.padding:
1468
- s -= .1 # it could arise a padding character is encountered while not being padding => small penalty
1469
- # give a bonus when the rate of printable characters is greater or equal than expected and a penalty when
1470
- # lower only for codecs that DO NOT tolerate errors (otherwise, the printables rate can be biased)
1471
- if not no_error:
1472
- pr = sc.get('printables_rate', 0)
1473
- if isinstance(pr, type(lambda: None)):
1474
- pr = float(pr(obj.printables))
1475
- if obj.printables - pr <= .05:
1476
- s += .1
1477
- expf = sc.get('expansion_factor', 1.)
1478
- if expf:
1479
- f = obj.len / float(len(new_input)) # expansion while encoding => at decoding: 1/f
1480
- if isinstance(expf, type(lambda: None)):
1481
- try: # this case allows to consider the current encoding name from the current codec
1482
- expf = expf(f, encoding)
1483
- except TypeError:
1484
- expf = expf(f)
1485
- if isinstance(expf, (int, float)):
1486
- expf = 1/f - .1 <= 1/expf <= 1/f + .1
1487
- elif isinstance(expf, (tuple, list)) and len(expf) == 2:
1488
- expf = 1/f - expf[1] <= 1/expf[0] <= 1/f + expf[1]
1489
- s += [-1., .1][expf]
1490
- # afterwards, if the input text has an entropy close to the expected one, give a bonus weighted on the
1491
- # number of input characters to take bad entropies of shorter strings into account
1492
- entr = sc.get('entropy', lambda e: e)
1493
- entr = entr.get(encoding, entr.get('default')) if isinstance(entr, dict) else entr
1494
- if isinstance(entr, type(lambda: None)):
1495
- try: # this case allows to consider the current encoding name from the current codec
1496
- entr = entr(obj.entropy, encoding)
1497
- except TypeError:
1498
- entr = entr(obj.entropy)
1499
- if entr is not None:
1500
- # use a quadratic heuristic to compute a weight for the entropy delta, aligned on (256,.2) and (512,1)
1501
- d_entr = min(3.04575e-06 * obj.len**2 + .000394 * obj.len, 1) * abs(entr - obj.entropy)
1502
- if d_entr <= .5:
1503
- s += .5 - d_entr
1504
- # finally, if relevant, apply a custom bonus (e.g. when a regex pattern is matched)
1505
- bonus = sc.get('bonus_func')
1506
- if bonus is not None:
1507
- if isinstance(bonus, type(lambda: None)):
1508
- bonus = bonus(obj, codec, encoding)
1509
- if bonus:
1510
- s += .2
1511
- else:
1512
- s = 1.
1513
- # exclude negative (and eventually null) scores as they are (hopefully) not relevant
1514
- if extended and s >= .0 or not extended and s > .0:
1515
- return s, new_input
1516
-
1517
-
1518
- def guess(input, stop_func=stopfunc.default, min_depth=0, max_depth=5, include=None, exclude=None, found=(),
1519
- stop=True, show=False, scoring_heuristic=True, extended=False, debug=False):
1520
- """ Try decoding without the knowledge of the encoding(s).
1521
-
1522
- :param input: input text to be guessed
1523
- :param stop_func: function defining the stop condition
1524
- :param min_depth: minimum search depth
1525
- :param max_depth: maximum search depth
1526
- ;param include: inclusion item OR list with category, codec or encoding names OR dictionary with lists per
1527
- depth (nothing means include every encoding)
1528
- :param exclude: exclusion item OR list with category, codec or encoding names OR dictionary with lists per
1529
- depth (nothing means exclude no encoding)
1530
- :param found: tuple of already found encodings
1531
- :param stop: whether to stop or not when a valid solution is found
1532
- :param show: whether to immediately show once a solution is found
1533
- :param scoring_heuristic: whether to apply the scoring heuristic during the search (if disabled, all scores are 1.,
1534
- meaning that every non-failing encoding will be considered with no order of precedence)
1535
- :param extended: whether to also consider null scores with the heuristic
1536
- :param debug: whether to show each attempt at each depth during computation
1537
- """
1538
- if len(input) == 0:
1539
- return ""
1540
- # check for min and max depths
1541
- if max_depth <= 0:
1542
- raise ValueError("Depth must be a non-null positive integer")
1543
- if min_depth > max_depth:
1544
- raise ValueError("Min depth shall be less than or equal to the max depth")
1545
- # take the tuple of found encodings into account
1546
- if len(found) > 0:
1547
- for encoding in found:
1548
- input = decode(input, encoding)
1549
- # handle the stop function as a regex if a string was given
1550
- if isinstance(stop_func, str):
1551
- stop_func = stopfunc.regex(stop_func)
1552
- # reformat include and exclude arguments ; supported formats:
1553
- for n, l in zip(["inc", "exc"], [include, exclude]):
1554
- if l is None:
1555
- if n == "inc":
1556
- include = l = {-1: CODECS_CATEGORIES}
1557
- else:
1558
- exclude = l = {}
1559
- # "category" OR "enc_name" OR whatever => means a single item for all depths
1560
- if isinstance(l, str):
1561
- if n == "inc":
1562
- include = l = {-1: [l]}
1563
- else:
1564
- exclude = l = {-1: [l]}
1565
- # ["enc_name1", "enc_name2", ...] => means for all depths
1566
- if isinstance(l, (list, tuple)):
1567
- if n == "inc":
1568
- include = l = {-1: l}
1569
- else:
1570
- exclude = l = {-1: l}
1571
- # {-1: [...], 2: [...], ...} => means prefedined depths with their lists of in-/excluded encodings
1572
- if not isinstance(l, dict) or not all(isinstance(k, int) for k in l.keys()):
1573
- raise ValueError("Include argument shall be a list or a dictionary with integer keys")
1574
- # precompute encodings lists per depth and cache the related CodecInfo objects
1575
- encodings, result = __make_encodings_dict(include, exclude), {}
1576
- try:
1577
- # breadth-first search
1578
- for d in range(max_depth):
1579
- __guess("", input, stop_func, 0, d+1, min_depth, encodings, result, tuple(found), stop, show,
1580
- scoring_heuristic, extended, debug)
1581
- if stop and len(result) > 0:
1582
- break
1583
- except KeyboardInterrupt:
1584
- pass
1585
- CODECS_CACHE.clear()
1586
- return result
1587
- codecs.guess = guess
1588
-
1589
-
1590
- def rank(input, extended=False, limit=-1, include=None, exclude=None):
1591
- """ Rank the most probable encodings based on the given input.
1592
-
1593
- :param input: input text to be evaluated
1594
- :param extended: whether to consider null scores too (NB: negative scores are not output !)
1595
- :param limit: number of encodings to be returned (-1 means all of them)
1596
- :param include: inclusion list with category, codec or encoding names (nothing means include every encoding)
1597
- :param exclude: exclusion list with category, codec or encoding names (nothing means exclude no encoding)
1598
- """
1599
- encodings = __make_encodings_dict(include if isinstance(include, dict) else {-1: include or CODECS_CATEGORIES},
1600
- exclude if isinstance(exclude, dict) else {-1: exclude or []})
1601
- r = list(__rank(None, input, "", encodings[-1], True, extended, True))
1602
- CODECS_CACHE.clear()
1603
- return r[:limit] if len(r) > 1 else r
1604
- codecs.rank = rank
1605
-
codext/__init__.py CHANGED
@@ -3,6 +3,8 @@
3
3
 
4
4
  """
5
5
  from .__common__ import *
6
+ from .guess import * # the guess/rank/stopfunc feature (wires codecs.guess and codecs.rank on import)
7
+ from .strength import * # the strength feature (wires codecs.strength on import)
6
8
  from .__info__ import __author__, __copyright__, __email__, __license__, __source__, __version__
7
9
 
8
10
 
codext/__main__.py CHANGED
@@ -3,8 +3,10 @@
3
3
 
4
4
  """
5
5
  import argparse
6
+ import sys
6
7
 
7
8
  from .__common__ import *
9
+ from .guess import stopfunc
8
10
  from .__info__ import __author__, __copyright__, __email__, __license__, __source__, __version__
9
11
 
10
12
 
@@ -27,6 +29,15 @@ def __format_list(items, include=True):
27
29
  return d
28
30
 
29
31
 
32
+ def __is_binary(data):
33
+ """ Bytes are binary data if they are neither valid UTF-8 nor latin-1 text (as returned by text codecs). """
34
+ try:
35
+ data.decode("utf-8")
36
+ return False
37
+ except UnicodeDecodeError:
38
+ return any(b < 32 and b not in b"\t\n\r" or 127 <= b < 160 for b in data)
39
+
40
+
30
41
  def __print_tabular(lst, space=4):
31
42
  try:
32
43
  cols, _ = os.get_terminal_size()
@@ -141,6 +152,14 @@ def main():
141
152
  help="categories, codecs and encodings to be explicitely used ;\n "
142
153
  "format: [category|codec|encoding] OR depth:[category|codec|encoding]")
143
154
  rank.add_argument("-l", "--limit", type=int, default=10, help="limit the number of displayed results")
155
+ strength = sparsers.add_parser("strength", help="evaluate the strength of a chain of codecs", **kw)
156
+ strength.add_argument("encoding", nargs="+", help="list of encodings forming the chain to evaluate")
157
+ strength.add_argument("-e", "--exclude", nargs="*", action="extend", metavar="CAT|COD|ENC",
158
+ help="categories, codecs and encodings the guessing adversary may not use")
159
+ strength.add_argument("-f", "--stop-function", default="text", metavar="FUNC",
160
+ help="stop function for the guessing adversary (default: text)")
161
+ strength.add_argument("-i", "--include", nargs="*", action="extend", metavar="CAT|COD|ENC",
162
+ help="categories, codecs and encodings the guessing adversary may use")
144
163
  search = sparsers.add_parser("search", help="search for codecs")
145
164
  search.add_argument("pattern", nargs="+", help="encoding pattern to search")
146
165
  listi = sparsers.add_parser("list", help="list items")
@@ -154,7 +173,7 @@ def main():
154
173
  remm = sparsers.add_parser("remove-macro", help="remove a macro from the registry")
155
174
  remm.add_argument("name", help="macro's name")
156
175
  args = parser.parse_args()
157
- if args.command in ["guess", "rank"]:
176
+ if args.command in ["guess", "rank", "strength"]:
158
177
  args.include, args.exclude = __format_list(args.include), __format_list(args.exclude, False)
159
178
  try:
160
179
  # if a search pattern is given, only handle it
@@ -171,6 +190,19 @@ def main():
171
190
  elif args.command == "remove-macro":
172
191
  remove_macro(args.name)
173
192
  return 0
193
+ # evaluate the strength of a chain of codecs (sample from a file, from a pipe, or a default one)
194
+ elif args.command == "strength":
195
+ sample = None
196
+ if args.infile:
197
+ sample = _input(args.infile)
198
+ elif not sys.stdin.isatty():
199
+ sample = _input(None)
200
+ if sample is not None:
201
+ sample = (sample.rstrip("\r\n") if isinstance(sample, str) else sample.rstrip(b"\r\n")) or None
202
+ print(codecs.strength_report(args.encoding, sample=sample,
203
+ stop_func=getattr(stopfunc, args.stop_function, stopfunc.default),
204
+ include=args.include, exclude=args.exclude))
205
+ return 0
174
206
  # list encodings or macros
175
207
  elif args.command == "list":
176
208
  if args.type == "encodings":
@@ -200,6 +232,10 @@ def main():
200
232
  if args.outfile:
201
233
  with codecs.open(args.outfile, 'wb') as f:
202
234
  f.write(c)
235
+ elif isinstance(c, bytes) and __is_binary(c):
236
+ # write binary data as is, as printing it would re-encode as UTF-8 the bytes that are not valid UTF-8
237
+ sys.stdout.buffer.write(c)
238
+ sys.stdout.flush()
203
239
  else:
204
240
  print(ensure_str(c or "Could not %scode :-(" % ["en", "de"][args.command == "decode"]), end="")
205
241
  elif args.command == "guess":
@@ -0,0 +1,6 @@
1
+ # -*- coding: UTF-8 -*-
2
+ from .aztec import *
3
+ from .datamatrix import *
4
+ from .fakeqr import *
5
+ from .pdf417 import *
6
+ from .qr import *