3030
3131__all__ = ["add" , "add_map" , "b" , "clear" , "codecs" , "decode" , "encode" , "ensure_str" , "examples" , "guess" ,
3232 "generate_strings_from_regex" , "get_alphabet_from_mask" , "handle_error" , "list_categories" , "list_encodings" ,
33- "lookup" , "maketrans" , "re" , "register" , "remove" , "reset" , "s2i" , "search" , "stopfunc" , "BytesIO" , "MASKS " ,
34- "PY3" ]
33+ "lookup" , "maketrans" , "rank" , " re" , "register" , "remove" , "reset" , "s2i" , "search" , "stopfunc" , "BytesIO" ,
34+ "MASKS" , " PY3" ]
3535CODECS_REGISTRY = None
3636MASKS = {
3737 'a' : printable ,
@@ -847,31 +847,56 @@ def generate_strings_from_regex(regex, star_plus_max=STAR_PLUS_MAX, repeat_max=R
847847""" )
848848stopfunc .printables = lambda s : all (c in printable for c in ensure_str (s ))
849849stopfunc .regex = lambda p : lambda s : re .search (p , ensure_str (s )) is not None
850- stopfunc .text = lambda s : stopfunc .printables (s ) and entropy (s ) < 4.5
850+ stopfunc .text = lambda s : stopfunc .printables (s ) and entropy (s ) < 4.4
851+
852+ def _lang (lang ):
853+ def _test (s ):
854+ if not stopfunc .text (s ):
855+ return False
856+ try :
857+ return detect (ensure_str (s )) == lang
858+ except :
859+ return False
860+ return _test
851861
852862try :
853863 from langdetect import detect , PROFILES_DIRECTORY
854864 for lang in [p .replace ("-" , "" ) for p in os .listdir (PROFILES_DIRECTORY )]:
855- setattr (stopfunc , "lang_%s" % lang , lambda s , l = lang : stopfunc . printables ( s ) and detect ( s ) == l )
865+ setattr (stopfunc , "lang_%s" % lang , _lang ( lang ) )
856866except ImportError :
857867 pass
858868
859869
860870__flag = lambda x : re .search (r"[Ff][Ll1][Aa4@][Gg96]" , x ) is not None
861871def _flag (x ):
862- try :
863- return __flag (ensure_str (b (x ).decode ("utf16" )))
864- except (UnicodeDecodeError , UnicodeEncodeError ):
865- return __flag (x )
872+ return __flag (ensure_str (x ))
866873stopfunc .flag = _flag
867874
868875
869- def __guess (prev_input , input , stop_func , depth , max_depth , codec_categories , exclude , result , found = (), stop = True ,
870- show = False , scoring_heuristic = False , extended = False , debug = False ):
876+ def __develop (encodings ):
877+ """ Private method for developing the input list of encodings, trying to extend it with every encoding name. """
878+ enc = []
879+ for e in (encodings or []):
880+ try :
881+ ci = lookup (e )
882+ g = ci .parameters ['guess' ]
883+ except :
884+ g = [e ]
885+ if e in g : # e.g. "rot-1" => ["rot-1", "rot-2", ...] ; only "rot-1" is to be selected
886+ enc .append (e )
887+ else : # e.g. "rot" => ["rot-1", "rot-2", ...] ; all the "rot-N" shall be selected
888+ enc .extend (g )
889+ return enc
890+
891+
892+ def __guess (prev_input , input , stop_func , depth , max_depth , min_depth , codec_categories , exclude , result , found = (),
893+ stop = True , show = False , scoring_heuristic = False , extended = False , debug = False ):
871894 """ Perform a breadth-first tree search using a ranking logic to select and prune the list of codecs. """
872- if depth > 0 and stop_func (input ):
895+ if depth > min_depth and stop_func (input ):
873896 if not stop and show and found not in result :
874- s = "[+] %s: %s" % (", " .join (found ), ensure_str (input ))
897+ s = repr (input )
898+ s = s [2 :- 1 ] if s .startswith ("b'" ) and s .endswith ("'" ) else s
899+ s = "[+] %s: %s" % (", " .join (found ), s )
875900 print (s if len (s ) <= 80 else s [:77 ] + "..." )
876901 result [found ] = input
877902 if depth >= max_depth or len (result ) > 0 and stop :
@@ -898,26 +923,26 @@ def expand(items, descr=None, transform=None):
898923 raise ValueError ("Bad %sformat %s" % (["%s " % descr , "" ][descr is None ], items ))
899924 return r if transform is None else transform (* r )
900925 # parse valid encodings, expanding included/excluded codecs
901- c , e = expand (codec_categories , "codec_categories" , list_encodings ), expand (exclude , "exclude" )
926+ c , e = expand (codec_categories , "codec_categories" , list_encodings ), __develop ( expand (exclude , "exclude" ) )
902927 for new_input , encoding in __rank (prev_input , input , c , scoring_heuristic , extended ):
903928 if len (result ) > 0 and stop :
904929 return
905930 if encoding in e :
906931 continue
907932 if debug :
908933 print ("[*] Depth %d/%d ; trying %s" % (depth + 1 , max_depth , encoding ))
909- __guess (input , new_input , stop_func , depth + 1 , max_depth , codec_categories , exclude , result ,
934+ __guess (input , new_input , stop_func , depth + 1 , max_depth , min_depth , codec_categories , exclude , result ,
910935 found + (encoding , ), stop , show , scoring_heuristic , extended , debug )
911936
912937
913- def __rank (prev_input , input , codecs , heuristic = False , extended = False ):
938+ def __rank (prev_input , input , codecs , heuristic = False , extended = False , yield_score = False ):
914939 """ Filter valid encodings and rank them by relevance. """
915940 ranking = {}
916941 for codec in codecs :
917942 for score , new_input , encoding in __score (prev_input , input , codec , heuristic , extended ):
918943 ranking [encoding ] = (score , new_input )
919944 for encoding , result in sorted (ranking .items (), key = lambda x : - x [1 ][0 ]):
920- yield result [1 ], encoding
945+ yield result if yield_score else result [1 ], encoding
921946
922947
923948class _Text (object ):
@@ -939,7 +964,7 @@ def __score(prev_input, input, codec, heuristic=False, extended=False):
939964 except :
940965 continue
941966 # ignore encodings that give an output identical to the input (identity transformation) or to the previous input
942- if b (input ) == b (new_input ) or b (prev_input ) == b (new_input ):
967+ if prev_input is not None and b (input ) == b (new_input ) or b (prev_input ) == b (new_input ):
943968 continue
944969 # compute input's characteristics only once and only if the control flow reaches this point
945970 pad = ci .parameters .get ('scoring' , {}).get ('padding_char' )
@@ -948,14 +973,14 @@ def __score(prev_input, input, codec, heuristic=False, extended=False):
948973 if heuristic :
949974 # from here, the goal (e.g. if the input is Base32) is to rank candidate encodings (e.g. multiple base
950975 # codecs) so that we can put the right one as early as possible and eventually exclude bad candidates
951- s = .0
976+ s = - ci . parameters . get ( 'penalty' , .0 )
952977 # first, apply a bonus if the length of input text's charset is exactly the same as encoding's charset ;
953978 # on the contrary, if the length of input text's charset is strictly greater, give a penalty
954979 lcs = ci .parameters .get ('scoring' , {}).get ('len_charset' , 256 )
955980 if isinstance (lcs , type (lambda : None )):
956981 lcs = int (lcs (encoding ))
957- if (pad and obj .padding and lcs + 1 == obj .lcharset ) or lcs = = obj .lcharset :
958- s += .3
982+ if (pad and obj .padding and lcs + 1 >= obj .lcharset ) or lcs > = obj .lcharset :
983+ s += max ( .0 , round ( .6 * ( .99 ** ( lcs - obj . lcharset )), 5 ) - .1 )
959984 elif (pad and obj .padding and lcs + 1 < obj .lcharset ) or lcs < obj .lcharset :
960985 s -= .2 # this can occur for encodings with no_error set to True
961986 # then, take padding into account, giving a bonus if padding is to be encountered and effectively present,
@@ -1000,7 +1025,7 @@ def __score(prev_input, input, codec, heuristic=False, extended=False):
10001025 yield s , new_input , encoding
10011026
10021027
1003- def guess (input , stop_func = stopfunc .printables , max_depth = 5 , codec_categories = None , exclude = None , result = None , found = (),
1028+ def guess (input , stop_func = stopfunc .printables , min_depth = 0 , max_depth = 5 , codec_categories = None , exclude = None , found = (),
10041029 stop = True , show = False , scoring_heuristic = False , extended = False , debug = False ):
10051030 """ Try decoding without the knowledge of the encoding(s). """
10061031 if max_depth <= 0 :
@@ -1011,13 +1036,28 @@ def guess(input, stop_func=stopfunc.printables, max_depth=5, codec_categories=No
10111036 if isinstance (stop_func , string_types ):
10121037 stop_func = stopfunc .regex (stop_func )
10131038 if len (input ) > 0 :
1014- result = result or {}
1015- # breadth-first search
1016- for d in range (max_depth ):
1017- __guess ("" , input , stop_func , 0 , d + 1 , codec_categories , exclude , result , tuple (found ), stop , show ,
1018- scoring_heuristic , extended , debug )
1019- if stop and len (result ) > 0 :
1020- return result
1039+ result = {}
1040+ try :
1041+ # breadth-first search
1042+ for d in range (max_depth ):
1043+ __guess ("" , input , stop_func , 0 , d + 1 , min_depth , codec_categories , exclude , result , tuple (found ), stop ,
1044+ show , scoring_heuristic , extended , debug )
1045+ if stop and len (result ) > 0 :
1046+ return result
1047+ except KeyboardInterrupt :
1048+ pass
10211049 return result
10221050codecs .guess = guess
10231051
1052+
1053+ def rank (input , extended = False , limit = - 1 , codec_categories = None , exclude = None ):
1054+ """ Rank the most probable encodings based on the given input. """
1055+ codecs = list_encodings (* (codec_categories or ()))
1056+ for e in __develop (exclude ):
1057+ try :
1058+ codecs .remove (e )
1059+ except ValueError :
1060+ pass
1061+ return list (__rank (None , input , codecs , True , extended , True ))[:limit ]
1062+ codecs .rank = rank
1063+
0 commit comments