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.
- codext/VERSION.txt +1 -1
- codext/__common__.py +87 -399
- codext/__init__.py +2 -0
- codext/__main__.py +37 -1
- codext/barcodes/__init__.py +6 -0
- codext/barcodes/aztec.py +726 -0
- codext/barcodes/datamatrix.py +669 -0
- codext/barcodes/fakeqr.py +216 -0
- codext/barcodes/pdf417.py +865 -0
- codext/barcodes/qr.py +671 -0
- codext/base/__init__.py +4 -0
- codext/base/_base.py +68 -14
- codext/base/_base2n.py +12 -2
- codext/base/base128.py +80 -0
- codext/base/base131072.py +219 -0
- codext/base/base256.py +52 -0
- codext/base/base45.py +4 -1
- codext/base/base69.py +73 -0
- codext/binary/__init__.py +2 -0
- codext/binary/hamming.py +87 -0
- codext/binary/manchester.py +51 -1
- codext/binary/nrzi.py +52 -0
- codext/checksums/__init__.py +1 -0
- codext/checksums/fletcher.py +39 -0
- codext/{crypto → ciphers}/__init__.py +10 -1
- codext/ciphers/_jsfloat.py +104 -0
- codext/ciphers/_square.py +67 -0
- codext/ciphers/aer256.py +35 -0
- codext/ciphers/armon64.py +37 -0
- codext/{crypto → ciphers}/bazeries.py +7 -18
- codext/ciphers/enigma.py +150 -0
- codext/ciphers/ezip64.py +35 -0
- codext/ciphers/foursquare.py +74 -0
- codext/ciphers/fractionatedmorse.py +91 -0
- codext/ciphers/okto3.py +35 -0
- codext/{crypto → ciphers}/playfair.py +13 -32
- codext/ciphers/twosquare.py +82 -0
- codext/ciphers/vernam.py +39 -0
- codext/ciphers/zara128.py +33 -0
- codext/compressions/__init__.py +2 -0
- codext/compressions/huffman.py +137 -0
- codext/compressions/zstd.py +60 -0
- codext/esoteric/__init__.py +10 -0
- codext/esoteric/_brainfuck.py +89 -0
- codext/esoteric/befunge.py +249 -0
- codext/esoteric/brainfuck.py +67 -0
- codext/esoteric/church.py +228 -0
- codext/esoteric/cow.py +160 -0
- codext/esoteric/lolcode.py +709 -0
- codext/esoteric/malbolge.py +198 -0
- codext/esoteric/ook.py +103 -0
- codext/esoteric/piet.py +430 -0
- codext/esoteric/pikalang.py +66 -0
- codext/frequency/__init__.py +4 -0
- codext/frequency/_ita2.py +40 -0
- codext/frequency/coquelet.py +98 -0
- codext/frequency/dtmf.py +182 -0
- codext/frequency/piccolo.py +106 -0
- codext/guess.py +428 -0
- codext/hashing/__init__.py +21 -0
- codext/hashing/_descrypt.py +50 -0
- codext/hashing/_h64.py +52 -0
- codext/hashing/argon2.py +42 -0
- codext/hashing/bcrypt.py +107 -0
- codext/hashing/cisco.py +41 -0
- codext/hashing/cryptvariants.py +136 -0
- codext/hashing/ctph.py +92 -0
- codext/hashing/fnv.py +59 -0
- codext/hashing/gost.py +137 -0
- codext/hashing/hmac.py +31 -0
- codext/hashing/keccak.py +87 -0
- codext/hashing/ldap.py +32 -0
- codext/hashing/md.py +93 -5
- codext/hashing/mscash.py +40 -0
- codext/hashing/mssql.py +35 -0
- codext/hashing/mysql.py +35 -0
- codext/hashing/oracle.py +49 -0
- codext/hashing/phpass.py +38 -0
- codext/hashing/postgres.py +25 -0
- codext/hashing/ripemd.py +73 -0
- codext/hashing/scrypt.py +44 -0
- codext/hashing/sha.py +32 -5
- codext/hashing/snefru.py +268 -0
- codext/hashing/whirlpool.py +87 -0
- codext/hashing/windows.py +101 -0
- codext/languages/__init__.py +2 -0
- codext/languages/fancy.py +119 -0
- codext/languages/galactic.py +90 -14
- codext/languages/tap.py +103 -45
- codext/languages/upsidedown.py +58 -0
- codext/obfuscation/__init__.py +17 -0
- codext/obfuscation/_jsbase64.py +79 -0
- codext/obfuscation/_jsuri.py +55 -0
- codext/obfuscation/arabica2rs.py +77 -0
- codext/obfuscation/atom128.py +30 -0
- codext/obfuscation/chinzo72c.py +78 -0
- codext/obfuscation/cisco.py +65 -0
- codext/obfuscation/esab46.py +30 -0
- codext/obfuscation/feron74.py +30 -0
- codext/obfuscation/gila7.py +30 -0
- codext/obfuscation/hazz15.py +31 -0
- codext/obfuscation/hindia4x.py +86 -0
- codext/obfuscation/japooc2s.py +83 -0
- codext/obfuscation/korex3s.py +70 -0
- codext/obfuscation/megan35.py +31 -0
- codext/obfuscation/tigo3fx.py +31 -0
- codext/obfuscation/tripo5.py +31 -0
- codext/obfuscation/zong22.py +31 -0
- codext/others/__init__.py +6 -0
- codext/others/asn1.py +482 -0
- codext/others/dvorak.py +45 -0
- codext/others/letters.py +3 -1
- codext/others/ns3.py +93 -0
- codext/others/soundex.py +482 -0
- codext/others/telegraph.py +278 -0
- codext/others/xy.py +30 -0
- codext/stegano/sms.py +2 -1
- codext/stegano/whitespace.py +30 -9
- codext/strength.py +164 -0
- codext/web/__init__.py +3 -0
- codext/web/_javascript.py +59 -0
- codext/web/aaencode.py +103 -0
- codext/web/jjencode.py +135 -0
- codext/web/jsfuck.py +635 -0
- {codext-2.0.0.dist-info → codext-2.0.1.dist-info}/METADATA +143 -32
- codext-2.0.1.dist-info/RECORD +214 -0
- {codext-2.0.0.dist-info → codext-2.0.1.dist-info}/entry_points.txt +4 -0
- codext-2.0.0.dist-info/RECORD +0 -116
- codext/{crypto → ciphers}/adfgx.py +0 -0
- codext/{crypto → ciphers}/affine.py +0 -0
- codext/{crypto → ciphers}/atbash.py +0 -0
- codext/{crypto → ciphers}/bacon.py +0 -0
- codext/{crypto → ciphers}/barbie.py +0 -0
- codext/{crypto → ciphers}/bifid.py +0 -0
- codext/{crypto → ciphers}/chaocipher.py +0 -0
- codext/{crypto → ciphers}/columnar.py +0 -0
- codext/{crypto → ciphers}/hill.py +0 -0
- codext/{crypto → ciphers}/nihilist.py +0 -0
- codext/{crypto → ciphers}/phillips.py +0 -0
- codext/{crypto → ciphers}/polybius.py +0 -0
- codext/{crypto → ciphers}/railfence.py +0 -0
- codext/{crypto → ciphers}/rot.py +0 -0
- codext/{crypto → ciphers}/scytale.py +0 -0
- codext/{crypto → ciphers}/shift.py +0 -0
- codext/{crypto → ciphers}/skip.py +0 -0
- codext/{crypto → ciphers}/vic.py +0 -0
- codext/{crypto → ciphers}/vigenere.py +14 -14
- /codext/{crypto → ciphers}/xor.py +0 -0
- /codext/{crypto → obfuscation}/citrix.py +0 -0
- {codext-2.0.0.dist-info → codext-2.0.1.dist-info}/WHEEL +0 -0
- {codext-2.0.0.dist-info → codext-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {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.
|
|
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",
|
|
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", "
|
|
39
|
-
"register", "remove", "reset", "s2i", "search", "
|
|
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 =
|
|
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":
|