diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e4e6a57..0000000 --- a/.gitmodules +++ /dev/null @@ -1,12 +0,0 @@ -[submodule "PACK"] - path = PACK - url = https://github.com/iphelix/pack.git -[submodule "hashcat-utils"] - path = hashcat-utils - url = https://github.com/hashcat/hashcat-utils.git -[submodule "princeprocessor"] - path = princeprocessor - url = https://github.com/hashcat/princeprocessor.git -[submodule "pack"] - path = pack - url = https://github.com/iphelix/pack.git diff --git a/PACK/ChangeLog b/PACK/ChangeLog new file mode 100644 index 0000000..43db990 --- /dev/null +++ b/PACK/ChangeLog @@ -0,0 +1,8 @@ +PACK 0.0.4 August 8, 2013 +------------------------- + +* Rewrote maskgen to generate optimized masks in addition to custom parameters. +* Rewrote policygen to generate non-compliant masks. +* Rewrote rulegen to use multiple processes for significant speed improvements on multi-core systems. +* Renamed dictstat to statsgen and removed all regular expressions for performance enhancements. +* Rewrote statsgen to attempt policy detection. \ No newline at end of file diff --git a/PACK/LICENSE b/PACK/LICENSE new file mode 100644 index 0000000..1b563eb --- /dev/null +++ b/PACK/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2013, Peter Kacherginsky +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + + Neither the name of the 'The Sprawl' nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/PACK/README b/PACK/README new file mode 100644 index 0000000..304fc69 --- /dev/null +++ b/PACK/README @@ -0,0 +1,677 @@ +Password Analysis and Cracking Kit by Peter Kacherginsky (iphelix) +================================================================== + +PACK (Password Analysis and Cracking Toolkit) is a collection of utilities developed to aid in analysis of password lists in order to enhance password cracking through pattern detection of masks, rules, character-sets and other password characteristics. The toolkit generates valid input files for Hashcat family of password crackers. + +NOTE: The toolkit itself is not able to crack passwords, but instead designed to make operation of password crackers more efficient. + +Selecting passwords lists for analysis +====================================== + +Before we can begin using the toolkit we must establish a selection criteria of password lists. Since we are looking to analyze the way people create their passwords, we must obtain as large of a sample of leaked passwords as possible. One such excellent list is based on RockYou.com compromise. This list both provides large and diverse enough collection that provides a good results for common passwords used by similar sites (e.g. social networking). The analysis obtained from this list may not work for organizations with specific password policies. As such, selecting sample input should be as close to your target as possible. In addition, try to avoid obtaining lists based on already cracked passwords as it will generate statistics bias of rules and masks used by individual(s) cracking the list and not actual users. + +StatsGen +======================================= + +The most basic analysis that you can perform is simply obtaining most common length, character-set and other characteristics of passwords in the provided list. In the example below, we will use 'rockyou.txt' containing approximately 14 million passwords. Launch `statsgen.py` with the following command line: + + $ python statsgen.py rockyou.txt + +Below is the output from the above command: + + _ + StatsGen #.#.# | | + _ __ __ _ ___| | _ + | '_ \ / _` |/ __| |/ / + | |_) | (_| | (__| < + | .__/ \__,_|\___|_|\_\ + | | + |_| iphelix@thesprawl.org + + + [*] Analyzing passwords in [rockyou.txt] + [+] Analyzing 100% (14344390/14344390) of passwords + NOTE: Statistics below is relative to the number of analyzed passwords, not total number of passwords + + [*] Length: + [+] 8: 20% (2966037) + [+] 7: 17% (2506271) + [+] 9: 15% (2191039) + [+] 10: 14% (2013695) + [+] 6: 13% (1947798) + ... + + [*] Character-set: + [+] loweralphanum: 42% (6074867) + [+] loweralpha: 25% (3726129) + [+] numeric: 16% (2346744) + [+] loweralphaspecialnum: 02% (426353) + [+] upperalphanum: 02% (407431) + ... + + [*] Password complexity: + [+] digit: min(0) max(255) + [+] lower: min(0) max(255) + [+] upper: min(0) max(187) + [+] special: min(0) max(255) + + [*] Simple Masks: + [+] stringdigit: 37% (5339556) + [+] string: 28% (4115314) + [+] digit: 16% (2346744) + [+] digitstring: 04% (663951) + [+] othermask: 04% (576324) + ... + + [*] Advanced Masks: + [+] ?l?l?l?l?l?l?l?l: 04% (687991) + [+] ?l?l?l?l?l?l: 04% (601152) + [+] ?l?l?l?l?l?l?l: 04% (585013) + [+] ?l?l?l?l?l?l?l?l?l: 03% (516830) + [+] ?d?d?d?d?d?d?d: 03% (487429) + ... + + +NOTE: You can reduce the number of outliers displayed by including the --hiderare flag which will not show any items with occurrence of less than 1%. + +Here is what we can immediately learn from the above list: + + * Most of the passwords have length 6 to 10 characters. + * The majority of passwords have loweralphanumeric character-set. + * There is no obvious minimum or maximum password complexity. + * Analyzed passwords tend to follow a simple masks "string followed by digits". + +The last section, "Advanced Masks", contains most frequently occurring masks using the Hashcat format. Individual symbols can be interpreted as follows: + + ?l - a single lowercase character + ?u - a single uppercase character + ?d - a single digit + ?s - a single special character + +For example, the very first mask, "?l?l?l?l?l?l?l?l", will match all of the lowercase alpha passwords. Given the sample size you will be able to crack approximately 4% of passwords. However, after generating the initial output, you may be interested in using filters to narrow down on password data. + +Using filters +------------- + +Let's see how RockYou users tend to select their passwords using the "stringdigit" simple mask (a string followed by numbers): + + $ python statsgen.py ../PACK-0.0.3/archive/rockyou.txt --simplemask stringdigit -q --hiderare + + [*] Analyzing passwords in [rockyou.txt] + [+] Analyzing 37% (5339556/14344390) of passwords + NOTE: Statistics below is relative to the number of analyzed passwords, not total number of passwords + + [*] Length: + [+] 8: 23% (1267260) + [+] 7: 18% (981432) + [+] 9: 17% (939971) + [+] 10: 14% (750938) + [+] 6: 11% (618983) + [+] 11: 05% (294869) + [+] 12: 03% (175875) + [+] 13: 01% (103047) + [+] 14: 01% (65958) + + [*] Character-set: + [+] loweralphanum: 88% (4720184) + [+] upperalphanum: 06% (325941) + [+] mixedalphanum: 05% (293431) + + [*] Password complexity: + [+] digit: min(1) max(252) + [+] lower: min(0) max(46) + [+] upper: min(0) max(31) + [+] special: min(0) max(0) + + [*] Simple Masks: + [+] stringdigit: 100% (5339556) + + [*] Advanced Masks: + [+] ?l?l?l?l?l?l?d?d: 07% (420318) + [+] ?l?l?l?l?l?d?d: 05% (292306) + [+] ?l?l?l?l?l?l?l?d?d: 05% (273624) + [+] ?l?l?l?l?d?d?d?d: 04% (235360) + [+] ?l?l?l?l?d?d: 04% (215074) + ... + +The very top of the output specifies what percentage of total passwords was analyzed. In this case, by cracking only passwords matching the "stringdigit" mask it is only possible to recover about 37% of the total set just as was displayed in the original output. Next, it appears that only 11% of this password type use anything other than lowercase. So it would be smart to concentrate on only lowercase strings matching this mask. At last, in the "Advanced Mask" section we can see that the majority of "stringdigit" passwords consist of a string with two or four digits following it. With the information gained from the above output, we can begin creating a mental image of target users' password generation patterns. + +There are a few other filters available for password length, mask, and character sets: + +**Length:** --minlength and/or --maxlength + +**Simple Mask:** --simplemask [numeric, loweralpha, upperalpha, mixedalpha, loweralphanum, etc.] + +**Character sets:** --charset [digit, string, stringdigit, digitstring, digitstringdigit, etc.] + +NOTE: More than one filter of the same class can be specified as a comma-separated list: + + --simplemask="stringdigit,digitstring" + +Saving advanced masks +--------------------- + +While the "Advanced Mask" section only displays patterns matching greater than 1% of all passwords, you can obtain and save a full list of password masks matching a given dictionary by using the following command: + + $ python statsgen.py rockyou.txt -o rockyou.masks + +All of the password masks and their frequencies will be saved into the specified file in the CSV format. Naturally, you can provide filters to only generate masks file matching specified parameters. The output file can be used as an input to MaskGen tool covered in the next section. + +MaskGen +================== + +MaskGen allows you to craft pattern-based mask attacks for input into Hashcat family of password crackers. The tool uses output produced by statsgen above with the '-o' flag in order to produce the most optimal mask attack sorted by mask complexity, mask occurrence or ratio of the two (optimal index). + +Let's run MaskGen with only StatGen's output as an argument: + + $ python maskgen.py rockyou.masks + + _ + MaskGen #.#.# | | + _ __ __ _ ___| | _ + | '_ \ / _` |/ __| |/ / + | |_) | (_| | (__| < + | .__/ \__,_|\___|_|\_\ + | | + |_| iphelix@thesprawl.org + + + [*] Analyzing masks in [rockyou.masks] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Sorting masks by their [optindex]. + [*] Finished generating masks: + Masks generated: 146578 + Masks coverage: 100% (14344390/14344390) + Masks runtime: >1 year + +There are several pieces of information that you should observe: + + * Default cracking speed used for calculations is 1,000,000,000 keys/sec + * Default sorting mode is [optindex] equivalent to --optindex flag. + * 146,578 unique masks were generated which have 100% coverage + * Total runtime of all generated masks is more than 1 year. + +Specifying target time +---------------------- + +Since you are usually limited in time to perform and craft attacks, maskgen allows you to specify how much time you have to perform mask attacks and will generate the most optimal collection of masks based on the sorting mode. Let's play a bit with different sorting modes and target times: + + $ python maskgen.py rockyou.masks --targettime 600 --optindex -q + [*] Analyzing masks in [rockyou.masks] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Sorting masks by their [optindex]. + [!] Target time exceeded. + [*] Finished generating masks: + Masks generated: 779 + Masks coverage: 56% (8116195/14344390) + Masks runtime: 0:11:36 + + + $ python maskgen.py rockyou.masks --targettime 600 --complexity -q + [*] Analyzing masks in [rockyou.masks] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Sorting masks by their [complexity]. + [!] Target time exceeded. + [*] Finished generating masks: + Masks generated: 5163 + Masks coverage: 31% (4572346/14344390) + Masks runtime: 0:10:01 + + + $ python maskgen.py rockyou.masks --targettime 600 --occurrence -q + [*] Analyzing masks in [rockyou.masks] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Sorting masks by their [occurrence]. + [!] Target time exceeded. + [*] Finished generating masks: + Masks generated: 4 + Masks coverage: 16% (2390986/14344390) + Masks runtime: 1:34:05 + +All of the above runs have target time of 600 seconds (or 10 minutes) with different sorting modes. Based on our experiments, masks generated using OptIndex sorting mode can crack 56% of RockYou passwords in about 10 minutes. At the same time masks generated using Occurrence sorting mode not only have pretty weak coverage of only 16%, but also exceeded specified target time by more than an hour. + +NOTE: Masks sorted by complexity can be very effective when attacking policy based lists. + +Let's see some of the masks generated by maskgen in optindex mode using the --showmasks flag: + + $ python maskgen.py rockyou.masks --targettime 43200 --optindex -q --showmasks + [*] Analyzing masks in [rockyou.masks] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Sorting masks by their [optindex]. + [L:] Mask: [ Occ: ] [ Time: ] + ... + [ 7] ?l?d?s?l?l?d?d [6 ] [ 0:00:00] + [ 8] ?s?l?l?l?l?l?l?s [3480 ] [ 0:05:36] + [ 9] ?l?l?l?l?d?d?d?d?s [1553 ] [ 0:02:30] + [ 8] ?d?l?d?d?d?l?l?l [47 ] [ 0:00:04] + [ 8] ?d?l?l?d?l?d?d?l [47 ] [ 0:00:04] + [ 8] ?d?l?l?d?d?l?d?l [47 ] [ 0:00:04] + [ 8] ?d?l?d?l?d?d?l?l [47 ] [ 0:00:04] + [ 8] ?d?d?l?l?d?l?d?l [47 ] [ 0:00:04] + [ 8] ?d?l?d?d?l?l?l?l [122 ] [ 0:00:11] + [ 8] ?u?u?d?u?d?d?d?d [18 ] [ 0:00:01] + [ 6] ?d?s?s?s?s?s [4 ] [ 0:00:00] + [10] ?l?l?l?l?l?l?l?l?d?d [213109 ] [ 5:48:02] + [!] Target time exceeded. + [*] Finished generating masks: + Masks generated: 3970 + Masks coverage: 74% (10620959/14344390) + Masks runtime: 16:10:38 + +Displayed masks follow a pretty intuitive format: + + + [ 9] ?l?l?l?l?d?d?d?d?s [1553 ] [ 0:02:30] + \ \ \ \ + \ \_ generated mask \ \_ mask runtime + \ \ + \_ mask length \_ mask occurrence + + +In the above sample you can see some of the logic that goes into mask generation. For example, while '?s?l?l?l?l?l?l?s' mask has one of the longest runtimes in the sample (5 minutes), it still has higher priority because of its relatively higher occurrence to '?l?l?l?l?d?d?d?d?s'. At the same time, while '?l?d?s?l?l?d?d' has pretty low coverage it still gets a higher priority than other masks because as only a six character mask it executes very quickly. + +Specifying mask filters +----------------------- + +You can further optimize your generated mask attacks by using filters. For example, you may have sufficiently powerful hardware where you can simple bruteforce all of the passwords up to 8 characters. In this case, you can generate masks only greater than 8 characters using the --minlength flag as follows: + + $ python maskgen.py rockyou.masks --targettime 43200 --optindex -q --minlength 8 + [*] Analyzing masks in [rockyou.masks] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Sorting masks by their [optindex]. + [!] Target time exceeded. + [*] Finished generating masks: + Masks generated: 585 + Masks coverage: 41% (5905182/14344390) + Masks runtime: 15:50:36 + +Naturally the generated mask coverage was reduced, but these filters become useful when preparing a collection of masks when attacking password lists other than the one used to generate them. + +The list below shows additional filters you can use: + + Individual Mask Filter Options: + --minlength=8 Minimum password length + --maxlength=8 Maximum password length + --mintime=3600 Minimum mask runtime (seconds) + --maxtime=3600 Maximum mask runtime (seconds) + --mincomplexity=1 Minimum complexity + --maxcomplexity=100 + Maximum complexity + --minoccurrence=1 Minimum occurrence + --maxoccurrence=100 + Maximum occurrence + +Occurrrence and complexity flags can be particularly powerful to fine-tune generated masks using different sorting modes. + +Saving generated masks +---------------------- + +Once you are satisfied with the above generated masks, you can save them using the -o flag: + + $ python maskgen.py rockyou.masks --targettime 43200 --optindex -q -o rockyou.hcmask + [*] Analyzing masks in [rockyou.masks] + [*] Saving generated masks to [rockyou.hcmask] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Sorting masks by their [optindex]. + [!] Target time exceeded. + [*] Finished generating masks: + Masks generated: 3970 + Masks coverage: 74% (10620959/14344390) + Masks runtime: 16:10:38 + +This will produce 'rockyou.hcmask' file which can be directly used by Hashcat suite of tools or as part of a custom script that loops through them. + +Checking mask coverage +---------------------- + +It is often useful to see how well generated masks perform against already cracked lists. Maskgen can compare a collection of masks against others to see how well they would perform if masks from one password list would be attempted against another. Let's compare how well masks generated from RockYou list will perform against another compromised list such as Gawker: + + $ python statsgen.py ../PACK-0.0.3/archive/gawker.dic -o gawker.masks + + $ python maskgen.py gawker.masks --checkmasksfile rockyou.hcmask -q + [*] Analyzing masks in [gawker.masks] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Checking coverage of masks in [rockyou.hcmask] + [*] Finished matching masks: + Masks matched: 1775 + Masks coverage: 96% (1048889/1084394) + Masks runtime: 16:25:44 + +Using the '--checkmasksfile' parameter we attempted to run masks inside 'rockyou.hcmask' file generated earlier against masks from a sample leaked list 'gawker.masks'. This results in a good 96% coverage where 1775 of the 3970 total generated RockYou-based masks matched masks in Gawker list. + +It is also possible to see the coverage of one or more masks by specifying them directly on the command-line as follows: + + $ python maskgen.py gawker.masks --checkmasks="?u?l?l?l?l?l?d,?l?l?l?l?l?d?d" -q + [*] Analyzing masks in [gawker.masks] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Checking coverage of the these masks [?u?l?l?l?l?l?d, ?l?l?l?l?l?d?d] + [*] Finished matching masks: + Masks matched: 2 + Masks coverage: 1% (18144/1084394) + Masks runtime: 0:00:04 + +Both of the specified masks matched with only 1% coverage. + +Specifying speed +---------------- + +Depending on your exact hardware specs and target hash you may want to increase or decrease keys/sec speed used during calculations using the '--pps' parameter: + + $ python maskgen.py rockyou.masks --targettime 43200 --pps 50000000 -q + [*] Analyzing masks in [rockyou.masks] + [*] Using 50,000,000 keys/sec for calculations. + [*] Sorting masks by their [optindex]. + [!] Target time exceeded. + [*] Finished generating masks: + Masks generated: 1192 + Masks coverage: 61% (8754548/14344390) + Masks runtime: 12:17:31 + +Using the '--pps' parameter to match you actual performance makes target time more meaningful. + +PolicyGen +========= + +A lot of the mask and dictionary attacks will fail in the corporate environment with minimum password complexity requirements. Instead of resorting to a pure bruteforcing attack, we can leverage known or guessed password complexity rules to avoid trying password candidates that are not compliant with the policy or inversely only audit for noncompliant passwords. Using PolicyGen, you will be able to generate a collection of masks following the password complexity in order to significantly reduce the cracking time. + +Below is a sample session where we generate all valid password masks for an environment requiring at least one digit, one upper, and one special characters. + + $ python policygen.py --minlength 8 --maxlength 8 --minlower 1 --minupper 1 --mindigit 1 --minspecial 1 -o complexity.hcmask + _ + PolicyGen #.#.# | | + _ __ __ _ ___| | _ + | '_ \ / _` |/ __| |/ / + | |_) | (_| | (__| < + | .__/ \__,_|\___|_|\_\ + | | + |_| iphelix@thesprawl.org + + + [*] Saving generated masks to [complexity.hcmask] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Password policy: + Pass Lengths: min:8 max:8 + Min strength: l:1 u:1 d:1 s:1 + Max strength: l:None u:None d:None s:None + [*] Generating [compliant] masks. + [*] Generating 8 character password masks. + [*] Total Masks: 65536 Time: 76 days, 18:50:04 + [*] Policy Masks: 40824 Time: 35 days, 0:33:09 + +From the above output you can see that we have generated 40824 masks matching the specified complexity that will take about 35 days to run at the speed of 1,000,000,000 keys/sec. + +In case you are simply performing a password audit and tasked to discover only non-compliant passwords you can specify '--noncompliant' flag to invert generated masks: + + $ python policygen.py --minlength 8 --maxlength 8 --minlower 1 --minupper 1 --mindigit 1 --minspecial 1 -o noncompliant.hcmask -q --noncompliant + [*] Saving generated masks to [noncompliant.hcmask] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Password policy: + Pass Lengths: min:8 max:8 + Min strength: l:1 u:1 d:1 s:1 + Max strength: l:None u:None d:None s:None + [*] Generating [non-compliant] masks. + [*] Generating 8 character password masks. + [*] Total Masks: 65536 Time: 76 days, 18:50:04 + [*] Policy Masks: 24712 Time: 41 days, 18:16:55 + +Let's see some of the non-compliant masks generated above using the '--showmasks' flag: + + $ python policygen.py --minlength 8 --maxlength 8 --minlower 1 --minupper 1 --mindigit 1 --minspecial 1 -o noncompliant.hcmask -q --noncompliant --showmasks + [*] Saving generated masks to [noncompliant.hcmask] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Password policy: + Pass Lengths: min:8 max:8 + Min strength: l:1 u:1 d:1 s:1 + Max strength: l:None u:None d:None s:None + [*] Generating [non-compliant] masks. + [*] Generating 8 character password masks. + [ 8] ?d?d?d?d?d?d?d?d [l: 0 u: 0 d: 8 s: 0] [ 0:00:00] + [ 8] ?d?d?d?d?d?d?d?l [l: 1 u: 0 d: 7 s: 0] [ 0:00:00] + [ 8] ?d?d?d?d?d?d?d?u [l: 0 u: 1 d: 7 s: 0] [ 0:00:00] + [ 8] ?d?d?d?d?d?d?d?s [l: 0 u: 0 d: 7 s: 1] [ 0:00:00] + ... + [ 8] ?s?s?s?s?s?s?s?d [l: 0 u: 0 d: 1 s: 7] [ 0:07:06] + [ 8] ?s?s?s?s?s?s?s?l [l: 1 u: 0 d: 0 s: 7] [ 0:18:28] + [ 8] ?s?s?s?s?s?s?s?u [l: 0 u: 1 d: 0 s: 7] [ 0:18:28] + [ 8] ?s?s?s?s?s?s?s?s [l: 0 u: 0 d: 0 s: 8] [ 0:23:26] + [*] Total Masks: 65536 Time: 76 days, 18:50:04 + [*] Policy Masks: 24712 Time: 41 days, 18:16:55 + +As you can see all of the masks have at least one missing password complexity requirement. Interestingly with fewer generated masks it takes longer to attack because of long running masks like '?s?s?s?s?s?s?s?s'. + +Specifying maximum complexity +----------------------------- + +It is also possible to specify maximum password complexity using --maxlower, --maxupper, --maxdigit and --maxspecial flags in order to fine-tune you attack. For example, below is a sample site which enforces password policy but does not allow any special characters: + + $ python policygen.py --minlength 8 --maxlength 8 --minlower 1 --minupper 1 --mindigit 1 --maxspecial 0 -o maxcomplexity.hcmask -q + [*] Saving generated masks to [maxcomplexity.hcmask] + [*] Using 1,000,000,000 keys/sec for calculations. + [*] Password policy: + Pass Lengths: min:8 max:8 + Min strength: l:1 u:1 d:1 s:None + Max strength: l:None u:None d:None s:0 + [*] Generating [compliant] masks. + [*] Generating 8 character password masks. + [*] Total Masks: 65536 Time: 76 days, 18:50:04 + [*] Policy Masks: 5796 Time: 1 day, 20:20:55 + +Rules Analysis +================== + +`rulegen.py` implements password analysis and rule generation for the Hashcat password cracker as described in the [Automatic Password Rule Analysis and Generation](http://thesprawl.org/research/automatic-password-rule-analysis-generation/) paper. Please review this document for detailed discussion on the theory of rule analysis and generation. + +Reversing source words and word mangling rules from already cracked passwords can be very effective in performing attacks against still encrypted hashes. By continuously recycling/expanding generated rules and words you may be able to crack a greater number of passwords. + +Prerequisites +----------------- +There are several prerequisites for the effective use of `rulegen.py`. The tool utilizes Enchant spell-checking library to interface with a number of spell-checking engines such as Aspell, MySpell, etc. You must install these tools prior to use. It is also critical to install dictionaries for whatever spell-checking engine you end up using (alternatively it is possible to use a custom wordlist). At last, I have bundled PyEnchant for convenience which should interface directly with Enchant's shared libraries; however, should there be any issues, simply remove the bundled 'enchant' directory and install PyEnchant for your distribution. + +For additional details on specific Hashcat rule syntax see [Hashcat Rule Based Attack](http://hashcat.net/wiki/doku.php?id=rule_based_attack). + +Analyzing a Single Password +------------------------------- + +The most basic use of `rulegen.py` involves analysis of a single password to automatically detect rules. Let's detect rules and potential source word used to generate a sample password `P@55w0rd123`: + + $ python rulegen.py --verbose --password P@55w0rd123 + _ + RuleGen #.#.# | | + _ __ __ _ ___| | _ + | '_ \ / _` |/ __| |/ / + | |_) | (_| | (__| < + | .__/ \__,_|\___|_|\_\ + | | + |_| iphelix@thesprawl.org + + + [*] Using Enchant 'aspell' module. For best results please install + 'aspell' module language dictionaries. + [*] Analyzing password: P@55w0rd123 + [-] Pas sword => {edit distance suboptimal: 8 (7)} => P@55w0rd123 + [+] Password => sa@ ss5 so0 $1 $2 $3 => P@55w0rd123 + [+] Passwords => sa@ ss5 so0 o81 $2 $3 => P@55w0rd123 + [+] Passwords => sa@ ss5 so0 i81 o92 $3 => P@55w0rd123 + [+] Passwords => sa@ ss5 so0 i81 i92 oA3 => P@55w0rd123 + [+] Password's => sa@ ss5 so0 o81 o92 $3 => P@55w0rd123 + [+] Password's => sa@ ss5 so0 o81 i92 oA3 => P@55w0rd123 + [+] Password's => sa@ ss5 so0 i81 o92 oA3 => P@55w0rd123 + +There are several flags that we have used for this example: + + * --password - specifies a single password to analyze. + * --verbose - prints out verbose information such as generated rules and performance statistics. + +Processing password files is covered in a section below; however, let's first discuss some of the available fine tuning options using a single password as an example. + +Spell-checking provider +--------------------------- + +Notice that we are using the `aspell` Enchant module for source word detection. The exact spell-checking engine can be changed using the `--provider` flag as follows: + + $ python rulegen.py --verbose --provider myspell --password P@55w0rd123 -q + [*] Using Enchant 'myspell' module. For best results please install + 'myspell' module language dictionaries. + ... + + +NOTE: Provider engine priority can be specified using a comma-separated list (e.g. --provider aspell,myspell). + +Forcing source word +----------------------- + +The use of the source word detection engine can be completely disabled by specifying a source word with the `--word` flag: + + $ python rulegen.py -q --verbose --word word --password P@55w0rd123 + [*] Analyzing password: P@55w0rd123 + [+] word => ^5 ^5 ^@ ^P so0 $1 $2 $3 => P@55w0rd123 + +By specifying different source words you can have a lot of fun experimenting with the rule generation engine. + +Defining Custom Dictionary +------------------------------ + +Inevitably you will come across a point where generating rules using the standard spelling-engine wordlist is no longer sufficient. You can specify a custom wordlist using the `--wordlist` flag. This is particularly useful when reusing source words from a previous analysis session: + + $ python rulegen.py -q --verbose --wordlist rockyou.txt --password 1pa55w0rd1 + [*] Using Enchant 'Personal Wordlist' module. For best results please install + 'Personal Wordlist' module language dictionaries. + [*] Analyzing password: 1pa55w0rd1 + [+] password => ^1 ss5 so0 $1 => 1pa55w0rd1 + +Custom wordlist can be particularly useful when using not normally found words such as slang as well as using already cracked passwords. + +Generating Suboptimal Rules and Words +----------------------------------------- + +While `rulegen.py` attempts to generate and record only the best source words and passwords, there may be cases when you are interested in more results. Use `--morewords` and `--morerules` flags to generate words and rules which may exceed optimal edit distance: + + $ python rulegen.py -q --verbose --password '$m0n3y$' --morerules --morewords + [*] Using Enchant 'aspell' module. For best results please install + 'aspell' module language dictionaries. + [*] Analyzing password: $m0n3y$ + [+] money => ^$ so0 se3 $$ => $m0n3y$ + [+] moneys => ^$ so0 se3 o6$ => $m0n3y$ + [+] mingy => ^$ si0 sg3 $$ => $m0n3y$ + [+] many => ^$ sa0 i43 $$ => $m0n3y$ + [+] Mooney => sM$ o1m so0 se3 $$ => $m0n3y$ + +It is possible to further expand generated words using `--maxworddist` and `--maxwords` flags. Similarly, you can produce more rules using `--maxrulelen` and `--maxrules` flags. + +Disabling Advanced Engines +------------------------------ + +`rulegen.py` includes a number of advanced engines to generate better quality words and rules. It is possible to disable them to observe the difference (or if they are causing issues) using `--simplewords` and `--simplerules` flags. Let's observe how both source words and rules change with these flags on: + + $ python rulegen.py -q --verbose --password '$m0n3y$' --simplewords --simplerules + [*] Using Enchant 'aspell' module. For best results please install + 'aspell' module language dictionaries. + [*] Analyzing password: $m0n3y$ + [-] Meany => {edit distance suboptimal: 5 (4)} => $m0n3y$ + [+] many => i0$ o20 i43 i6$ => $m0n3y$ + [+] mingy => i0$ o20 o43 i6$ => $m0n3y$ + [+] money => i0$ o20 o43 i6$ => $m0n3y$ + [+] mangy => i0$ o20 o43 i6$ => $m0n3y$ + [+] manky => i0$ o20 o43 i6$ => $m0n3y$ + +Notice the quality of generated words and rules was reduced significantly with words like 'manky' having less relationship to the actual source word 'money'. At the same time, generated rules were reduced to simple insertions, deletions and replacements. + +Processing password lists +----------------------------- + +Now that you have mastered all of the different flags and switches, we can attempt to generate words and rules for a collection of passwords. Let's generate a text file `korelogic.txt` containing the following fairly complex test passwords: + + &~defcon + '#(4)\ + August19681 + '&a123456 + 10-D'Ann + ~|Bailey + Krist0f3r + f@cebOOK + Nuclear$( + zxcvbn2010! + 13Hark's + NjB3qqm + Sydney93? + antalya%] + Annl05de + ;-Fluffy + +Now let's observe `rulegen.py` analysis by simply specifying the password file as the first argument: + + $ python rulegen.py korelogic.txt -q + [*] Using Enchant 'aspell' module. For best results please install + 'aspell' module language dictionaries. + [*] Analyzing passwords file: korelogic.txt: + [*] Press Ctrl-C to end execution and generate statistical analysis. + [*] Saving rules to analysis.rule + [*] Saving words to analysis.word + [*] Finished processing 16 passwords in 1.00 seconds at the rate of 15.94 p/sec + [*] Generating statistics for [analysis] rules and words. + [-] Skipped 0 all numeric passwords (0.00%) + [-] Skipped 2 passwords with less than 25% alpha characters (12.50%) + [-] Skipped 0 passwords with non ascii characters (0.00%) + + [*] Top 10 rules + [+] ^3 ^1 o4r - 3 (2.00%) + [+] i61 i79 i86 i98 oA1 - 2 (1.00%) + [+] ^- ^0 ^1 i4' o5A - 2 (1.00%) + [+] sS1 i13 T2 - 1 (0.00%) + [+] i61 se9 i86 i98 oA1 - 1 (0.00%) + [+] o61 i79 i86 i98 oA1 - 1 (0.00%) + [+] ^- ^0 ^1 so' i5A - 1 (0.00%) + [+] D3 si0 i55 $e - 1 (0.00%) + [+] i61 i79 se6 i98 oA1 - 1 (0.00%) + [+] i3a o5y o6a i7% o8] - 1 (0.00%) + + [*] Top 10 words + [+] Analyze - 1 (0.00%) + [+] defcon - 1 (0.00%) + [+] Kristen - 1 (0.00%) + [+] Bailey - 1 (0.00%) + [+] Augusts - 1 (0.00%) + [+] Annelid - 1 (0.00%) + [+] Hack's - 1 (0.00%) + [+] antlers - 1 (0.00%) + [+] antelope - 1 (0.00%) + [+] xxxv - 1 (0.00%) + +Using all default settings we were able to produce several high quality rules. The application displays some basic Top 10 rules and words statistics. All of the generated rules and words are saved using basename 'analysis' by default: + +* analysis.word - unsorted and ununiqued source words +* analysis-sorted.word - occurrence sorted and unique source words +* analysis.rule - unsorted and ununiqued rules +* analysis-sorted.rule - occurrence sorted and unique rules + +Notice that several passwords such as '#(4)\ and '&a123456 were skipped because they do not have sufficient characteristics to be processed. Other than alpha character count, the program will skip all numeric passwords and passwords containing non-ASCII characters. The latter is due to a bug in the Enchant engine which I hope to fix in the future thus allowing word processing of many languages. + +Specifying output basename +------------------------------ + +As previously mentioned `rulegen.py` saves output files using the 'analysis' basename by default. You can change file basename with the `--basename` or `-b` flag as follows: + + $ python rulegen.py korelogic.txt -q -b korelogic + [*] Using Enchant 'aspell' module. For best results please install + 'aspell' module language dictionaries. + [*] Analyzing passwords file: korelogic.txt: + [*] Press Ctrl-C to end execution and generate statistical analysis. + [*] Saving rules to korelogic.rule + [*] Saving words to korelogic.word + + +Debugging rules +-------------------- + +There may be situations where you run into issues generating rules for the Hashcat password cracker. `rulegen.py` includes the `--hashcat` flag to validate generated words and rules using hashcat itself running in --stdout mode. In order for this mode to work correctly, you must download the latest version of hashcat-cli and edit the `HASHCAT_PATH` variable in the source. For example, at the time of this writing I have placed the hashcat-0.## folder in the PACK directory and defined `HASHCAT_PATH` as 'hashcat-0.##/'. + +You can also observe the inner workings of the rule generation engine with the `--debug` flag. Don't worry about messages of certain rule failings, this is the result of the halting problem solver trying to find an optimal and valid solution. + +Conclusion +============== + +While this guide introduces a number of methods to analyze passwords, reverse rules and generate masks, there are a number of other tricks that are waiting for you to discover. I would be excited if you told me about some unusual use or suggestions for any of the covered tools. + +Happy Cracking! + + -Peter \ No newline at end of file diff --git a/PACK/enchant/__init__.py b/PACK/enchant/__init__.py new file mode 100644 index 0000000..46f7487 --- /dev/null +++ b/PACK/enchant/__init__.py @@ -0,0 +1,907 @@ +# pyenchant +# +# Copyright (C) 2004-2011, Ryan Kelly +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPsE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# +# In addition, as a special exception, you are +# given permission to link the code of this program with +# non-LGPL Spelling Provider libraries (eg: a MSFT Office +# spell checker backend) and distribute linked combinations including +# the two. You must obey the GNU Lesser General Public License in all +# respects for all of the code used other than said providers. If you modify +# this file, you may extend this exception to your version of the +# file, but you are not obligated to do so. If you do not wish to +# do so, delete this exception statement from your version. +# +""" +enchant: Access to the enchant spellchecking library +===================================================== + +This module provides several classes for performing spell checking +via the Enchant spellchecking library. For more details on Enchant, +visit the project website: + + http://www.abisource.com/enchant/ + +Spellchecking is performed using 'Dict' objects, which represent +a language dictionary. Their use is best demonstrated by a quick +example:: + + >>> import enchant + >>> d = enchant.Dict("en_US") # create dictionary for US English + >>> d.check("enchant") + True + >>> d.check("enchnt") + False + >>> d.suggest("enchnt") + ['enchant', 'enchants', 'enchanter', 'penchant', 'incant', 'enchain', 'enchanted'] + +Languages are identified by standard string tags such as "en" (English) +and "fr" (French). Specific language dialects can be specified by +including an additional code - for example, "en_AU" refers to Australian +English. The later form is preferred as it is more widely supported. + +To check whether a dictionary exists for a given language, the function +'dict_exists' is available. Dictionaries may also be created using the +function 'request_dict'. + +A finer degree of control over the dictionaries and how they are created +can be obtained using one or more 'Broker' objects. These objects are +responsible for locating dictionaries for a specific language. + +In Python 2.x, unicode strings are supported transparently in the +standard manner - if a unicode string is given as an argument, the +result will be a unicode string. Note that Enchant works in UTF-8 +internally, so passing an ASCII string to a dictionary for a language +requiring Unicode may result in UTF-8 strings being returned. + +In Python 3.x unicode strings are expected throughout. Bytestrings +should not be passed into any functions. + +Errors that occur in this module are reported by raising subclasses +of 'Error'. + +""" +_DOC_ERRORS = ['enchnt', 'enchnt', 'fr'] + +# Make version info available +__ver_major__ = 1 +__ver_minor__ = 6 +__ver_patch__ = 6 +__ver_sub__ = "" +__version__ = "%d.%d.%d%s" % (__ver_major__, __ver_minor__, + __ver_patch__, __ver_sub__) + +import os + +try: + from enchant import _enchant as _e +except ImportError: + if not os.environ.get("PYENCHANT_IGNORE_MISSING_LIB", False): + raise + _e = None + +from enchant.errors import * +from enchant.utils import EnchantStr, get_default_language +from enchant.pypwl import PyPWL + +# Due to the unfortunate name collision between the enchant "tokenize" module +# and the stdlib "tokenize" module, certain values of sys.path can cause +# the former to override the latter and break the "warnings" module. +# This hacks around it by making a dumming "warnings" module. +try: + import warnings +except ImportError: + class warnings(object): + def warn(self, *args, **kwds): + pass + + + warnings = warnings() + + +class ProviderDesc(object): + """Simple class describing an Enchant provider. + + Each provider has the following information associated with it: + + * name: Internal provider name (e.g. "aspell") + * desc: Human-readable description (e.g. "Aspell Provider") + * file: Location of the library containing the provider + + """ + _DOC_ERRORS = ["desc"] + + def __init__(self, name, desc, file): + self.name = name + self.desc = desc + self.file = file + + def __str__(self): + return "" % self.desc + + def __repr__(self): + return str(self) + + def __eq__(self, pd): + """Equality operator on ProviderDesc objects.""" + return (self.name == pd.name and \ + self.desc == pd.desc and \ + self.file == pd.file) + + def __hash__(self): + """Hash operator on ProviderDesc objects.""" + return hash(self.name + self.desc + self.file) + + +class _EnchantObject(object): + """Base class for enchant objects. + + This class implements some general functionality for interfacing with + the '_enchant' C-library in a consistent way. All public objects + from the 'enchant' module are subclasses of this class. + + All enchant objects have an attribute '_this' which contains the + pointer to the underlying C-library object. The method '_check_this' + can be called to ensure that this point is not None, raising an + exception if it is. + """ + + def __init__(self): + """_EnchantObject constructor.""" + self._this = None + # To be importable when enchant C lib is missing, we need + # to create a dummy default broker. + if _e is not None: + self._init_this() + + def _check_this(self, msg=None): + """Check that self._this is set to a pointer, rather than None.""" + if msg is None: + msg = "%s unusable: the underlying C-library object has been freed." + msg = msg % (self.__class__.__name__,) + if self._this is None: + raise Error(msg) + + def _init_this(self): + """Initialise the underlying C-library object pointer.""" + raise NotImplementedError + + def _raise_error(self, default="Unspecified Error", eclass=Error): + """Raise an exception based on available error messages. + + This method causes an Error to be raised. Subclasses should + override it to retrieve an error indication from the underlying + API if possible. If such a message cannot be retrieved, the + argument value is used. The class of the exception + can be specified using the argument + """ + raise eclass(default) + + _raise_error._DOC_ERRORS = ["eclass"] + + def __getstate__(self): + """Customize pickling of PyEnchant objects. + + Since it's not safe for multiple objects to share the same C-library + object, we make sure it's unset when pickling. + """ + state = self.__dict__.copy() + state["_this"] = None + return state + + def __setstate__(self, state): + self.__dict__.update(state) + self._init_this() + + +class Broker(_EnchantObject): + """Broker object for the Enchant spellchecker. + + Broker objects are responsible for locating and managing dictionaries. + Unless custom functionality is required, there is no need to use Broker + objects directly. The 'enchant' module provides a default broker object + so that 'Dict' objects can be created directly. + + The most important methods of this class include: + + * dict_exists: check existence of a specific language dictionary + * request_dict: obtain a dictionary for specific language + * set_ordering: specify which dictionaries to try for for a + given language. + + """ + + def __init__(self): + """Broker object constructor. + + This method is the constructor for the 'Broker' object. No + arguments are required. + """ + _EnchantObject.__init__(self) + + def _init_this(self): + self._this = _e.broker_init() + if not self._this: + raise Error("Could not initialise an enchant broker.") + + def __del__(self): + """Broker object destructor.""" + if _e is not None: + self._free() + + def _raise_error(self, default="Unspecified Error", eclass=Error): + """Overrides _EnchantObject._raise_error to check broker errors.""" + err = _e.broker_get_error(self._this) + if err == "" or err is None: + raise eclass(default) + raise eclass(err) + + def _free(self): + """Free system resource associated with a Broker object. + + This method can be called to free the underlying system resources + associated with a Broker object. It is called automatically when + the object is garbage collected. If called explicitly, the + Broker and any associated Dict objects must no longer be used. + """ + if self._this is not None: + _e.broker_free(self._this) + self._this = None + + def request_dict(self, tag=None): + """Request a Dict object for the language specified by . + + This method constructs and returns a Dict object for the + requested language. 'tag' should be a string of the appropriate + form for specifying a language, such as "fr" (French) or "en_AU" + (Australian English). The existence of a specific language can + be tested using the 'dict_exists' method. + + If is not given or is None, an attempt is made to determine + the current language in use. If this cannot be determined, Error + is raised. + + NOTE: this method is functionally equivalent to calling the Dict() + constructor and passing in the argument. + + """ + return Dict(tag, self) + + request_dict._DOC_ERRORS = ["fr"] + + def _request_dict_data(self, tag): + """Request raw C pointer data for a dictionary. + + This method call passes on the call to the C library, and does + some internal bookkeeping. + """ + self._check_this() + tag = EnchantStr(tag) + new_dict = _e.broker_request_dict(self._this, tag.encode()) + if new_dict is None: + eStr = "Dictionary for language '%s' could not be found" + self._raise_error(eStr % (tag,), DictNotFoundError) + return new_dict + + def request_pwl_dict(self, pwl): + """Request a Dict object for a personal word list. + + This method behaves as 'request_dict' but rather than returning + a dictionary for a specific language, it returns a dictionary + referencing a personal word list. A personal word list is a file + of custom dictionary entries, one word per line. + """ + self._check_this() + pwl = EnchantStr(pwl) + new_dict = _e.broker_request_pwl_dict(self._this, pwl.encode()) + if new_dict is None: + eStr = "Personal Word List file '%s' could not be loaded" + self._raise_error(eStr % (pwl,)) + d = Dict(False) + d._switch_this(new_dict, self) + return d + + def _free_dict(self, dict): + """Free memory associated with a dictionary. + + This method frees system resources associated with a Dict object. + It is equivalent to calling the object's 'free' method. Once this + method has been called on a dictionary, it must not be used again. + """ + self._check_this() + _e.broker_free_dict(self._this, dict._this) + dict._this = None + dict._broker = None + + def dict_exists(self, tag): + """Check availability of a dictionary. + + This method checks whether there is a dictionary available for + the language specified by 'tag'. It returns True if a dictionary + is available, and False otherwise. + """ + self._check_this() + tag = EnchantStr(tag) + val = _e.broker_dict_exists(self._this, tag.encode()) + return bool(val) + + def set_ordering(self, tag, ordering): + """Set dictionary preferences for a language. + + The Enchant library supports the use of multiple dictionary programs + and multiple languages. This method specifies which dictionaries + the broker should prefer when dealing with a given language. 'tag' + must be an appropriate language specification and 'ordering' is a + string listing the dictionaries in order of preference. For example + a valid ordering might be "aspell,myspell,ispell". + The value of 'tag' can also be set to "*" to set a default ordering + for all languages for which one has not been set explicitly. + """ + self._check_this() + tag = EnchantStr(tag) + ordering = EnchantStr(ordering) + _e.broker_set_ordering(self._this, tag.encode(), ordering.encode()) + + def describe(self): + """Return list of provider descriptions. + + This method returns a list of descriptions of each of the + dictionary providers available. Each entry in the list is a + ProviderDesc object. + """ + self._check_this() + self.__describe_result = [] + _e.broker_describe(self._this, self.__describe_callback) + return [ProviderDesc(*r) for r in self.__describe_result] + + def __describe_callback(self, name, desc, file): + """Collector callback for dictionary description. + + This method is used as a callback into the _enchant function + 'enchant_broker_describe'. It collects the given arguments in + a tuple and appends them to the list '__describe_result'. + """ + s = EnchantStr("") + name = s.decode(name) + desc = s.decode(desc) + file = s.decode(file) + self.__describe_result.append((name, desc, file)) + + def list_dicts(self): + """Return list of available dictionaries. + + This method returns a list of dictionaries available to the + broker. Each entry in the list is a two-tuple of the form: + + (tag,provider) + + where is the language lag for the dictionary and + is a ProviderDesc object describing the provider + through which that dictionary can be obtained. + """ + self._check_this() + self.__list_dicts_result = [] + _e.broker_list_dicts(self._this, self.__list_dicts_callback) + return [(r[0], ProviderDesc(*r[1])) for r in self.__list_dicts_result] + + def __list_dicts_callback(self, tag, name, desc, file): + """Collector callback for listing dictionaries. + + This method is used as a callback into the _enchant function + 'enchant_broker_list_dicts'. It collects the given arguments into + an appropriate tuple and appends them to '__list_dicts_result'. + """ + s = EnchantStr("") + tag = s.decode(tag) + name = s.decode(name) + desc = s.decode(desc) + file = s.decode(file) + self.__list_dicts_result.append((tag, (name, desc, file))) + + def list_languages(self): + """List languages for which dictionaries are available. + + This function returns a list of language tags for which a + dictionary is available. + """ + langs = [] + for (tag, prov) in self.list_dicts(): + if tag not in langs: + langs.append(tag) + return langs + + def __describe_dict(self, dict_data): + """Get the description tuple for a dict data object. + must be a C-library pointer to an enchant dictionary. + The return value is a tuple of the form: + (,,,) + """ + # Define local callback function + cb_result = [] + + def cb_func(tag, name, desc, file): + s = EnchantStr("") + tag = s.decode(tag) + name = s.decode(name) + desc = s.decode(desc) + file = s.decode(file) + cb_result.append((tag, name, desc, file)) + + # Actually call the describer function + _e.dict_describe(dict_data, cb_func) + return cb_result[0] + + __describe_dict._DOC_ERRORS = ["desc"] + + def get_param(self, name): + """Get the value of a named parameter on this broker. + + Parameters are used to provide runtime information to individual + provider backends. See the method 'set_param' for more details. + """ + name = EnchantStr(name) + return name.decode(_e.broker_get_param(self._this, name.encode())) + + get_param._DOC_ERRORS = ["param"] + + def set_param(self, name, value): + """Set the value of a named parameter on this broker. + + Parameters are used to provide runtime information to individual + provider backends. For example, the myspell provider will search + any directories given in the "enchant.myspell.dictionary.path" + parameter when looking for its dictionary files. + """ + name = EnchantStr(name) + value = EnchantStr(value) + _e.broker_set_param(self._this, name.encode(), value.encode()) + + +class Dict(_EnchantObject): + """Dictionary object for the Enchant spellchecker. + + Dictionary objects are responsible for checking the spelling of words + and suggesting possible corrections. Each dictionary is owned by a + Broker object, but unless a new Broker has explicitly been created + then this will be the 'enchant' module default Broker and is of little + interest. + + The important methods of this class include: + + * check(): check whether a word id spelled correctly + * suggest(): suggest correct spellings for a word + * add(): add a word to the user's personal dictionary + * remove(): add a word to the user's personal exclude list + * add_to_session(): add a word to the current spellcheck session + * store_replacement(): indicate a replacement for a given word + + Information about the dictionary is available using the following + attributes: + + * tag: the language tag of the dictionary + * provider: a ProviderDesc object for the dictionary provider + + """ + + def __init__(self, tag=None, broker=None): + """Dict object constructor. + + A dictionary belongs to a specific language, identified by the + string . If the tag is not given or is None, an attempt to + determine the language currently in use is made using the 'locale' + module. If the current language cannot be determined, Error is raised. + + If is instead given the value of False, a 'dead' Dict object + is created without any reference to a language. This is typically + only useful within PyEnchant itself. Any other non-string value + for raises Error. + + Each dictionary must also have an associated Broker object which + obtains the dictionary information from the underlying system. This + may be specified using . If not given, the default broker + is used. + """ + # Initialise misc object attributes to None + self.provider = None + # If no tag was given, use the default language + if tag is None: + tag = get_default_language() + if tag is None: + err = "No tag specified and default language could not " + err = err + "be determined." + raise Error(err) + self.tag = tag + # If no broker was given, use the default broker + if broker is None: + broker = _broker + self._broker = broker + # Now let the superclass initialise the C-library object + _EnchantObject.__init__(self) + + def _init_this(self): + # Create dead object if False was given. + # Otherwise, use the broker to get C-library pointer data. + self._this = None + if self.tag: + this = self._broker._request_dict_data(self.tag) + self._switch_this(this, self._broker) + + def __del__(self): + """Dict object destructor.""" + # Calling free() might fail if python is shutting down + try: + self._free() + except AttributeError: + pass + + def _switch_this(self, this, broker): + """Switch the underlying C-library pointer for this object. + + As all useful state for a Dict is stored by the underlying C-library + pointer, it is very convenient to allow this to be switched at + run-time. Pass a new dict data object into this method to affect + the necessary changes. The creating Broker object (at the Python + level) must also be provided. + + This should *never* *ever* be used by application code. It's + a convenience for developers only, replacing the clunkier + parameter to __init__ from earlier versions. + """ + # Free old dict data + Dict._free(self) + # Hook in the new stuff + self._this = this + self._broker = broker + # Update object properties + desc = self.__describe(check_this=False) + self.tag = desc[0] + self.provider = ProviderDesc(*desc[1:]) + + _switch_this._DOC_ERRORS = ["init"] + + def _check_this(self, msg=None): + """Extend _EnchantObject._check_this() to check Broker validity. + + It is possible for the managing Broker object to be freed without + freeing the Dict. Thus validity checking must take into account + self._broker._this as well as self._this. + """ + if self._broker is None or self._broker._this is None: + self._this = None + _EnchantObject._check_this(self, msg) + + def _raise_error(self, default="Unspecified Error", eclass=Error): + """Overrides _EnchantObject._raise_error to check dict errors.""" + err = _e.dict_get_error(self._this) + if err == "" or err is None: + raise eclass(default) + raise eclass(err) + + def _free(self): + """Free the system resources associated with a Dict object. + + This method frees underlying system resources for a Dict object. + Once it has been called, the Dict object must no longer be used. + It is called automatically when the object is garbage collected. + """ + if self._broker is not None and self._this is not None: + self._broker._free_dict(self) + + def check(self, word): + """Check spelling of a word. + + This method takes a word in the dictionary language and returns + True if it is correctly spelled, and false otherwise. + """ + self._check_this() + word = EnchantStr(word) + val = _e.dict_check(self._this, word.encode()) + if val == 0: + return True + if val > 0: + return False + self._raise_error() + + def suggest(self, word): + """Suggest possible spellings for a word. + + This method tries to guess the correct spelling for a given + word, returning the possibilities in a list. + """ + self._check_this() + word = EnchantStr(word) + suggs = _e.dict_suggest(self._this, word.encode()) + return [word.decode(w) for w in suggs] + + def add(self, word): + """Add a word to the user's personal word list.""" + self._check_this() + word = EnchantStr(word) + _e.dict_add(self._this, word.encode()) + + def remove(self, word): + """Add a word to the user's personal exclude list.""" + self._check_this() + word = EnchantStr(word) + _e.dict_remove(self._this, word.encode()) + + def add_to_pwl(self, word): + """Add a word to the user's personal word list.""" + warnings.warn("Dict.add_to_pwl is deprecated, please use Dict.add", + category=DeprecationWarning, stacklevel=2) + self._check_this() + word = EnchantStr(word) + _e.dict_add_to_pwl(self._this, word.encode()) + + def add_to_session(self, word): + """Add a word to the session personal list.""" + self._check_this() + word = EnchantStr(word) + _e.dict_add_to_session(self._this, word.encode()) + + def remove_from_session(self, word): + """Add a word to the session exclude list.""" + self._check_this() + word = EnchantStr(word) + _e.dict_remove_from_session(self._this, word.encode()) + + def is_added(self, word): + """Check whether a word is in the personal word list.""" + self._check_this() + word = EnchantStr(word) + return _e.dict_is_added(self._this, word.encode()) + + def is_removed(self, word): + """Check whether a word is in the personal exclude list.""" + self._check_this() + word = EnchantStr(word) + return _e.dict_is_removed(self._this, word.encode()) + + def is_in_session(self, word): + """Check whether a word is in the session list.""" + warnings.warn("Dict.is_in_session is deprecated, " \ + "please use Dict.is_added", + category=DeprecationWarning, stacklevel=2) + self._check_this() + word = EnchantStr(word) + return _e.dict_is_in_session(self._this, word.encode()) + + def store_replacement(self, mis, cor): + """Store a replacement spelling for a miss-spelled word. + + This method makes a suggestion to the spellchecking engine that the + miss-spelled word is in fact correctly spelled as . Such + a suggestion will typically mean that appears early in the + list of suggested spellings offered for later instances of . + """ + if not mis: + raise ValueError("can't store replacement for an empty string") + if not cor: + raise ValueError("can't store empty string as a replacement") + self._check_this() + mis = EnchantStr(mis) + cor = EnchantStr(cor) + _e.dict_store_replacement(self._this, mis.encode(), cor.encode()) + + store_replacement._DOC_ERRORS = ["mis", "mis"] + + def __describe(self, check_this=True): + """Return a tuple describing the dictionary. + + This method returns a four-element tuple describing the underlying + spellchecker system providing the dictionary. It will contain the + following strings: + + * language tag + * name of dictionary provider + * description of dictionary provider + * dictionary file + + Direct use of this method is not recommended - instead, access this + information through the 'tag' and 'provider' attributes. + """ + if check_this: + self._check_this() + _e.dict_describe(self._this, self.__describe_callback) + return self.__describe_result + + def __describe_callback(self, tag, name, desc, file): + """Collector callback for dictionary description. + + This method is used as a callback into the _enchant function + 'enchant_dict_describe'. It collects the given arguments in + a tuple and stores them in the attribute '__describe_result'. + """ + s = EnchantStr("") + tag = s.decode(tag) + name = s.decode(name) + desc = s.decode(desc) + file = s.decode(file) + self.__describe_result = (tag, name, desc, file) + + +class DictWithPWL(Dict): + """Dictionary with separately-managed personal word list. + + NOTE: As of version 1.4.0, enchant manages a per-user pwl and + exclude list. This class is now only needed if you want + to explicitly maintain a separate word list in addition to + the default one. + + This class behaves as the standard Dict class, but also manages a + personal word list stored in a separate file. The file must be + specified at creation time by the 'pwl' argument to the constructor. + Words added to the dictionary are automatically appended to the pwl file. + + A personal exclude list can also be managed, by passing another filename + to the constructor in the optional 'pel' argument. If this is not given, + requests to exclude words are ignored. + + If either 'pwl' or 'pel' are None, an in-memory word list is used. + This will prevent calls to add() and remove() from affecting the user's + default word lists. + + The Dict object managing the PWL is available as the 'pwl' attribute. + The Dict object managing the PEL is available as the 'pel' attribute. + + To create a DictWithPWL from the user's default language, use None + as the 'tag' argument. + """ + _DOC_ERRORS = ["pel", "pel", "PEL", "pel"] + + def __init__(self, tag, pwl=None, pel=None, broker=None): + """DictWithPWL constructor. + + The argument 'pwl', if not None, names a file containing the + personal word list. If this file does not exist, it is created + with default permissions. + + The argument 'pel', if not None, names a file containing the personal + exclude list. If this file does not exist, it is created with + default permissions. + """ + Dict.__init__(self, tag, broker) + if pwl is not None: + if not os.path.exists(pwl): + f = open(pwl, "wt") + f.close() + del f + self.pwl = self._broker.request_pwl_dict(pwl) + else: + self.pwl = PyPWL() + if pel is not None: + if not os.path.exists(pel): + f = open(pel, "wt") + f.close() + del f + self.pel = self._broker.request_pwl_dict(pel) + else: + self.pel = PyPWL() + + def _check_this(self, msg=None): + """Extend Dict._check_this() to check PWL validity.""" + if self.pwl is None: + self._free() + if self.pel is None: + self._free() + Dict._check_this(self, msg) + self.pwl._check_this(msg) + self.pel._check_this(msg) + + def _free(self): + """Extend Dict._free() to free the PWL as well.""" + if self.pwl is not None: + self.pwl._free() + self.pwl = None + if self.pel is not None: + self.pel._free() + self.pel = None + Dict._free(self) + + def check(self, word): + """Check spelling of a word. + + This method takes a word in the dictionary language and returns + True if it is correctly spelled, and false otherwise. It checks + both the dictionary and the personal word list. + """ + if self.pel.check(word): + return False + if self.pwl.check(word): + return True + if Dict.check(self, word): + return True + return False + + def suggest(self, word): + """Suggest possible spellings for a word. + + This method tries to guess the correct spelling for a given + word, returning the possibilities in a list. + """ + suggs = Dict.suggest(self, word) + suggs.extend([w for w in self.pwl.suggest(word) if w not in suggs]) + for i in range(len(suggs) - 1, -1, -1): + if self.pel.check(suggs[i]): + del suggs[i] + return suggs + + def add(self, word): + """Add a word to the associated personal word list. + + This method adds the given word to the personal word list, and + automatically saves the list to disk. + """ + self._check_this() + self.pwl.add(word) + self.pel.remove(word) + + def remove(self, word): + """Add a word to the associated exclude list.""" + self._check_this() + self.pwl.remove(word) + self.pel.add(word) + + def add_to_pwl(self, word): + """Add a word to the associated personal word list. + + This method adds the given word to the personal word list, and + automatically saves the list to disk. + """ + self._check_this() + self.pwl.add_to_pwl(word) + self.pel.remove(word) + + def is_added(self, word): + """Check whether a word is in the personal word list.""" + self._check_this() + return self.pwl.is_added(word) + + def is_removed(self, word): + """Check whether a word is in the personal exclude list.""" + self._check_this() + return self.pel.is_added(word) + + +## Create a module-level default broker object, and make its important +## methods available at the module level. +_broker = Broker() +request_dict = _broker.request_dict +request_pwl_dict = _broker.request_pwl_dict +dict_exists = _broker.dict_exists +list_dicts = _broker.list_dicts +list_languages = _broker.list_languages +get_param = _broker.get_param +set_param = _broker.set_param + + +# Expose the "get_version" function. +def get_enchant_version(): + """Get the version string for the underlying enchant library.""" + return _e.get_version() + + +# Run unit tests when called from comand-line +if __name__ == "__main__": + import sys + import enchant.tests + + res = enchant.tests.runtestsuite() + if len(res.errors) > 0 or len(res.failures) > 0: + sys.exit(1) + sys.exit(0) diff --git a/PACK/enchant/_enchant.py b/PACK/enchant/_enchant.py new file mode 100644 index 0000000..3871bbd --- /dev/null +++ b/PACK/enchant/_enchant.py @@ -0,0 +1,366 @@ +# pyenchant +# +# Copyright (C) 2004-2008, Ryan Kelly +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPsE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# +# In addition, as a special exception, you are +# given permission to link the code of this program with +# non-LGPL Spelling Provider libraries (eg: a MSFT Office +# spell checker backend) and distribute linked combinations including +# the two. You must obey the GNU Lesser General Public License in all +# respects for all of the code used other than said providers. If you modify +# this file, you may extend this exception to your version of the +# file, but you are not obligated to do so. If you do not wish to +# do so, delete this exception statement from your version. +# +""" + + enchant._enchant: ctypes-based wrapper for enchant C library + + This module implements the low-level interface to the underlying + C library for enchant. The interface is based on ctypes and tries + to do as little as possible while making the higher-level components + easier to write. + + The following conveniences are provided that differ from the underlying + C API: + + * the "enchant" prefix has been removed from all functions, since + python has a proper module system + * callback functions do not take a user_data argument, since + python has proper closures that can manage this internally + * string lengths are not passed into functions such as dict_check, + since python strings know how long they are + +""" + +import sys, os, os.path +from ctypes import * +from ctypes.util import find_library + +from enchant import utils +from enchant.errors import * +from enchant.utils import unicode + +# Locate and load the enchant dll. +# We've got several options based on the host platform. + +e = None + + +def _e_path_possibilities(): + """Generator yielding possible locations of the enchant library.""" + yield os.environ.get("PYENCHANT_LIBRARY_PATH") + yield find_library("enchant") + yield find_library("libenchant") + yield find_library("libenchant-1") + if sys.platform == 'darwin': + # enchant lib installed by macports + yield "/opt/local/lib/libenchant.dylib" + + +# On win32 we ship a bundled version of the enchant DLLs. +# Use them if they're present. +if sys.platform == "win32": + e_path = None + try: + e_path = utils.get_resource_filename("libenchant.dll") + except (Error, ImportError): + try: + e_path = utils.get_resource_filename("libenchant-1.dll") + except (Error, ImportError): + pass + if e_path is not None: + # We need to use LoadLibraryEx with LOAD_WITH_ALTERED_SEARCH_PATH so + # that we don't accidentally suck in other versions of e.g. glib. + if not isinstance(e_path, unicode): + e_path = unicode(e_path, sys.getfilesystemencoding()) + LoadLibraryEx = windll.kernel32.LoadLibraryExW + LOAD_WITH_ALTERED_SEARCH_PATH = 0x00000008 + e_handle = LoadLibraryEx(e_path, None, LOAD_WITH_ALTERED_SEARCH_PATH) + if not e_handle: + raise WinError() + e = CDLL(e_path, handle=e_handle) + +# On darwin we ship a bundled version of the enchant DLLs. +# Use them if they're present. +if e is None and sys.platform == "darwin": + try: + e_path = utils.get_resource_filename("lib/libenchant.1.dylib") + except (Error, ImportError): + pass + else: + # Enchant doesn't natively support relocatable binaries on OSX. + # We fake it by patching the enchant source to expose a char**, which + # we can write the runtime path into ourelves. + e = CDLL(e_path) + try: + e_dir = os.path.dirname(os.path.dirname(e_path)) + prefix_dir = POINTER(c_char_p).in_dll(e, "enchant_prefix_dir_p") + prefix_dir.contents = c_char_p(e_dir) + except AttributeError: + e = None + +# Not found yet, search various standard system locations. +if e is None: + for e_path in _e_path_possibilities(): + if e_path is not None: + try: + e = cdll.LoadLibrary(e_path) + except OSError: + pass + else: + break + +# No usable enchant install was found :-( +if e is None: + raise ImportError("enchant C library not found") + + +# Define various callback function types + +def CALLBACK(restype, *argtypes): + """Factory for generating callback function prototypes. + + This is factored into a factory so I can easily change the definition + for experimentation or debugging. + """ + return CFUNCTYPE(restype, *argtypes) + + +t_broker_desc_func = CALLBACK(None, c_char_p, c_char_p, c_char_p, c_void_p) +t_dict_desc_func = CALLBACK(None, c_char_p, c_char_p, c_char_p, c_char_p, c_void_p) + +# Simple typedefs for readability + +t_broker = c_void_p +t_dict = c_void_p + +# Now we can define the types of each function we are going to use + +broker_init = e.enchant_broker_init +broker_init.argtypes = [] +broker_init.restype = t_broker + +broker_free = e.enchant_broker_free +broker_free.argtypes = [t_broker] +broker_free.restype = None + +broker_request_dict = e.enchant_broker_request_dict +broker_request_dict.argtypes = [t_broker, c_char_p] +broker_request_dict.restype = t_dict + +broker_request_pwl_dict = e.enchant_broker_request_pwl_dict +broker_request_pwl_dict.argtypes = [t_broker, c_char_p] +broker_request_pwl_dict.restype = t_dict + +broker_free_dict = e.enchant_broker_free_dict +broker_free_dict.argtypes = [t_broker, t_dict] +broker_free_dict.restype = None + +broker_dict_exists = e.enchant_broker_dict_exists +broker_dict_exists.argtypes = [t_broker, c_char_p] +broker_free_dict.restype = c_int + +broker_set_ordering = e.enchant_broker_set_ordering +broker_set_ordering.argtypes = [t_broker, c_char_p, c_char_p] +broker_set_ordering.restype = None + +broker_get_error = e.enchant_broker_get_error +broker_get_error.argtypes = [t_broker] +broker_get_error.restype = c_char_p + +broker_describe1 = e.enchant_broker_describe +broker_describe1.argtypes = [t_broker, t_broker_desc_func, c_void_p] +broker_describe1.restype = None + + +def broker_describe(broker, cbfunc): + def cbfunc1(*args): + cbfunc(*args[:-1]) + + broker_describe1(broker, t_broker_desc_func(cbfunc1), None) + + +broker_list_dicts1 = e.enchant_broker_list_dicts +broker_list_dicts1.argtypes = [t_broker, t_dict_desc_func, c_void_p] +broker_list_dicts1.restype = None + + +def broker_list_dicts(broker, cbfunc): + def cbfunc1(*args): + cbfunc(*args[:-1]) + + broker_list_dicts1(broker, t_dict_desc_func(cbfunc1), None) + + +try: + broker_get_param = e.enchant_broker_get_param +except AttributeError: + # Make the lookup error occur at runtime + def broker_get_param(broker, param_name): + return e.enchant_broker_get_param(param_name) +else: + broker_get_param.argtypes = [t_broker, c_char_p] + broker_get_param.restype = c_char_p + +try: + broker_set_param = e.enchant_broker_set_param +except AttributeError: + # Make the lookup error occur at runtime + def broker_set_param(broker, param_name): + return e.enchant_broker_set_param(param_name) +else: + broker_set_param.argtypes = [t_broker, c_char_p, c_char_p] + broker_set_param.restype = None + +try: + get_version = e.enchant_get_version +except AttributeError: + # Make the lookup error occur at runtime + def get_version(): + return e.enchant_get_version() +else: + get_version.argtypes = [] + get_version.restype = c_char_p + +dict_check1 = e.enchant_dict_check +dict_check1.argtypes = [t_dict, c_char_p, c_size_t] +dict_check1.restype = c_int + + +def dict_check(dict, word): + return dict_check1(dict, word, len(word)) + + +dict_suggest1 = e.enchant_dict_suggest +dict_suggest1.argtypes = [t_dict, c_char_p, c_size_t, POINTER(c_size_t)] +dict_suggest1.restype = POINTER(c_char_p) + + +def dict_suggest(dict, word): + numSuggsP = pointer(c_size_t(0)) + suggs_c = dict_suggest1(dict, word, len(word), numSuggsP) + suggs = [] + n = 0 + while n < numSuggsP.contents.value: + suggs.append(suggs_c[n]) + n = n + 1 + if numSuggsP.contents.value > 0: + dict_free_string_list(dict, suggs_c) + return suggs + + +dict_add1 = e.enchant_dict_add +dict_add1.argtypes = [t_dict, c_char_p, c_size_t] +dict_add1.restype = None + + +def dict_add(dict, word): + return dict_add1(dict, word, len(word)) + + +dict_add_to_pwl1 = e.enchant_dict_add +dict_add_to_pwl1.argtypes = [t_dict, c_char_p, c_size_t] +dict_add_to_pwl1.restype = None + + +def dict_add_to_pwl(dict, word): + return dict_add_to_pwl1(dict, word, len(word)) + + +dict_add_to_session1 = e.enchant_dict_add_to_session +dict_add_to_session1.argtypes = [t_dict, c_char_p, c_size_t] +dict_add_to_session1.restype = None + + +def dict_add_to_session(dict, word): + return dict_add_to_session1(dict, word, len(word)) + + +dict_remove1 = e.enchant_dict_remove +dict_remove1.argtypes = [t_dict, c_char_p, c_size_t] +dict_remove1.restype = None + + +def dict_remove(dict, word): + return dict_remove1(dict, word, len(word)) + + +dict_remove_from_session1 = e.enchant_dict_remove_from_session +dict_remove_from_session1.argtypes = [t_dict, c_char_p, c_size_t] +dict_remove_from_session1.restype = c_int + + +def dict_remove_from_session(dict, word): + return dict_remove_from_session1(dict, word, len(word)) + + +dict_is_added1 = e.enchant_dict_is_added +dict_is_added1.argtypes = [t_dict, c_char_p, c_size_t] +dict_is_added1.restype = c_int + + +def dict_is_added(dict, word): + return dict_is_added1(dict, word, len(word)) + + +dict_is_removed1 = e.enchant_dict_is_removed +dict_is_removed1.argtypes = [t_dict, c_char_p, c_size_t] +dict_is_removed1.restype = c_int + + +def dict_is_removed(dict, word): + return dict_is_removed1(dict, word, len(word)) + + +dict_is_in_session1 = e.enchant_dict_is_in_session +dict_is_in_session1.argtypes = [t_dict, c_char_p, c_size_t] +dict_is_in_session1.restype = c_int + + +def dict_is_in_session(dict, word): + return dict_is_in_session1(dict, word, len(word)) + + +dict_store_replacement1 = e.enchant_dict_store_replacement +dict_store_replacement1.argtypes = [t_dict, c_char_p, c_size_t, c_char_p, c_size_t] +dict_store_replacement1.restype = None + + +def dict_store_replacement(dict, mis, cor): + return dict_store_replacement1(dict, mis, len(mis), cor, len(cor)) + + +dict_free_string_list = e.enchant_dict_free_string_list +dict_free_string_list.argtypes = [t_dict, POINTER(c_char_p)] +dict_free_string_list.restype = None + +dict_get_error = e.enchant_dict_get_error +dict_get_error.argtypes = [t_dict] +dict_get_error.restype = c_char_p + +dict_describe1 = e.enchant_dict_describe +dict_describe1.argtypes = [t_dict, t_dict_desc_func, c_void_p] +dict_describe1.restype = None + + +def dict_describe(dict, cbfunc): + def cbfunc1(tag, name, desc, file, data): + cbfunc(tag, name, desc, file) + + dict_describe1(dict, t_dict_desc_func(cbfunc1), None) diff --git a/PACK/enchant/checker/CmdLineChecker.py b/PACK/enchant/checker/CmdLineChecker.py new file mode 100644 index 0000000..fd3be0b --- /dev/null +++ b/PACK/enchant/checker/CmdLineChecker.py @@ -0,0 +1,203 @@ +# pyenchant +# +# Copyright (C) 2004-2008, Ryan Kelly +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# +# In addition, as a special exception, you are +# given permission to link the code of this program with +# non-LGPL Spelling Provider libraries (eg: a MSFT Office +# spell checker backend) and distribute linked combinations including +# the two. You must obey the GNU Lesser General Public License in all +# respects for all of the code used other than said providers. If you modify +# this file, you may extend this exception to your version of the +# file, but you are not obligated to do so. If you do not wish to +# do so, delete this exception statement from your version. +# +""" + + enchant.checker.CmdLineChecker: Command-Line spell checker + + This module provides the class CmdLineChecker, which interactively + spellchecks a piece of text by interacting with the user on the + command line. It can also be run as a script to spellcheck a file. + +""" + +import sys + +from enchant.checker import SpellChecker +from enchant.utils import printf + + +class CmdLineChecker: + """A simple command-line spell checker. + + This class implements a simple command-line spell checker. It must + be given a SpellChecker instance to operate on, and interacts with + the user by printing instructions on stdout and reading commands from + stdin. + """ + _DOC_ERRORS = ["stdout", "stdin"] + + def __init__(self): + self._stop = False + self._checker = None + + def set_checker(self, chkr): + self._checker = chkr + + def get_checker(self, chkr): + return self._checker + + def run(self): + """Run the spellchecking loop.""" + self._stop = False + for err in self._checker: + self.error = err + printf(["ERROR:", err.word]) + printf(["HOW ABOUT:", err.suggest()]) + status = self.read_command() + while not status and not self._stop: + status = self.read_command() + if self._stop: + break + printf(["DONE"]) + + def print_help(self): + printf(["0..N: replace with the numbered suggestion"]) + printf(["R0..rN: always replace with the numbered suggestion"]) + printf(["i: ignore this word"]) + printf(["I: always ignore this word"]) + printf(["a: add word to personal dictionary"]) + printf(["e: edit the word"]) + printf(["q: quit checking"]) + printf(["h: print this help message"]) + printf(["----------------------------------------------------"]) + printf(["HOW ABOUT:", self.error.suggest()]) + + def read_command(self): + cmd = raw_input(">> ") + cmd = cmd.strip() + + if cmd.isdigit(): + repl = int(cmd) + suggs = self.error.suggest() + if repl >= len(suggs): + printf(["No suggestion number", repl]) + return False + printf(["Replacing '%s' with '%s'" % (self.error.word, suggs[repl])]) + self.error.replace(suggs[repl]) + return True + + if cmd[0] == "R": + if not cmd[1:].isdigit(): + printf(["Badly formatted command (try 'help')"]) + return False + repl = int(cmd[1:]) + suggs = self.error.suggest() + if repl >= len(suggs): + printf(["No suggestion number", repl]) + return False + self.error.replace_always(suggs[repl]) + return True + + if cmd == "i": + return True + + if cmd == "I": + self.error.ignore_always() + return True + + if cmd == "a": + self.error.add() + return True + + if cmd == "e": + repl = raw_input("New Word: ") + self.error.replace(repl.strip()) + return True + + if cmd == "q": + self._stop = True + return True + + if "help".startswith(cmd.lower()): + self.print_help() + return False + + printf(["Badly formatted command (try 'help')"]) + return False + + def run_on_file(self, infile, outfile=None, enc=None): + """Run spellchecking on the named file. + This method can be used to run the spellchecker over the named file. + If is not given, the corrected contents replace the contents + of . If is given, the corrected contents will be + written to that file. Use "-" to have the contents written to stdout. + If is given, it specifies the encoding used to read the + file's contents into a unicode string. The output will be written + in the same encoding. + """ + inStr = "".join(file(infile, "r").readlines()) + if enc is not None: + inStr = inStr.decode(enc) + self._checker.set_text(inStr) + self.run() + outStr = self._checker.get_text() + if enc is not None: + outStr = outStr.encode(enc) + if outfile is None: + outF = file(infile, "w") + elif outfile == "-": + outF = sys.stdout + else: + outF = file(outfile, "w") + outF.write(outStr) + outF.close() + + run_on_file._DOC_ERRORS = ["outfile", "infile", "outfile", "stdout"] + + +def _run_as_script(): + """Run the command-line spellchecker as a script. + This function allows the spellchecker to be invoked from the command-line + to check spelling in a file. + """ + # Check necessary command-line options + from optparse import OptionParser + op = OptionParser() + op.add_option("-o", "--output", dest="outfile", metavar="FILE", + help="write changes into FILE") + op.add_option("-l", "--lang", dest="lang", metavar="TAG", default="en_US", + help="use language idenfified by TAG") + op.add_option("-e", "--encoding", dest="enc", metavar="ENC", + help="file is unicode with encoding ENC") + (opts, args) = op.parse_args() + # Sanity check + if len(args) < 1: + raise ValueError("Must name a file to check") + if len(args) > 1: + raise ValueError("Can only check a single file") + # Create and run the checker + chkr = SpellChecker(opts.lang) + cmdln = CmdLineChecker() + cmdln.set_checker(chkr) + cmdln.run_on_file(args[0], opts.outfile, opts.enc) + + +if __name__ == "__main__": + _run_as_script() diff --git a/PACK/enchant/checker/GtkSpellCheckerDialog.py b/PACK/enchant/checker/GtkSpellCheckerDialog.py new file mode 100644 index 0000000..0d8f69a --- /dev/null +++ b/PACK/enchant/checker/GtkSpellCheckerDialog.py @@ -0,0 +1,304 @@ +# GtkSpellCheckerDialog for pyenchant +# +# Copyright (C) 2004-2005, Fredrik Corneliusson +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# +# In addition, as a special exception, you are +# given permission to link the code of this program with +# non-LGPL Spelling Provider libraries (eg: a MSFT Office +# spell checker backend) and distribute linked combinations including +# the two. You must obey the GNU Lesser General Public License in all +# respects for all of the code used other than said providers. If you modify +# this file, you may extend this exception to your version of the +# file, but you are not obligated to do so. If you do not wish to +# do so, delete this exception statement from your version. +# + +import gtk +import gobject + +from enchant.utils import printf, unicode + +# columns +COLUMN_SUGGESTION = 0 + + +def create_list_view(col_label, ): + # create list widget + list_ = gtk.ListStore(str) + list_view = gtk.TreeView(model=list_) + + list_view.set_rules_hint(True) + list_view.get_selection().set_mode(gtk.SELECTION_SINGLE) + # Add Colums + renderer = gtk.CellRendererText() + renderer.set_data("column", COLUMN_SUGGESTION) + column = gtk.TreeViewColumn(col_label, renderer, text=COLUMN_SUGGESTION) + list_view.append_column(column) + return list_view + + +class GtkSpellCheckerDialog(gtk.Window): + def __init__(self, *args, **kwargs): + gtk.Window.__init__(self, *args, **kwargs) + self.set_title('Spell check') + self.set_default_size(350, 200) + + self._checker = None + self._numContext = 40 + + self.errors = None + + # create accel group + accel_group = gtk.AccelGroup() + self.add_accel_group(accel_group) + + # list of widgets to disable if there's no spell error left + self._conditional_widgets = [] + conditional = self._conditional_widgets.append + + # layout + mainbox = gtk.VBox(spacing=5) + hbox = gtk.HBox(spacing=5) + self.add(mainbox) + mainbox.pack_start(hbox, padding=5) + + box1 = gtk.VBox(spacing=5) + hbox.pack_start(box1, padding=5) + conditional(box1) + + # unreconized word + text_view_lable = gtk.Label('Unreconized word') + text_view_lable.set_justify(gtk.JUSTIFY_LEFT) + box1.pack_start(text_view_lable, False, False) + + text_view = gtk.TextView() + text_view.set_wrap_mode(gtk.WRAP_WORD) + text_view.set_editable(False) + text_view.set_cursor_visible(False) + self.error_text = text_view.get_buffer() + text_buffer = text_view.get_buffer() + text_buffer.create_tag("fg_black", foreground="black") + text_buffer.create_tag("fg_red", foreground="red") + + box1.pack_start(text_view) + + # Change to + change_to_box = gtk.HBox() + box1.pack_start(change_to_box, False, False) + + change_to_label = gtk.Label('Change to:') + self.replace_text = gtk.Entry() + text_view_lable.set_justify(gtk.JUSTIFY_LEFT) + change_to_box.pack_start(change_to_label, False, False) + change_to_box.pack_start(self.replace_text) + + # scrolled window + sw = gtk.ScrolledWindow() + sw.set_shadow_type(gtk.SHADOW_ETCHED_IN) + sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + box1.pack_start(sw) + + self.suggestion_list_view = create_list_view('Suggestions') + self.suggestion_list_view.connect("button_press_event", self._onButtonPress) + self.suggestion_list_view.connect("cursor-changed", self._onSuggestionChanged) + sw.add(self.suggestion_list_view) + + # ---Buttons---#000000#FFFFFF---------------------------------------------------- + button_box = gtk.VButtonBox() + hbox.pack_start(button_box, False, False) + + # Ignore + button = gtk.Button("Ignore") + button.connect("clicked", self._onIgnore) + button.add_accelerator("activate", accel_group, + gtk.keysyms.Return, 0, gtk.ACCEL_VISIBLE) + button_box.pack_start(button) + conditional(button) + + # Ignore all + button = gtk.Button("Ignore All") + button.connect("clicked", self._onIgnoreAll) + button_box.pack_start(button) + conditional(button) + + # Replace + button = gtk.Button("Replace") + button.connect("clicked", self._onReplace) + button_box.pack_start(button) + conditional(button) + + # Replace all + button = gtk.Button("Replace All") + button.connect("clicked", self._onReplaceAll) + button_box.pack_start(button) + conditional(button) + + # Recheck button + button = gtk.Button("_Add") + button.connect("clicked", self._onAdd) + + button_box.pack_start(button) + conditional(button) + + # Close button + button = gtk.Button(stock=gtk.STOCK_CLOSE) + button.connect("clicked", self._onClose) + button.add_accelerator("activate", accel_group, + gtk.keysyms.Escape, 0, gtk.ACCEL_VISIBLE) + button_box.pack_end(button) + + # dictionary label + self._dict_lable = gtk.Label('') + mainbox.pack_start(self._dict_lable, False, False, padding=5) + + mainbox.show_all() + + def _onIgnore(self, w, *args): + printf(["ignore"]) + self._advance() + + def _onIgnoreAll(self, w, *args): + printf(["ignore all"]) + self._checker.ignore_always() + self._advance() + + def _onReplace(self, *args): + printf(["Replace"]) + repl = self._getRepl() + self._checker.replace(repl) + self._advance() + + def _onReplaceAll(self, *args): + printf(["Replace all"]) + repl = self._getRepl() + self._checker.replace_always(repl) + self._advance() + + def _onAdd(self, *args): + """Callback for the "add" button.""" + self._checker.add() + self._advance() + + def _onClose(self, w, *args): + self.emit('delete_event', gtk.gdk.Event(gtk.gdk.BUTTON_PRESS)) + return True + + def _onButtonPress(self, widget, event): + if event.type == gtk.gdk._2BUTTON_PRESS: + printf(["Double click!"]) + self._onReplace() + + def _onSuggestionChanged(self, widget, *args): + selection = self.suggestion_list_view.get_selection() + model, iter = selection.get_selected() + if iter: + suggestion = model.get_value(iter, COLUMN_SUGGESTION) + self.replace_text.set_text(suggestion) + + def _getRepl(self): + """Get the chosen replacement string.""" + repl = self.replace_text.get_text() + repl = self._checker.coerce_string(repl) + return repl + + def _fillSuggestionList(self, suggestions): + model = self.suggestion_list_view.get_model() + model.clear() + for suggestion in suggestions: + value = unicode("%s" % (suggestion,)) + model.append([value, ]) + + def setSpellChecker(self, checker): + assert checker, 'checker cant be None' + self._checker = checker + self._dict_lable.set_text('Dictionary:%s' % (checker.dict.tag,)) + + def getSpellChecker(self, checker): + return self._checker + + def updateUI(self): + self._advance() + + def _disableButtons(self): + for w in self._conditional_widgets: + w.set_sensitive(False) + + def _enableButtons(self): + for w in self._conditional_widgets: + w.set_sensitive(True) + + def _advance(self): + """Advance to the next error. + This method advances the SpellChecker to the next error, if + any. It then displays the error and some surrounding context, + and well as listing the suggested replacements. + """ + # Disable interaction if no checker + if self._checker is None: + self._disableButtons() + self.emit('check-done') + return + + # Advance to next error, disable if not available + try: + self._checker.next() + except StopIteration: + self._disableButtons() + self.error_text.set_text("") + self._fillSuggestionList([]) + self.replace_text.set_text("") + return + self._enableButtons() + + # Display error context with erroneous word in red + self.error_text.set_text('') + iter = self.error_text.get_iter_at_offset(0) + append = self.error_text.insert_with_tags_by_name + + lContext = self._checker.leading_context(self._numContext) + tContext = self._checker.trailing_context(self._numContext) + append(iter, lContext, 'fg_black') + append(iter, self._checker.word, 'fg_red') + append(iter, tContext, 'fg_black') + + # Display suggestions in the replacements list + suggs = self._checker.suggest() + self._fillSuggestionList(suggs) + if suggs: + self.replace_text.set_text(suggs[0]) + else: + self.replace_text.set_text("") + + +def _test(): + from enchant.checker import SpellChecker + text = "This is sme text with a fw speling errors in it. Here are a fw more to tst it ut." + printf(["BEFORE:", text]) + chk_dlg = GtkSpellCheckerDialog() + chk_dlg.show() + chk_dlg.connect('delete_event', gtk.main_quit) + + chkr = SpellChecker("en_US", text) + + chk_dlg.setSpellChecker(chkr) + chk_dlg.updateUI() + gtk.main() + + +if __name__ == "__main__": + _test() diff --git a/PACK/enchant/checker/__init__.py b/PACK/enchant/checker/__init__.py new file mode 100644 index 0000000..304c0aa --- /dev/null +++ b/PACK/enchant/checker/__init__.py @@ -0,0 +1,379 @@ +# pyenchant +# +# Copyright (C) 2004-2008, Ryan Kelly +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# +# In addition, as a special exception, you are +# given permission to link the code of this program with +# non-LGPL Spelling Provider libraries (eg: a MSFT Office +# spell checker backend) and distribute linked combinations including +# the two. You must obey the GNU Lesser General Public License in all +# respects for all of the code used other than said providers. If you modify +# this file, you may extend this exception to your version of the +# file, but you are not obligated to do so. If you do not wish to +# do so, delete this exception statement from your version. +# +""" + +enchant.checker: High-level spellchecking functionality +======================================================== + +This package is designed to host higher-level spellchecking functionality +than is available in the base enchant package. It should make writing +applications that follow common usage idioms significantly easier. + +The most useful class is SpellChecker, which implements a spellchecking +loop over a block of text. It is capable of modifying the text in-place +if given an array of characters to work with. + +This package also contains several interfaces to the SpellChecker class, +such as a wxPython GUI dialog and a command-line interface. + +""" + +import array +import warnings + +import enchant +from enchant.errors import * +from enchant.tokenize import get_tokenizer +from enchant.utils import bytes, unicode, basestring, next +from enchant.utils import get_default_language + + +class SpellChecker: + """Class implementing stateful spellchecking behaviour. + + This class is designed to implement a spell-checking loop over + a block of text, correcting/ignoring/replacing words as required. + This loop is implemented using an iterator paradigm so it can be + embedded inside other loops of control. + + The SpellChecker object is stateful, and the appropriate methods + must be called to alter its state and affect the progress of + the spell checking session. At any point during the checking + session, the attribute 'word' will hold the current erroneously + spelled word under consideration. The action to take on this word + is determined by calling methods such as 'replace', 'replace_always' + and 'ignore_always'. Once this is done, calling 'next' advances + to the next misspelled word. + + As a quick (and rather silly) example, the following code replaces + each misspelled word with the string "SPAM": + + >>> text = "This is sme text with a fw speling errors in it." + >>> chkr = SpellChecker("en_US",text) + >>> for err in chkr: + ... err.replace("SPAM") + ... + >>> chkr.get_text() + 'This is SPAM text with a SPAM SPAM errors in it.' + >>> + + Internally, the SpellChecker always works with arrays of (possibly + unicode) character elements. This allows the in-place modification + of the string as it is checked, and is the closest thing Python has + to a mutable string. The text can be set as any of a normal string, + unicode string, character array or unicode character array. The + 'get_text' method will return the modified array object if an + array is used, or a new string object if a string it used. + + Words input to the SpellChecker may be either plain strings or + unicode objects. They will be converted to the same type as the + text being checked, using python's default encoding/decoding + settings. + + If using an array of characters with this object and the + array is modified outside of the spellchecking loop, use the + 'set_offset' method to reposition the internal loop pointer + to make sure it doesn't skip any words. + + """ + _DOC_ERRORS = ["sme", "fw", "speling", "chkr", "chkr", "chkr"] + + def __init__(self, lang=None, text=None, tokenize=None, chunkers=None, filters=None): + """Constructor for the SpellChecker class. + + SpellChecker objects can be created in two ways, depending on + the nature of the first argument. If it is a string, it + specifies a language tag from which a dictionary is created. + Otherwise, it must be an enchant Dict object to be used. + + Optional keyword arguments are: + + * text: to set the text to be checked at creation time + * tokenize: a custom tokenization function to use + * chunkers: a list of chunkers to apply during tokenization + * filters: a list of filters to apply during tokenization + + If is not given and the first argument is a Dict, + its 'tag' attribute must be a language tag so that a tokenization + function can be created automatically. If this attribute is missing + the user's default language will be used. + """ + if lang is None: + lang = get_default_language() + if isinstance(lang, basestring): + dict = enchant.Dict(lang) + else: + dict = lang + try: + lang = dict.tag + except AttributeError: + lang = get_default_language() + if lang is None: + raise DefaultLanguageNotFoundError + self.lang = lang + self.dict = dict + if tokenize is None: + try: + tokenize = get_tokenizer(lang, chunkers, filters) + except TokenizerNotFoundError: + # Fall back to default tokenization if no match for 'lang' + tokenize = get_tokenizer(None, chunkers, filters) + self._tokenize = tokenize + + self.word = None + self.wordpos = None + self._ignore_words = {} + self._replace_words = {} + # Default to the empty string as the text to be checked + self._text = array.array('u') + self._use_tostring = False + self._tokens = iter([]) + + if text is not None: + self.set_text(text) + + def __iter__(self): + """Each SpellChecker object is its own iterator""" + return self + + def set_text(self, text): + """Set the text to be spell-checked. + + This method must be called, or the 'text' argument supplied + to the constructor, before calling the 'next()' method. + """ + # Convert to an array object if necessary + if isinstance(text, basestring): + if type(text) is unicode: + self._text = array.array('u', text) + else: + self._text = array.array('c', text) + self._use_tostring = True + else: + self._text = text + self._use_tostring = False + self._tokens = self._tokenize(self._text) + + def get_text(self): + """Return the spell-checked text.""" + if self._use_tostring: + return self._array_to_string(self._text) + return self._text + + def _array_to_string(self, text): + """Format an internal array as a standard string.""" + if text.typecode == 'u': + return text.tounicode() + return text.tostring() + + def wants_unicode(self): + """Check whether the checker wants unicode strings. + + This method will return True if the checker wants unicode strings + as input, False if it wants normal strings. It's important to + provide the correct type of string to the checker. + """ + if self._text.typecode == 'u': + return True + return False + + def coerce_string(self, text, enc=None): + """Coerce string into the required type. + + This method can be used to automatically ensure that strings + are of the correct type required by this checker - either unicode + or standard. If there is a mismatch, conversion is done using + python's default encoding unless another encoding is specified. + """ + if self.wants_unicode(): + if not isinstance(text, unicode): + if enc is None: + return text.decode() + else: + return text.decode(enc) + return text + if not isinstance(text, bytes): + if enc is None: + return text.encode() + else: + return text.encode(enc) + return text + + def __next__(self): + return self.next() + + def next(self): + """Process text up to the next spelling error. + + This method is designed to support the iterator protocol. + Each time it is called, it will advance the 'word' attribute + to the next spelling error in the text. When no more errors + are found, it will raise StopIteration. + + The method will always return self, so that it can be used + sensibly in common idioms such as: + + for err in checker: + err.do_something() + + """ + # Find the next spelling error. + # The uncaught StopIteration from next(self._tokens) + # will provide the StopIteration for this method + while True: + (word, pos) = next(self._tokens) + # decode back to a regular string + word = self._array_to_string(word) + if self.dict.check(word): + continue + if word in self._ignore_words: + continue + self.word = word + self.wordpos = pos + if word in self._replace_words: + self.replace(self._replace_words[word]) + continue + break + return self + + def replace(self, repl): + """Replace the current erroneous word with the given string.""" + repl = self.coerce_string(repl) + aRepl = array.array(self._text.typecode, repl) + if repl: + self.dict.store_replacement(self.word, repl) + self._text[self.wordpos:self.wordpos + len(self.word)] = aRepl + incr = len(repl) - len(self.word) + self._tokens.set_offset(self._tokens.offset + incr, replaced=True) + + def replace_always(self, word, repl=None): + """Always replace given word with given replacement. + + If a single argument is given, this is used to replace the + current erroneous word. If two arguments are given, that + combination is added to the list for future use. + """ + if repl is None: + repl = word + word = self.word + repl = self.coerce_string(repl) + word = self.coerce_string(word) + self._replace_words[word] = repl + if self.word == word: + self.replace(repl) + + def ignore_always(self, word=None): + """Add given word to list of words to ignore. + + If no word is given, the current erroneous word is added. + """ + if word is None: + word = self.word + word = self.coerce_string(word) + if word not in self._ignore_words: + self._ignore_words[word] = True + + def add_to_personal(self, word=None): + """Add given word to the personal word list. + + If no word is given, the current erroneous word is added. + """ + warnings.warn("SpellChecker.add_to_personal is deprecated, " \ + "please use SpellChecker.add", + category=DeprecationWarning, stacklevel=2) + self.add(word) + + def add(self, word=None): + """Add given word to the personal word list. + + If no word is given, the current erroneous word is added. + """ + if word is None: + word = self.word + self.dict.add(word) + + def suggest(self, word=None): + """Return suggested spellings for the given word. + + If no word is given, the current erroneous word is used. + """ + if word is None: + word = self.word + suggs = self.dict.suggest(word) + return suggs + + def check(self, word): + """Check correctness of the given word.""" + return self.dict.check(word) + + def set_offset(self, off, whence=0): + """Set the offset of the tokenization routine. + + For more details on the purpose of the tokenization offset, + see the documentation of the 'enchant.tokenize' module. + The optional argument whence indicates the method by + which to change the offset: + * 0 (the default) treats as an increment + * 1 treats as a distance from the start + * 2 treats as a distance from the end + """ + if whence == 0: + self._tokens.set_offset(self._tokens.offset + off) + elif whence == 1: + assert (off > 0) + self._tokens.set_offset(off) + elif whence == 2: + assert (off > 0) + self._tokens.set_offset(len(self._text) - 1 - off) + else: + raise ValueError("Invalid value for whence: %s" % (whence,)) + + def leading_context(self, chars): + """Get characters of leading context. + + This method returns up to characters of leading + context - the text that occurs in the string immediately + before the current erroneous word. + """ + start = max(self.wordpos - chars, 0) + context = self._text[start:self.wordpos] + return self._array_to_string(context) + + def trailing_context(self, chars): + """Get characters of trailing context. + + This method returns up to characters of trailing + context - the text that occurs in the string immediately + after the current erroneous word. + """ + start = self.wordpos + len(self.word) + end = min(start + chars, len(self._text)) + context = self._text[start:end] + return self._array_to_string(context) diff --git a/PACK/enchant/checker/tests.py b/PACK/enchant/checker/tests.py new file mode 100644 index 0000000..e99b552 --- /dev/null +++ b/PACK/enchant/checker/tests.py @@ -0,0 +1,246 @@ +# pyenchant +# +# Copyright (C) 2004-2009, Ryan Kelly +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# +# In addition, as a special exception, you are +# given permission to link the code of this program with +# non-LGPL Spelling Provider libraries (eg: a MSFT Office +# spell checker backend) and distribute linked combinations including +# the two. You must obey the GNU Lesser General Public License in all +# respects for all of the code used other than said providers. If you modify +# this file, you may extend this exception to your version of the +# file, but you are not obligated to do so. If you do not wish to +# do so, delete this exception statement from your version. +# +""" + + enchant.checker.tests: Unittests for enchant SpellChecker class + +""" + +import unittest + +import enchant +import enchant.tokenize +from enchant.utils import * +from enchant.errors import * +from enchant.checker import * + + +class TestChecker(unittest.TestCase): + """TestCases for checking behaviour of SpellChecker class.""" + + def test_basic(self): + """Test a basic run of the SpellChecker class.""" + text = """This is sme text with a few speling erors in it. Its gret + for checking wheather things are working proprly with the SpellChecker + class. Not gret for much elss though.""" + chkr = SpellChecker("en_US", text=text) + for n, err in enumerate(chkr): + if n == 0: + # Fix up "sme" -> "some" properly + self.assertEqual(err.word, "sme") + self.assertEqual(err.wordpos, 8) + self.assertTrue("some" in err.suggest()) + err.replace("some") + if n == 1: + # Ignore "speling" + self.assertEqual(err.word, "speling") + if n == 2: + # Check context around "erors", and replace + self.assertEqual(err.word, "erors") + self.assertEqual(err.leading_context(5), "ling ") + self.assertEqual(err.trailing_context(5), " in i") + err.replace(raw_unicode("errors")) + if n == 3: + # Replace-all on gret as it appears twice + self.assertEqual(err.word, "gret") + err.replace_always("great") + if n == 4: + # First encounter with "wheather", move offset back + self.assertEqual(err.word, "wheather") + err.set_offset(-1 * len(err.word)) + if n == 5: + # Second encounter, fix up "wheather' + self.assertEqual(err.word, "wheather") + err.replace("whether") + if n == 6: + # Just replace "proprly", but also add an ignore + # for "SpellChecker" + self.assertEqual(err.word, "proprly") + err.replace("properly") + err.ignore_always("SpellChecker") + if n == 7: + # The second "gret" should have been replaced + # So it's now on "elss" + self.assertEqual(err.word, "elss") + err.replace("else") + if n > 7: + self.fail("Extraneous spelling errors were found") + text2 = """This is some text with a few speling errors in it. Its great + for checking whether things are working properly with the SpellChecker + class. Not great for much else though.""" + self.assertEqual(chkr.get_text(), text2) + + def test_filters(self): + """Test SpellChecker with the 'filters' argument.""" + text = """I contain WikiWords that ShouldBe skipped by the filters""" + chkr = SpellChecker("en_US", text=text, + filters=[enchant.tokenize.WikiWordFilter]) + for err in chkr: + # There are no errors once the WikiWords are skipped + self.fail("Extraneous spelling errors were found") + self.assertEqual(chkr.get_text(), text) + + def test_chunkers(self): + """Test SpellChecker with the 'chunkers' argument.""" + text = """I contain tags that should be skipped""" + chkr = SpellChecker("en_US", text=text, + chunkers=[enchant.tokenize.HTMLChunker]) + for err in chkr: + # There are no errors when the tag is skipped + self.fail("Extraneous spelling errors were found") + self.assertEqual(chkr.get_text(), text) + + def test_chunkers_and_filters(self): + """Test SpellChecker with the 'chunkers' and 'filters' arguments.""" + text = """I contain tags that should be skipped + along with a >> dlg = wxSpellCheckerDialog(None,-1,"") + >>> chkr = SpellChecker("en_AU",text) + >>> dlg.SetSpellChecker(chkr) + >>> dlg.Show() + + This is most useful when the text to be checked is in the form of + a character array, as it will be modified in place as the user + interacts with the dialog. For checking strings, the final result + will need to be obtained from the SpellChecker object: + + >>> dlg = wxSpellCheckerDialog(None,-1,"") + >>> chkr = SpellChecker("en_AU",text) + >>> dlg.SetSpellChecker(chkr) + >>> dlg.ShowModal() + >>> text = dlg.GetSpellChecker().get_text() + + Currently the checker must deal with strings of the same type as + returned by wxPython - unicode or normal string depending on the + underlying system. This needs to be fixed, somehow... + """ + _DOC_ERRORS = ["dlg", "chkr", "dlg", "SetSpellChecker", "chkr", "dlg", + "dlg", "chkr", "dlg", "SetSpellChecker", "chkr", "dlg", + "ShowModal", "dlg", "GetSpellChecker"] + + # Remember dialog size across invocations by storing it on the class + sz = (300, 70) + + def __init__(self, parent=None, id=-1, title="Checking Spelling..."): + wx.Dialog.__init__(self, parent, id, title, size=wxSpellCheckerDialog.sz, + style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER) + self._numContext = 40 + self._checker = None + self._buttonsEnabled = True + self.error_text = wx.TextCtrl(self, -1, "", style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_RICH) + self.replace_text = wx.TextCtrl(self, -1, "", style=wx.TE_PROCESS_ENTER) + self.replace_list = wx.ListBox(self, -1, style=wx.LB_SINGLE) + self.InitLayout() + wx.EVT_LISTBOX(self, self.replace_list.GetId(), self.OnReplSelect) + wx.EVT_LISTBOX_DCLICK(self, self.replace_list.GetId(), self.OnReplace) + + def InitLayout(self): + """Lay out controls and add buttons.""" + sizer = wx.BoxSizer(wx.HORIZONTAL) + txtSizer = wx.BoxSizer(wx.VERTICAL) + btnSizer = wx.BoxSizer(wx.VERTICAL) + replaceSizer = wx.BoxSizer(wx.HORIZONTAL) + txtSizer.Add(wx.StaticText(self, -1, "Unrecognised Word:"), 0, wx.LEFT | wx.TOP, 5) + txtSizer.Add(self.error_text, 1, wx.ALL | wx.EXPAND, 5) + replaceSizer.Add(wx.StaticText(self, -1, "Replace with:"), 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) + replaceSizer.Add(self.replace_text, 1, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) + txtSizer.Add(replaceSizer, 0, wx.EXPAND, 0) + txtSizer.Add(self.replace_list, 2, wx.ALL | wx.EXPAND, 5) + sizer.Add(txtSizer, 1, wx.EXPAND, 0) + self.buttons = [] + for label, action, tip in ( \ + ("Ignore", self.OnIgnore, "Ignore this word and continue"), + ("Ignore All", self.OnIgnoreAll, "Ignore all instances of this word and continue"), + ("Replace", self.OnReplace, "Replace this word"), + ("Replace All", self.OnReplaceAll, "Replace all instances of this word"), + ("Add", self.OnAdd, "Add this word to the dictionary"), + ("Done", self.OnDone, "Finish spell-checking and accept changes"), + ): + btn = wx.Button(self, -1, label) + btn.SetToolTip(wx.ToolTip(tip)) + btnSizer.Add(btn, 0, wx.ALIGN_RIGHT | wx.ALL, 4) + btn.Bind(wx.EVT_BUTTON, action) + self.buttons.append(btn) + sizer.Add(btnSizer, 0, wx.ALL | wx.EXPAND, 5) + self.SetAutoLayout(True) + self.SetSizer(sizer) + sizer.Fit(self) + + def Advance(self): + """Advance to the next error. + + This method advances the SpellChecker to the next error, if + any. It then displays the error and some surrounding context, + and well as listing the suggested replacements. + """ + # Disable interaction if no checker + if self._checker is None: + self.EnableButtons(False) + return False + # Advance to next error, disable if not available + try: + self._checker.next() + except StopIteration: + self.EnableButtons(False) + self.error_text.SetValue("") + self.replace_list.Clear() + self.replace_text.SetValue("") + if self.IsModal(): # test needed for SetSpellChecker call + # auto-exit when checking complete + self.EndModal(wx.ID_OK) + return False + self.EnableButtons() + # Display error context with erroneous word in red. + # Restoring default style was misbehaving under win32, so + # I am forcing the rest of the text to be black. + self.error_text.SetValue("") + self.error_text.SetDefaultStyle(wx.TextAttr(wx.BLACK)) + lContext = self._checker.leading_context(self._numContext) + self.error_text.AppendText(lContext) + self.error_text.SetDefaultStyle(wx.TextAttr(wx.RED)) + self.error_text.AppendText(self._checker.word) + self.error_text.SetDefaultStyle(wx.TextAttr(wx.BLACK)) + tContext = self._checker.trailing_context(self._numContext) + self.error_text.AppendText(tContext) + # Display suggestions in the replacements list + suggs = self._checker.suggest() + self.replace_list.Set(suggs) + self.replace_text.SetValue(suggs and suggs[0] or '') + return True + + def EnableButtons(self, state=True): + """Enable the checking-related buttons""" + if state != self._buttonsEnabled: + for btn in self.buttons[:-1]: + btn.Enable(state) + self._buttonsEnabled = state + + def GetRepl(self): + """Get the chosen replacement string.""" + repl = self.replace_text.GetValue() + return repl + + def OnAdd(self, evt): + """Callback for the "add" button.""" + self._checker.add() + self.Advance() + + def OnDone(self, evt): + """Callback for the "close" button.""" + wxSpellCheckerDialog.sz = self.error_text.GetSizeTuple() + if self.IsModal(): + self.EndModal(wx.ID_OK) + else: + self.Close() + + def OnIgnore(self, evt): + """Callback for the "ignore" button. + This simply advances to the next error. + """ + self.Advance() + + def OnIgnoreAll(self, evt): + """Callback for the "ignore all" button.""" + self._checker.ignore_always() + self.Advance() + + def OnReplace(self, evt): + """Callback for the "replace" button.""" + repl = self.GetRepl() + if repl: + self._checker.replace(repl) + self.Advance() + + def OnReplaceAll(self, evt): + """Callback for the "replace all" button.""" + repl = self.GetRepl() + self._checker.replace_always(repl) + self.Advance() + + def OnReplSelect(self, evt): + """Callback when a new replacement option is selected.""" + sel = self.replace_list.GetSelection() + if sel == -1: + return + opt = self.replace_list.GetString(sel) + self.replace_text.SetValue(opt) + + def GetSpellChecker(self): + """Get the spell checker object.""" + return self._checker + + def SetSpellChecker(self, chkr): + """Set the spell checker, advancing to the first error. + Return True if error(s) to correct, else False.""" + self._checker = chkr + return self.Advance() + + +def _test(): + class TestDialog(wxSpellCheckerDialog): + def __init__(self, *args): + wxSpellCheckerDialog.__init__(self, *args) + wx.EVT_CLOSE(self, self.OnClose) + + def OnClose(self, evnt): + chkr = dlg.GetSpellChecker() + if chkr is not None: + printf(["AFTER:", chkr.get_text()]) + self.Destroy() + + from enchant.checker import SpellChecker + text = "This is sme text with a fw speling errors in it. Here are a fw more to tst it ut." + printf(["BEFORE:", text]) + app = wx.PySimpleApp() + dlg = TestDialog() + chkr = SpellChecker("en_US", text) + dlg.SetSpellChecker(chkr) + dlg.Show() + app.MainLoop() + + +if __name__ == "__main__": + _test() diff --git a/PACK/enchant/errors.py b/PACK/enchant/errors.py new file mode 100644 index 0000000..cd6930c --- /dev/null +++ b/PACK/enchant/errors.py @@ -0,0 +1,57 @@ +# pyenchant +# +# Copyright (C) 2004-2008, Ryan Kelly +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPsE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# +# In addition, as a special exception, you are +# given permission to link the code of this program with +# non-LGPL Spelling Provider libraries (eg: a MSFT Office +# spell checker backend) and distribute linked combinations including +# the two. You must obey the GNU Lesser General Public License in all +# respects for all of the code used other than said providers. If you modify +# this file, you may extend this exception to your version of the +# file, but you are not obligated to do so. If you do not wish to +# do so, delete this exception statement from your version. +# +""" +enchant.errors: Error class definitions for the enchant library +================================================================ + +All error classes are defined in this separate sub-module, so that they +can safely be imported without causing circular dependencies. + +""" + + +class Error(Exception): + """Base exception class for the enchant module.""" + pass + + +class DictNotFoundError(Error): + """Exception raised when a requested dictionary could not be found.""" + pass + + +class TokenizerNotFoundError(Error): + """Exception raised when a requested tokenizer could not be found.""" + pass + + +class DefaultLanguageNotFoundError(Error): + """Exception raised when a default language could not be found.""" + pass diff --git a/PACK/enchant/lib/enchant/README.txt b/PACK/enchant/lib/enchant/README.txt new file mode 100644 index 0000000..1943a29 --- /dev/null +++ b/PACK/enchant/lib/enchant/README.txt @@ -0,0 +1,4 @@ + +This directory contains the plugin DLLs for enchant when installed on +a Microsoft Windows system. + diff --git a/PACK/enchant/pypwl.py b/PACK/enchant/pypwl.py new file mode 100644 index 0000000..9356ca9 --- /dev/null +++ b/PACK/enchant/pypwl.py @@ -0,0 +1,285 @@ +# pyenchant +# +# Copyright (C) 2004-2011 Ryan Kelly +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# +# In addition, as a special exception, you are +# given permission to link the code of this program with +# non-LGPL Spelling Provider libraries (eg: a MSFT Office +# spell checker backend) and distribute linked combinations including +# the two. You must obey the GNU Lesser General Public License in all +# respects for all of the code used other than said providers. If you modify +# this file, you may extend this exception to your version of the +# file, but you are not obligated to do so. If you do not wish to +# do so, delete this exception statement from your version. +# +""" + +pypwl: pure-python personal word list in the style of Enchant +============================================================== + +This module provides a pure-python version of the personal word list +functionality found in the spellchecking package Enchant. While the +same effect can be achieved (with better performance) using the python +bindings for Enchant, it requires a C extension. + +This pure-python implementation uses the same algorithm but without any +external dependencies or C code (in fact, it was the author's original +prototype for the C version found in Enchant). + +""" + +from __future__ import generators + +import os +import warnings + + +class Trie: + """Class implementing a trie-based dictionary of words. + + A Trie is a recursive data structure storing words by their prefix. + "Fuzzy matching" can be done by allowing a certain number of missteps + when traversing the Trie. + """ + + def __init__(self, words=()): + self._eos = False # whether I am the end of a word + self._keys = {} # letters at this level of the trie + for w in words: + self.insert(w) + + def insert(self, word): + if word == "": + self._eos = True + else: + key = word[0] + try: + subtrie = self[key] + except KeyError: + subtrie = Trie() + self[key] = subtrie + subtrie.insert(word[1:]) + + def remove(self, word): + if word == "": + self._eos = False + else: + key = word[0] + try: + subtrie = self[key] + except KeyError: + pass + else: + subtrie.remove(word[1:]) + + def search(self, word, nerrs=0): + """Search for the given word, possibly making errors. + + This method searches the trie for the given , making + precisely errors. It returns a list of words found. + """ + res = [] + # Terminate if we've run out of errors + if nerrs < 0: + return res + # Precise match at the end of the word + if nerrs == 0 and word == "": + if self._eos: + res.append("") + # Precisely match word[0] + try: + subtrie = self[word[0]] + subres = subtrie.search(word[1:], nerrs) + for w in subres: + w2 = word[0] + w + if w2 not in res: + res.append(w2) + except (IndexError, KeyError): + pass + # match with deletion of word[0] + try: + subres = self.search(word[1:], nerrs - 1) + for w in subres: + if w not in res: + res.append(w) + except (IndexError,): + pass + # match with insertion before word[0] + try: + for k in self._keys: + subres = self[k].search(word, nerrs - 1) + for w in subres: + w2 = k + w + if w2 not in res: + res.append(w2) + except (IndexError, KeyError): + pass + # match on substitution of word[0] + try: + for k in self._keys: + subres = self[k].search(word[1:], nerrs - 1) + for w in subres: + w2 = k + w + if w2 not in res: + res.append(w2) + except (IndexError, KeyError): + pass + # All done! + return res + + search._DOC_ERRORS = ["nerrs"] + + def __getitem__(self, key): + return self._keys[key] + + def __setitem__(self, key, val): + self._keys[key] = val + + def __iter__(self): + if self._eos: + yield "" + for k in self._keys: + for w2 in self._keys[k]: + yield k + w2 + + +class PyPWL: + """Pure-python implementation of Personal Word List dictionary. + This class emulates the PWL objects provided by PyEnchant, but + implemented purely in python. + """ + + def __init__(self, pwl=None): + """PyPWL constructor. + This method takes as its only argument the name of a file + containing the personal word list, one word per line. Entries + will be read from this file, and new entries will be written to + it automatically. + + If is not specified or None, the list is maintained in + memory only. + """ + self.provider = None + self._words = Trie() + if pwl is not None: + self.pwl = os.path.abspath(pwl) + self.tag = self.pwl + pwlF = file(pwl) + for ln in pwlF: + word = ln.strip() + self.add_to_session(word) + pwlF.close() + else: + self.pwl = None + self.tag = "PyPWL" + + def check(self, word): + """Check spelling of a word. + + This method takes a word in the dictionary language and returns + True if it is correctly spelled, and false otherwise. + """ + res = self._words.search(word) + return bool(res) + + def suggest(self, word): + """Suggest possible spellings for a word. + + This method tries to guess the correct spelling for a given + word, returning the possibilities in a list. + """ + limit = 10 + maxdepth = 5 + # Iterative deepening until we get enough matches + depth = 0 + res = self._words.search(word, depth) + while len(res) < limit and depth < maxdepth: + depth += 1 + for w in self._words.search(word, depth): + if w not in res: + res.append(w) + # Limit number of suggs + return res[:limit] + + def add(self, word): + """Add a word to the user's personal dictionary. + For a PWL, this means appending it to the file. + """ + if self.pwl is not None: + pwlF = file(self.pwl, "a") + pwlF.write("%s\n" % (word.strip(),)) + pwlF.close() + self.add_to_session(word) + + def add_to_pwl(self, word): + """Add a word to the user's personal dictionary. + For a PWL, this means appending it to the file. + """ + warnings.warn("PyPWL.add_to_pwl is deprecated, please use PyPWL.add", + category=DeprecationWarning, stacklevel=2) + self.add(word) + + def remove(self, word): + """Add a word to the user's personal exclude list.""" + # There's no exclude list for a stand-alone PWL. + # Just remove it from the list. + self._words.remove(word) + if self.pwl is not None: + pwlF = file(self.pwl, "wt") + for w in self._words: + pwlF.write("%s\n" % (w.strip(),)) + pwlF.close() + + def add_to_session(self, word): + """Add a word to the session list.""" + self._words.insert(word) + + def is_in_session(self, word): + """Check whether a word is in the session list.""" + warnings.warn("PyPWL.is_in_session is deprecated, please use PyPWL.is_added", category=DeprecationWarning) + # Consider all words to be in the session list + return self.check(word) + + def store_replacement(self, mis, cor): + """Store a replacement spelling for a miss-spelled word. + + This method makes a suggestion to the spellchecking engine that the + miss-spelled word is in fact correctly spelled as . Such + a suggestion will typically mean that appears early in the + list of suggested spellings offered for later instances of . + """ + # Too much work for this simple spellchecker + pass + + store_replacement._DOC_ERRORS = ["mis", "mis"] + + def is_added(self, word): + """Check whether a word is in the personal word list.""" + return self.check(word) + + def is_removed(self, word): + """Check whether a word is in the personal exclude list.""" + return False + + # No-op methods to support internal use as a Dict() replacement + + def _check_this(self, msg): + pass + + def _free(self): + pass diff --git a/PACK/enchant/share/enchant/README.txt b/PACK/enchant/share/enchant/README.txt new file mode 100644 index 0000000..6d9b66d --- /dev/null +++ b/PACK/enchant/share/enchant/README.txt @@ -0,0 +1,4 @@ + +This directory contains dictionary files for Enchant when installed on a +Microsoft Windows system. Each subdirectory contains dictionaries for +a particular spellchecking system. diff --git a/PACK/enchant/share/enchant/ispell/README.txt b/PACK/enchant/share/enchant/ispell/README.txt new file mode 100644 index 0000000..e69de29 diff --git a/PACK/enchant/share/enchant/myspell/README.txt b/PACK/enchant/share/enchant/myspell/README.txt new file mode 100644 index 0000000..ae8145f --- /dev/null +++ b/PACK/enchant/share/enchant/myspell/README.txt @@ -0,0 +1,3 @@ + +This directory contains dictionaries for the myspell backend to enchant. + diff --git a/PACK/enchant/tests.py b/PACK/enchant/tests.py new file mode 100644 index 0000000..c067377 --- /dev/null +++ b/PACK/enchant/tests.py @@ -0,0 +1,616 @@ +# pyenchant +# +# Copyright (C) 2004-2009, Ryan Kelly +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPsE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# +# In addition, as a special exception, you are +# given permission to link the code of this program with +# non-LGPL Spelling Provider libraries (eg: a MSFT Office +# spell checker backend) and distribute linked combinations including +# the two. You must obey the GNU Lesser General Public License in all +# respects for all of the code used other than said providers. If you modify +# this file, you may extend this exception to your version of the +# file, but you are not obligated to do so. If you do not wish to +# do so, delete this exception statement from your version. +# +""" + + enchant.tests: testcases for pyenchant + +""" + +import os +import sys +import unittest +import pickle + +try: + import subprocess +except ImportError: + subprocess = None + +import enchant +from enchant import * +from enchant import _enchant as _e +from enchant.utils import unicode, raw_unicode, printf, trim_suggestions + + +def runcmd(cmd): + if subprocess is not None: + kwds = dict(stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) + p = subprocess.Popen(cmd, **kwds) + (stdout, stderr) = p.communicate() + if p.returncode: + if sys.version_info[0] >= 3: + stderr = stderr.decode(sys.getdefaultencoding(), "replace") + sys.stderr.write(stderr) + return p.returncode + else: + return os.system(cmd) + + +class TestBroker(unittest.TestCase): + """Test cases for the proper functioning of Broker objects. + + These tests assume that there is at least one working provider + with a dictionary for the "en_US" language. + """ + + def setUp(self): + self.broker = Broker() + + def tearDown(self): + del self.broker + + def test_HasENUS(self): + """Test that the en_US language is available.""" + self.assertTrue(self.broker.dict_exists("en_US")) + + def test_LangsAreAvail(self): + """Test whether all advertised languages are in fact available.""" + for lang in self.broker.list_languages(): + if not self.broker.dict_exists(lang): + assert False, "language '" + lang + "' advertised but non-existent" + + def test_ProvsAreAvail(self): + """Test whether all advertised providers are in fact available.""" + for (lang, prov) in self.broker.list_dicts(): + self.assertTrue(self.broker.dict_exists(lang)) + if not self.broker.dict_exists(lang): + assert False, "language '" + lang + "' advertised but non-existent" + if prov not in self.broker.describe(): + assert False, "provier '" + str(prov) + "' advertised but non-existent" + + def test_ProvOrdering(self): + """Test that provider ordering works correctly.""" + langs = {} + provs = [] + # Find the providers for each language, and a list of all providers + for (tag, prov) in self.broker.list_dicts(): + # Skip hyphenation dictionaries installed by OOo + if tag.startswith("hyph_") and prov.name == "myspell": + continue + # Canonicalize separators + tag = tag.replace("-", "_") + langs[tag] = [] + # NOTE: we are excluding Zemberek here as it appears to return + # a broker for any language, even nonexistent ones + if prov not in provs and prov.name != "zemberek": + provs.append(prov) + for prov in provs: + for tag in langs: + b2 = Broker() + b2.set_ordering(tag, prov.name) + try: + d = b2.request_dict(tag) + if d.provider != prov: + raise ValueError() + langs[tag].append(prov) + except: + pass + # Check availability using a single entry in ordering + for tag in langs: + for prov in langs[tag]: + b2 = Broker() + b2.set_ordering(tag, prov.name) + d = b2.request_dict(tag) + self.assertEqual((d.provider, tag), (prov, tag)) + del d + del b2 + # Place providers that dont have the language in the ordering + for tag in langs: + for prov in langs[tag]: + order = prov.name + for prov2 in provs: + if prov2 not in langs[tag]: + order = prov2.name + "," + order + b2 = Broker() + b2.set_ordering(tag, order) + d = b2.request_dict(tag) + self.assertEqual((d.provider, tag, order), (prov, tag, order)) + del d + del b2 + + def test_UnicodeTag(self): + """Test that unicode language tags are accepted""" + d1 = self.broker._request_dict_data(raw_unicode("en_US")) + self.assertTrue(d1) + _e.broker_free_dict(self.broker._this, d1) + d1 = Dict(raw_unicode("en_US")) + self.assertTrue(d1) + + def test_GetSetParam(self): + try: + self.broker.get_param("pyenchant.unittest") + except AttributeError: + return + self.assertEqual(self.broker.get_param("pyenchant.unittest"), None) + self.broker.set_param("pyenchant.unittest", "testing") + self.assertEqual(self.broker.get_param("pyenchant.unittest"), "testing") + self.assertEqual(Broker().get_param("pyenchant.unittest"), None) + + +class TestDict(unittest.TestCase): + """Test cases for the proper functioning of Dict objects. + These tests assume that there is at least one working provider + with a dictionary for the "en_US" language. + """ + + def setUp(self): + self.dict = Dict("en_US") + + def tearDown(self): + del self.dict + + def test_HasENUS(self): + """Test that the en_US language is available through default broker.""" + self.assertTrue(dict_exists("en_US")) + + def test_check(self): + """Test that check() works on some common words.""" + self.assertTrue(self.dict.check("hello")) + self.assertTrue(self.dict.check("test")) + self.assertFalse(self.dict.check("helo")) + self.assertFalse(self.dict.check("testt")) + + def test_broker(self): + """Test that the dict's broker is set correctly.""" + self.assertTrue(self.dict._broker is enchant._broker) + + def test_tag(self): + """Test that the dict's tag is set correctly.""" + self.assertEqual(self.dict.tag, "en_US") + + def test_suggest(self): + """Test that suggest() gets simple suggestions right.""" + self.assertTrue(self.dict.check("hello")) + self.assertTrue("hello" in self.dict.suggest("helo")) + + def test_suggestHang1(self): + """Test whether suggest() hangs on some inputs (Bug #1404196)""" + self.assertTrue(len(self.dict.suggest("Thiis")) >= 0) + self.assertTrue(len(self.dict.suggest("Thiiis")) >= 0) + self.assertTrue(len(self.dict.suggest("Thiiiis")) >= 0) + + def test_unicode1(self): + """Test checking/suggesting for unicode strings""" + # TODO: find something that actually returns suggestions + us1 = raw_unicode(r"he\u2149lo") + self.assertTrue(type(us1) is unicode) + self.assertFalse(self.dict.check(us1)) + for s in self.dict.suggest(us1): + self.assertTrue(type(s) is unicode) + + def test_session(self): + """Test that adding words to the session works as required.""" + self.assertFalse(self.dict.check("Lozz")) + self.assertFalse(self.dict.is_added("Lozz")) + self.dict.add_to_session("Lozz") + self.assertTrue(self.dict.is_added("Lozz")) + self.assertTrue(self.dict.check("Lozz")) + self.dict.remove_from_session("Lozz") + self.assertFalse(self.dict.check("Lozz")) + self.assertFalse(self.dict.is_added("Lozz")) + self.dict.remove_from_session("hello") + self.assertFalse(self.dict.check("hello")) + self.assertTrue(self.dict.is_removed("hello")) + self.dict.add_to_session("hello") + + def test_AddRemove(self): + """Test adding/removing from default user dictionary.""" + nonsense = "kxhjsddsi" + self.assertFalse(self.dict.check(nonsense)) + self.dict.add(nonsense) + self.assertTrue(self.dict.is_added(nonsense)) + self.assertTrue(self.dict.check(nonsense)) + self.dict.remove(nonsense) + self.assertFalse(self.dict.is_added(nonsense)) + self.assertFalse(self.dict.check(nonsense)) + self.dict.remove("pineapple") + self.assertFalse(self.dict.check("pineapple")) + self.assertTrue(self.dict.is_removed("pineapple")) + self.assertFalse(self.dict.is_added("pineapple")) + self.dict.add("pineapple") + self.assertTrue(self.dict.check("pineapple")) + + def test_DefaultLang(self): + """Test behaviour of default language selection.""" + defLang = utils.get_default_language() + if defLang is None: + # If no default language, shouldnt work + self.assertRaises(Error, Dict) + else: + # If there is a default language, should use it + # Of course, no need for the dict to actually exist + try: + d = Dict() + self.assertEqual(d.tag, defLang) + except DictNotFoundError: + pass + + def test_pickling(self): + """Test that pickling doensn't corrupt internal state.""" + d1 = Dict("en") + self.assertTrue(d1.check("hello")) + d2 = pickle.loads(pickle.dumps(d1)) + self.assertTrue(d1.check("hello")) + self.assertTrue(d2.check("hello")) + d1._free() + self.assertTrue(d2.check("hello")) + + +class TestPWL(unittest.TestCase): + """Test cases for the proper functioning of PWLs and DictWithPWL objects. + These tests assume that there is at least one working provider + with a dictionary for the "en_US" language. + """ + + def setUp(self): + self._tempDir = self._mkdtemp() + self._fileName = "pwl.txt" + + def tearDown(self): + import shutil + shutil.rmtree(self._tempDir) + + def _mkdtemp(self): + import tempfile + return tempfile.mkdtemp() + + def _path(self, nm=None): + if nm is None: + nm = self._fileName + nm = os.path.join(self._tempDir, nm) + if not os.path.exists(nm): + open(nm, 'w').close() + return nm + + def setPWLContents(self, contents): + """Set the contents of the PWL file.""" + pwlFile = open(self._path(), "w") + for ln in contents: + pwlFile.write(ln) + pwlFile.write("\n") + pwlFile.flush() + pwlFile.close() + + def getPWLContents(self): + """Retrieve the contents of the PWL file.""" + pwlFile = open(self._path(), "r") + contents = pwlFile.readlines() + pwlFile.close() + return [c.strip() for c in contents] + + def test_check(self): + """Test that basic checking works for PWLs.""" + self.setPWLContents(["Sazz", "Lozz"]) + d = request_pwl_dict(self._path()) + self.assertTrue(d.check("Sazz")) + self.assertTrue(d.check("Lozz")) + self.assertFalse(d.check("hello")) + + def test_UnicodeFN(self): + """Test that unicode PWL filenames are accepted.""" + d = request_pwl_dict(unicode(self._path())) + self.assertTrue(d) + + def test_add(self): + """Test that adding words to a PWL works correctly.""" + d = request_pwl_dict(self._path()) + self.assertFalse(d.check("Flagen")) + d.add("Esquilax") + d.add("Esquilam") + self.assertTrue(d.check("Esquilax")) + self.assertTrue("Esquilax" in self.getPWLContents()) + self.assertTrue(d.is_added("Esquilax")) + + def test_suggestions(self): + """Test getting suggestions from a PWL.""" + self.setPWLContents(["Sazz", "Lozz"]) + d = request_pwl_dict(self._path()) + self.assertTrue("Sazz" in d.suggest("Saz")) + self.assertTrue("Lozz" in d.suggest("laz")) + self.assertTrue("Sazz" in d.suggest("laz")) + d.add("Flagen") + self.assertTrue("Flagen" in d.suggest("Flags")) + self.assertFalse("sazz" in d.suggest("Flags")) + + def test_DWPWL(self): + """Test functionality of DictWithPWL.""" + self.setPWLContents(["Sazz", "Lozz"]) + d = DictWithPWL("en_US", self._path(), self._path("pel.txt")) + self.assertTrue(d.check("Sazz")) + self.assertTrue(d.check("Lozz")) + self.assertTrue(d.check("hello")) + self.assertFalse(d.check("helo")) + self.assertFalse(d.check("Flagen")) + d.add("Flagen") + self.assertTrue(d.check("Flagen")) + self.assertTrue("Flagen" in self.getPWLContents()) + self.assertTrue("Flagen" in d.suggest("Flagn")) + self.assertTrue("hello" in d.suggest("helo")) + d.remove("hello") + self.assertFalse(d.check("hello")) + self.assertTrue("hello" not in d.suggest("helo")) + d.remove("Lozz") + self.assertFalse(d.check("Lozz")) + + def test_DWPWL_empty(self): + """Test functionality of DictWithPWL using transient dicts.""" + d = DictWithPWL("en_US", None, None) + self.assertTrue(d.check("hello")) + self.assertFalse(d.check("helo")) + self.assertFalse(d.check("Flagen")) + d.add("Flagen") + self.assertTrue(d.check("Flagen")) + d.remove("hello") + self.assertFalse(d.check("hello")) + d.add("hello") + self.assertTrue(d.check("hello")) + + def test_PyPWL(self): + """Test our pure-python PWL implementation.""" + d = PyPWL() + self.assertTrue(list(d._words) == []) + d.add("hello") + d.add("there") + d.add("duck") + ws = list(d._words) + self.assertTrue(len(ws) == 3) + self.assertTrue("hello" in ws) + self.assertTrue("there" in ws) + self.assertTrue("duck" in ws) + d.remove("duck") + d.remove("notinthere") + ws = list(d._words) + self.assertTrue(len(ws) == 2) + self.assertTrue("hello" in ws) + self.assertTrue("there" in ws) + + def test_UnicodeCharsInPath(self): + """Test that unicode chars in PWL paths are accepted.""" + self._fileName = raw_unicode(r"test_\xe5\xe4\xf6_ing") + d = request_pwl_dict(self._path()) + self.assertTrue(d) + + +class TestUtils(unittest.TestCase): + """Test cases for various utility functions.""" + + def test_trim_suggestions(self): + word = "gud" + suggs = ["good", "god", "bad+"] + self.assertEquals(trim_suggestions(word, suggs, 40), ["god", "good", "bad+"]) + self.assertEquals(trim_suggestions(word, suggs, 4), ["god", "good", "bad+"]) + self.assertEquals(trim_suggestions(word, suggs, 3), ["god", "good", "bad+"]) + self.assertEquals(trim_suggestions(word, suggs, 2), ["god", "good"]) + self.assertEquals(trim_suggestions(word, suggs, 1), ["god"]) + self.assertEquals(trim_suggestions(word, suggs, 0), []) + + +class TestDocStrings(unittest.TestCase): + """Test the spelling on all docstrings we can find in this module. + + This serves two purposes - to provide a lot of test data for the + checker routines, and to make sure we don't suffer the embarrassment + of having spelling errors in a spellchecking package! + """ + + WORDS = ["spellchecking", "utf", "dict", "unicode", "bytestring", "bytestrings", + "str", "pyenchant", "ascii", "utils", "setup", "distutils", "pkg", + "filename", "tokenization", "tuple", "tuples", "tokenizer", + "tokenizers", "testcase", "testcases", "whitespace", "wxpython", + "spellchecker", "dialog", "urls", "wikiwords", "enchantobject", + "providerdesc", "spellcheck", "pwl", "aspell", "myspell", + "docstring", "docstrings", "stopiteration", "pwls", "pypwl", + "dictwithpwl", "skippable", "dicts", "dict's", "filenames", + "trie", "api", "ctypes", "wxspellcheckerdialog", "stateful", + "cmdlinechecker", "spellchecks", "callback", "clunkier", "iterator", + "ispell", "cor", "backends"] + + def test_docstrings(self): + """Test that all our docstrings are error-free.""" + import enchant + import enchant.utils + import enchant.pypwl + import enchant.tokenize + import enchant.tokenize.en + import enchant.checker + import enchant.checker.CmdLineChecker + try: + import enchant.checker.GtkSpellCheckerDialog + except ImportError: + pass + try: + import enchant.checker.wxSpellCheckerDialog + except ImportError: + pass + errors = [] + # Naive recursion here would blow the stack, instead we + # simulate it with our own stack + tocheck = [enchant] + checked = [] + while tocheck: + obj = tocheck.pop() + checked.append(obj) + newobjs = list(self._check_docstrings(obj, errors)) + tocheck.extend([obj for obj in newobjs if obj not in checked]) + self.assertEqual(len(errors), 0) + + def _check_docstrings(self, obj, errors): + import enchant + if hasattr(obj, "__doc__"): + skip_errors = [w for w in getattr(obj, "_DOC_ERRORS", [])] + chkr = enchant.checker.SpellChecker("en_AU", obj.__doc__, filters=[enchant.tokenize.URLFilter]) + for err in chkr: + if len(err.word) == 1: + continue + if err.word.lower() in self.WORDS: + continue + if skip_errors and skip_errors[0] == err.word: + skip_errors.pop(0) + continue + errors.append((obj, err.word, err.wordpos)) + msg = "\nDOCSTRING SPELLING ERROR: %s %s %d %s\n" % (obj, err.word, err.wordpos, chkr.suggest()) + printf([msg], file=sys.stderr) + # Find and yield all child objects that should be checked + for name in dir(obj): + if name.startswith("__"): + continue + child = getattr(obj, name) + if hasattr(child, "__file__"): + if not hasattr(globals(), "__file__"): + continue + if not child.__file__.startswith(os.path.dirname(__file__)): + continue + else: + cmod = getattr(child, "__module__", None) + if not cmod: + cclass = getattr(child, "__class__", None) + cmod = getattr(cclass, "__module__", None) + if cmod and not cmod.startswith("enchant"): + continue + yield child + + +class TestInstallEnv(unittest.TestCase): + """Run all testcases in a variety of install environments.""" + + def setUp(self): + self._tempDir = self._mkdtemp() + self._insDir = "build" + + def tearDown(self): + import shutil + shutil.rmtree(self._tempDir) + + def _mkdtemp(self): + import tempfile + return tempfile.mkdtemp() + + def install(self): + import os, sys, shutil + insdir = os.path.join(self._tempDir, self._insDir) + os.makedirs(insdir) + shutil.copytree("enchant", os.path.join(insdir, "enchant")) + + def runtests(self): + import os, sys + insdir = os.path.join(self._tempDir, self._insDir) + if str is not unicode and isinstance(insdir, unicode): + insdir = insdir.encode(sys.getfilesystemencoding()) + os.environ["PYTHONPATH"] = insdir + script = os.path.join(insdir, "enchant", "__init__.py") + res = runcmd("\"%s\" %s" % (sys.executable, script,)) + self.assertEqual(res, 0) + + def test_basic(self): + """Test proper functioning of TestInstallEnv suite.""" + self.install() + self.runtests() + + test_basic._DOC_ERRORS = ["TestInstallEnv"] + + def test_UnicodeInstallPath(self): + """Test installation in a path containing unicode chars.""" + self._insDir = raw_unicode(r'test_\xe5\xe4\xf6_ing') + self.install() + self.runtests() + + +class TestPy2exe(unittest.TestCase): + """Run all testcases inside a py2exe executable""" + _DOC_ERRORS = ["py", "exe"] + + def setUp(self): + self._tempDir = self._mkdtemp() + + def tearDown(self): + import shutil + shutil.rmtree(self._tempDir) + + def test_py2exe(self): + """Test pyenchant running inside a py2exe executable.""" + import os, sys, shutil + from os import path + from os.path import dirname + try: + import py2exe + except ImportError: + return + os.environ["PYTHONPATH"] = dirname(dirname(__file__)) + setup_py = path.join(dirname(__file__), "..", "tools", "setup.py2exe.py") + if not path.exists(setup_py): + return + buildCmd = '%s %s -q py2exe --dist-dir="%s"' + buildCmd = buildCmd % (sys.executable, setup_py, self._tempDir) + res = runcmd(buildCmd) + self.assertEqual(res, 0) + testCmd = self._tempDir + "\\test_pyenchant.exe" + self.assertTrue(os.path.exists(testCmd)) + res = runcmd(testCmd) + self.assertEqual(res, 0) + + test_py2exe._DOC_ERRORS = ["py", "exe"] + + def _mkdtemp(self): + import tempfile + return tempfile.mkdtemp() + + +def buildtestsuite(recurse=True): + from enchant.checker.tests import TestChecker + from enchant.tokenize.tests import TestTokenization, TestFilters + from enchant.tokenize.tests import TestTokenizeEN + suite = unittest.TestSuite() + if recurse: + suite.addTest(unittest.makeSuite(TestInstallEnv)) + suite.addTest(unittest.makeSuite(TestPy2exe)) + suite.addTest(unittest.makeSuite(TestBroker)) + suite.addTest(unittest.makeSuite(TestDict)) + suite.addTest(unittest.makeSuite(TestPWL)) + suite.addTest(unittest.makeSuite(TestUtils)) + suite.addTest(unittest.makeSuite(TestDocStrings)) + suite.addTest(unittest.makeSuite(TestChecker)) + suite.addTest(unittest.makeSuite(TestTokenization)) + suite.addTest(unittest.makeSuite(TestTokenizeEN)) + suite.addTest(unittest.makeSuite(TestFilters)) + return suite + + +def runtestsuite(recurse=False): + return unittest.TextTestRunner(verbosity=0).run(buildtestsuite(recurse=recurse)) diff --git a/PACK/enchant/tokenize/__init__.py b/PACK/enchant/tokenize/__init__.py new file mode 100644 index 0000000..50514da --- /dev/null +++ b/PACK/enchant/tokenize/__init__.py @@ -0,0 +1,536 @@ +# pyenchant +# +# Copyright (C) 2004-2009, Ryan Kelly +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# +# In addition, as a special exception, you are +# given permission to link the code of this program with +# non-LGPL Spelling Provider libraries (eg: a MSFT Office +# spell checker backend) and distribute linked combinations including +# the two. You must obey the GNU Lesser General Public License in all +# respects for all of the code used other than said providers. If you modify +# this file, you may extend this exception to your version of the +# file, but you are not obligated to do so. If you do not wish to +# do so, delete this exception statement from your version. +# +""" + +enchant.tokenize: String tokenization functions for PyEnchant +================================================================ + +An important task in spellchecking is breaking up large bodies of +text into their constituent words, each of which is then checked +for correctness. This package provides Python functions to split +strings into words according to the rules of a particular language. + +Each tokenization function accepts a string as its only positional +argument, and returns an iterator that yields tuples of the following +form, one for each word found:: + + (,) + +The meanings of these fields should be clear: is the word +that was found and is the position within the text at which +the word began (zero indexed, of course). The function will work +on any string-like object that supports array-slicing; in particular +character-array objects from the 'array' module may be used. + +The iterator also provides the attribute 'offset' which gives the current +position of the tokenizer inside the string being split, and the method +'set_offset' for manually adjusting this position. This can be used for +example if the string's contents have changed during the tokenization +process. + +To obtain an appropriate tokenization function for the language +identified by , use the function 'get_tokenizer(tag)':: + + tknzr = get_tokenizer("en_US") + for (word,pos) in tknzr("text to be tokenized goes here") + do_something(word) + +This library is designed to be easily extendible by third-party +authors. To register a tokenization function for the language +, implement it as the function 'tokenize' within the +module enchant.tokenize.. The 'get_tokenizer' function +will automatically detect it. Note that the underscore must be +used as the tag component separator in this case, in order to +form a valid python module name. (e.g. "en_US" rather than "en-US") + +Currently, a tokenizer has only been implemented for the English +language. Based on the author's limited experience, this should +be at least partially suitable for other languages. + +This module also provides various implementations of "Chunkers" and +"Filters". These classes are designed to make it easy to work with +text in a vareity of common formats, by detecting and excluding parts +of the text that don't need to be checked. + +A Chunker is a class designed to break a body of text into large chunks +of checkable content; for example the HTMLChunker class extracts the +text content from all HTML tags but excludes the tags themselves. +A Filter is a class designed to skip individual words during the checking +process; for example the URLFilter class skips over any words that +have the format of a URL. + +For exmaple, to spellcheck an HTML document it is necessary to split the +text into chunks based on HTML tags, and to filter out common word forms +such as URLs and WikiWords. This would look something like the following:: + + tknzr = get_tokenier("en_US",(HTMLChunker,),(URLFilter,WikiWordFilter))) + + text = "the url is http://example.com" + for (word,pos) in tknzer(text): + ...check each word and react accordingly... + +""" +_DOC_ERRORS = ["pos", "pos", "tknzr", "URLFilter", "WikiWordFilter", + "tkns", "tknzr", "pos", "tkns"] + +import re +import warnings + +import enchant +from enchant.utils import next, xrange +from enchant.errors import * + +# For backwards-compatability. This will eventually be removed, but how +# does one mark a module-level constant as deprecated? +Error = TokenizerNotFoundError + + +class tokenize: + """Base class for all tokenizer objects. + + Each tokenizer must be an iterator and provide the 'offset' + attribute as described in the documentation for this module. + + While tokenizers are in fact classes, they should be treated + like functions, and so are named using lower_case rather than + the CamelCase more traditional of class names. + """ + _DOC_ERRORS = ["CamelCase"] + + def __init__(self, text): + self._text = text + self._offset = 0 + + def __next__(self): + return self.next() + + def next(self): + raise NotImplementedError() + + def __iter__(self): + return self + + def set_offset(self, offset, replaced=False): + self._offset = offset + + def _get_offset(self): + return self._offset + + def _set_offset(self, offset): + msg = "changing a tokenizers 'offset' attribute is deprecated;" \ + " use the 'set_offset' method" + warnings.warn(msg, category=DeprecationWarning, stacklevel=2) + self.set_offset(offset) + + offset = property(_get_offset, _set_offset) + + +def get_tokenizer(tag=None, chunkers=None, filters=None): + """Locate an appropriate tokenizer by language tag. + + This requires importing the function 'tokenize' from an appropriate + module. Modules tried are named after the language tag, tried in the + following order: + * the entire tag (e.g. "en_AU.py") + * the base country code of the tag (e.g. "en.py") + + If the language tag is None, a default tokenizer (actually the English + one) is returned. It's unicode aware and should work OK for most + latin-derived languages. + + If a suitable function cannot be found, raises TokenizerNotFoundError. + + If given and not None, 'chunkers' and 'filters' must be lists of chunker + classes and filter classes resectively. These will be applied to the + tokenizer during creation. + """ + if tag is None: + tag = "en" + # "filters" used to be the second argument. Try to catch cases + # where it is given positionally and issue a DeprecationWarning. + if chunkers is not None and filters is None: + chunkers = list(chunkers) + if chunkers: + try: + chunkers_are_filters = issubclass(chunkers[0], Filter) + except TypeError: + pass + else: + if chunkers_are_filters: + msg = "passing 'filters' as a non-keyword argument " \ + "to get_tokenizer() is deprecated" + warnings.warn(msg, category=DeprecationWarning, stacklevel=2) + filters = chunkers + chunkers = None + # Ensure only '_' used as separator + tag = tag.replace("-", "_") + # First try the whole tag + tkFunc = _try_tokenizer(tag) + if tkFunc is None: + # Try just the base + base = tag.split("_")[0] + tkFunc = _try_tokenizer(base) + if tkFunc is None: + msg = "No tokenizer found for language '%s'" % (tag,) + raise TokenizerNotFoundError(msg) + # Given the language-specific tokenizer, we now build up the + # end result as follows: + # * chunk the text using any given chunkers in turn + # * begin with basic whitespace tokenization + # * apply each of the given filters in turn + # * apply language-specific rules + tokenizer = basic_tokenize + if chunkers is not None: + chunkers = list(chunkers) + for i in xrange(len(chunkers) - 1, -1, -1): + tokenizer = wrap_tokenizer(chunkers[i], tokenizer) + if filters is not None: + for f in filters: + tokenizer = f(tokenizer) + tokenizer = wrap_tokenizer(tokenizer, tkFunc) + return tokenizer + + +get_tokenizer._DOC_ERRORS = ["py", "py"] + + +class empty_tokenize(tokenize): + """Tokenizer class that yields no elements.""" + _DOC_ERRORS = [] + + def __init__(self): + tokenize.__init__(self, "") + + def next(self): + raise StopIteration() + + +class unit_tokenize(tokenize): + """Tokenizer class that yields the text as a single token.""" + _DOC_ERRORS = [] + + def __init__(self, text): + tokenize.__init__(self, text) + self._done = False + + def next(self): + if self._done: + raise StopIteration() + self._done = True + return (self._text, 0) + + +class basic_tokenize(tokenize): + """Tokenizer class that performs very basic word-finding. + + This tokenizer does the most basic thing that could work - it splits + text into words based on whitespace boundaries, and removes basic + punctuation symbols from the start and end of each word. + """ + _DOC_ERRORS = [] + + # Chars to remove from start/end of words + strip_from_start = '"' + "'`([" + strip_from_end = '"' + "'`]).!,?;:" + + def next(self): + text = self._text + offset = self._offset + while True: + if offset >= len(text): + break + # Find start of next word + while offset < len(text) and text[offset].isspace(): + offset += 1 + sPos = offset + # Find end of word + while offset < len(text) and not text[offset].isspace(): + offset += 1 + ePos = offset + self._offset = offset + # Strip chars from font/end of word + while sPos < len(text) and text[sPos] in self.strip_from_start: + sPos += 1 + while 0 < ePos and text[ePos - 1] in self.strip_from_end: + ePos -= 1 + # Return if word isnt empty + if (sPos < ePos): + return (text[sPos:ePos], sPos) + raise StopIteration() + + +def _try_tokenizer(modName): + """Look for a tokenizer in the named module. + + Returns the function if found, None otherwise. + """ + modBase = "enchant.tokenize." + funcName = "tokenize" + modName = modBase + modName + try: + mod = __import__(modName, globals(), {}, funcName) + return getattr(mod, funcName) + except ImportError: + return None + + +def wrap_tokenizer(tk1, tk2): + """Wrap one tokenizer inside another. + + This function takes two tokenizer functions 'tk1' and 'tk2', + and returns a new tokenizer function that passes the output + of tk1 through tk2 before yielding it to the calling code. + """ + # This logic is already implemented in the Filter class. + # We simply use tk2 as the _split() method for a filter + # around tk1. + tkW = Filter(tk1) + tkW._split = tk2 + return tkW + + +wrap_tokenizer._DOC_ERRORS = ["tk", "tk", "tk", "tk"] + + +class Chunker(tokenize): + """Base class for text chunking functions. + + A chunker is designed to chunk text into large blocks of tokens. It + has the same interface as a tokenizer but is for a different purpose. + """ + pass + + +class Filter(object): + """Base class for token filtering functions. + + A filter is designed to wrap a tokenizer (or another filter) and do + two things: + + * skip over tokens + * split tokens into sub-tokens + + Subclasses have two basic options for customising their behaviour. The + method _skip(word) may be overridden to return True for words that + should be skipped, and false otherwise. The method _split(word) may + be overridden as tokenization function that will be applied to further + tokenize any words that aren't skipped. + """ + + def __init__(self, tokenizer): + """Filter class constructor.""" + self._tokenizer = tokenizer + + def __call__(self, *args, **kwds): + tkn = self._tokenizer(*args, **kwds) + return self._TokenFilter(tkn, self._skip, self._split) + + def _skip(self, word): + """Filter method for identifying skippable tokens. + + If this method returns true, the given word will be skipped by + the filter. This should be overridden in subclasses to produce the + desired functionality. The default behaviour is not to skip any words. + """ + return False + + def _split(self, word): + """Filter method for sub-tokenization of tokens. + + This method must be a tokenization function that will split the + given word into sub-tokens according to the needs of the filter. + The default behaviour is not to split any words. + """ + return unit_tokenize(word) + + class _TokenFilter(object): + """Private inner class implementing the tokenizer-wrapping logic. + + This might seem convoluted, but we're trying to create something + akin to a meta-class - when Filter(tknzr) is called it must return + a *callable* that can then be applied to a particular string to + perform the tokenization. Since we need to manage a lot of state + during tokenization, returning a class is the best option. + """ + _DOC_ERRORS = ["tknzr"] + + def __init__(self, tokenizer, skip, split): + self._skip = skip + self._split = split + self._tokenizer = tokenizer + # for managing state of sub-tokenization + self._curtok = empty_tokenize() + self._curword = "" + self._curpos = 0 + + def __iter__(self): + return self + + def __next__(self): + return self.next() + + def next(self): + # Try to get the next sub-token from word currently being split. + # If unavailable, move on to the next word and try again. + try: + (word, pos) = next(self._curtok) + return (word, pos + self._curpos) + except StopIteration: + (word, pos) = next(self._tokenizer) + while self._skip(word): + (word, pos) = next(self._tokenizer) + self._curword = word + self._curpos = pos + self._curtok = self._split(word) + return self.next() + + # Pass on access to 'offset' to the underlying tokenizer. + def _get_offset(self): + return self._tokenizer.offset + + def _set_offset(self, offset): + msg = "changing a tokenizers 'offset' attribute is deprecated;" \ + " use the 'set_offset' method" + warnings.warn(msg, category=DeprecationWarning, stacklevel=2) + self.set_offset(offset) + + offset = property(_get_offset, _set_offset) + + def set_offset(self, val, replaced=False): + self._tokenizer.set_offset(val, replaced=replaced) + # If we stay within the current word, also set on _curtok. + # Otherwise, throw away _curtok and set to empty iterator. + subval = val - self._curpos + if subval >= 0 and subval < len(self._curword) and not replaced: + self._curtok.set_offset(subval) + else: + self._curtok = empty_tokenize() + self._curword = "" + self._curpos = 0 + + +# Pre-defined chunkers and filters start here + +class URLFilter(Filter): + """Filter skipping over URLs. + This filter skips any words matching the following regular expression: + + ^[a-zA-z]+:\/\/[^\s].* + + That is, any words that are URLs. + """ + _DOC_ERRORS = ["zA"] + _pattern = re.compile(r"^[a-zA-z]+:\/\/[^\s].*") + + def _skip(self, word): + if self._pattern.match(word): + return True + return False + + +class WikiWordFilter(Filter): + """Filter skipping over WikiWords. + This filter skips any words matching the following regular expression: + + ^([A-Z]\w+[A-Z]+\w+) + + That is, any words that are WikiWords. + """ + _pattern = re.compile(r"^([A-Z]\w+[A-Z]+\w+)") + + def _skip(self, word): + if self._pattern.match(word): + return True + return False + + +class EmailFilter(Filter): + """Filter skipping over email addresses. + This filter skips any words matching the following regular expression: + + ^.+@[^\.].*\.[a-z]{2,}$ + + That is, any words that resemble email addresses. + """ + _pattern = re.compile(r"^.+@[^\.].*\.[a-z]{2,}$") + + def _skip(self, word): + if self._pattern.match(word): + return True + return False + + +class HTMLChunker(Chunker): + """Chunker for breaking up HTML documents into chunks of checkable text. + + The operation of this chunker is very simple - anything between a "<" + and a ">" will be ignored. Later versions may improve the algorithm + slightly. + """ + + def next(self): + text = self._text + offset = self.offset + while True: + if offset >= len(text): + break + # Skip to the end of the current tag, if any. + if text[offset] == "<": + maybeTag = offset + if self._is_tag(text, offset): + while text[offset] != ">": + offset += 1 + if offset == len(text): + offset = maybeTag + 1 + break + else: + offset += 1 + else: + offset = maybeTag + 1 + sPos = offset + # Find the start of the next tag. + while offset < len(text) and text[offset] != "<": + offset += 1 + ePos = offset + self._offset = offset + # Return if chunk isnt empty + if (sPos < offset): + return (text[sPos:offset], sPos) + raise StopIteration() + + def _is_tag(self, text, offset): + if offset + 1 < len(text): + if text[offset + 1].isalpha(): + return True + if text[offset + 1] == "/": + return True + return False + +# TODO: LaTeXChunker diff --git a/PACK/enchant/tokenize/en.py b/PACK/enchant/tokenize/en.py new file mode 100644 index 0000000..8ee1204 --- /dev/null +++ b/PACK/enchant/tokenize/en.py @@ -0,0 +1,172 @@ +# pyenchant +# +# Copyright (C) 2004-2008, Ryan Kelly +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# +# In addition, as a special exception, you are +# given permission to link the code of this program with +# non-LGPL Spelling Provider libraries (eg: a MSFT Office +# spell checker backend) and distribute linked combinations including +# the two. You must obey the GNU Lesser General Public License in all +# respects for all of the code used other than said providers. If you modify +# this file, you may extend this exception to your version of the +# file, but you are not obligated to do so. If you do not wish to +# do so, delete this exception statement from your version. +# +""" + + enchant.tokenize.en: Tokenizer for the English language + + This module implements a PyEnchant text tokenizer for the English + language, based on very simple rules. + +""" + +import unicodedata + +import enchant.tokenize +from enchant.utils import unicode + + +class tokenize(enchant.tokenize.tokenize): + """Iterator splitting text into words, reporting position. + + This iterator takes a text string as input, and yields tuples + representing each distinct word found in the text. The tuples + take the form: + + (,) + + Where is the word string found and is the position + of the start of the word within the text. + + The optional argument may be used to specify a + list of additional characters that can form part of a word. + By default, this list contains only the apostrophe ('). Note that + these characters cannot appear at the start or end of a word. + """ + + _DOC_ERRORS = ["pos", "pos"] + + def __init__(self, text, valid_chars=("'",)): + self._valid_chars = valid_chars + self._text = text + self._offset = 0 + # Select proper implementation of self._consume_alpha. + # 'text' isn't necessarily a string (it could be e.g. a mutable array) + # so we can't use isinstance(text,unicode) to detect unicode. + # Instead we typetest the first character of the text. + # If there's no characters then it doesn't matter what implementation + # we use since it won't be called anyway. + try: + char1 = text[0] + except IndexError: + self._consume_alpha = self._consume_alpha_b + else: + if isinstance(char1, unicode): + self._consume_alpha = self._consume_alpha_u + else: + self._consume_alpha = self._consume_alpha_b + + def _consume_alpha_b(self, text, offset): + """Consume an alphabetic character from the given bytestring. + + Given a bytestring and the current offset, this method returns + the number of characters occupied by the next alphabetic character + in the string. Non-ASCII bytes are interpreted as utf-8 and can + result in multiple characters being consumed. + """ + assert offset < len(text) + if text[offset].isalpha(): + return 1 + elif text[offset] >= "\x80": + return self._consume_alpha_utf8(text, offset) + return 0 + + def _consume_alpha_utf8(self, text, offset): + """Consume a sequence of utf8 bytes forming an alphabetic character.""" + incr = 2 + u = "" + while not u and incr <= 4: + try: + try: + # In the common case this will be a string + u = text[offset:offset + incr].decode("utf8") + except AttributeError: + # Looks like it was e.g. a mutable char array. + try: + s = text[offset:offset + incr].tostring() + except AttributeError: + s = "".join([c for c in text[offset:offset + incr]]) + u = s.decode("utf8") + except UnicodeDecodeError: + incr += 1 + if not u: + return 0 + if u.isalpha(): + return incr + if unicodedata.category(u)[0] == "M": + return incr + return 0 + + def _consume_alpha_u(self, text, offset): + """Consume an alphabetic character from the given unicode string. + + Given a unicode string and the current offset, this method returns + the number of characters occupied by the next alphabetic character + in the string. Trailing combining characters are consumed as a + single letter. + """ + assert offset < len(text) + incr = 0 + if text[offset].isalpha(): + incr = 1 + while offset + incr < len(text): + if unicodedata.category(text[offset + incr])[0] != "M": + break + incr += 1 + return incr + + def next(self): + text = self._text + offset = self._offset + while offset < len(text): + # Find start of next word (must be alpha) + while offset < len(text): + incr = self._consume_alpha(text, offset) + if incr: + break + offset += 1 + curPos = offset + # Find end of word using, allowing valid_chars + while offset < len(text): + incr = self._consume_alpha(text, offset) + if not incr: + if text[offset] in self._valid_chars: + incr = 1 + else: + break + offset += incr + # Return if word isnt empty + if (curPos != offset): + # Make sure word doesn't end with a valid_char + while text[offset - 1] in self._valid_chars: + offset = offset - 1 + self._offset = offset + return (text[curPos:offset], curPos) + self._offset = offset + raise StopIteration() diff --git a/PACK/enchant/tokenize/tests.py b/PACK/enchant/tokenize/tests.py new file mode 100644 index 0000000..569b593 --- /dev/null +++ b/PACK/enchant/tokenize/tests.py @@ -0,0 +1,326 @@ +# pyenchant +# +# Copyright (C) 2004-2008, Ryan Kelly +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# +# In addition, as a special exception, you are +# given permission to link the code of this program with +# non-LGPL Spelling Provider libraries (eg: a MSFT Office +# spell checker backend) and distribute linked combinations including +# the two. You must obey the GNU Lesser General Public License in all +# respects for all of the code used other than said providers. If you modify +# this file, you may extend this exception to your version of the +# file, but you are not obligated to do so. If you do not wish to +# do so, delete this exception statement from your version. +# +""" + + enchant.tokenize.tests: unittests for enchant tokenization functions. + +""" + +import unittest +import array + +from enchant.tokenize import * +from enchant.tokenize.en import tokenize as tokenize_en +from enchant.utils import raw_unicode, unicode, bytes + + +class TestTokenization(unittest.TestCase): + """TestCases for testing the basic tokenization functionality.""" + + def test_basic_tokenize(self): + """Simple regression test for basic white-space tokenization.""" + input = """This is a paragraph. It's not very special, but it's designed +2 show how the splitter works with many-different combos +of words. Also need to "test" the (handling) of 'quoted' words.""" + output = [ + ("This", 0), ("is", 5), ("a", 8), ("paragraph", 10), ("It's", 22), + ("not", 27), ("very", 31), ("special", 36), ("but", 45), ("it's", 49), + ("designed", 54), ("2", 63), ("show", 65), ("how", 70), ("the", 74), + ("splitter", 78), ("works", 87), ("with", 93), ("many-different", 98), + ("combos", 113), ("of", 120), ("words", 123), + ("Also", 130), ("need", 135), + ("to", 140), ("test", 144), ("the", 150), ("handling", 155), + ("of", 165), ("quoted", 169), ("words", 177) + ] + self.assertEqual(output, [i for i in basic_tokenize(input)]) + for (itmO, itmV) in zip(output, basic_tokenize(input)): + self.assertEqual(itmO, itmV) + + def test_tokenize_strip(self): + """Test special-char-stripping edge-cases in basic_tokenize.""" + input = "((' \"\" 'text' has (lots) of (special chars} >>]" + output = [("", 4), ("text", 15), ("has", 21), ("lots", 26), ("of", 32), + ("special", 36), ("chars}", 44), (">>", 51)] + self.assertEqual(output, [i for i in basic_tokenize(input)]) + for (itmO, itmV) in zip(output, basic_tokenize(input)): + self.assertEqual(itmO, itmV) + + def test_wrap_tokenizer(self): + """Test wrapping of one tokenizer with another.""" + input = "this-string will be split@according to diff'rnt rules" + from enchant.tokenize import en + tknzr = wrap_tokenizer(basic_tokenize, en.tokenize) + tknzr = tknzr(input) + self.assertEqual(tknzr._tokenizer.__class__, basic_tokenize) + self.assertEqual(tknzr._tokenizer.offset, 0) + for (n, (word, pos)) in enumerate(tknzr): + if n == 0: + self.assertEqual(pos, 0) + self.assertEqual(word, "this") + if n == 1: + self.assertEqual(pos, 5) + self.assertEqual(word, "string") + if n == 2: + self.assertEqual(pos, 12) + self.assertEqual(word, "will") + # Test setting offset to a previous token + tknzr.set_offset(5) + self.assertEqual(tknzr.offset, 5) + self.assertEqual(tknzr._tokenizer.offset, 5) + self.assertEqual(tknzr._curtok.__class__, empty_tokenize) + if n == 3: + self.assertEqual(word, "string") + self.assertEqual(pos, 5) + if n == 4: + self.assertEqual(pos, 12) + self.assertEqual(word, "will") + if n == 5: + self.assertEqual(pos, 17) + self.assertEqual(word, "be") + # Test setting offset past the current token + tknzr.set_offset(20) + self.assertEqual(tknzr.offset, 20) + self.assertEqual(tknzr._tokenizer.offset, 20) + self.assertEqual(tknzr._curtok.__class__, empty_tokenize) + if n == 6: + self.assertEqual(pos, 20) + self.assertEqual(word, "split") + if n == 7: + self.assertEqual(pos, 26) + self.assertEqual(word, "according") + # Test setting offset to middle of current token + tknzr.set_offset(23) + self.assertEqual(tknzr.offset, 23) + self.assertEqual(tknzr._tokenizer.offset, 23) + self.assertEqual(tknzr._curtok.offset, 3) + if n == 8: + self.assertEqual(pos, 23) + self.assertEqual(word, "it") + # OK, I'm pretty happy with the behaviour, no need to + # continue testing the rest of the string + + +class TestFilters(unittest.TestCase): + """TestCases for the various Filter subclasses.""" + + text = """this text with http://url.com and SomeLinksLike + ftp://my.site.com.au/some/file AndOthers not:/quite.a.url + with-an@aemail.address as well""" + + def setUp(self): + pass + + def test_URLFilter(self): + """Test filtering of URLs""" + tkns = get_tokenizer("en_US", filters=(URLFilter,))(self.text) + out = [t for t in tkns] + exp = [("this", 0), ("text", 5), ("with", 10), ("and", 30), + ("SomeLinksLike", 34), ("AndOthers", 93), ("not", 103), ("quite", 108), + ("a", 114), ("url", 116), ("with", 134), ("an", 139), ("aemail", 142), + ("address", 149), ("as", 157), ("well", 160)] + self.assertEqual(out, exp) + + def test_WikiWordFilter(self): + """Test filtering of WikiWords""" + tkns = get_tokenizer("en_US", filters=(WikiWordFilter,))(self.text) + out = [t for t in tkns] + exp = [("this", 0), ("text", 5), ("with", 10), ("http", 15), ("url", 22), ("com", 26), + ("and", 30), ("ftp", 62), ("my", 68), ("site", 71), ("com", 76), ("au", 80), + ("some", 83), ("file", 88), ("not", 103), ("quite", 108), + ("a", 114), ("url", 116), ("with", 134), ("an", 139), ("aemail", 142), + ("address", 149), ("as", 157), ("well", 160)] + self.assertEqual(out, exp) + + def test_EmailFilter(self): + """Test filtering of email addresses""" + tkns = get_tokenizer("en_US", filters=(EmailFilter,))(self.text) + out = [t for t in tkns] + exp = [("this", 0), ("text", 5), ("with", 10), ("http", 15), ("url", 22), ("com", 26), + ("and", 30), ("SomeLinksLike", 34), + ("ftp", 62), ("my", 68), ("site", 71), ("com", 76), ("au", 80), + ("some", 83), ("file", 88), ("AndOthers", 93), ("not", 103), ("quite", 108), + ("a", 114), ("url", 116), + ("as", 157), ("well", 160)] + self.assertEqual(out, exp) + + def test_CombinedFilter(self): + """Test several filters combined""" + tkns = get_tokenizer("en_US", filters=(URLFilter, WikiWordFilter, EmailFilter))(self.text) + out = [t for t in tkns] + exp = [("this", 0), ("text", 5), ("with", 10), + ("and", 30), ("not", 103), ("quite", 108), + ("a", 114), ("url", 116), + ("as", 157), ("well", 160)] + self.assertEqual(out, exp) + + +class TestChunkers(unittest.TestCase): + """TestCases for the various Chunker subclasses.""" + + def test_HTMLChunker(self): + """Test filtering of URLs""" + text = """hellomy titlethis is a + simple HTML document for

testing purposes

. + It < contains > various <-- special characters. + """ + tkns = get_tokenizer("en_US", chunkers=(HTMLChunker,))(text) + out = [t for t in tkns] + exp = [("hello", 0), ("my", 24), ("title", 27), ("this", 53), ("is", 58), + ("a", 61), ("simple", 82), ("HTML", 93), ("document", 98), ("for", 107), + ("test", 115), ("ing", 122), ("purposes", 130), ("It", 160), + ("contains", 165), ("various", 176), ("special", 188), + ("characters", 196)] + self.assertEqual(out, exp) + for (word, pos) in out: + self.assertEqual(text[pos:pos + len(word)], word) + + +class TestTokenizeEN(unittest.TestCase): + """TestCases for checking behaviour of English tokenization.""" + + def test_tokenize_en(self): + """Simple regression test for English tokenization.""" + input = """This is a paragraph. It's not very special, but it's designed +2 show how the splitter works with many-different combos +of words. Also need to "test" the handling of 'quoted' words.""" + output = [ + ("This", 0), ("is", 5), ("a", 8), ("paragraph", 10), ("It's", 22), + ("not", 27), ("very", 31), ("special", 36), ("but", 45), ("it's", 49), + ("designed", 54), ("show", 65), ("how", 70), ("the", 74), + ("splitter", 78), ("works", 87), ("with", 93), ("many", 98), + ("different", 103), ("combos", 113), ("of", 120), ("words", 123), + ("Also", 130), ("need", 135), + ("to", 140), ("test", 144), ("the", 150), ("handling", 154), + ("of", 163), ("quoted", 167), ("words", 175) + ] + for (itmO, itmV) in zip(output, tokenize_en(input)): + self.assertEqual(itmO, itmV) + + def test_unicodeBasic(self): + """Test tokenization of a basic unicode string.""" + input = raw_unicode( + r"Ik ben ge\u00EFnteresseerd in de co\u00F6rdinatie van mijn knie\u00EBn, maar kan niet \u00E9\u00E9n \u00E0 twee enqu\u00EAtes vinden die recht doet aan mijn carri\u00E8re op Cura\u00E7ao") + output = input.split(" ") + output[8] = output[8][0:-1] + for (itmO, itmV) in zip(output, tokenize_en(input)): + self.assertEqual(itmO, itmV[0]) + self.assertTrue(input[itmV[1]:].startswith(itmO)) + + def test_unicodeCombining(self): + """Test tokenization with unicode combining symbols.""" + input = raw_unicode( + r"Ik ben gei\u0308nteresseerd in de co\u00F6rdinatie van mijn knie\u00EBn, maar kan niet e\u0301e\u0301n \u00E0 twee enqu\u00EAtes vinden die recht doet aan mijn carri\u00E8re op Cura\u00E7ao") + output = input.split(" ") + output[8] = output[8][0:-1] + for (itmO, itmV) in zip(output, tokenize_en(input)): + self.assertEqual(itmO, itmV[0]) + self.assertTrue(input[itmV[1]:].startswith(itmO)) + + def test_utf8_bytes(self): + """Test tokenization of UTF8-encoded bytes (bug #2500184).""" + # Python3 doesn't support bytestrings, don't run this test + if str is unicode: + return + input = "A r\xc3\xa9sum\xc3\xa9, also spelled resum\xc3\xa9 or resume" + output = input.split(" ") + output[1] = output[1][0:-1] + for (itmO, itmV) in zip(output, tokenize_en(input)): + self.assertEqual(itmO, itmV[0]) + self.assertTrue(input[itmV[1]:].startswith(itmO)) + + def test_utf8_bytes_at_end(self): + """Test tokenization of UTF8-encoded bytes at end of word.""" + # Python3 doesn't support bytestrings, don't run this test + if str is unicode: + return + input = "A r\xc3\xa9sum\xc3\xa9, also spelled resum\xc3\xa9 or resume" + output = input.split(" ") + output[1] = output[1][0:-1] + for (itmO, itmV) in zip(output, tokenize_en(input)): + self.assertEqual(itmO, itmV[0]) + + def test_utf8_bytes_in_an_array(self): + """Test tokenization of UTF8-encoded bytes stored in an array.""" + # Python3 doesn't support bytestrings, don't run this test + if str is unicode: + return + input = "A r\xc3\xa9sum\xc3\xa9, also spelled resum\xc3\xa9 or resume" + output = input.split(" ") + output[1] = output[1][0:-1] + input = array.array('c', input) + output = [array.array('c', w) for w in output] + for (itmO, itmV) in zip(output, tokenize_en(array.array('c', input))): + self.assertEqual(itmO, itmV[0]) + self.assertEqual(input[itmV[1]:itmV[1] + len(itmV[0])], itmO) + + def test_bug1591450(self): + """Check for tokenization regressions identified in bug #1591450.""" + input = """Testing markup and {y:i}so-forth...leading dots and trail--- well, you get-the-point. Also check numbers: 999 1,000 12:00 .45. Done?""" + output = [ + ("Testing", 0), ("i", 9), ("markup", 11), ("i", 19), ("and", 22), + ("y", 27), ("i", 29), ("so", 31), ("forth", 34), ("leading", 42), + ("dots", 50), ("and", 55), ("trail", 59), ("well", 68), + ("you", 74), ("get", 78), ("the", 82), ("point", 86), + ("Also", 93), ("check", 98), ("numbers", 104), ("Done", 134), + ] + for (itmO, itmV) in zip(output, tokenize_en(input)): + self.assertEqual(itmO, itmV) + + def test_bug2785373(self): + """Testcases for bug #2785373""" + input = "So, one dey when I wes 17, I left." + for _ in tokenize_en(input): + pass + input = raw_unicode("So, one dey when I wes 17, I left.") + for _ in tokenize_en(input): + pass + + def test_finnish_text(self): + """Test tokenizing some Finnish text. + + This really should work since there are no special rules to apply, + just lots of non-ascii characters. + """ + inputT = raw_unicode( + 'T\\xe4m\\xe4 on kappale. Eip\\xe4 ole kovin 2 nen, mutta tarkoitus on n\\xe4ytt\\xe4\\xe4 miten sanastaja \\ntoimii useiden-erilaisten sanarypp\\xe4iden kimpussa.\\nPit\\xe4\\xe4p\\xe4 viel\\xe4 \'tarkistaa\' sanat jotka "lainausmerkeiss\\xe4". Heittomerkki ja vaa\'an.\\nUlkomaisia sanoja s\\xfcss, spa\\xdf.') + outputT = [ + (raw_unicode('T\\xe4m\\xe4'), 0), (raw_unicode('on'), 5), (raw_unicode('kappale'), 8), + (raw_unicode('Eip\\xe4'), 17), (raw_unicode('ole'), 22), (raw_unicode('kovin'), 26), + (raw_unicode('nen'), 34), (raw_unicode('mutta'), 39), (raw_unicode('tarkoitus'), 45), + (raw_unicode('on'), 55), (raw_unicode('n\\xe4ytt\\xe4\\xe4'), 58), (raw_unicode('miten'), 66), + (raw_unicode('sanastaja'), 72), (raw_unicode('toimii'), 83), (raw_unicode('useiden'), 90), + (raw_unicode('erilaisten'), 98), (raw_unicode('sanarypp\\xe4iden'), 109), (raw_unicode('kimpussa'), 123), + (raw_unicode('Pit\\xe4\\xe4p\\xe4'), 133), (raw_unicode('viel\\xe4'), 141), (raw_unicode('tarkistaa'), 148), + (raw_unicode('sanat'), 159), (raw_unicode('jotka'), 165), (raw_unicode('lainausmerkeiss\\xe4'), 172), + (raw_unicode('Heittomerkki'), 191), (raw_unicode('ja'), 204), (raw_unicode("vaa'an"), 207), + (raw_unicode('Ulkomaisia'), 215), (raw_unicode('sanoja'), 226), (raw_unicode('s\\xfcss'), 233), + (raw_unicode('spa\\xdf'), 239), ] + for (itmO, itmV) in zip(outputT, tokenize_en(inputT)): + self.assertEqual(itmO, itmV) diff --git a/PACK/enchant/utils.py b/PACK/enchant/utils.py new file mode 100644 index 0000000..e919fd7 --- /dev/null +++ b/PACK/enchant/utils.py @@ -0,0 +1,354 @@ +# pyenchant +# +# Copyright (C) 2004-2008 Ryan Kelly +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA. +# +# In addition, as a special exception, you are +# given permission to link the code of this program with +# non-LGPL Spelling Provider libraries (eg: a MSFT Office +# spell checker backend) and distribute linked combinations including +# the two. You must obey the GNU Lesser General Public License in all +# respects for all of the code used other than said providers. If you modify +# this file, you may extend this exception to your version of the +# file, but you are not obligated to do so. If you do not wish to +# do so, delete this exception statement from your version. +# +""" + +enchant.utils: Misc utilities for the enchant package +======================================================== + +This module provides miscellaneous utilities for use with the +enchant spellchecking package. Currently available functionality +includes: + + * string/unicode compatibility wrappers + * functions for dealing with locale/language settings + * ability to list supporting data files (win32 only) + * functions for bundling supporting data files from a build + +""" + +import os +import sys +import codecs + +from enchant.errors import * + +# Attempt to access local language information +try: + import locale +except ImportError: + locale = None + +# +# Unicode/Bytes compatabilty wrappers. +# +# These allow us to support both Python 2.x and Python 3.x from +# the same codebase. +# +# We provide explicit type objects "bytes" and "unicode" that can be +# used to construct instances of the appropriate type. The class +# "EnchantStr" derives from the default "str" type and implements the +# necessary logic for encoding/decoding as strings are passed into +# the underlying C library (where they must always be utf-8 encoded +# byte strings). +# + +try: + unicode = unicode +except NameError: + str = str + unicode = str + bytes = bytes + basestring = (str, bytes) +else: + str = str + unicode = unicode + bytes = str + basestring = basestring + + +def raw_unicode(raw): + """Make a unicode string from a raw string. + + This function takes a string containing unicode escape characters, + and returns the corresponding unicode string. Useful for writing + unicode string literals in your python source while being upwards- + compatible with Python 3. For example, instead of doing this: + + s = u"hello\u2149" # syntax error in Python 3 + + Or this: + + s = "hello\u2149" # not what you want in Python 2.x + + You can do this: + + s = raw_unicode(r"hello\u2149") # works everywhere! + + """ + return raw.encode("utf8").decode("unicode-escape") + + +def raw_bytes(raw): + """Make a bytes object out of a raw string. + + This is analogous to raw_unicode, but processes byte escape characters + to produce a bytes object. + """ + return codecs.escape_decode(raw)[0] + + +class EnchantStr(str): + """String subclass for interfacing with enchant C library. + + This class encapsulates the logic for interfacing between python native + string/unicode objects and the underlying enchant library, which expects + all strings to be UTF-8 character arrays. It is a subclass of the + default string class 'str' - on Python 2.x that makes it an ascii string, + on Python 3.x it is a unicode object. + + Initialise it with a string or unicode object, and use the encode() method + to obtain an object suitable for passing to the underlying C library. + When strings are read back into python, use decode(s) to translate them + back into the appropriate python-level string type. + + This allows us to following the common Python 2.x idiom of returning + unicode when unicode is passed in, and byte strings otherwise. It also + lets the interface be upwards-compatible with Python 3, in which string + objects are unicode by default. + """ + + def __new__(cls, value): + """EnchantStr data constructor. + + This method records whether the initial string was unicode, then + simply passes it along to the default string constructor. + """ + if type(value) is unicode: + was_unicode = True + if str is not unicode: + value = value.encode("utf-8") + else: + was_unicode = False + if str is not bytes: + raise Error("Don't pass bytestrings to pyenchant") + self = str.__new__(cls, value) + self._was_unicode = was_unicode + return self + + def encode(self): + """Encode this string into a form usable by the enchant C library.""" + if str is unicode: + return str.encode(self, "utf-8") + else: + return self + + def decode(self, value): + """Decode a string returned by the enchant C library.""" + if self._was_unicode: + if str is unicode: + # On some python3 versions, ctypes converts c_char_p + # to str() rather than bytes() + if isinstance(value, str): + value = value.encode() + return value.decode("utf-8") + else: + return value.decode("utf-8") + else: + return value + + +def printf(values, sep=" ", end="\n", file=None): + """Compatability wrapper from print statement/function. + + This function is a simple Python2/Python3 compatability wrapper + for printing to stdout. + """ + if file is None: + file = sys.stdout + file.write(sep.join(map(str, values))) + file.write(end) + + +try: + next = next +except NameError: + def next(iter): + """Compatability wrapper for advancing an iterator.""" + return iter.next() + +try: + xrange = xrange +except NameError: + xrange = range + + +# +# Other useful functions. +# + + +def levenshtein(s1, s2): + """Calculate the Levenshtein distance between two strings. + + This is straight from Wikipedia. + """ + if len(s1) < len(s2): + return levenshtein(s2, s1) + if not s1: + return len(s2) + + previous_row = xrange(len(s2) + 1) + for i, c1 in enumerate(s1): + current_row = [i + 1] + for j, c2 in enumerate(s2): + insertions = previous_row[j + 1] + 1 + deletions = current_row[j] + 1 + substitutions = previous_row[j] + (c1 != c2) + current_row.append(min(insertions, deletions, substitutions)) + previous_row = current_row + + return previous_row[-1] + + +def trim_suggestions(word, suggs, maxlen, calcdist=None): + """Trim a list of suggestions to a maximum length. + + If the list of suggested words is too long, you can use this function + to trim it down to a maximum length. It tries to keep the "best" + suggestions based on similarity to the original word. + + If the optional "calcdist" argument is provided, it must be a callable + taking two words and returning the distance between them. It will be + used to determine which words to retain in the list. The default is + a simple Levenshtein distance. + """ + if calcdist is None: + calcdist = levenshtein + decorated = [(calcdist(word, s), s) for s in suggs] + decorated.sort() + return [s for (l, s) in decorated[:maxlen]] + + +def get_default_language(default=None): + """Determine the user's default language, if possible. + + This function uses the 'locale' module to try to determine + the user's preferred language. The return value is as + follows: + + * if a locale is available for the LC_MESSAGES category, + that language is used + * if a default locale is available, that language is used + * if the keyword argument is given, it is used + * if nothing else works, None is returned + + Note that determining the user's language is in general only + possible if they have set the necessary environment variables + on their system. + """ + try: + import locale + tag = locale.getlocale()[0] + if tag is None: + tag = locale.getdefaultlocale()[0] + if tag is None: + raise Error("No default language available") + return tag + except Exception: + pass + return default + + +get_default_language._DOC_ERRORS = ["LC"] + + +def get_resource_filename(resname): + """Get the absolute path to the named resource file. + + This serves widely the same purpose as pkg_resources.resource_filename(), + but tries to avoid loading pkg_resources unless we're actually in + an egg. + """ + path = os.path.dirname(os.path.abspath(__file__)) + path = os.path.join(path, resname) + if os.path.exists(path): + return path + if hasattr(sys, "frozen"): + exe_path = unicode(sys.executable, sys.getfilesystemencoding()) + exe_dir = os.path.dirname(exe_path) + path = os.path.join(exe_dir, resname) + if os.path.exists(path): + return path + else: + import pkg_resources + try: + path = pkg_resources.resource_filename("enchant", resname) + except KeyError: + pass + else: + path = os.path.abspath(path) + if os.path.exists(path): + return path + raise Error("Could not locate resource '%s'" % (resname,)) + + +def win32_data_files(): + """Get list of supporting data files, for use with setup.py + + This function returns a list of the supporting data files available + to the running version of PyEnchant. This is in the format expected + by the data_files argument of the distutils setup function. It's + very useful, for example, for including the data files in an executable + produced by py2exe. + + Only really tested on the win32 platform (it's the only platform for + which we ship our own supporting data files) + """ + # Include the main enchant DLL + try: + libEnchant = get_resource_filename("libenchant.dll") + except Error: + libEnchant = get_resource_filename("libenchant-1.dll") + mainDir = os.path.dirname(libEnchant) + dataFiles = [('', [libEnchant])] + # And some specific supporting DLLs + for dll in os.listdir(mainDir): + if not dll.endswith(".dll"): + continue + for prefix in ("iconv", "intl", "libglib", "libgmodule"): + if dll.startswith(prefix): + break + else: + continue + dataFiles[0][1].append(os.path.join(mainDir, dll)) + # And anything found in the supporting data directories + dataDirs = ("share/enchant/myspell", "share/enchant/ispell", "lib/enchant") + for dataDir in dataDirs: + files = [] + fullDir = os.path.join(mainDir, os.path.normpath(dataDir)) + for fn in os.listdir(fullDir): + fullFn = os.path.join(fullDir, fn) + if os.path.isfile(fullFn): + files.append(fullFn) + dataFiles.append((dataDir, files)) + return dataFiles + + +win32_data_files._DOC_ERRORS = ["py", "py", "exe"] diff --git a/PACK/maskgen.py b/PACK/maskgen.py new file mode 100755 index 0000000..9c941f8 --- /dev/null +++ b/PACK/maskgen.py @@ -0,0 +1,312 @@ +#!/usr/bin/python +# MaskGen - Generate Password Masks +# +# This tool is part of PACK (Password Analysis and Cracking Kit) +# +# VERSION 0.0.3 +# +# Copyright (C) 2013 Peter Kacherginsky +# All rights reserved. +# +# Please see the attached LICENSE file for additional licensing information. + +import sys +import csv +import datetime +from operator import itemgetter +from optparse import OptionParser, OptionGroup + +VERSION = "0.0.3" + + +class MaskGen: + def __init__(self): + # Masks collections with meta data + self.masks = dict() + + self.target_time = None + self.output_file = None + + self.minlength = None + self.maxlength = None + self.mintime = None + self.maxtime = None + self.mincomplexity = None + self.maxcomplexity = None + self.minoccurrence = None + self.maxoccurrence = None + + # PPS (Passwords per Second) Cracking Speed + self.pps = 1000000000 + self.showmasks = False + + # Counter for total masks coverage + self.total_occurrence = 0 + + def getcomplexity(self, mask): + """ Return mask complexity. """ + count = 1 + for char in mask[1:].split("?"): + if char == "l": + count *= 26 + elif char == "u": + count *= 26 + elif char == "d": + count *= 10 + elif char == "s": + count *= 33 + elif char == "a": + count *= 95 + else: + print + "[!] Error, unknown mask ?%s in a mask %s" % (char, mask) + + return count + + def loadmasks(self, filename): + """ Load masks and apply filters. """ + maskReader = csv.reader(open(args[0], 'r'), delimiter=',', quotechar='"') + + for (mask, occurrence) in maskReader: + + if mask == "": continue + + mask_occurrence = int(occurrence) + mask_length = len(mask) / 2 + mask_complexity = self.getcomplexity(mask) + mask_time = mask_complexity / self.pps + + self.total_occurrence += mask_occurrence + + # Apply filters based on occurrence, length, complexity and time + if (self.minoccurrence == None or mask_occurrence >= self.minoccurrence) and \ + (self.maxoccurrence == None or mask_occurrence <= self.maxoccurrence) and \ + (self.mincomplexity == None or mask_complexity <= self.mincomplexity) and \ + (self.maxcomplexity == None or mask_complexity <= self.maxcomplexity) and \ + (self.mintime == None or mask_time <= self.mintime) and \ + (self.maxtime == None or mask_time <= self.maxtime) and \ + (self.maxlength == None or mask_length <= self.maxlength) and \ + (self.minlength == None or mask_length >= self.minlength): + self.masks[mask] = dict() + self.masks[mask]['length'] = mask_length + self.masks[mask]['occurrence'] = mask_occurrence + self.masks[mask]['complexity'] = 1 - mask_complexity + self.masks[mask]['time'] = mask_time + self.masks[mask]['optindex'] = 1 - mask_complexity / mask_occurrence + + def generate_masks(self, sorting_mode): + """ Generate optimal password masks sorted by occurrence, complexity or optindex """ + sample_count = 0 + sample_time = 0 + sample_occurrence = 0 + + # TODO Group by time here 1 minutes, 1 hour, 1 day, 1 month, 1 year.... + # Group by length 1,2,3,4,5,6,7,8,9,10.... + # Group by occurrence 10%, 20%, 30%, 40%, 50%.... + + if self.showmasks: print + "[L:] Mask: [ Occ: ] [ Time: ]" + for mask in sorted(self.masks.keys(), key=lambda mask: self.masks[mask][sorting_mode], reverse=True): + + if self.showmasks: + time_human = ">1 year" if self.masks[mask]['time'] > 60 * 60 * 24 * 365 else str( + datetime.timedelta(seconds=self.masks[mask]['time'])) + print + "[{:>2}] {:<30} [{:<7}] [{:>8}] ".format(self.masks[mask]['length'], mask, + self.masks[mask]['occurrence'], time_human) + + if self.output_file: + self.output_file.write("%s\n" % mask) + + sample_occurrence += self.masks[mask]['occurrence'] + sample_time += self.masks[mask]['time'] + sample_count += 1 + + if self.target_time and sample_time > self.target_time: + print + "[!] Target time exceeded." + break + + print + "[*] Finished generating masks:" + print + " Masks generated: %s" % sample_count + print + " Masks coverage: %d%% (%d/%d)" % ( + sample_occurrence * 100 / self.total_occurrence, sample_occurrence, self.total_occurrence) + time_human = ">1 year" if sample_time > 60 * 60 * 24 * 365 else str(datetime.timedelta(seconds=sample_time)) + print + " Masks runtime: %s" % time_human + + def getmaskscoverage(self, checkmasks): + + sample_count = 0 + sample_occurrence = 0 + + total_complexity = 0 + + if self.showmasks: print + "[L:] Mask: [ Occ: ] [ Time: ]" + for mask in checkmasks: + mask = mask.strip() + mask_complexity = self.getcomplexity(mask) + + total_complexity += mask_complexity + + if mask in self.masks: + + if self.showmasks: + time_human = ">1 year" if self.masks[mask]['time'] > 60 * 60 * 24 * 365 else str( + datetime.timedelta(seconds=self.masks[mask]['time'])) + print + "[{:>2}] {:<30} [{:<7}] [{:>8}] ".format(self.masks[mask]['length'], mask, + self.masks[mask]['occurrence'], time_human) + + if self.output_file: + self.output_file.write("%s\n" % mask) + + sample_occurrence += self.masks[mask]['occurrence'] + sample_count += 1 + + if self.target_time and total_complexity / self.pps > self.target_time: + print + "[!] Target time exceeded." + break + + # TODO: Something wrong here, complexity and time doesn't match with estimated from policygen + total_time = total_complexity / self.pps + time_human = ">1 year" if total_time > 60 * 60 * 24 * 365 else str(datetime.timedelta(seconds=total_time)) + print + "[*] Finished matching masks:" + print + " Masks matched: %s" % sample_count + print + " Masks coverage: %d%% (%d/%d)" % ( + sample_occurrence * 100 / self.total_occurrence, sample_occurrence, self.total_occurrence) + print + " Masks runtime: %s" % time_human + + +if __name__ == "__main__": + + header = " _ \n" + header += " MaskGen %s | |\n" % VERSION + header += " _ __ __ _ ___| | _\n" + header += " | '_ \ / _` |/ __| |/ /\n" + header += " | |_) | (_| | (__| < \n" + header += " | .__/ \__,_|\___|_|\_\\\n" + header += " | | \n" + header += " |_| iphelix@thesprawl.org\n" + header += "\n" + + parser = OptionParser("%prog pass0.masks [pass1.masks ...] [options]", version="%prog " + VERSION) + + parser.add_option("-t", "--targettime", dest="target_time", type="int", metavar="86400", + help="Target time of all masks (seconds)") + parser.add_option("-o", "--outputmasks", dest="output_masks", metavar="masks.hcmask", help="Save masks to a file") + + filters = OptionGroup(parser, "Individual Mask Filter Options") + filters.add_option("--minlength", dest="minlength", type="int", metavar="8", help="Minimum password length") + filters.add_option("--maxlength", dest="maxlength", type="int", metavar="8", help="Maximum password length") + filters.add_option("--mintime", dest="mintime", type="int", metavar="3600", help="Minimum mask runtime (seconds)") + filters.add_option("--maxtime", dest="maxtime", type="int", metavar="3600", help="Maximum mask runtime (seconds)") + filters.add_option("--mincomplexity", dest="mincomplexity", type="int", metavar="1", help="Minimum complexity") + filters.add_option("--maxcomplexity", dest="maxcomplexity", type="int", metavar="100", help="Maximum complexity") + filters.add_option("--minoccurrence", dest="minoccurrence", type="int", metavar="1", help="Minimum occurrence") + filters.add_option("--maxoccurrence", dest="maxoccurrence", type="int", metavar="100", help="Maximum occurrence") + parser.add_option_group(filters) + + sorting = OptionGroup(parser, "Mask Sorting Options") + sorting.add_option("--optindex", action="store_true", dest="optindex", help="sort by mask optindex (default)", + default=False) + sorting.add_option("--occurrence", action="store_true", dest="occurrence", help="sort by mask occurrence", + default=False) + sorting.add_option("--complexity", action="store_true", dest="complexity", help="sort by mask complexity", + default=False) + parser.add_option_group(sorting) + + coverage = OptionGroup(parser, "Check mask coverage") + coverage.add_option("--checkmasks", dest="checkmasks", help="check mask coverage", + metavar="?u?l?l?l?l?l?d,?l?l?l?l?l?d?d") + coverage.add_option("--checkmasksfile", dest="checkmasks_file", help="check mask coverage in a file", + metavar="masks.hcmask") + parser.add_option_group(coverage) + + parser.add_option("--showmasks", dest="showmasks", help="Show matching masks", action="store_true", default=False) + + misc = OptionGroup(parser, "Miscellaneous options") + misc.add_option("--pps", dest="pps", help="Passwords per Second", type="int", metavar="1000000000") + misc.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, help="Don't show headers.") + parser.add_option_group(misc) + + (options, args) = parser.parse_args() + + # Print program header + if not options.quiet: + print + header + + if len(args) < 1: + parser.error("no masks file specified! Please provide statsgen output.") + exit(1) + + print + "[*] Analyzing masks in [%s]" % args[0] + + maskgen = MaskGen() + + # Settings + if options.target_time: maskgen.target_time = options.target_time + if options.output_masks: + print + "[*] Saving generated masks to [%s]" % options.output_masks + maskgen.output_file = open(options.output_masks, 'w') + + # Filters + if options.minlength: maskgen.minlength = options.minlength + if options.maxlength: maskgen.maxlength = options.maxlength + if options.mintime: maskgen.mintime = options.mintime + if options.maxtime: maskgen.maxtime = options.maxtime + if options.mincomplexity: maskgen.mincomplexity = options.mincomplexity + if options.maxcomplexity: maskgen.maxcomplexity = options.maxcomplexity + if options.minoccurrence: maskgen.minoccurrence = options.minoccurrence + if options.maxoccurrence: maskgen.maxoccurrence = options.maxoccurrence + + # Misc + if options.pps: maskgen.pps = options.pps + if options.showmasks: maskgen.showmasks = options.showmasks + + print + "[*] Using {:,d} keys/sec for calculations.".format(maskgen.pps) + + # Load masks + for arg in args: + maskgen.loadmasks(arg) + + # Matching masks from the command-line + if options.checkmasks: + checkmasks = [m.strip() for m in options.checkmasks.split(',')] + print + "[*] Checking coverage of the these masks [%s]" % ", ".join(checkmasks) + maskgen.getmaskscoverage(checkmasks) + + # Matching masks from a file + elif options.checkmasks_file: + checkmasks_file = open(options.checkmasks_file, 'r') + print + "[*] Checking coverage of masks in [%s]" % options.checkmasks_file + maskgen.getmaskscoverage(checkmasks_file) + + # Printing masks in a file + else: + # Process masks according to specified sorting algorithm + if options.occurrence: + sorting_mode = "occurrence" + elif options.complexity: + sorting_mode = "complexity" + else: + sorting_mode = "optindex" + + print + "[*] Sorting masks by their [%s]." % sorting_mode + maskgen.generate_masks(sorting_mode) diff --git a/PACK/policygen.py b/PACK/policygen.py new file mode 100755 index 0000000..8451b3d --- /dev/null +++ b/PACK/policygen.py @@ -0,0 +1,243 @@ +#!/usr/bin/python +# PolicyGen - Analyze and Generate password masks according to a password policy +# +# This tool is part of PACK (Password Analysis and Cracking Kit) +# +# VERSION 0.0.2 +# +# Copyright (C) 2013 Peter Kacherginsky +# All rights reserved. +# +# Please see the attached LICENSE file for additional licensing information. + +import sys, string, random +import datetime +from optparse import OptionParser, OptionGroup +import itertools + +VERSION = "0.0.2" + + +class PolicyGen: + def __init__(self): + self.output_file = None + + self.minlength = 8 + self.maxlength = 8 + self.mindigit = None + self.minlower = None + self.minupper = None + self.minspecial = None + self.maxdigit = None + self.maxlower = None + self.maxupper = None + self.maxspecial = None + + # PPS (Passwords per Second) Cracking Speed + self.pps = 1000000000 + self.showmasks = False + + def getcomplexity(self, mask): + """ Return mask complexity. """ + count = 1 + for char in mask[1:].split("?"): + if char == "l": + count *= 26 + elif char == "u": + count *= 26 + elif char == "d": + count *= 10 + elif char == "s": + count *= 33 + elif char == "a": + count *= 95 + else: + print + "[!] Error, unknown mask ?%s in a mask %s" % (char, mask) + + return count + + def generate_masks(self, noncompliant): + """ Generate all possible password masks matching the policy """ + + total_count = 0 + sample_count = 0 + + # NOTE: It is better to collect total complexity + # not to lose precision when dividing by pps + total_complexity = 0 + sample_complexity = 0 + + # TODO: Randomize or even statistically arrange matching masks + for length in xrange(self.minlength, self.maxlength + 1): + print + "[*] Generating %d character password masks." % length + total_length_count = 0 + sample_length_count = 0 + + total_length_complexity = 0 + sample_length_complexity = 0 + + for masklist in itertools.product(['?d', '?l', '?u', '?s'], repeat=length): + + mask = ''.join(masklist) + + lowercount = 0 + uppercount = 0 + digitcount = 0 + specialcount = 0 + + mask_complexity = self.getcomplexity(mask) + + total_length_count += 1 + total_length_complexity += mask_complexity + + # Count charachter types in a mask + for char in mask[1:].split("?"): + if char == "l": + lowercount += 1 + elif char == "u": + uppercount += 1 + elif char == "d": + digitcount += 1 + elif char == "s": + specialcount += 1 + + # Filter according to password policy + # NOTE: Perform exact opposite (XOR) operation if noncompliant + # flag was set when calling the function. + if ((self.minlower == None or lowercount >= self.minlower) and \ + (self.maxlower == None or lowercount <= self.maxlower) and \ + (self.minupper == None or uppercount >= self.minupper) and \ + (self.maxupper == None or uppercount <= self.maxupper) and \ + (self.mindigit == None or digitcount >= self.mindigit) and \ + (self.maxdigit == None or digitcount <= self.maxdigit) and \ + (self.minspecial == None or specialcount >= self.minspecial) and \ + (self.maxspecial == None or specialcount <= self.maxspecial)) ^ noncompliant: + + sample_length_count += 1 + sample_length_complexity += mask_complexity + + if self.showmasks: + mask_time = mask_complexity / self.pps + time_human = ">1 year" if mask_time > 60 * 60 * 24 * 365 else str( + datetime.timedelta(seconds=mask_time)) + print + "[{:>2}] {:<30} [l:{:>2} u:{:>2} d:{:>2} s:{:>2}] [{:>8}] ".format(length, mask, lowercount, + uppercount, digitcount, + specialcount, time_human) + + if self.output_file: + self.output_file.write("%s\n" % mask) + + total_count += total_length_count + sample_count += sample_length_count + + total_complexity += total_length_complexity + sample_complexity += sample_length_complexity + + total_time = total_complexity / self.pps + total_time_human = ">1 year" if total_time > 60 * 60 * 24 * 365 else str(datetime.timedelta(seconds=total_time)) + print + "[*] Total Masks: %d Time: %s" % (total_count, total_time_human) + + sample_time = sample_complexity / self.pps + sample_time_human = ">1 year" if sample_time > 60 * 60 * 24 * 365 else str( + datetime.timedelta(seconds=sample_time)) + print + "[*] Policy Masks: %d Time: %s" % (sample_count, sample_time_human) + + +if __name__ == "__main__": + + header = " _ \n" + header += " PolicyGen %s | |\n" % VERSION + header += " _ __ __ _ ___| | _\n" + header += " | '_ \ / _` |/ __| |/ /\n" + header += " | |_) | (_| | (__| < \n" + header += " | .__/ \__,_|\___|_|\_\\\n" + header += " | | \n" + header += " |_| iphelix@thesprawl.org\n" + header += "\n" + + # parse command line arguments + parser = OptionParser("%prog [options]\n\nType --help for more options", version="%prog " + VERSION) + parser.add_option("-o", "--outputmasks", dest="output_masks", help="Save masks to a file", metavar="masks.hcmask") + parser.add_option("--pps", dest="pps", help="Passwords per Second", type="int", metavar="1000000000") + parser.add_option("--showmasks", dest="showmasks", help="Show matching masks", action="store_true", default=False) + parser.add_option("--noncompliant", dest="noncompliant", help="Generate masks for noncompliant passwords", + action="store_true", default=False) + + group = OptionGroup(parser, "Password Policy", + "Define the minimum (or maximum) password strength policy that you would like to test") + group.add_option("--minlength", dest="minlength", type="int", metavar="8", default=8, + help="Minimum password length") + group.add_option("--maxlength", dest="maxlength", type="int", metavar="8", default=8, + help="Maximum password length") + group.add_option("--mindigit", dest="mindigit", type="int", metavar="1", help="Minimum number of digits") + group.add_option("--minlower", dest="minlower", type="int", metavar="1", + help="Minimum number of lower-case characters") + group.add_option("--minupper", dest="minupper", type="int", metavar="1", + help="Minimum number of upper-case characters") + group.add_option("--minspecial", dest="minspecial", type="int", metavar="1", + help="Minimum number of special characters") + group.add_option("--maxdigit", dest="maxdigit", type="int", metavar="3", help="Maximum number of digits") + group.add_option("--maxlower", dest="maxlower", type="int", metavar="3", + help="Maximum number of lower-case characters") + group.add_option("--maxupper", dest="maxupper", type="int", metavar="3", + help="Maximum number of upper-case characters") + group.add_option("--maxspecial", dest="maxspecial", type="int", metavar="3", + help="Maximum number of special characters") + parser.add_option_group(group) + + parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, help="Don't show headers.") + + (options, args) = parser.parse_args() + + # Print program header + if not options.quiet: + print + header + + policygen = PolicyGen() + + # Settings + if options.output_masks: + print + "[*] Saving generated masks to [%s]" % options.output_masks + policygen.output_file = open(options.output_masks, 'w') + + # Password policy + if options.minlength != None: policygen.minlength = options.minlength + if options.maxlength != None: policygen.maxlength = options.maxlength + if options.mindigit != None: policygen.mindigit = options.mindigit + if options.minlower != None: policygen.minlower = options.minlower + if options.minupper != None: policygen.minupper = options.minupper + if options.minspecial != None: policygen.minspecial = options.minspecial + if options.maxdigit != None: policygen.maxdigits = options.maxdigit + if options.maxlower != None: policygen.maxlower = options.maxlower + if options.maxupper != None: policygen.maxupper = options.maxupper + if options.maxspecial != None: policygen.maxspecial = options.maxspecial + + # Misc + if options.pps: policygen.pps = options.pps + if options.showmasks: policygen.showmasks = options.showmasks + + print + "[*] Using {:,d} keys/sec for calculations.".format(policygen.pps) + + # Print current password policy + print + "[*] Password policy:" + print + " Pass Lengths: min:%d max:%d" % (policygen.minlength, policygen.maxlength) + print + " Min strength: l:%s u:%s d:%s s:%s" % ( + policygen.minlower, policygen.minupper, policygen.mindigit, policygen.minspecial) + print + " Max strength: l:%s u:%s d:%s s:%s" % ( + policygen.maxlower, policygen.maxupper, policygen.maxdigit, policygen.maxspecial) + + print + "[*] Generating [%s] masks." % ("compliant" if not options.noncompliant else "non-compliant") + policygen.generate_masks(options.noncompliant) diff --git a/PACK/rulegen.py b/PACK/rulegen.py new file mode 100755 index 0000000..c4ccc9b --- /dev/null +++ b/PACK/rulegen.py @@ -0,0 +1,1193 @@ +#!/usr/bin/env python +# Rulegen.py - Advanced automated password rule and wordlist generator for the +# Hashcat password cracker using the Levenshtein Reverse Path +# algorithm and Enchant spell checking library. +# +# This tool is part of PACK (Password Analysis and Cracking Kit) +# +# VERSION 0.0.3 +# +# Copyright (C) 2013 Peter Kacherginsky +# All rights reserved. +# +# Please see the attached LICENSE file for additional licensing information. + +import sys +import re +import time +import operator +import enchant + +from optparse import OptionParser, OptionGroup + +from collections import Counter + +import subprocess + +import multiprocessing + +VERSION = "0.0.3" + +# Testing rules with hashcat --stdout +HASHCAT_PATH = "hashcat/" + + +# Rule Generator class responsible for the complete cycle of rule generation +class RuleGen: + # Initialize Rule Generator class + def __init__(self, language="en", providers="aspell,myspell", basename='analysis', threads=4): + + self.enchant_broker = enchant.Broker() + self.enchant_broker.set_ordering("*", providers) + + self.enchant = enchant.Dict(language, self.enchant_broker) + + # Output options + self.basename = basename + + # Finetuning word generation + self.max_word_dist = 10 + self.max_words = 10 + self.more_words = False + self.simple_words = False + + # Finetuning rule generation + self.max_rule_len = 10 + self.max_rules = 10 + self.more_rules = False + self.simple_rules = False + self.brute_rules = False + + # Debugging options + self.verbose = False + self.debug = False + self.word = None # Custom word to use. + self.quiet = False + + ######################################################################## + # Word and Rule Statistics + self.numeric_stats_total = 0 + self.special_stats_total = 0 + self.foreign_stats_total = 0 + + ######################################################################## + # Preanalysis Password Patterns + self.password_pattern = dict() + self.password_pattern["insertion"] = re.compile('^[^a-z]*(?P.+?)[^a-z]*$', re.IGNORECASE) + self.password_pattern["email"] = re.compile('^(?P.+?)@[A-Z0-9.-]+\.[A-Z]{2,4}', re.IGNORECASE) + self.password_pattern["alldigits"] = re.compile('^(\d+)$', re.IGNORECASE) + self.password_pattern["allspecial"] = re.compile('^([^a-z0-9]+)$', re.IGNORECASE) + + ######################################################################## + # Hashcat Rules Engine + self.hashcat_rule = dict() + + # Dummy rule + self.hashcat_rule[':'] = lambda x: x # Do nothing + + # Case rules + self.hashcat_rule["l"] = lambda x: x.lower() # Lowercase all letters + self.hashcat_rule["u"] = lambda x: x.upper() # Capitalize all letters + self.hashcat_rule["c"] = lambda x: x.capitalize() # Capitalize the first letter + self.hashcat_rule["C"] = lambda x: x[0].lower() + x[ + 1:].upper() # Lowercase the first found character, uppercase the rest + self.hashcat_rule["t"] = lambda x: x.swapcase() # Toggle the case of all characters in word + self.hashcat_rule["T"] = lambda x, y: x[:y] + x[y].swapcase() + x[ + y + 1:] # Toggle the case of characters at position N + self.hashcat_rule["E"] = lambda x: " ".join( + [i[0].upper() + i[1:] for i in x.split(" ")]) # Upper case the first letter and every letter after a space + + # Rotation rules + self.hashcat_rule["r"] = lambda x: x[::-1] # Reverse the entire word + self.hashcat_rule["{"] = lambda x: x[1:] + x[0] # Rotate the word left + self.hashcat_rule["}"] = lambda x: x[-1] + x[:-1] # Rotate the word right + + # Duplication rules + self.hashcat_rule["d"] = lambda x: x + x # Duplicate entire word + self.hashcat_rule["p"] = lambda x, y: x * y # Duplicate entire word N times + self.hashcat_rule["f"] = lambda x: x + x[::-1] # Duplicate word reversed + self.hashcat_rule["z"] = lambda x, y: x[0] * y + x # Duplicate first character N times + self.hashcat_rule["Z"] = lambda x, y: x + x[-1] * y # Duplicate last character N times + self.hashcat_rule["q"] = lambda x: "".join([i + i for i in x]) # Duplicate every character + self.hashcat_rule["y"] = lambda x, y: x[:y] + x # Duplicate first N characters + self.hashcat_rule["Y"] = lambda x, y: x + x[-y:] # Duplicate last N characters + + # Cutting rules + self.hashcat_rule["["] = lambda x: x[1:] # Delete first character + self.hashcat_rule["]"] = lambda x: x[:-1] # Delete last character + self.hashcat_rule["D"] = lambda x, y: x[:y] + x[y + 1:] # Deletes character at position N + self.hashcat_rule["'"] = lambda x, y: x[:y] # Truncate word at position N + self.hashcat_rule["x"] = lambda x, y, z: x[:y] + x[y + z:] # Delete M characters, starting at position N + self.hashcat_rule["@"] = lambda x, y: x.replace(y, '') # Purge all instances of X + + # Insertion rules + self.hashcat_rule["$"] = lambda x, y: x + y # Append character to end + self.hashcat_rule["^"] = lambda x, y: y + x # Prepend character to front + self.hashcat_rule["i"] = lambda x, y, z: x[:y] + z + x[y:] # Insert character X at position N + + # Replacement rules + self.hashcat_rule["o"] = lambda x, y, z: x[:y] + z + x[y + 1:] # Overwrite character at position N with X + self.hashcat_rule["s"] = lambda x, y, z: x.replace(y, z) # Replace all instances of X with Y + self.hashcat_rule["L"] = lambda x, y: x[:y] + chr(ord(x[y]) << 1) + x[ + y + 1:] # Bitwise shift left character @ N + self.hashcat_rule["R"] = lambda x, y: x[:y] + chr(ord(x[y]) >> 1) + x[ + y + 1:] # Bitwise shift right character @ N + self.hashcat_rule["+"] = lambda x, y: x[:y] + chr(ord(x[y]) + 1) + x[ + y + 1:] # Increment character @ N by 1 ascii value + self.hashcat_rule["-"] = lambda x, y: x[:y] + chr(ord(x[y]) - 1) + x[ + y + 1:] # Decrement character @ N by 1 ascii value + self.hashcat_rule["."] = lambda x, y: x[:y] + x[y + 1] + x[ + y + 1:] # Replace character @ N with value at @ N plus 1 + self.hashcat_rule[","] = lambda x, y: x[:y] + x[y - 1] + x[ + y + 1:] # Replace character @ N with value at @ N minus 1 + + # Swappping rules + self.hashcat_rule["k"] = lambda x: x[1] + x[0] + x[2:] # Swap first two characters + self.hashcat_rule["K"] = lambda x: x[:-2] + x[-1] + x[-2] # Swap last two characters + self.hashcat_rule["*"] = lambda x, y, z: x[:y] + x[z] + x[y + 1:z] + x[y] + x[z + 1:] if z > y else x[:z] + x[ + y] + x[z + 1:y] + x[z] + x[y + 1:] # Swap character X with Y + + ######################################################################## + # Common numeric and special character substitutions (1337 5p34k) + self.leet = dict() + self.leet["1"] = "i" + self.leet["2"] = "z" + self.leet["3"] = "e" + self.leet["4"] = "a" + self.leet["5"] = "s" + self.leet["6"] = "b" + self.leet["7"] = "t" + self.leet["8"] = "b" + self.leet["9"] = "g" + self.leet["0"] = "o" + self.leet["!"] = "i" + self.leet["|"] = "i" + self.leet["@"] = "a" + self.leet["$"] = "s" + self.leet["+"] = "t" + + ######################################################################## + # Preanalysis rules to bruteforce for each word + self.preanalysis_rules = [] + self.preanalysis_rules.append(([], self.hashcat_rule[':'])) # Blank rule + self.preanalysis_rules.append((['r'], self.hashcat_rule['r'])) # Reverse rule + # self.preanalysis_rules.append((['{'],self.hashcat_rule['}'])) # Rotate left + # self.preanalysis_rules.append((['}'],self.hashcat_rule['{'])) # Rotate right + + ############################################################################ + # Calculate Levenshtein edit path matrix + def levenshtein(self, word, password): + matrix = [] + + # Generate and populate the initial matrix + for i in xrange(len(password) + 1): + matrix.append([]) + for j in xrange(len(word) + 1): + if i == 0: + matrix[i].append(j) + elif j == 0: + matrix[i].append(i) + else: + matrix[i].append(0) + + # Calculate edit distance for each substring + for i in xrange(1, len(password) + 1): + for j in xrange(1, len(word) + 1): + if password[i - 1] == word[j - 1]: + matrix[i][j] = matrix[i - 1][j - 1] + else: + insertion = matrix[i - 1][j] + 1 + deletion = matrix[i][j - 1] + 1 + substitution = matrix[i - 1][j - 1] + 1 + matrix[i][j] = min(insertion, deletion, substitution) + + return matrix + + def levenshtein_distance(self, s1, s2): + """Calculate the Levenshtein distance between two strings. + + This is straight from Wikipedia. + """ + if len(s1) < len(s2): + return self.levenshtein_distance(s2, s1) + if not s1: + return len(s2) + + previous_row = xrange(len(s2) + 1) + for i, c1 in enumerate(s1): + current_row = [i + 1] + for j, c2 in enumerate(s2): + insertions = previous_row[j + 1] + 1 + deletions = current_row[j] + 1 + substitutions = previous_row[j] + (c1 != c2) + current_row.append(min(insertions, deletions, substitutions)) + previous_row = current_row + + return previous_row[-1] + + def levenshtein_print(self, matrix, word, password): + """ Print word X password matrix """ + print + " %s" % " ".join(list(word)) + for i, row in enumerate(matrix): + if i == 0: print + " ", + else: print + password[i - 1], + print + " ".join("%2d" % col for col in row) + + def generate_levenshtein_rules(self, word, password): + """ Generates levenshtein rules. Returns a list of lists of levenshtein rules. """ + + # 1) Generate Levenshtein matrix + matrix = self.levenshtein(word, password) + + # 2) Trace reverse paths through the matrix. + paths = self.levenshtein_reverse_recursive(matrix, len(matrix) - 1, len(matrix[0]) - 1, 0) + + # 3) Return a collection of reverse paths. + return [path for path in paths if len(path) <= matrix[-1][-1]] + + def levenshtein_reverse_recursive(self, matrix, i, j, path_len): + """ Calculate reverse Levenshtein paths. + Recursive, Depth First, Short-circuited algorithm by Peter Kacherginsky + Generates a list of edit operations necessary to transform a source word + into a password. Edit operations are recorded in the form: + (operation, password_offset, word_offset) + Where an operation can be either insertion, deletion or replacement. + """ + + if i == 0 and j == 0 or path_len > matrix[-1][-1]: + return [[]] + else: + paths = list() + + cost = matrix[i][j] + + # Calculate minimum cost of each operation + cost_delete = cost_insert = cost_equal_or_replace = sys.maxint + if i > 0: cost_insert = matrix[i - 1][j] + if j > 0: cost_delete = matrix[i][j - 1] + if i > 0 and j > 0: cost_equal_or_replace = matrix[i - 1][j - 1] + cost_min = min(cost_delete, cost_insert, cost_equal_or_replace) + + # Recurse through reverse path for each operation + if cost_insert == cost_min: + insert_paths = self.levenshtein_reverse_recursive(matrix, i - 1, j, path_len + 1) + for insert_path in insert_paths: paths.append(insert_path + [('insert', i - 1, j)]) + + if cost_delete == cost_min: + delete_paths = self.levenshtein_reverse_recursive(matrix, i, j - 1, path_len + 1) + for delete_path in delete_paths: paths.append(delete_path + [('delete', i, j - 1)]) + + if cost_equal_or_replace == cost_min: + if cost_equal_or_replace == cost: + equal_paths = self.levenshtein_reverse_recursive(matrix, i - 1, j - 1, path_len) + for equal_path in equal_paths: paths.append(equal_path) + else: + replace_paths = self.levenshtein_reverse_recursive(matrix, i - 1, j - 1, path_len + 1) + for replace_path in replace_paths: paths.append(replace_path + [('replace', i - 1, j - 1)]) + + return paths + + def load_custom_wordlist(self, wordlist_file): + self.enchant = enchant.request_pwl_dict(wordlist_file) + + def generate_words(self, password): + """ Generate source word candidates.""" + + if self.debug: print + "[*] Generating source words for %s" % password + + words = list() + words_collection = list() + + # Let's collect best edit distance as soon as possible to prevent + # less efficient pre_rules like reversal and rotation from slowing + # us down with garbage + best_found_distance = 9999 + + ####################################################################### + # Generate words for each preanalysis rule + if not self.brute_rules: + self.preanalysis_rules = self.preanalysis_rules[:1] + + for pre_rule, pre_rule_lambda in self.preanalysis_rules: + + pre_password = pre_rule_lambda(password) + + # Generate word suggestions + if self.word: + suggestions = [self.word] + elif self.simple_words: + suggestions = self.generate_simple_words(pre_password) + else: + suggestions = self.generate_advanced_words(pre_password) + + # HACK: Perform some additional expansion on multi-word suggestions + # TODO: May be I should split these two and see if I can generate + # rules for each of the suggestions + for suggestion in suggestions[:self.max_words]: + suggestion = suggestion.replace(' ', '') + suggestion = suggestion.replace('-', '') + if not suggestion in suggestions: + suggestions.append(suggestion) + + if len(suggestions) != len(set(suggestions)): + print + sorted(suggestions) + print + sorted(set(suggestions)) + + for suggestion in suggestions: + distance = self.levenshtein_distance(suggestion, pre_password) + + word = dict() + word["suggestion"] = suggestion + word["distance"] = distance + word["password"] = pre_password + word["pre_rule"] = pre_rule + word["best_rule_length"] = 9999 + + words.append(word) + + ####################################################################### + # Perform Optimization + for word in sorted(words, key=lambda word: word["distance"], reverse=False): + + # Optimize for best distance + if not self.more_words: + if word["distance"] < best_found_distance: + best_found_distance = word["distance"] + + elif word["distance"] > best_found_distance: + if self.verbose: + print + "[-] %s => {edit distance suboptimal: %d (%d)} => %s" % \ + (word["suggestion"], word["distance"], best_found_distance, word["password"]) + break + + # Filter words with too big edit distance + if word["distance"] <= self.max_word_dist: + if self.debug: + print + "[+] %s => {edit distance: %d (%d)} = > %s" % \ + (word["suggestion"], word["distance"], best_found_distance, word["password"]) + + words_collection.append(word) + + else: + if self.verbose: + print + "[-] %s => {max distance exceeded: %d (%d)} => %s" % \ + (word["suggestion"], word["distance"], self.max_word_dist, word["password"]) + + if self.max_words: + words_collection = words_collection[:self.max_words] + + return words_collection + + def generate_simple_words(self, password): + """ Generate simple words. A simple spellcheck.""" + + return self.enchant.suggest(password) + + def generate_advanced_words(self, password): + """ Generate advanced words. + Perform some additional non-destructive cleaning to help spell-checkers: + 1) Remove non-alpha prefixes and appendixes. + 2) Perform common pattern matches (e.g. email). + 3) Replace non-alpha character substitutions (1337 5p34k) + """ + + # Remove non-alpha prefix and/or appendix + insertion_matches = self.password_pattern["insertion"].match(password) + if insertion_matches: + password = insertion_matches.group('password') + + # Pattern matches + email_matches = self.password_pattern["email"].match(password) + if email_matches: + password = email_matches.group('password') + + # Replace common special character replacements (1337 5p34k) + preanalysis_password = '' + for c in password: + if c in self.leet: + preanalysis_password += self.leet[c] + else: + preanalysis_password += c + password = preanalysis_password + + if self.debug: "[*] Preanalysis Password: %s" % password + + return self.enchant.suggest(password) + + ############################################################################ + # Hashcat specific offset definition 0-9,A-Z + def int_to_hashcat(self, N): + if N < 10: + return N + else: + return chr(65 + N - 10) + + def hashcat_to_int(self, N): + if N.isdigit(): + return int(N) + else: + return ord(N) - 65 + 10 + + def generate_hashcat_rules(self, suggestion, password): + """ Generate hashcat rules. Returns a length sorted list of lists of hashcat rules.""" + + # 2) Generate Levenshtein Rules + lev_rules = self.generate_levenshtein_rules(suggestion, password) + + # 3) Generate Hashcat Rules + hashcat_rules = [] + hashcat_rules_collection = [] + + ####################################################################### + # Generate hashcat rule for each levenshtein rule + for lev_rule in lev_rules: + + if self.simple_rules: + hashcat_rule = self.generate_simple_hashcat_rules(suggestion, lev_rule, password) + else: + hashcat_rule = self.generate_advanced_hashcat_rules(suggestion, lev_rule, password) + + if hashcat_rule == None: + print + "[!] Processing FAILED: %s => ;( => %s" % (suggestion, password) + print + " Sorry about that, please report this failure to" + print + " the developer: iphelix [at] thesprawl.org" + + else: + hashcat_rules.append(hashcat_rule) + + best_found_rule_length = 9999 + + ####################################################################### + # Perform Optimization + for hashcat_rule in sorted(hashcat_rules, key=lambda hashcat_rule: len(hashcat_rule)): + + rule_length = len(hashcat_rule) + + if not self.more_rules: + if rule_length < best_found_rule_length: + best_found_rule_length = rule_length + + elif rule_length > best_found_rule_length: + if self.verbose: + print + "[-] %s => {best rule length exceeded: %d (%d)} => %s" % \ + (suggestion, rule_length, best_found_rule_length, password) + break + + if rule_length <= self.max_rule_len: + hashcat_rules_collection.append(hashcat_rule) + + return hashcat_rules_collection + + def generate_simple_hashcat_rules(self, word, rules, password): + """ Generate basic hashcat rules using only basic insert,delete,replace rules. """ + hashcat_rules = [] + + if self.debug: print + "[*] Simple Processing %s => %s" % (word, password) + + # Dynamically apply rules to the source word + # NOTE: Special case were word == password this would work as well. + word_rules = word + + for (op, p, w) in rules: + + if self.debug: print + "\t[*] Simple Processing Started: %s - %s" % (word_rules, " ".join(hashcat_rules)) + + if op == 'insert': + hashcat_rules.append("i%s%s" % (self.int_to_hashcat(p), password[p])) + word_rules = self.hashcat_rule['i'](word_rules, p, password[p]) + + elif op == 'delete': + hashcat_rules.append("D%s" % self.int_to_hashcat(p)) + word_rules = self.hashcat_rule['D'](word_rules, p) + + elif op == 'replace': + hashcat_rules.append("o%s%s" % (self.int_to_hashcat(p), password[p])) + word_rules = self.hashcat_rule['o'](word_rules, p, password[p]) + + if self.debug: print + "\t[*] Simple Processing Ended: %s => %s => %s" % (word_rules, " ".join(hashcat_rules), password) + + # Check if rules result in the correct password + if word_rules == password: + return hashcat_rules + else: + if self.debug: print + "[!] Simple Processing FAILED: %s => %s => %s (%s)" % (word, " ".join(hashcat_rules), password, word_rules) + return None + + def generate_advanced_hashcat_rules(self, word, rules, password): + """ Generate advanced hashcat rules using full range of available rules. """ + hashcat_rules = [] + + if self.debug: print + "[*] Advanced Processing %s => %s" % (word, password) + + # Dynamically apply and store rules in word_rules variable. + # NOTE: Special case where word == password this would work as well. + word_rules = word + + # Generate case statistics + password_lower = len([c for c in password if c.islower()]) + password_upper = len([c for c in password if c.isupper()]) + + for i, (op, p, w) in enumerate(rules): + + if self.debug: print + "\t[*] Advanced Processing Started: %s - %s" % (word_rules, " ".join(hashcat_rules)) + + if op == 'insert': + hashcat_rules.append("i%s%s" % (self.int_to_hashcat(p), password[p])) + word_rules = self.hashcat_rule['i'](word_rules, p, password[p]) + + elif op == 'delete': + hashcat_rules.append("D%s" % self.int_to_hashcat(p)) + word_rules = self.hashcat_rule['D'](word_rules, p) + + elif op == 'replace': + + # Detecting global replacement such as sXY, l, u, C, c is a non + # trivial problem because different characters may be added or + # removed from the word by other rules. A reliable way to solve + # this problem is to apply all of the rules the source word + # and keep track of its state at any given time. At the same + # time, global replacement rules can be tested by completing + # the rest of the rules using a simplified engine. + + # The sequence of if statements determines the priority of rules + + # This rule was made obsolete by a prior global replacement + if word_rules[p] == password[p]: + if self.debug: print + "\t[*] Advanced Processing Obsolete Rule: %s - %s" % (word_rules, " ".join(hashcat_rules)) + + # Swapping rules + elif p < len(password) - 1 and p < len(word_rules) - 1 and word_rules[p] == password[p + 1] and + word_rules[p + 1] == password[p]: + # Swap first two characters + if p == 0 and self.generate_simple_hashcat_rules(self.hashcat_rule['k'](word_rules), rules[i + 1:], + password): + hashcat_rules.append("k") + word_rules = self.hashcat_rule['k'](word_rules) + # Swap last two characters + elif p == len(word_rules) - 2 and self.generate_simple_hashcat_rules( + self.hashcat_rule['K'](word_rules), rules[i + 1:], password): + hashcat_rules.append("K") + word_rules = self.hashcat_rule['K'](word_rules) + # Swap any two characters (only adjacent swapping is supported) + elif self.generate_simple_hashcat_rules(self.hashcat_rule['*'](word_rules, p, p + 1), rules[i + 1:], + password): + hashcat_rules.append("*%s%s" % (self.int_to_hashcat(p), self.int_to_hashcat(p + 1))) + word_rules = self.hashcat_rule['*'](word_rules, p, p + 1) + else: + hashcat_rules.append("o%s%s" % (self.int_to_hashcat(p), password[p])) + word_rules = self.hashcat_rule['o'](word_rules, p, password[p]) + + # Case Toggle: Uppercased a letter + elif word_rules[p].islower() and word_rules[p].upper() == password[ + p]: # Toggle the case of all characters in word (mixed cases) + if password_upper and password_lower and self.generate_simple_hashcat_rules( + self.hashcat_rule['t'](word_rules), rules[i + 1:], password): + hashcat_rules.append("t") + word_rules = self.hashcat_rule['t'](word_rules) + + # Capitalize all letters + elif self.generate_simple_hashcat_rules(self.hashcat_rule['u'](word_rules), rules[i + 1:], + password): + hashcat_rules.append("u") + word_rules = self.hashcat_rule['u'](word_rules) + + # Capitalize the first letter + elif p == 0 and self.generate_simple_hashcat_rules(self.hashcat_rule['c'](word_rules), + rules[i + 1:], password): + hashcat_rules.append("c") + word_rules = self.hashcat_rule['c'](word_rules) + + # Toggle the case of characters at position N + else: + hashcat_rules.append("T%s" % self.int_to_hashcat(p)) + word_rules = self.hashcat_rule['T'](word_rules, p) + + # Case Toggle: Lowercased a letter + elif word_rules[p].isupper() and word_rules[p].lower() == password[p]: + + # Toggle the case of all characters in word (mixed cases) + if password_upper and password_lower and self.generate_simple_hashcat_rules( + self.hashcat_rule['t'](word_rules), rules[i + 1:], password): + hashcat_rules.append("t") + word_rules = self.hashcat_rule['t'](word_rules) + + # Lowercase all letters + elif self.generate_simple_hashcat_rules(self.hashcat_rule['l'](word_rules), rules[i + 1:], + password): + hashcat_rules.append("l") + word_rules = self.hashcat_rule['l'](word_rules) + + # Lowercase the first found character, uppercase the rest + elif p == 0 and self.generate_simple_hashcat_rules(self.hashcat_rule['C'](word_rules), + rules[i + 1:], password): + hashcat_rules.append("C") + word_rules = self.hashcat_rule['C'](word_rules) + + # Toggle the case of characters at position N + else: + hashcat_rules.append("T%s" % self.int_to_hashcat(p)) + word_rules = self.hashcat_rule['T'](word_rules, p) + + # Special case substitution of 'all' instances (1337 $p34k) + elif word_rules[p].isalpha() and not password[p].isalpha() and self.generate_simple_hashcat_rules( + self.hashcat_rule['s'](word_rules, word_rules[p], password[p]), rules[i + 1:], password): + + # If we have already detected this rule, then skip it thus + # reducing total rule count. + # BUG: Elisabeth => sE3 sl1 u o3Z sE3 => 31IZAB3TH + # if not "s%s%s" % (word_rules[p],password[p]) in hashcat_rules: + hashcat_rules.append("s%s%s" % (word_rules[p], password[p])) + word_rules = self.hashcat_rule['s'](word_rules, word_rules[p], password[p]) + + # Replace next character with current + elif p < len(password) - 1 and p < len(word_rules) - 1 and password[p] == password[p + 1] and password[ + p] == word_rules[p + 1]: + hashcat_rules.append(".%s" % self.int_to_hashcat(p)) + word_rules = self.hashcat_rule['.'](word_rules, p) + + # Replace previous character with current + elif p > 0 and w > 0 and password[p] == password[p - 1] and password[p] == word_rules[p - 1]: + hashcat_rules.append(",%s" % self.int_to_hashcat(p)) + word_rules = self.hashcat_rule[','](word_rules, p) + + # ASCII increment + elif ord(word_rules[p]) + 1 == ord(password[p]): + hashcat_rules.append("+%s" % self.int_to_hashcat(p)) + word_rules = self.hashcat_rule['+'](word_rules, p) + + # ASCII decrement + elif ord(word_rules[p]) - 1 == ord(password[p]): + hashcat_rules.append("-%s" % self.int_to_hashcat(p)) + word_rules = self.hashcat_rule['-'](word_rules, p) + + # SHIFT left + elif ord(word_rules[p]) << 1 == ord(password[p]): + hashcat_rules.append("L%s" % self.int_to_hashcat(p)) + word_rules = self.hashcat_rule['L'](word_rules, p) + + # SHIFT right + elif ord(word_rules[p]) >> 1 == ord(password[p]): + hashcat_rules.append("R%s" % self.int_to_hashcat(p)) + word_rules = self.hashcat_rule['R'](word_rules, p) + + # Position based replacements. + else: + hashcat_rules.append("o%s%s" % (self.int_to_hashcat(p), password[p])) + word_rules = self.hashcat_rule['o'](word_rules, p, password[p]) + + if self.debug: print + "\t[*] Advanced Processing Ended: %s %s" % (word_rules, " ".join(hashcat_rules)) + + ######################################################################## + # Prefix rules + last_prefix = 0 + prefix_rules = list() + for hashcat_rule in hashcat_rules: + if hashcat_rule[0] == "i" and self.hashcat_to_int(hashcat_rule[1]) == last_prefix: + prefix_rules.append("^%s" % hashcat_rule[2]) + last_prefix += 1 + elif len(prefix_rules): + hashcat_rules = prefix_rules[::-1] + hashcat_rules[len(prefix_rules):] + break + else: + break + else: + hashcat_rules = prefix_rules[::-1] + hashcat_rules[len(prefix_rules):] + + #################################################################### + # Appendix rules + last_appendix = len(password) - 1 + appendix_rules = list() + for hashcat_rule in hashcat_rules[::-1]: + if hashcat_rule[0] == "i" and self.hashcat_to_int(hashcat_rule[1]) == last_appendix: + appendix_rules.append("$%s" % hashcat_rule[2]) + last_appendix -= 1 + elif len(appendix_rules): + hashcat_rules = hashcat_rules[:-len(appendix_rules)] + appendix_rules[::-1] + break + else: + break + else: + hashcat_rules = hashcat_rules[:-len(appendix_rules)] + appendix_rules[::-1] + + #################################################################### + # Truncate left rules + last_precut = 0 + precut_rules = list() + for hashcat_rule in hashcat_rules: + if hashcat_rule[0] == "D" and self.hashcat_to_int(hashcat_rule[1]) == last_precut: + precut_rules.append("[") + elif len(precut_rules): + hashcat_rules = precut_rules[::-1] + hashcat_rules[len(precut_rules):] + break + else: + break + else: + hashcat_rules = precut_rules[::-1] + hashcat_rules[len(precut_rules):] + + #################################################################### + # Truncate right rules + last_postcut = len(password) + postcut_rules = list() + for hashcat_rule in hashcat_rules[::-1]: + + if hashcat_rule[0] == "D" and self.hashcat_to_int(hashcat_rule[1]) >= last_postcut: + postcut_rules.append("]") + elif len(postcut_rules): + hashcat_rules = hashcat_rules[:-len(postcut_rules)] + postcut_rules[::-1] + break + else: + break + else: + hashcat_rules = hashcat_rules[:-len(postcut_rules)] + postcut_rules[::-1] + + # Check if rules result in the correct password + if word_rules == password: + return hashcat_rules + else: + if self.debug: print + "[!] Advanced Processing FAILED: %s => %s => %s (%s)" % ( + word, " ".join(hashcat_rules), password, word_rules) + return None + + +def check_reversible_password(self, password): + """ Check whether the password is likely to be reversed successfuly. """ + + # Skip all numeric passwords + if password.isdigit(): + if self.verbose and not self.quiet: print + "[!] %s => {skipping numeric} => %s" % (password, password) + self.numeric_stats_total += 1 + return False + + # Skip passwords with less than 25% of alpha character + # TODO: Make random word detection more reliable based on word entropy. + elif len([c for c in password if c.isalpha()]) < len(password) / 4: + if self.verbose and not self.quiet: print + "[!] %s => {skipping alpha less than 25%%} => %s" % (password, password) + self.special_stats_total += 1 + return False + + # Only check english ascii passwords for now + # TODO: Add support for more languages. + elif [c for c in password if ord(c) < 32 or ord(c) > 126]: + if self.verbose and not self.quiet: print + "[!] %s => {skipping non ascii english} => %s" % (password, password) + self.foreign_stats_total += 1 + return False + + else: + return True + + +def analyze_password(self, password, rules_queue=multiprocessing.Queue(), words_queue=multiprocessing.Queue()): + """ Analyze a single password. """ + + if self.verbose: print + "[*] Analyzing password: %s" % password + + words = [] + + # Short-cut words in the dictionary + if self.enchant.check(password) and not self.word: + + word = dict() + word["password"] = password + word["suggestion"] = password + word["hashcat_rules"] = [[], ] + word["pre_rule"] = [] + word["best_rule_length"] = 9999 + + words.append(word) + + # Generate rules for words not in the dictionary + else: + + # Generate source words list + words = self.generate_words(password) + + # Generate levenshtein reverse paths for each suggestion + for word in words: + # Generate a collection of hashcat_rules lists + word["hashcat_rules"] = self.generate_hashcat_rules(word["suggestion"], word["password"]) + + self.print_hashcat_rules(words, password, rules_queue, words_queue) + + +def print_hashcat_rules(self, words, password, rules_queue, words_queue): + best_found_rule_length = 9999 + + # Sorted list based on rule length + for word in sorted(words, key=lambda word: len(word["hashcat_rules"][0])): + + words_queue.put(word["suggestion"]) + + for hashcat_rule in word["hashcat_rules"]: + + rule_length = len(hashcat_rule) + + if not self.more_rules: + if rule_length < best_found_rule_length: + best_found_rule_length = rule_length + + elif rule_length > best_found_rule_length: + if self.verbose: + print + "[-] %s => {best rule length exceeded: %d (%d)} => %s" % \ + (word["suggestion"], rule_length, best_found_rule_length, password) + break + + if rule_length <= self.max_rule_len: + + hashcat_rule_str = " ".join(hashcat_rule + word["pre_rule"] or [':']) + if self.verbose: print + "[+] %s => %s => %s" % (word["suggestion"], hashcat_rule_str, password) + + rules_queue.put(hashcat_rule_str) + + +def password_worker(self, i, passwords_queue, rules_queue, words_queue): + if self.debug: print + "[*] Password analysis worker [%d] started." % i + try: + while True: + password = passwords_queue.get() + + # Interrupted by a Death Pill + if password == None: break + + self.analyze_password(password, rules_queue, words_queue) + except (KeyboardInterrupt, SystemExit): + if self.debug: print + "[*] Password analysis worker [%d] terminated." % i + + if self.debug: print + "[*] Password analysis worker [%d] stopped." % i + + +def rule_worker(self, rules_queue, output_rules_filename): + """ Worker to store generated rules. """ + print + "[*] Saving rules to %s" % output_rules_filename + + f = open(output_rules_filename, 'w') + if self.debug: print + "[*] Rule worker started." + try: + while True: + rule = rules_queue.get() + + # Interrupted by a Death Pill + if rule == None: break + + f.write("%s\n" % rule) + f.flush() + + except (KeyboardInterrupt, SystemExit): + if self.debug: print + "[*] Rule worker terminated." + + f.close() + if self.debug: print + "[*] Rule worker stopped." + + +def word_worker(self, words_queue, output_words_filename): + """ Worker to store generated rules. """ + print + "[*] Saving words to %s" % output_words_filename + + f = open(output_words_filename, 'w') + if self.debug: print + "[*] Word worker started." + try: + while True: + word = words_queue.get() + + # Interrupted by a Death Pill + if word == None: break + + f.write("%s\n" % word) + f.flush() + + except (KeyboardInterrupt, SystemExit): + if self.debug: print + "[*] Word worker terminated." + + f.close() + if self.debug: print + "[*] Word worker stopped." + + +# Analyze passwords file +def analyze_passwords_file(self, passwords_file): + """ Analyze provided passwords file. """ + + print + "[*] Analyzing passwords file: %s:" % passwords_file + print + "[*] Press Ctrl-C to end execution and generate statistical analysis." + + # Setup queues + passwords_queue = multiprocessing.Queue(multiprocessing.cpu_count() * 100) + rules_queue = multiprocessing.Queue() + words_queue = multiprocessing.Queue() + + # Start workers + for i in range(multiprocessing.cpu_count()): + multiprocessing.Process(target=self.password_worker, + args=(i, passwords_queue, rules_queue, words_queue)).start() + multiprocessing.Process(target=self.rule_worker, args=(rules_queue, "%s.rule" % self.basename)).start() + multiprocessing.Process(target=self.word_worker, args=(words_queue, "%s.word" % self.basename)).start() + + # Continue with the main thread + + f = open(passwords_file, 'r') + + password_count = 0 + analysis_start = time.time() + segment_start = analysis_start + try: + for password in f: + password = password.rstrip('\r\n') + if len(password) > 0: + + # Provide analysis time feedback to the user + if not self.quiet and password_count != 0 and password_count % 5000 == 0: + segment_time = time.time() - segment_start + print + "[*] Processed %d passwords in %.2f seconds at the rate of %.2f p/sec" % \ + (password_count, segment_start - analysis_start, 5000 / segment_time) + segment_start = time.time() + + password_count += 1 + + # Perform preliminary checks and add password to the queue + if self.check_reversible_password(password): + passwords_queue.put(password) + + except (KeyboardInterrupt, SystemExit): + print + "\n[!] Rulegen was interrupted." + + else: + # Signal workers to stop. + for i in range(multiprocessing.cpu_count()): + passwords_queue.put(None) + + # Wait for all of the queued passwords to finish. + while not passwords_queue.empty(): + time.sleep(1) + + # Signal writers to stop. + rules_queue.put(None) + words_queue.put(None) + + f.close() + + analysis_time = time.time() - analysis_start + print + "[*] Finished processing %d passwords in %.2f seconds at the rate of %.2f p/sec" % ( + password_count, analysis_time, float(password_count) / analysis_time) + + print + "[*] Generating statistics for [%s] rules and words." % self.basename + print + "[-] Skipped %d all numeric passwords (%0.2f%%)" % \ + (self.numeric_stats_total, float(self.numeric_stats_total) * 100.0 / float(password_count)) + print + "[-] Skipped %d passwords with less than 25%% alpha characters (%0.2f%%)" % \ + (self.special_stats_total, float(self.special_stats_total) * 100.0 / float(password_count)) + print + "[-] Skipped %d passwords with non ascii characters (%0.2f%%)" % \ + (self.foreign_stats_total, float(self.foreign_stats_total) * 100.0 / float(password_count)) + + # TODO: Counter breaks on large files. uniq -c | sort -rn is still the most + # optimal way. + rules_file = open("%s.rule" % self.basename, 'r') + rules_sorted_file = open("%s-sorted.rule" % self.basename, 'w') + rules_counter = Counter(rules_file) + rule_counter_total = sum(rules_counter.values()) + + print + "\n[*] Top 10 rules" + rules_i = 0 + for (rule, count) in rules_counter.most_common(): + rules_sorted_file.write(rule) + if rules_i < 10: print + "[+] %s - %d (%0.2f%%)" % (rule.rstrip('\r\n'), count, count * 100 / rule_counter_total) + rules_i += 1 + + rules_file.close() + rules_sorted_file.close() + + words_file = open("%s.word" % self.basename, 'r') + words_sorted_file = open("%s-sorted.word" % self.basename, 'w') + words_counter = Counter(words_file) + word_counter_total = sum(rules_counter.values()) + + print + "\n[*] Top 10 words" + words_i = 0 + for (word, count) in words_counter.most_common(): + words_sorted_file.write(word) + if words_i < 10: print + "[+] %s - %d (%0.2f%%)" % (word.rstrip('\r\n'), count, count * 100 / word_counter_total) + words_i += 1 + + words_file.close() + words_sorted_file.close() + + +############################################################################ +def verify_hashcat_rules(self, word, rules, password): + f = open("%s/test.rule" % HASHCAT_PATH, 'w') + f.write(" ".join(rules)) + f.close() + + f = open("%s/test.word" % HASHCAT_PATH, 'w') + f.write(word) + f.close() + + p = subprocess.Popen(["%s/hashcat-cli64.bin" % HASHCAT_PATH, "-r", "%s/test.rule" % HASHCAT_PATH, "--stdout", + "%s/test.word" % HASHCAT_PATH], stdout=subprocess.PIPE) + out, err = p.communicate() + out = out.strip() + + if out == password: + hashcat_rules_str = " ".join(rules or [':']) + if self.verbose: print + "[+] %s => %s => %s" % (word, hashcat_rules_str, password) + + else: + print + "[!] Hashcat Verification FAILED: %s => %s => %s (%s)" % (word, " ".join(rules or [':']), password, out) + + +if __name__ == "__main__": + + header = " _ \n" + header += " RuleGen %s | |\n" % VERSION + header += " _ __ __ _ ___| | _\n" + header += " | '_ \ / _` |/ __| |/ /\n" + header += " | |_) | (_| | (__| < \n" + header += " | .__/ \__,_|\___|_|\_\\\n" + header += " | | \n" + header += " |_| iphelix@thesprawl.org\n" + header += "\n" + + parser = OptionParser("%prog [options] passwords.txt", version="%prog " + VERSION) + + parser.add_option("-b", "--basename", + help="Output base name. The following files will be generated: basename.words, basename.rules and basename.stats", + default="analysis", metavar="rockyou") + parser.add_option("-w", "--wordlist", help="Use a custom wordlist for rule analysis.", metavar="wiki.dict") + parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, help="Don't show headers.") + parser.add_option("--threads", type="int", default=10, help="Parallel threads to use for processing.") + + wordtune = OptionGroup(parser, "Fine tune source word generation:") + wordtune.add_option("--maxworddist", help="Maximum word edit distance (Levenshtein)", type="int", default=10, + metavar="10") + wordtune.add_option("--maxwords", help="Maximum number of source word candidates to consider", type="int", + default=5, metavar="5") + wordtune.add_option("--morewords", help="Consider suboptimal source word candidates", action="store_true", + default=False) + wordtune.add_option("--simplewords", help="Generate simple source words for given passwords", action="store_true", + default=False) + parser.add_option_group(wordtune) + + ruletune = OptionGroup(parser, "Fine tune rule generation:") + ruletune.add_option("--maxrulelen", help="Maximum number of operations in a single rule", type="int", default=10, + metavar="10") + ruletune.add_option("--maxrules", help="Maximum number of rules to consider", type="int", default=5, metavar="5") + ruletune.add_option("--morerules", help="Generate suboptimal rules", action="store_true", default=False) + ruletune.add_option("--simplerules", help="Generate simple rules insert,delete,replace", action="store_true", + default=False) + ruletune.add_option("--bruterules", help="Bruteforce reversal and rotation rules (slow)", action="store_true", + default=False) + parser.add_option_group(ruletune) + + spelltune = OptionGroup(parser, "Fine tune spell checker engine:") + spelltune.add_option("--providers", help="Comma-separated list of provider engines", default="aspell,myspell", + metavar="aspell,myspell") + parser.add_option_group(spelltune) + + debug = OptionGroup(parser, "Debuggin options:") + debug.add_option("-v", "--verbose", help="Show verbose information.", action="store_true", default=False) + debug.add_option("-d", "--debug", help="Debug rules.", action="store_true", default=False) + debug.add_option("--password", help="Process the last argument as a password not a file.", action="store_true", + default=False) + debug.add_option("--word", help="Use a custom word for rule analysis", metavar="Password") + debug.add_option("--hashcat", help="Test generated rules with hashcat-cli", action="store_true", default=False) + parser.add_option_group(debug) + + (options, args) = parser.parse_args() + + # Print program header + if not options.quiet: + print + header + + if len(args) < 1: + parser.error("no passwords file specified") + exit(1) + + rulegen = RuleGen(language="en", providers=options.providers, basename=options.basename, threads=options.threads) + + # Finetuning word generation + rulegen.max_word_dist = options.maxworddist + rulegen.max_words = options.maxwords + rulegen.more_words = options.morewords + rulegen.simple_words = options.simplewords + + # Finetuning rule generation + rulegen.max_rule_len = options.maxrulelen + rulegen.max_rules = options.maxrules + rulegen.more_rules = options.morerules + rulegen.simple_rules = options.simplerules + rulegen.brute_rules = options.bruterules + if rulegen.brute_rules: print + "[!] Bruteforcing reversal and rotation rules. (slower)" + + # Debugging options + rulegen.word = options.word + rulegen.verbose = options.verbose + rulegen.debug = options.debug + rulegen.hashcat = options.hashcat + rulegen.quiet = options.quiet + + # Custom wordlist + if not options.word: + if options.wordlist: rulegen.load_custom_wordlist(options.wordlist) + print + "[*] Using Enchant '%s' module. For best results please install" % rulegen.enchant.provider.name + print + " '%s' module language dictionaries." % rulegen.enchant.provider.name + + # Analyze a single password or several passwords in a file + if options.password: + rulegen.analyze_password(args[0]) + else: + rulegen.analyze_passwords_file(args[0]) diff --git a/PACK/statsgen.py b/PACK/statsgen.py new file mode 100755 index 0000000..17b9c0f --- /dev/null +++ b/PACK/statsgen.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python +# StatsGen - Password Statistical Analysis tool +# +# This tool is part of PACK (Password Analysis and Cracking Kit) +# +# VERSION 0.0.3 +# +# Copyright (C) 2013 Peter Kacherginsky +# All rights reserved. +# +# Please see the attached LICENSE file for additional licensing information. + +import sys +import re, operator, string +from optparse import OptionParser, OptionGroup +import time + +VERSION = "0.0.3" + + +class StatsGen: + def __init__(self): + self.output_file = None + + # Filters + self.minlength = None + self.maxlength = None + self.simplemasks = None + self.charsets = None + self.quiet = False + self.debug = True + + # Stats dictionaries + self.stats_length = dict() + self.stats_simplemasks = dict() + self.stats_advancedmasks = dict() + self.stats_charactersets = dict() + + # Ignore stats with less than 1% coverage + self.hiderare = False + + self.filter_counter = 0 + self.total_counter = 0 + + # Minimum password complexity counters + self.mindigit = None + self.minupper = None + self.minlower = None + self.minspecial = None + + self.maxdigit = None + self.maxupper = None + self.maxlower = None + self.maxspecial = None + + def analyze_password(self, password): + + # Password length + pass_length = len(password) + + # Character-set and policy counters + digit = 0 + lower = 0 + upper = 0 + special = 0 + + simplemask = list() + advancedmask_string = "" + + # Detect simple and advanced masks + for letter in password: + + if letter in string.digits: + digit += 1 + advancedmask_string += "?d" + if not simplemask or not simplemask[-1] == 'digit': simplemask.append('digit') + + elif letter in string.lowercase: + lower += 1 + advancedmask_string += "?l" + if not simplemask or not simplemask[-1] == 'string': simplemask.append('string') + + + elif letter in string.uppercase: + upper += 1 + advancedmask_string += "?u" + if not simplemask or not simplemask[-1] == 'string': simplemask.append('string') + + else: + special += 1 + advancedmask_string += "?s" + if not simplemask or not simplemask[-1] == 'special': simplemask.append('special') + + # String representation of masks + simplemask_string = ''.join(simplemask) if len(simplemask) <= 3 else 'othermask' + + # Policy + policy = (digit, lower, upper, special) + + # Determine character-set + if digit and not lower and not upper and not special: + charset = 'numeric' + elif not digit and lower and not upper and not special: + charset = 'loweralpha' + elif not digit and not lower and upper and not special: + charset = 'upperalpha' + elif not digit and not lower and not upper and special: + charset = 'special' + + elif not digit and lower and upper and not special: + charset = 'mixedalpha' + elif digit and lower and not upper and not special: + charset = 'loweralphanum' + elif digit and not lower and upper and not special: + charset = 'upperalphanum' + elif not digit and lower and not upper and special: + charset = 'loweralphaspecial' + elif not digit and not lower and upper and special: + charset = 'upperalphaspecial' + elif digit and not lower and not upper and special: + charset = 'specialnum' + + elif not digit and lower and upper and special: + charset = 'mixedalphaspecial' + elif digit and not lower and upper and special: + charset = 'upperalphaspecialnum' + elif digit and lower and not upper and special: + charset = 'loweralphaspecialnum' + elif digit and lower and upper and not special: + charset = 'mixedalphanum' + else: + charset = 'all' + + return (pass_length, charset, simplemask_string, advancedmask_string, policy) + + def generate_stats(self, filename): + """ Generate password statistics. """ + + f = open(filename, 'r') + + for password in f: + password = password.rstrip('\r\n') + + if len(password) == 0: continue + + self.total_counter += 1 + + (pass_length, characterset, simplemask, advancedmask, policy) = self.analyze_password(password) + (digit, lower, upper, special) = policy + + if (self.charsets == None or characterset in self.charsets) and \ + (self.simplemasks == None or simplemask in self.simplemasks) and \ + (self.maxlength == None or pass_length <= self.maxlength) and \ + (self.minlength == None or pass_length >= self.minlength): + + self.filter_counter += 1 + + if self.mindigit == None or digit < self.mindigit: self.mindigit = digit + if self.maxdigit == None or digit > self.maxdigit: self.maxdigit = digit + + if self.minupper == None or upper < self.minupper: self.minupper = upper + if self.maxupper == None or upper > self.maxupper: self.maxupper = upper + + if self.minlower == None or lower < self.minlower: self.minlower = lower + if self.maxlower == None or lower > self.maxlower: self.maxlower = lower + + if self.minspecial == None or special < self.minspecial: self.minspecial = special + if self.maxspecial == None or special > self.maxspecial: self.maxspecial = special + + if pass_length in self.stats_length: + self.stats_length[pass_length] += 1 + else: + self.stats_length[pass_length] = 1 + + if characterset in self.stats_charactersets: + self.stats_charactersets[characterset] += 1 + else: + self.stats_charactersets[characterset] = 1 + + if simplemask in self.stats_simplemasks: + self.stats_simplemasks[simplemask] += 1 + else: + self.stats_simplemasks[simplemask] = 1 + + if advancedmask in self.stats_advancedmasks: + self.stats_advancedmasks[advancedmask] += 1 + else: + self.stats_advancedmasks[advancedmask] = 1 + + f.close() + + def print_stats(self): + """ Print password statistics. """ + + print + "[+] Analyzing %d%% (%d/%d) of passwords" % ( + self.filter_counter * 100 / self.total_counter, self.filter_counter, self.total_counter) + print + " NOTE: Statistics below is relative to the number of analyzed passwords, not total number of passwords" + print + "\n[*] Length:" + for (length, count) in sorted(self.stats_length.iteritems(), key=operator.itemgetter(1), reverse=True): + if self.hiderare and not count * 100 / self.filter_counter > 0: continue + print + "[+] %25d: %02d%% (%d)" % (length, count * 100 / self.filter_counter, count) + + print + "\n[*] Character-set:" + for (char, count) in sorted(self.stats_charactersets.iteritems(), key=operator.itemgetter(1), reverse=True): + if self.hiderare and not count * 100 / self.filter_counter > 0: continue + print + "[+] %25s: %02d%% (%d)" % (char, count * 100 / self.filter_counter, count) + + print + "\n[*] Password complexity:" + print + "[+] digit: min(%s) max(%s)" % (self.mindigit, self.maxdigit) + print + "[+] lower: min(%s) max(%s)" % (self.minlower, self.maxlower) + print + "[+] upper: min(%s) max(%s)" % (self.minupper, self.maxupper) + print + "[+] special: min(%s) max(%s)" % (self.minspecial, self.maxspecial) + + print + "\n[*] Simple Masks:" + for (simplemask, count) in sorted(self.stats_simplemasks.iteritems(), key=operator.itemgetter(1), reverse=True): + if self.hiderare and not count * 100 / self.filter_counter > 0: continue + print + "[+] %25s: %02d%% (%d)" % (simplemask, count * 100 / self.filter_counter, count) + + print + "\n[*] Advanced Masks:" + for (advancedmask, count) in sorted(self.stats_advancedmasks.iteritems(), key=operator.itemgetter(1), + reverse=True): + if count * 100 / self.filter_counter > 0: + print + "[+] %25s: %02d%% (%d)" % (advancedmask, count * 100 / self.filter_counter, count) + + if self.output_file: + self.output_file.write("%s,%d\n" % (advancedmask, count)) + + +if __name__ == "__main__": + + header = " _ \n" + header += " StatsGen %s | |\n" % VERSION + header += " _ __ __ _ ___| | _\n" + header += " | '_ \ / _` |/ __| |/ /\n" + header += " | |_) | (_| | (__| < \n" + header += " | .__/ \__,_|\___|_|\_\\\n" + header += " | | \n" + header += " |_| iphelix@thesprawl.org\n" + header += "\n" + + parser = OptionParser("%prog [options] passwords.txt\n\nType --help for more options", version="%prog " + VERSION) + + filters = OptionGroup(parser, "Password Filters") + filters.add_option("--minlength", dest="minlength", type="int", metavar="8", help="Minimum password length") + filters.add_option("--maxlength", dest="maxlength", type="int", metavar="8", help="Maximum password length") + filters.add_option("--charset", dest="charsets", help="Password charset filter (comma separated)", + metavar="loweralpha,numeric") + filters.add_option("--simplemask", dest="simplemasks", help="Password mask filter (comma separated)", + metavar="stringdigit,allspecial") + parser.add_option_group(filters) + + parser.add_option("-o", "--output", dest="output_file", help="Save masks and stats to a file", + metavar="password.masks") + parser.add_option("--hiderare", action="store_true", dest="hiderare", default=False, + help="Hide statistics covering less than 1% of the sample") + + parser.add_option("-q", "--quiet", action="store_true", dest="quiet", default=False, help="Don't show headers.") + (options, args) = parser.parse_args() + + # Print program header + if not options.quiet: + print + header + + if len(args) != 1: + parser.error("no passwords file specified") + exit(1) + + print + "[*] Analyzing passwords in [%s]" % args[0] + + statsgen = StatsGen() + + if not options.minlength == None: statsgen.minlength = options.minlength + if not options.maxlength == None: statsgen.maxlength = options.maxlength + if not options.charsets == None: statsgen.charsets = [x.strip() for x in options.charsets.split(',')] + if not options.simplemasks == None: statsgen.simplemasks = [x.strip() for x in options.simplemasks.split(',')] + + if options.hiderare: statsgen.hiderare = options.hiderare + + if options.output_file: + print + "[*] Saving advanced masks and occurrences to [%s]" % options.output_file + statsgen.output_file = open(options.output_file, 'w') + + statsgen.generate_stats(args[0]) + statsgen.print_stats() diff --git a/hashcat-utils/CHANGES b/hashcat-utils/CHANGES new file mode 100644 index 0000000..104eeb7 --- /dev/null +++ b/hashcat-utils/CHANGES @@ -0,0 +1,9 @@ +* v1.1 -> v1.2 + +- Open Source the project +- License is MIT +- Moved repository to github: https://github.com/hashcat/hashcat-utils +- Added CHANGES +- Added LICENSE +- Added README.md + diff --git a/hashcat-utils/LICENSE b/hashcat-utils/LICENSE new file mode 100644 index 0000000..ea53078 --- /dev/null +++ b/hashcat-utils/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Jens Steube + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/hashcat-utils/README.md b/hashcat-utils/README.md new file mode 100644 index 0000000..554aaa5 --- /dev/null +++ b/hashcat-utils/README.md @@ -0,0 +1,28 @@ +hashcat-utils +============== + +Hashcat-utils are a set of small utilities that are useful in advanced password cracking + +Brief description +-------------- + +They all are packed into multiple stand-alone binaries. + +All of these utils are designed to execute only one specific function. + +Since they all work with STDIN and STDOUT you can group them into chains. + +Detailed description +-------------- + +tbd + +Compile +-------------- + +Simply run make + +Binary distribution +-------------- + +Binaries for Linux, Windows and OSX: https://github.com/hashcat/hashcat-utils/releases diff --git a/hashcat-utils/bin/cleanup-rules.app b/hashcat-utils/bin/cleanup-rules.app new file mode 100755 index 0000000..52c50fd Binary files /dev/null and b/hashcat-utils/bin/cleanup-rules.app differ diff --git a/hashcat-utils/bin/cleanup-rules.bin b/hashcat-utils/bin/cleanup-rules.bin new file mode 100755 index 0000000..d6e34d5 Binary files /dev/null and b/hashcat-utils/bin/cleanup-rules.bin differ diff --git a/hashcat-utils/bin/cleanup-rules.exe b/hashcat-utils/bin/cleanup-rules.exe new file mode 100755 index 0000000..584947a Binary files /dev/null and b/hashcat-utils/bin/cleanup-rules.exe differ diff --git a/hashcat-utils/bin/combinator.app b/hashcat-utils/bin/combinator.app new file mode 100755 index 0000000..3612d9f Binary files /dev/null and b/hashcat-utils/bin/combinator.app differ diff --git a/hashcat-utils/bin/combinator.bin b/hashcat-utils/bin/combinator.bin new file mode 100755 index 0000000..177698b Binary files /dev/null and b/hashcat-utils/bin/combinator.bin differ diff --git a/hashcat-utils/bin/combinator.exe b/hashcat-utils/bin/combinator.exe new file mode 100755 index 0000000..8282b18 Binary files /dev/null and b/hashcat-utils/bin/combinator.exe differ diff --git a/hashcat-utils/bin/combinator3.app b/hashcat-utils/bin/combinator3.app new file mode 100755 index 0000000..22e58bf Binary files /dev/null and b/hashcat-utils/bin/combinator3.app differ diff --git a/hashcat-utils/bin/combinator3.bin b/hashcat-utils/bin/combinator3.bin new file mode 100755 index 0000000..bd91494 Binary files /dev/null and b/hashcat-utils/bin/combinator3.bin differ diff --git a/hashcat-utils/bin/combinator3.exe b/hashcat-utils/bin/combinator3.exe new file mode 100755 index 0000000..a3412d1 Binary files /dev/null and b/hashcat-utils/bin/combinator3.exe differ diff --git a/hashcat-utils/bin/combipow.app b/hashcat-utils/bin/combipow.app new file mode 100755 index 0000000..e5459e2 Binary files /dev/null and b/hashcat-utils/bin/combipow.app differ diff --git a/hashcat-utils/bin/combipow.bin b/hashcat-utils/bin/combipow.bin new file mode 100755 index 0000000..499814a Binary files /dev/null and b/hashcat-utils/bin/combipow.bin differ diff --git a/hashcat-utils/bin/combipow.exe b/hashcat-utils/bin/combipow.exe new file mode 100755 index 0000000..237e14d Binary files /dev/null and b/hashcat-utils/bin/combipow.exe differ diff --git a/hashcat-utils/bin/cutb.app b/hashcat-utils/bin/cutb.app new file mode 100755 index 0000000..fea8977 Binary files /dev/null and b/hashcat-utils/bin/cutb.app differ diff --git a/hashcat-utils/bin/cutb.bin b/hashcat-utils/bin/cutb.bin new file mode 100755 index 0000000..c4aeaef Binary files /dev/null and b/hashcat-utils/bin/cutb.bin differ diff --git a/hashcat-utils/bin/cutb.exe b/hashcat-utils/bin/cutb.exe new file mode 100755 index 0000000..d4a21ae Binary files /dev/null and b/hashcat-utils/bin/cutb.exe differ diff --git a/hashcat-utils/bin/expander.app b/hashcat-utils/bin/expander.app new file mode 100755 index 0000000..23025d4 Binary files /dev/null and b/hashcat-utils/bin/expander.app differ diff --git a/hashcat-utils/bin/expander.bin b/hashcat-utils/bin/expander.bin new file mode 100755 index 0000000..351f841 Binary files /dev/null and b/hashcat-utils/bin/expander.bin differ diff --git a/hashcat-utils/bin/expander.exe b/hashcat-utils/bin/expander.exe new file mode 100755 index 0000000..69966b9 Binary files /dev/null and b/hashcat-utils/bin/expander.exe differ diff --git a/hashcat-utils/bin/gate.app b/hashcat-utils/bin/gate.app new file mode 100755 index 0000000..930020c Binary files /dev/null and b/hashcat-utils/bin/gate.app differ diff --git a/hashcat-utils/bin/gate.bin b/hashcat-utils/bin/gate.bin new file mode 100755 index 0000000..5cbf5bf Binary files /dev/null and b/hashcat-utils/bin/gate.bin differ diff --git a/hashcat-utils/bin/gate.exe b/hashcat-utils/bin/gate.exe new file mode 100755 index 0000000..43b8a2e Binary files /dev/null and b/hashcat-utils/bin/gate.exe differ diff --git a/hashcat-utils/bin/generate-rules.app b/hashcat-utils/bin/generate-rules.app new file mode 100755 index 0000000..4f929e9 Binary files /dev/null and b/hashcat-utils/bin/generate-rules.app differ diff --git a/hashcat-utils/bin/generate-rules.bin b/hashcat-utils/bin/generate-rules.bin new file mode 100755 index 0000000..0dd33e1 Binary files /dev/null and b/hashcat-utils/bin/generate-rules.bin differ diff --git a/hashcat-utils/bin/generate-rules.exe b/hashcat-utils/bin/generate-rules.exe new file mode 100755 index 0000000..cb4035b Binary files /dev/null and b/hashcat-utils/bin/generate-rules.exe differ diff --git a/hashcat-utils/bin/hcstatgen.app b/hashcat-utils/bin/hcstatgen.app new file mode 100755 index 0000000..f8b9e56 Binary files /dev/null and b/hashcat-utils/bin/hcstatgen.app differ diff --git a/hashcat-utils/bin/hcstatgen.bin b/hashcat-utils/bin/hcstatgen.bin new file mode 100755 index 0000000..76d278b Binary files /dev/null and b/hashcat-utils/bin/hcstatgen.bin differ diff --git a/hashcat-utils/bin/hcstatgen.exe b/hashcat-utils/bin/hcstatgen.exe new file mode 100755 index 0000000..b5bb198 Binary files /dev/null and b/hashcat-utils/bin/hcstatgen.exe differ diff --git a/hashcat-utils/bin/keyspace.app b/hashcat-utils/bin/keyspace.app new file mode 100755 index 0000000..8b84174 Binary files /dev/null and b/hashcat-utils/bin/keyspace.app differ diff --git a/hashcat-utils/bin/keyspace.bin b/hashcat-utils/bin/keyspace.bin new file mode 100755 index 0000000..7ac6ec1 Binary files /dev/null and b/hashcat-utils/bin/keyspace.bin differ diff --git a/hashcat-utils/bin/keyspace.exe b/hashcat-utils/bin/keyspace.exe new file mode 100755 index 0000000..ea5a0ba Binary files /dev/null and b/hashcat-utils/bin/keyspace.exe differ diff --git a/hashcat-utils/bin/len.app b/hashcat-utils/bin/len.app new file mode 100755 index 0000000..e794812 Binary files /dev/null and b/hashcat-utils/bin/len.app differ diff --git a/hashcat-utils/bin/len.bin b/hashcat-utils/bin/len.bin new file mode 100755 index 0000000..6904693 Binary files /dev/null and b/hashcat-utils/bin/len.bin differ diff --git a/hashcat-utils/bin/len.exe b/hashcat-utils/bin/len.exe new file mode 100755 index 0000000..774599f Binary files /dev/null and b/hashcat-utils/bin/len.exe differ diff --git a/hashcat-utils/bin/mli2.app b/hashcat-utils/bin/mli2.app new file mode 100755 index 0000000..5935e8c Binary files /dev/null and b/hashcat-utils/bin/mli2.app differ diff --git a/hashcat-utils/bin/mli2.bin b/hashcat-utils/bin/mli2.bin new file mode 100755 index 0000000..d105fa1 Binary files /dev/null and b/hashcat-utils/bin/mli2.bin differ diff --git a/hashcat-utils/bin/mli2.exe b/hashcat-utils/bin/mli2.exe new file mode 100755 index 0000000..ace59c5 Binary files /dev/null and b/hashcat-utils/bin/mli2.exe differ diff --git a/hashcat-utils/bin/morph.app b/hashcat-utils/bin/morph.app new file mode 100755 index 0000000..99bc2f5 Binary files /dev/null and b/hashcat-utils/bin/morph.app differ diff --git a/hashcat-utils/bin/morph.bin b/hashcat-utils/bin/morph.bin new file mode 100755 index 0000000..b9e9928 Binary files /dev/null and b/hashcat-utils/bin/morph.bin differ diff --git a/hashcat-utils/bin/morph.exe b/hashcat-utils/bin/morph.exe new file mode 100755 index 0000000..e769519 Binary files /dev/null and b/hashcat-utils/bin/morph.exe differ diff --git a/hashcat-utils/bin/permute.app b/hashcat-utils/bin/permute.app new file mode 100755 index 0000000..1b0d886 Binary files /dev/null and b/hashcat-utils/bin/permute.app differ diff --git a/hashcat-utils/bin/permute.bin b/hashcat-utils/bin/permute.bin new file mode 100755 index 0000000..5d13f51 Binary files /dev/null and b/hashcat-utils/bin/permute.bin differ diff --git a/hashcat-utils/bin/permute.exe b/hashcat-utils/bin/permute.exe new file mode 100755 index 0000000..89ca3c4 Binary files /dev/null and b/hashcat-utils/bin/permute.exe differ diff --git a/hashcat-utils/bin/permute_exist.app b/hashcat-utils/bin/permute_exist.app new file mode 100755 index 0000000..82e7739 Binary files /dev/null and b/hashcat-utils/bin/permute_exist.app differ diff --git a/hashcat-utils/bin/permute_exist.bin b/hashcat-utils/bin/permute_exist.bin new file mode 100755 index 0000000..86e3ab9 Binary files /dev/null and b/hashcat-utils/bin/permute_exist.bin differ diff --git a/hashcat-utils/bin/permute_exist.exe b/hashcat-utils/bin/permute_exist.exe new file mode 100755 index 0000000..0172ebe Binary files /dev/null and b/hashcat-utils/bin/permute_exist.exe differ diff --git a/hashcat-utils/bin/prepare.app b/hashcat-utils/bin/prepare.app new file mode 100755 index 0000000..11b5a62 Binary files /dev/null and b/hashcat-utils/bin/prepare.app differ diff --git a/hashcat-utils/bin/prepare.bin b/hashcat-utils/bin/prepare.bin new file mode 100755 index 0000000..3aa6ec3 Binary files /dev/null and b/hashcat-utils/bin/prepare.bin differ diff --git a/hashcat-utils/bin/prepare.exe b/hashcat-utils/bin/prepare.exe new file mode 100755 index 0000000..c4f9386 Binary files /dev/null and b/hashcat-utils/bin/prepare.exe differ diff --git a/hashcat-utils/bin/req-exclude.app b/hashcat-utils/bin/req-exclude.app new file mode 100755 index 0000000..921818b Binary files /dev/null and b/hashcat-utils/bin/req-exclude.app differ diff --git a/hashcat-utils/bin/req-exclude.bin b/hashcat-utils/bin/req-exclude.bin new file mode 100755 index 0000000..df01217 Binary files /dev/null and b/hashcat-utils/bin/req-exclude.bin differ diff --git a/hashcat-utils/bin/req-exclude.exe b/hashcat-utils/bin/req-exclude.exe new file mode 100755 index 0000000..4a8e9f0 Binary files /dev/null and b/hashcat-utils/bin/req-exclude.exe differ diff --git a/hashcat-utils/bin/req-include.app b/hashcat-utils/bin/req-include.app new file mode 100755 index 0000000..2194fe5 Binary files /dev/null and b/hashcat-utils/bin/req-include.app differ diff --git a/hashcat-utils/bin/req-include.bin b/hashcat-utils/bin/req-include.bin new file mode 100755 index 0000000..8ec0f3f Binary files /dev/null and b/hashcat-utils/bin/req-include.bin differ diff --git a/hashcat-utils/bin/req-include.exe b/hashcat-utils/bin/req-include.exe new file mode 100755 index 0000000..08a27f1 Binary files /dev/null and b/hashcat-utils/bin/req-include.exe differ diff --git a/hashcat-utils/bin/rli.app b/hashcat-utils/bin/rli.app new file mode 100755 index 0000000..d1e4464 Binary files /dev/null and b/hashcat-utils/bin/rli.app differ diff --git a/hashcat-utils/bin/rli.bin b/hashcat-utils/bin/rli.bin new file mode 100755 index 0000000..f22ae11 Binary files /dev/null and b/hashcat-utils/bin/rli.bin differ diff --git a/hashcat-utils/bin/rli.exe b/hashcat-utils/bin/rli.exe new file mode 100755 index 0000000..f7cb223 Binary files /dev/null and b/hashcat-utils/bin/rli.exe differ diff --git a/hashcat-utils/bin/rli2.app b/hashcat-utils/bin/rli2.app new file mode 100755 index 0000000..aeeeb0f Binary files /dev/null and b/hashcat-utils/bin/rli2.app differ diff --git a/hashcat-utils/bin/rli2.bin b/hashcat-utils/bin/rli2.bin new file mode 100755 index 0000000..6012682 Binary files /dev/null and b/hashcat-utils/bin/rli2.bin differ diff --git a/hashcat-utils/bin/rli2.exe b/hashcat-utils/bin/rli2.exe new file mode 100755 index 0000000..66420ec Binary files /dev/null and b/hashcat-utils/bin/rli2.exe differ diff --git a/hashcat-utils/bin/seprule.pl b/hashcat-utils/bin/seprule.pl new file mode 100755 index 0000000..9be08c6 --- /dev/null +++ b/hashcat-utils/bin/seprule.pl @@ -0,0 +1,71 @@ +#!/usr/bin/env perl + +## Name........: seprule +## Autor.......: Jens Steube +## License.....: MIT + +use strict; +use warnings; + +## +## configuration +## + +my @rp = ('0'..'9', 'A'..'Z'); + +my $width = 3; +my $rule = "i"; +my $sep = " "; + +## +## code +## + +my $rp_size = scalar @rp; + +my $total = $rp_size ** $width; + +my $db; + +for (my $i = 0; $i < $total; $i++) +{ + my $left = $i; + + my @out; + + for (my $c = 0; $c < $width; $c++) + { + my $m = $left % $rp_size; + my $d = $left / $rp_size; + + push (@out, $m); + + $left = $d; + } + + @out = sort { $a <=> $b } @out; + + my $val = join ("", @out); + + next if (exists $db->{$val}); + + $db->{$val} = undef; + + my @final; + + for (my $c = 0; $c < $width; $c++) + { + my $s = sprintf ("T%s", $rp[$out[$c]]); + + push (@final, $s); + } + + for (my $c = 0; $c < $width; $c++) + { + my $s = sprintf ("%s%s%s", $rule, $rp[$out[$c]], $sep); + + push (@final, $s); + } + + print join (" ", "l", @final), "\n"; +} diff --git a/hashcat-utils/bin/splitlen.app b/hashcat-utils/bin/splitlen.app new file mode 100755 index 0000000..fa1f9d9 Binary files /dev/null and b/hashcat-utils/bin/splitlen.app differ diff --git a/hashcat-utils/bin/splitlen.bin b/hashcat-utils/bin/splitlen.bin new file mode 100755 index 0000000..8746968 Binary files /dev/null and b/hashcat-utils/bin/splitlen.bin differ diff --git a/hashcat-utils/bin/splitlen.exe b/hashcat-utils/bin/splitlen.exe new file mode 100755 index 0000000..46e71f1 Binary files /dev/null and b/hashcat-utils/bin/splitlen.exe differ diff --git a/hashcat-utils/bin/strip-bsn.app b/hashcat-utils/bin/strip-bsn.app new file mode 100755 index 0000000..0a4e0e3 Binary files /dev/null and b/hashcat-utils/bin/strip-bsn.app differ diff --git a/hashcat-utils/bin/strip-bsn.bin b/hashcat-utils/bin/strip-bsn.bin new file mode 100755 index 0000000..e26acd2 Binary files /dev/null and b/hashcat-utils/bin/strip-bsn.bin differ diff --git a/hashcat-utils/bin/strip-bsn.exe b/hashcat-utils/bin/strip-bsn.exe new file mode 100755 index 0000000..2d0cccc Binary files /dev/null and b/hashcat-utils/bin/strip-bsn.exe differ diff --git a/hashcat-utils/bin/strip-bsr.app b/hashcat-utils/bin/strip-bsr.app new file mode 100755 index 0000000..ff56c97 Binary files /dev/null and b/hashcat-utils/bin/strip-bsr.app differ diff --git a/hashcat-utils/bin/strip-bsr.bin b/hashcat-utils/bin/strip-bsr.bin new file mode 100755 index 0000000..429b617 Binary files /dev/null and b/hashcat-utils/bin/strip-bsr.bin differ diff --git a/hashcat-utils/bin/strip-bsr.exe b/hashcat-utils/bin/strip-bsr.exe new file mode 100755 index 0000000..b92935c Binary files /dev/null and b/hashcat-utils/bin/strip-bsr.exe differ diff --git a/hashcat-utils/bin/tmesis.pl b/hashcat-utils/bin/tmesis.pl new file mode 100755 index 0000000..0b9d43e --- /dev/null +++ b/hashcat-utils/bin/tmesis.pl @@ -0,0 +1,59 @@ +#!/usr/bin/env perl + +## Name........: tmesis +## Autor.......: Jens Steube +## License.....: MIT + +use strict; +use warnings; + +#tmesis will take a wordlist and produce insertion rules that would insert each word of the wordlist to preset positions. +#For example: +#Word ‘password’ will create insertion rules that would insert ‘password’ from position 0 to position F (15) and It will mutate the string ‘123456’ as follows. +#password123456 +#1password23456 +#12password3456 +#123password456 +#1234password56 +#12345password6 +#123456password +# +#Hints: +#*Use tmesis to create rules to attack hashlists the came from the source. Run initial analysis on the cracked passwords , collect the top 10 – 20 words appear on the passwords and use tmesis to generate rules. +#*use tmesis generated rules in combination with best64.rules +# +# inspired by T0XlC + +my $min_rule_pos = 0; +my $max_rule_pos = 15; + +my $db; + +my @intpos_to_rulepos = ('0'..'9', 'A'..'Z'); + +my $function = "i"; +#my $function = "o"; + +while (my $word = <>) +{ + chomp $word; + + my $word_len = length $word; + + my @word_buf = split "", $word; + + for (my $rule_pos = $min_rule_pos; $rule_pos < $max_rule_pos - $word_len; $rule_pos++) + { + my @rule; + + for (my $word_pos = 0; $word_pos < $word_len; $word_pos++) + { + my $function_full = $function . $intpos_to_rulepos[$rule_pos + $word_pos] . $word_buf[$word_pos]; + + push @rule, $function_full; + } + + print join (" ", @rule), "\n"; + } +} + diff --git a/hashcat-utils/bin/topmorph.pl b/hashcat-utils/bin/topmorph.pl new file mode 100755 index 0000000..d98b0ef --- /dev/null +++ b/hashcat-utils/bin/topmorph.pl @@ -0,0 +1,71 @@ +#!/usr/bin/env perl + +## Name........: topmorph +## Autor.......: Jens Steube +## License.....: MIT + +use strict; +use warnings; + +my @intpos_to_rulepos = ('0'..'9', 'A'..'Z'); + +my $function = "i"; +#my $function = "o"; + +if (scalar @ARGV != 5) +{ + print "usage: $0 dictionary depth width pos_min pos_max\n"; + + exit -1; +} + +my ($dictionary, $depth, $width, $pos_min, $pos_max) = @ARGV; + +if ($width > 20) +{ + print "width > 20\n"; + + exit -1; +} + +for (my $pos = $pos_min; $pos <= $pos_max; $pos++) +{ + my $db; + + open (IN, $dictionary) or die "$dictionary: $!\n"; + + while (my $line = ) + { + chomp $line; + + my $len = length $line; + + next if (($len - $pos) < $width); + + my $word = substr ($line, $pos, $width); + + next unless defined $word; + + $db->{$word}++; + } + + close (IN); + + my @keys = sort { $db->{$b} <=> $db->{$a} } keys %{$db}; + + for (my $i = 0; $i < $depth; $i++) + { + my @chars = split "", $keys[$i]; + + my @rule; + + for (my $j = 0; $j < $width; $j++) + { + my $function_full = join "", $function, $intpos_to_rulepos[$pos + $j], $chars[$j]; + + push @rule, $function_full; + } + + print join (" ", @rule), "\n"; + } +} diff --git a/princeprocessor/CHANGES b/princeprocessor/CHANGES new file mode 100644 index 0000000..59363f6 --- /dev/null +++ b/princeprocessor/CHANGES @@ -0,0 +1,55 @@ +* v0.20 -> v0.21: + +- Exit if stdout is closed or has a error +- Fix for "Bug --pw-min" issue +- Print position when stopped +- Allow wordlist as fileparameter +- Load only NUM words from input wordlist or use 0 to disable + +* v0.19 -> v0.20: + +- Add dupe suppression +- Add a fake-GMP header using uint128_t macros. This is to replace depency on GMP +- Add --case-permute amplifier option, default is disabled +- Fixed buffer overflow +- Fixed accidental reverted changes +- Fixed a bug where ee actually couldn't correctly support output longer than 31 but 32 is supported +- More memory savings: Use only the actual space needed for each word + +* v0.18 -> v0.19: + +- Fixed missing free() in shutdown section +- Fixed wrong version number in source +- Fixed discrepancies with logic and error messages +- Added validation check pw-max > elem-cnt-max +- Untie IN_LEN_* from PW_* to allow --pw-max > 16 without recompilation +- If out of memory, tell how much we tried to allocate +- Allow hex input for --skip and --limit +- Optimized output performance + +* v0.17 -> v0.18: + +- Fixed major bug where all candidates are of the same length till chain changes + +* v0.16 -> v0.17: + +- Fixed download url for binaries in README +- Fixed copy paste bug in input verification +- Fixed bug where pw_orders is not sorted +- Fixed memory leak +- Removed O_BINARY for stderr +- Removed some unused code +- Renamed variables so that they match the meaning from the presentation slides +- Optimized seeking performance +- Optimized output performance + +* v0.15 -> v0.16: + +- Open Source the project +- License is MIT +- Moved repository to github: https://github.com/jsteube/princeprocessor +- Added CHANGES +- Added LICENSE +- Added README.md +- Changed default value for --pw-max from 24 to 16 for faster startup time + diff --git a/princeprocessor/LICENSE b/princeprocessor/LICENSE new file mode 100644 index 0000000..ebcbc29 --- /dev/null +++ b/princeprocessor/LICENSE @@ -0,0 +1,33 @@ +The MIT License (MIT) + +Copyright (c) 2015 Jens Steube, +Copyright (c) 2015 magnum + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +------ + +malloc_tiny() and the hashed dupe suppression are based on code from John the +Ripper password cracker: +Copyright (c) 1996-99,2002-2003,2005-2006,2010-2012 by Solar Designer + +Redistribution and use in source and binary forms, with or without +modification, are permitted. + +There's ABSOLUTELY NO WARRANTY, express or implied. diff --git a/princeprocessor/README.md b/princeprocessor/README.md new file mode 100644 index 0000000..0c39e41 --- /dev/null +++ b/princeprocessor/README.md @@ -0,0 +1,38 @@ +princeprocessor +============== + +Standalone password candidate generator using the PRINCE algorithm + +The name PRINCE is used as an acronym and stands for PRobability INfinite Chained Elements, which are the building blocks of the algorithm + +Brief description +-------------- + +The princeprocessor is a password candidate generator and can be thought of as an advanced combinator attack. Rather than taking as input two different wordlists and then outputting all the possible two word combinations though, princeprocessor only has one input wordlist and builds "chains" of combined words. These chains can have 1 to N words from the input wordlist concatenated together. So for example if it is outputting guesses of length four, it could generate them using combinations from the input wordlist such as: + +- 4 letter word +- 2 letter word + 2 letter word +- 1 letter word + 3 letter word +- 3 letter word + 1 letter word +- 1 letter word + 1 letter word + 2 letter word +- 1 letter word + 2 letter word + 1 letter word +- 2 letter word + 1 letter word + 1 letter word +- 1 letter word + 1 letter word + 1 letter word + 1 letter word + +Detailed description +-------------- + +I'm going to write a detailed description in case I'm extremely bored. Till that, use the following resources: + +- My talk about princeprocessor on Passwords^14 conference in Trondheim, Norway. Slides: https://hashcat.net/events/p14-trondheim/prince-attack.pdf +- Thanks to Matt Weir, he made a nice analysis of princeprocessor. You can find the post on his blog: http://reusablesec.blogspot.de/2014/12/tool-deep-dive-prince.html + +Compile +-------------- + +Simply run make + +Binary distribution +-------------- + +Binaries for Linux, Windows and OSX: https://github.com/jsteube/princeprocessor/releases diff --git a/princeprocessor/pp.save b/princeprocessor/pp.save new file mode 100644 index 0000000..573541a --- /dev/null +++ b/princeprocessor/pp.save @@ -0,0 +1 @@ +0 diff --git a/princeprocessor/pp64.app b/princeprocessor/pp64.app new file mode 100755 index 0000000..36fd6da Binary files /dev/null and b/princeprocessor/pp64.app differ diff --git a/princeprocessor/pp64.bin b/princeprocessor/pp64.bin new file mode 100755 index 0000000..416507c Binary files /dev/null and b/princeprocessor/pp64.bin differ diff --git a/princeprocessor/pp64.exe b/princeprocessor/pp64.exe new file mode 100644 index 0000000..1eaff1d Binary files /dev/null and b/princeprocessor/pp64.exe differ diff --git a/princeprocessor/rules/prince_generated.rule b/princeprocessor/rules/prince_generated.rule new file mode 100644 index 0000000..f6af013 --- /dev/null +++ b/princeprocessor/rules/prince_generated.rule @@ -0,0 +1,8461 @@ +## Name: prince.rule +## Version: 1.00 +## Compatibility: hashcat v0.48+, oclHashcat v1.31+ +## +## Description: +## +## automatically generated ruleset, optimized rule for use with princeprocessor +## This rule can be used as amplifier in case you're attacking a fast hash + +: +[ +[ [ [ +[ [ [ [ +[ { +] +] { +] } +{ +{ [ +{ { +{ { { +{ { { { +{ { { { { +} +} } +} } } +} } } } +} } } } } +-0 +-0 ] +-0 { +{ -0 +} -0 ++0 ++0 } +-0 -0 ++0 +0 +-0 -0 -0 ++0 +0 +0 +-0 -0 -0 -0 ++0 +0 +0 +0 +-0 *02 +-0 *02 [ +-0 *04 ++0 *05 +-0 *05 D0 ++0 *08 +-0 -1 +} *01 +*01 +*01 [ +*01 } ++0 -1 ++0 +1 +*01 +1 +-0 -1 -2 ++0 +1 +2 +-0 -1 -2 -3 ++0 +1 +2 +3 +-0 -1 -3 -2 ++0 +1 +3 +2 +*01 *41 +*01 *51 +*01 *51 t +*01 *53 +*01 *53 R6 +*01 *65 +*01 *65 R7 ++0 *18 +*01 *83 +*01 f +*01 L1 +*01 L5 +*01 R0 +*01 R6 +*01 R6 *53 +*01 R7 +*01 R7 *65 +s1y +s81 +scx +soi +sx5 +*01 t +*01 t *51 +*01 x42 +*01 x61 +-0 +2 +[ *02 +{ *02 +*02 +*02 [ +*02 *06 +-0 -2 -1 +-0 *21 ++0 +2 +1 +-0 -2 -1 -3 +*02 *13 ++0 +2 +1 +3 +*02 *17 +s0P +-0 -2 -3 -1 ++0 +2 +3 +1 +*02 +5 +*02 *57 +sdl +*02 +9 +*02 *97 +*02 D3 +*02 k +*02 k *13 +*02 l +*02 L3 +*02 R4 +s0P +s0P +s1q +scl +sdl +sdl +*02 T6 +*02 T7 +*02 x21 +*02 x24 +*02 x42 +*02 x63 +-0 +3 +[ *03 +{ *03 +*03 +*03 [ ++0 -3 ++0 +3 +*03 +0 +*03 *05 +*03 +0 l +-0 *31 +*03 +1 ++0 *31 +-0 -3 -1 -2 ++0 +3 +1 +2 ++0 *32 ++0 *32 } +-0 -3 -2 -1 ++0 +3 +2 +1 +*03 *36 +*03 *41 +*03 *47 +-0 *34 C +*03 '5 ++0 *37 +*03 *70 +*03 D1 +*03 l +0 +*03 R3 +sbe +sjx +sqt +ssl +*03 x34 +*03 x43 +*04 +*04 [ +*04 ] +*04 { ++0 +4 +*04 *07 +*04 *17 +*04 +3 +*04 +4 +*04 +6 +*04 *73 +*04 '8 +*04 +8 D0 +*04 D0 +*04 D1 +*04 l +slk +smv +son +sry +sry +*04 T0 +*04 T6 +*04 u +*04 x32 +-0 -5 +[ *05 +{ *05 +*05 +*05 [ ++0 '5 +*05 *06 R4 +*05 -1 +*05 *12 +-0 *52 +*05 *26 +*05 +3 +*05 *30 +*05 -5 +*05 -5 ] +*05 '5 +*05 ] -5 ++0 *58 +*05 c +*05 D0 u ++0 '5 k +*05 K +*05 l +*05 r +*05 R1 +*05 R4 +*05 R4 *06 +saC +*05 T1 +*05 T6 +*05 u D0 +*05 x02 +*05 x03 +*05 x21 +*05 x35 +*05 x45 +-0 +6 +*06 ++0 '6 ++0 +6 +*06 *15 +-0 *63 +-0 *64 +*06 '5 +*06 '6 +*06 +6 +*06 *67 +*06 *87 +*06 D2 +*06 D3 +sMf +*06 l +*06 R0 +*06 R7 +s7A +sMf +*06 t +*06 T4 +*06 T6 +*06 u +*06 x02 +*06 x34 +*06 x41 +-0 -7 +-0 '7 +-0 +7 +*07 ++0 '7 +*07 -0 +*07 +0 +*07 *18 +*07 *24 +*07 *24 T1 +*07 *53 x31 +*07 c +*07 D2 +*07 D6 +*07 R7 +*07 T1 +*07 T1 *24 +*07 x31 +-0 +8 +*08 +*08 [ +*08 *13 ++0 *86 +*08 D6 +*08 T0 +-0 '8 x41 +*08 x45 +[ *09 +*09 +*09 [ +*09 '6 +*09 *91 +*09 *97 +*09 [ l +*09 l [ +s86 +*09 x03 +*09 x34 +-0 'A +*0A +*0A [ +*0A LB x06 +*0B ++0 'B +*0B x74 +-0 c +-0 C ++0 C +-0 C *34 +-0 C T0 +-0 D1 ++0 D2 ++0 D3 ++0 D3 x25 ++0 D4 ++0 D5 +-0 D7 ++0 D8 +-0 f ++0 f +-0 f *56 +-0 k +-0 K ++0 k '5 +-0 l ++0 l ++0 L4 ++0 L5 t +-0 L7 ++0 L7 +ss# +-0 l x24 +-0 r ++0 R1 +-0 R3 ++0 R4 +-0 R5 x12 ++0 R6 +s1q +s1y +s23 +s4k +s7S +sao +sc. +sfT +sfT +spH +spm +ss# +ss# +ssW +st2 +-0 t +{ -0 t ++0 t ++0 T2 +-0 x12 ++0 x12 ++0 x12 R8 +-0 x13 ++0 x13 +-0 x23 +-0 x24 ++0 x24 +-0 x24 l +-0 x25 ++0 x25 ++0 x25 D3 +-0 x26 +-0 x41 ++0 x42 +-0 x51 +-0 x52 ++0 x52 +sfT +-0 x53 ++0 x53 ++0 x53 x73 ++0 x56 ++0 x72 ++0 x82 +-0 x84 ++0 x92 ++0 xA1 +-1 +-1 { +-1 } +[ -1 +[ +1 +] -1 +{ +1 +} -1 ++1 ++1 [ ++1 { ++1 } +*10 { ++1 *04 +*10 *42 +slk ++1 *06 +*10 -8 +*10 c +*10 { c +*10 c { +*10 D3 r +*10 l +*10 R1 +*10 r D3 +*10 t +*10 t x81 +*10 x81 +*10 x81 t +-1 -1 ++1 +1 +-1 -1 -1 ++1 +1 +1 +-1 -1 -1 -1 ++1 +1 +1 +1 +-1 *12 +-1 *13 ++1 *14 +-1 -2 +-1 +2 +{ *12 +*12 +*12 } ++1 +2 +-1 *20 +*12 +1 +-1 -2 -3 +-1 *23 ++1 +2 +3 +-1 -2 -3 -4 ++1 +2 +3 +4 +-1 -2 -4 -3 ++1 +2 +4 +3 +*12 '5 +*12 *68 +-1 *27 +-1 -2 C +*12 f +*12 f u +*12 k +*12 K +*12 k l +ss2 +*12 l +*12 L5 +*12 l k +*12 R3 +s34 +sma +snp +ss2 +ss2 +*12 u f +*12 x51 +] ] *13 +] *13 +] *13 ] +{ *13 +*13 ++1 -3 ++1 +3 +*13 *04 +-1 *31 r +-1 -3 -2 ++1 +3 +2 +-1 -3 -2 -4 ++1 +3 +2 +4 +*13 *26 +-1 -3 -4 -2 +*13 *42 ++1 +3 +4 +2 +*13 *45 R1 +*13 *65 +*13 c +*13 D1 +*13 D9 +*13 k +*13 K x31 +*13 R1 +*13 R1 *45 +s73 +sau +sc2 +svr +*13 u +*13 x12 +*13 x31 +*13 x31 K ++1 -3 x42 +*13 x43 +*13 x62 +] *14 +} *14 +*14 +*14 ] +*14 } ++1 -4 +*14 *10 +*14 *10 C +*14 -2 +*14 *20 +-1 -4 -2 -3 ++1 +4 +2 +3 +*14 ] *27 +*14 *27 ] +-1 *43 +-1 -4 -3 -2 ++1 +4 +3 +2 +*14 *34 +*14 c +*14 C +*14 C *10 +*14 R2 +*14 R3 +svm +*14 x02 +-1 -5 +-1 [ -5 +[ *15 +{ *15 +*15 +*15 ] +*15 } ++1 -5 ++1 '5 +-1 } *50 ++1 *50 +*15 *10 +-1 *52 +*15 *23 +-1 *52 K +} -1 *53 +*15 +3 +*15 *41 +*15 *41 *43 +*15 *43 +*15 D1 +*15 f +[ *15 K +*15 r +snp +snp +*15 T4 +*15 u x01 +*15 x01 +*15 x01 u +} *15 x12 +*15 x13 +*15 x34 +*15 x42 +-1 +6 +{ *16 +*16 +*16 ] +*16 *05 +*16 *05 *30 +*16 *23 +*16 *30 +*16 *47 +*16 *48 +*16 '7 +*16 D0 x04 +*16 D1 +*16 D5 +*16 D5 u +*16 r +snp +-1 +6 T5 +*16 u D5 +*16 x03 +*16 x04 D0 +*16 x43 +-1 '7 +{ *17 +*17 ++1 '7 +*17 '6 +*17 +7 +*17 *73 +*17 -8 +*17 r +*17 x06 +*17 x12 +*17 x15 +*17 x42 +*17 x51 +*18 ++1 -8 +*18 D7 +*18 D8 +*18 l +*18 x03 +*18 x62 +*18 x75 +*19 +-1 *90 +-1 *91 ++1 *91 +*19 x08 -4 +*19 x41 +-1 'A +*1A 'A +*1A x03 +*1A xA1 ++1 'B +*1B x01 +-1 c ++1 c +-1 C +-1 C -2 +srD +-1 D0 ++1 D4 ++1 D4 -6 ++1 D7 ++1 D8 ++1 D9 +-1 f ++1 f +-1 k +-1 K +-1 K { +-1 K *52 +ssi +-1 l ++1 l +-1 L0 ++1 L2 ++1 L7 +-1 r +-1 R0 ++1 R0 ++1 R1 +-1 r *31 ++1 R4 +s0Q +s9s +sbq +sel +sfr +skv +skw +skx +slk +slk +sly +smd +srD +srg +ssi +ssi +szl +-1 t ++1 t +-1 T2 ++1 T2 +-1 T5 +-1 T5 +6 +-1 u +-1 x23 ++1 x23 ++1 x24 +-1 x32 ++1 x42 -3 ++1 x45 ++1 x52 ++1 x62 ++1 x63 +-1 x73 ++1 x82 ++1 x84 ++1 x92 +-2 +-2 { +[ -2 +[ +2 +] +2 ++2 +] *20 +{ *20 +*20 +*20 } ++2 +0 +*20 -0 +*20 *03 +*20 *14 +*20 *14 c +*20 +2 ++2 *02 +scu +*20 '4 +*20 *43 +-2 *05 +*20 *50 +*20 c +*20 c *14 +*20 D5 +*20 r +s29 +s4q +s52 +s9! +sri +svt +syt +{ *20 u +*20 } u +*20 u } +*20 x23 +*20 x32 +*20 x41 +*20 [ x41 +*20 x41 [ +*20 x71 +*21 +-2 *10 +*21 *03 +*21 *13 +*21 *20 +*21 *28 +*21 -3 +*21 *36 +*21 +4 +*21 +4 l +*21 *56 +*21 *72 +*21 *78 +*21 C +*21 c x31 +*21 D3 *42 +*21 l +4 +*21 R0 +sdE +suo +*21 u +*21 x24 +*21 x25 +*21 x31 c +*21 x41 +-2 -2 ++2 +2 +-2 -2 -2 ++2 +2 +2 +-2 -2 -2 -2 ++2 +2 +2 +2 +-2 *23 +-2 *26 +] +2 *26 +-2 -3 +[ *23 +*23 +*23 } ++2 +3 ++2 *30 +*23 *05 +-2 *31 +-2 *31 u ++2 *32 +*23 *31 +-2 -3 -4 ++2 +3 +4 +-2 -3 -4 -5 ++2 +3 +4 +5 +-2 -3 -5 -4 ++2 +3 +5 +4 +*23 -6 +*23 *86 +*23 c +*23 C +*23 k +*23 L4 +spe +*23 t +*23 x03 +*23 x42 +*23 x51 +{ *24 +} *24 +*24 +*24 ] ++2 '4 +*24 -0 +*24 *15 +*24 +2 +-2 -4 -3 ++2 +4 +3 +*24 *30 +-2 -4 -3 -5 ++2 +4 +3 +5 +*24 *37 +*24 *37 c +*24 -4 +-2 -4 -5 -3 ++2 +4 +5 +3 +*24 ] *70 +*24 *70 ] +} *24 *74 +*24 c +*24 c *37 +*24 D0 +*24 D2 +*24 f +*24 K +*24 R3 +src +*24 T1 +*24 x03 +-2 '5 +[ *25 +*25 +*25 ] ++2 +5 +*25 *10 +*25 *10 *24 +*25 *13 +*25 *13 c +-2 *52 +*25 +2 +*25 *24 +*25 *24 *10 +*25 +3 +*25 *30 +-2 -5 -3 -4 ++2 +5 +3 +4 +[ +2 *54 +*25 *40 +*25 *42 R5 +-2 -5 -4 -3 ++2 +5 +4 +3 +*25 '5 +*25 +5 +*25 *54 +*25 '7 ++2 *57 t +*25 -7 x03 +*25 *84 '7 +*25 c +*25 c *13 +*25 D1 +*25 D4 +*25 l x32 +*25 R5 +*25 R5 *42 +ssv +*25 T0 +*25 T5 +*25 x13 +*25 x32 +*25 x32 l +-2 -6 +*26 +*26 ] ++2 *62 +*26 ] *37 +*26 *37 ] +*26 *48 +*26 '5 +*26 D3 +*26 D6 +*26 r +*26 T6 x15 +*26 u +*26 x62 +-2 '7 +-2 +7 +{ *27 +*27 +*27 ] +-2 *75 ++2 *75 +-2 *75 c +*27 *65 +*27 *74 +*27 *76 +*27 D2 +*27 k +-2 +7 l +*27 x01 +*27 x12 +*27 x15 f +*27 x52 +-2 -8 +*28 +*28 '7 +*28 c +*28 T1 +*28 x05 +*29 +s4w +*29 x03 +*29 x04 +*29 x13 +s4w +*29 x14 +-2 'A +*2B DA +*2B x14 +-2 c +} -2 c ++2 c +-2 c *75 +-2 D0 +-2 D0 *54 +-2 D1 ++2 D1 ++2 D3 ++2 D5 ++2 D5 *79 +-2 f +-2 f [ ++2 f +} -2 k +-2 l ++2 l +-2 L0 +-2 L1 ++2 L1 +-2 L2 ++2 L2 +-2 L3 ++2 L5 ++2 L6 u +-2 l +7 +-2 r +-2 R0 +-2 R1 +-2 R4 +-2 R6 +s7i +scu +scu +sCy +sdy +sgb +sGq +si5 +sui +swc +swc ++2 t +-2 T4 ++2 t *57 ++2 T6 +-2 u ++2 u +-2 u *31 ++2 u L6 ++2 x02 +-2 x31 ++2 x41 +-2 x52 +-2 x53 +-2 x54 +swc +-3 +-3 { +[ -3 +] -3 +{ -3 +} +3 ++3 ++3 { +-3 -0 +} *30 +*30 +*30 [ +*30 { +*30 -0 +*30 *02 +*30 -2 +*30 +3 +*30 { *35 +*30 -4 +*30 *43 +*30 *64 +*30 -9 +*30 c +*30 C +} *30 D0 +*30 D3 +*30 f +*30 [ f +*30 R5 +*30 R6 +sdD +*30 T1 +*30 u +*30 { u +*30 u { +*30 x23 +*30 x26 +*30 x32 +*30 x41 +*30 { x53 +*30 x53 { +-3 -1 +-3 +1 +[ *31 ++3 -1 +*31 -0 ] +*31 ] -0 +*31 +2 ++3 *12 +*31 *20 +*31 *34 +{ *31 *51 +-3 *16 +*31 *74 +*31 '8 +*31 *91 +*31 'A +[ *31 c +*31 D2 +*31 D4 +*31 f +*31 k t +*31 l +*31 r +sg' +sns +sns +*31 t +*31 T3 +*31 T6 +sns +*31 t k +*31 x02 +*31 x24 +*31 x32 +-3 +2 +{ *32 +*32 +*32 { ++3 -2 +-3 *20 +*32 *04 +*32 *16 *43 +*32 -2 +*32 +2 +*32 *25 ++3 *23 +*32 *34 ++3 *23 k +-3 *24 +*32 *43 *16 +*32 *45 +*32 +5 +*32 *67 +*32 -8 +*32 '9 +*32 D0 +*32 D6 +*32 D7 +*32 l +*32 r +*32 R5 +s9* +*32 T0 +*32 T7 +*32 x34 +*32 x74 +-3 -3 ++3 +3 +sr1 +-3 -3 -3 ++3 +3 +3 +-3 -3 -3 -3 ++3 +3 +3 +3 ++3 *34 r +-3 -4 +-3 +4 +{ *34 +*34 +*34 } ++3 +4 +*34 +0 ++3 *42 +-3 *43 +*34 +3 +-3 -4 -5 +-3 *45 +*34 +5 ++3 +4 +5 +-3 -4 -5 -6 ++3 +4 +5 +6 +*34 *62 +-3 -4 -6 -5 ++3 +4 +6 +5 +*34 *74 +*34 c +sd> +*34 D7 +*34 K } +*34 l +*34 r +*34 r u +sd> +sdg +sm_ +stc +*34 u r +*34 x13 +stc +*34 x51 +*34 x52 +[ { *35 +{ *35 +*35 ++3 +5 +*35 *04 +*35 +1 +*35 *24 +*35 *31 +-3 -5 -4 +*35 +4 ++3 +5 +4 +*35 *41 +-3 -5 -4 -6 ++3 +5 +4 +6 +} *35 '5 +*35 +5 +} *35 *50 +*35 *50 +5 +*35 *54 +*35 *56 +-3 -5 -6 -4 ++3 +5 +6 +4 +-3 *57 +*35 D2 +{ *35 K +*35 L2 +*35 L3 +*35 r +*35 R4 +*35 t +*35 x12 +*35 x23 +*35 x43 +-3 -6 +-3 '6 +-3 +6 +*36 +*36 [ +*36 ] +*36 { ++3 -6 ++3 +6 +*36 -0 +5 +*36 [ *05 +*36 *13 +*36 *13 x62 +*36 *17 +*36 *19 ++3 *62 +*36 *26 +-3 *64 +*36 +4 +-3 -6 -4 -5 ++3 +6 +4 +5 +-3 *65 +*36 *50 +*36 +5 -0 +-3 -6 -5 -4 ++3 +6 +5 +4 +*36 '6 +*36 c +*36 [ D3 +*36 D3 [ +*36 D3 R0 +*36 D5 +*36 R0 D3 +*36 x32 +] *36 x43 +*36 x62 +*36 x62 *13 +{ *37 +*37 +*37 ] ++3 '7 +*37 *05 +*37 *14 +*37 *32 +*37 *87 +*37 c +-3 '7 u +*37 x12 +*37 x13 +*37 x25 +-3 '8 +*38 +-3 *82 +*38 'A +*38 D0 +*38 x23 +*38 x26 +*39 ++3 '9 +*39 +5 +*39 +8 +*39 'A +*39 D3 -5 +*39 x04 +*39 x05 +*39 x32 +*3A '8 +*3A x05 +*3A x24 +*3B '8 ++3 c +-3 D2 +-3 D2 u ++3 D5 +-3 D6 ++3 D6 +-3 D7 +-3 f x12 +-3 k ++3 k ++3 K ++3 { K ++3 k *23 +-3 l +-3 L2 +-3 r ++3 r ++3 r *34 ++3 R5 +sc6 +s0b +s1d +s2+ +s27 +s27 +sc6 +sCg +sfk +sl0 +smu +snz +soe +spj +sr1 +sr1 +ssp +ssv +sTB +stg +suf ++3 t ++3 T5 +-3 T6 +s27 +-3 u +} +3 u ++3 u +-3 u '7 +-3 u D2 +-3 x02 ++3 x12 +-3 x41 +-3 x51 +-3 x52 ++3 x53 ++3 x71 +-3 x72 +-4 +-4 } +] -4 +] -4 } +} -4 +} +4 +} +4 } ++4 ++4 [ ++4 ] ++4 { ++4 } +-4 -0 +} *40 ++4 +0 +*40 *02 +*40 *06 +*40 *12 +*40 *15 +-4 *02 ++4 *02 +*40 *34 +[ *40 *51 +*40 *54 +*40 *57 +-4 -0 '7 +*40 '9 +*40 *96 +*40 c +*40 D3 +*40 D4 +*40 D6 x41 +[ *40 k +*40 t +*40 u x34 +*40 x24 +*40 x34 +*40 x34 u +*40 x41 D6 +*40 x42 +*40 x42 x61 +*40 x45 +-4 -1 +-4 -1 ] +-4 ] -1 +[ *41 +{ *41 +*41 +*41 ] +*41 { +*41 } } ++4 +1 +*41 -0 +*41 *16 +*41 *32 +-4 *14 +*41 *47 +*41 *86 +*41 ] c +*41 c ] +*41 D1 +*41 f +*41 k { +*41 u +*41 x21 +*41 x32 +*41 x34 +*41 x37 +*41 x47 +-4 -2 +-4 +2 +*42 +*42 } +*42 *21 ++4 *23 +*42 *41 ++4 *25 +-4 *26 +*42 +6 +*42 *64 +*42 r +*42 R0 +*42 R5 +*42 R6 +skL +[ *42 t +*42 t +*42 T0 +*42 x46 +-4 +3 +*43 +*43 [ { +*43 ] +*43 { +*43 { [ +*43 -0 ++4 *30 +*43 *01 } +*43 *12 +*43 *16 +*43 +2 +*43 ] *20 +*43 *20 ] +*43 *35 K +-4 *34 +*43 *48 +*43 -5 +*43 *52 +*43 -5 t +*43 -6 +*43 D2 +*43 k +*43 K +*43 K *35 +*43 R3 +s25 +skC +suo +*43 t +*43 { t +*43 t { +*43 t -5 +*43 T6 +*43 x53 +-4 -4 ++4 +4 +-4 *40 ++4 *40 +-4 *42 +-4 *42 x42 +-4 -4 -4 ++4 +4 +4 +-4 -4 -4 -4 ++4 +4 +4 +4 +-4 *45 +-4 -5 +*45 +*45 { +*45 } ++4 +5 +*45 *01 +*45 *03 +*45 -2 +*45 { *25 +*45 *35 +-4 -5 -6 ++4 +5 +6 +*45 *62 +-4 -5 -6 -7 ++4 +5 +6 +7 +*45 -7 +*45 *71 +-4 -5 -7 -6 ++4 +5 +7 +6 +*45 D0 +*45 r +*45 R1 +*45 R4 +s2_ +sl8 +so9 +*45 T1 +*45 u +*45 x04 +*45 x32 +*45 } x52 +*45 x52 } +-4 '6 +*46 +*46 [ +*46 ] +*46 { ++4 '6 +*46 *16 +*46 *23 +*46 *25 +*46 -4 +*46 +4 ++4 *64 +-4 -6 -5 ++4 +6 +5 +-4 -6 -5 -7 ++4 +6 +5 +7 +*46 '6 +*46 '6 } +*46 } '6 +*46 *60 +*46 -7 +-4 -6 -7 -5 ++4 +6 +7 +5 +*46 D1 +*46 D4 +*46 l +*46 L7 +s6b +*46 T0 +*46 T5 +*46 ] x24 +*46 x24 ] +*47 +-4 '7 -0 +*47 *34 +*47 *36 +-4 -7 -5 -6 ++4 +7 +5 +6 +*47 *58 +-4 -7 -6 -5 +*47 *65 ++4 +7 +6 +5 +*47 *67 +*47 *75 +*47 *81 +*47 -9 +*47 C +*47 D4 +*47 T4 +-4 -8 +{ *48 +} *48 +*48 +*48 [ +*48 *08 +*48 +5 '6 ++4 *86 +*48 '6 +5 +*48 *86 +*48 C x43 +*48 D1 +*48 D1 t +*48 l +*48 T7 +*48 t D1 +*48 x43 +*48 x43 C +*49 ++4 '9 +*49 '9 +*49 L4 +swh +*49 x14 +*49 x24 +*49 x53 +*4A +*4B x16 +*4B x63 +-4 c +{ -4 c ++4 c ++4 C +-4 D2 ++4 D3 +-4 D5 ++4 D5 +-4 D5 L6 +-4 D6 +-4 D7 +'4 f +-4 k +-4 l ++4 l +-4 L4 ++4 L5 +-4 L5 +5 ++4 L7 ++4 r +s0M +s2m +s7 +se. +seg +sK9 +so5 +srB +s.z +-4 t ++4 t +-4 T4 ++4 T5 +-4 u +[ -4 u ++4 u +-4 x03 ++4 x03 +-4 x12 +-4 x31 ++4 x54 ++4 x62 +-4 x63 ++4 x74 +-4 x75 +-4 x82 ++4 x84 +-5 +-5 [ +-5 { +-5 } +'5 +[ -5 } +] -5 ++5 ++5 ] ++5 { ++5 } +} *50 +*50 { ++5 -0 +*50 +0 +*50 *02 +*50 +2 +*50 +3 { +*50 *31 +*50 -4 +*50 *41 +*50 +5 +*50 c K +*50 K c +*50 L5 +} *50 r +*50 R5 +*50 t +*50 u +*50 x23 +*50 x26 +*50 x32 +*50 x43 +*50 x51 +*50 x53 +'5 -1 +[ *51 +*51 +*51 [ +*51 { +*51 -0 +*51 +1 +*51 *14 +*51 ] *25 +*51 *25 ] +-5 *13 +'5 *13 +*51 -3 +*51 *31 +*51 *37 +'5 *14 +[ *51 *40 +'5 *14 *20 +*51 *52 +*51 *54 +-5 *16 +*51 { +6 +{ *51 C +*51 C +*51 D1 *12 +*51 D2 +*51 D5 +*51 K +*51 K R6 +-5 +1 L0 +*51 r +*51 R1 +-5 +1 R5 +*51 R6 +*51 R6 K +*51 r K +*51 T6 +*51 x32 +*51 x32 *30 +*51 x35 +-5 +2 +*52 [ +*52 ] ++5 [ -2 +'5 *20 +*52 +0 +'5 *20 *14 +*52 +1 +*52 *24 +-5 *24 +-5 *25 +*52 *53 u +-5 *25 l +*52 '6 +*52 +6 +*52 *70 +*52 ] c +*52 c ] +s3v +*52 [ k +*52 L0 +s3v +se0 +shl +srd +srd +*52 u +*52 u *53 +*52 x04 +*52 x12 +*52 x61 +-5 +3 +'5 -3 +'5 +3 +*53 +*53 [ +*53 ] +*53 +0 +} *53 -1 +*53 *12 +*53 *12 L5 +*53 *20 +*53 -4 +*53 *46 +*53 '5 ++5 *35 +*53 *50 +saj +*53 -7 +*53 +8 +*53 C +*53 f +*53 L5 +*53 L5 *12 +*53 L7 +*53 L7 u +-5 +3 R3 +*53 R5 +*53 R6 +s5s +saj +sbm +sbn +smb +sun +sur +*53 T4 +*53 T5 +*53 u +*53 x13 +*53 x31 +*53 x41 +*53 x52 +*53 x52 T5 +-5 +4 +'5 +4 +{ *54 +*54 +*54 } ++5 -4 +*54 +0 +[ *54 +2 +-5 *43 +*54 *35 t +*54 -4 +-5 *46 +*54 +7 +*54 *71 '5 +*54 -8 +*54 *86 +*54 c +*54 C +*54 D2 +*54 D7 +*54 l +*54 R3 +sBN +svN +syn +*54 t +*54 t *35 +sBN +*54 x12 +*54 x23 +*54 x64 +-5 -5 ++5 +5 +-5 *50 +-5 *50 K ++5 *54 +-5 -5 -5 ++5 +5 +5 +-5 -5 -5 -5 ++5 +5 +5 +5 +-5 -6 +-5 '6 +] { *56 +*56 ++5 -6 ++5 '6 ++5 +6 ++5 *60 +*56 *13 u +-5 *62 '6 +*56 *30 +*56 -4 +*56 +4 +*56 +5 ++5 *65 +*56 -6 +*56 } -6 +-5 -6 -7 +*56 '7 ++5 +6 +7 +*56 *76 +-5 -6 -7 -8 ++5 +6 +7 +8 +-5 *68 +*56 *81 x03 +*56 *85 +-5 -6 -8 -7 ++5 +6 +8 +7 +*56 'A +*56 D0 +*56 l +*56 L0 L5 +*56 L5 L0 +*56 r +*56 R4 +s40 +se2 +sgo +*56 u +*56 u *13 +*56 x03 +*57 +*57 ] +*57 } ++5 -7 ++5 +7 +*57 *06 +*57 *25 +*57 ] *36 +*57 *36 ] +-5 *74 +*57 *46 +-5 *75 +-5 *75 *56 +-5 -7 -6 ++5 +7 +6 +-5 -7 -6 -8 ++5 +7 +6 +8 ++5 *78 +-5 -7 -8 -6 ++5 +7 +8 +6 +-5 *79 ++5 *79 +*57 c +*57 c D6 +*57 D3 +*57 D6 +*57 D6 c +*57 D8 +sdl +*57 T5 D3 +*57 x15 +*57 x32 +-5 +8 +*58 +*58 ] { +*58 { ] ++5 '8 +-5 -8 -6 -7 ++5 +8 +6 +7 +-5 -8 -7 -6 +*58 *76 ++5 +8 +7 +6 +*58 '8 +*58 D4 +*58 D5 +s13 +*58 D7 +*58 l +s13 +*58 x91 +-5 -9 +-5 '9 +*59 ++5 '9 +*59 D2 +*59 x25 +*59 x42 +*5A +*5A x43 +*5A x73 +-5 c ++5 c +-5 C +'5 C +-5 D1 ++5 D1 +-5 D2 +-5 D3 +sw7 +-5 D4 ++5 D4 +-5 D6 ++5 D6 +'5 f ++5 f +-5 k ++5 k +-5 K +-5 K *50 +-5 l +'5 l ++5 l ++5 L0 +-5 L0 +1 +-5 l *25 ++5 L6 +-5 r [ ++5 r +-5 R3 +3 +-5 R5 +1 +s1i +s1N +s36 +s6a +s6b +s6v +saj +sb- +sgm +shH +sjy +sL7 +slr +smj +sro +suV +sw7 +-5 t +'5 t ++5 t +-5 T0 +-5 T2 +-5 T3 ++5 T3 ++5 T4 ++5 T7 +'5 u ++5 x01 +-5 x02 +-5 x03 ++5 x04 ++5 x62 +-5 x63 ++5 x63 +-5 x81 +-6 +-6 [ +-6 ] +-6 } +'6 +{ +6 +} -6 +} '6 ++6 ++6 [ ++6 { +-6 +0 +] *60 +*60 *01 +*60 *02 +*60 *07 +*60 -1 ++6 *02 +'6 *03 +*60 -5 +*60 *54 +*60 -8 +*60 c +*60 C +*60 D0 +*60 D1 +*60 } D4 +*60 D4 } +*60 D6 +*60 k T5 +*60 T5 k +*60 u x34 +*60 x03 +*60 x34 u +*60 x35 +-6 -0 x51 +*60 x51 +*60 x54 +-6 -1 +'6 -1 +'6 +1 +} *61 ++6 +1 +*61 -1 +*61 *21 +*61 -4 ++6 *14 +*61 '5 +*61 D6 +*61 T0 +*61 T2 +*61 x03 k +*61 x12 +*61 x21 +*61 x21 *13 +*61 x52 +*61 x54 +*61 x61 +6 +*62 } +-6 *20 +*62 -0 +'6 *21 +'6 *21 u +*62 -2 +*62 +2 +'6 *23 +*62 *46 +-6 *24 D0 +*62 *52 +*62 *73 +*62 D4 x43 +*62 l +*62 x01 r +*62 x02 +*62 x03 +*62 x32 +*62 x35 +*62 x38 +*62 x43 D4 +*62 x45 +*62 x46 +*62 x51 +-6 -3 +'6 +3 +} *63 +*63 } +*63 +1 ++6 *32 ++6 *32 l +*63 '5 +*63 +7 +*63 D3 +*63 D7 +*63 K +*63 L6 +slp +*63 T2 +*63 u +*63 x53 +'6 +4 +*64 ++6 +4 +*64 *20 ++6 *43 +*64 *34 +*64 '5 +*64 *65 [ +'6 +4 c +*64 D0 +1 +*64 K +*64 R3 +*64 x53 +*64 x61 +*64 x72 +*64 x73 +'6 +5 +{ *65 +*65 +*65 [ { +*65 { [ ++6 -5 +'6 *51 +*65 *24 +*65 +3 +*65 *35 +'6 *54 +*65 -5 +*65 *51 ++6 *57 +*65 *73 +*65 *8A +*65 '9 +*65 D3 +*65 R3 +*65 R7 +*65 x42 +*65 x65 +-6 -6 ++6 +6 ++6 *60 +-6 *63 ++6 *63 +-6 -6 -6 ++6 +6 +6 +-6 -6 -6 -6 ++6 +6 +6 +6 +-6 *67 +-6 -7 +[ *67 +*67 ++6 -7 ++6 +7 +-6 *70 ++6 *73 +*67 -5 +*67 *56 +scu +-6 -7 -8 ++6 +7 +8 +*67 *86 +-6 -7 -8 -9 ++6 +7 +8 +9 +-6 -7 -9 -8 ++6 +7 +9 +8 +*67 c +*67 D9 +*67 k +*67 K +*67 r +*67 R4 +s- +s1/ +scu +*67 T3 +-6 -8 +*68 +*68 *20 ++6 *83 D3 +-6 *85 +*68 -6 +-6 -8 -7 ++6 +8 +7 +-6 -8 -7 -9 ++6 +8 +7 +9 +*68 *85 +-6 -8 -9 -7 ++6 +8 +9 +7 +*68 D6 +*68 x42 +*68 { x52 +*68 x52 { +-6 -9 +*69 ++6 '9 +-6 -9 -7 -8 ++6 +9 +7 +8 +*69 '8 +-6 -9 -8 -7 ++6 +9 +8 +7 +*69 'A +*69 x92 +*6A ++6 'A +*6A D7 +s70 +*6A x34 +s70 +*6A x37 +*6A x94 +*6B x16 +*6B x84 +-6 c +'6 c ++6 c +-6 C +'6 c +4 +-6 D0 +'6 D1 +'6 D2 ++6 D2 ++6 D3 +-6 D4 ++6 D4 ++6 D7 ++6 D8 +'6 f x45 ++6 k +-6 l +'6 l ++6 l +-6 L0 +'6 L0 +'6 L1 +s61 +-6 L2 +'6 L2 +-6 L2 u ++6 l *32 +-6 L5 +'6 L5 +-6 L6 +'6 R2 ++6 R2 +sob +-6 R4 +'6 R5 ++6 R5 +-6 R6 +-6 R6 u +s0g +s3* +s57 +s61 +s87 +sfs +shb +sjv +skP +slg +slK +snG +sob +sqg ++6 t +-6 T0 +'6 T1 +-6 T1 x51 +-6 T2 +-6 T7 ++6 T7 +-6 u +'6 u ++6 u +'6 u *21 +-6 u L2 ++6 x02 +-6 x03 +-6 x04 ++6 x12 +-6 x13 ++6 [ x21 ++6 x21 [ +-6 x23 +-6 x31 ++6 x31 ++6 x32 ++6 x42 +-6 x51 ++6 x51 +-6 x51 -0 +-6 x51 T1 +-6 x72 ++6 x73 +-6 ] x74 +-6 x74 ] ++6 x74 +-6 x82 +-6 x84 +-7 +-7 [ +-7 } +'7 [ +'7 } +[ +7 +{ -7 +{ +7 +} '7 ++7 ++7 [ ++7 } +[ *70 +*70 +*70 ] ++7 +0 +*70 *05 ++7 +0 *13 +'7 +0 +4 +*70 *41 x52 +*70 '5 +-7 *06 +-7 *06 u +*70 +9 +*70 L0 +*70 L0 T1 +*70 t +*70 T1 L0 +*70 x01 +*70 x52 +*70 x52 *41 +*70 x53 +*70 x64 +*70 x72 +*71 [ ++7 -1 +*71 *29 ++7 *13 +0 +'7 ] *14 +'7 *14 ] +*71 '5 +*71 *62 +*71 -7 +*71 '7 +*71 '9 +*71 c +*71 D2 r +*71 D6 +*71 l +*71 x02 +*71 x03 +*71 x04 +*71 x13 +*71 x35 +} [ *72 +*72 +*72 -2 +*72 +3 +*72 -4 +*72 '6 +*72 D7 +s32 +*72 x03 +*72 x23 +*72 x53 +*72 x73 +-7 -3 +'7 -3 +*73 +*73 [ +'7 *30 +*73 *02 +'7 *32 +*73 *72 +*73 *72 C +*73 C +*73 C *72 +*73 D1 +*73 L5 +*73 r +*73 x21 +*73 x31 +*73 x62 +*73 x74 +'7 -4 +*74 +'7 +4 +0 +'7 [ *41 +-7 *43 +*74 -3 +*74 +4 +*74 +5 +'7 *46 +*74 c +*74 D1 +*74 k +*74 L5 +s6a +sds +*74 u +*74 u x72 +*74 x31 +*74 x34 +*74 x54 +*74 x56 +*74 x64 +*74 x65 +*74 x72 +*74 x72 u +'7 -5 +'7 +5 +*75 +*75 [ +*75 { ++7 -5 ++7 *56 [ ++7 *57 +*75 *76 u ++7 *59 +'7 -5 C +*75 D1 +} *75 D3 +*75 D4 +*75 D5 +*75 D6 +sGw +*75 t +*75 T5 +*75 u +*75 u *76 +sGw +'7 -6 +'7 +6 +*76 +*76 ] +*76 } ++7 -6 +*76 *31 +*76 *34 +*76 *36 +*76 *47 +-7 *65 +*76 +5 +*76 +6 +*76 +6 x42 +*76 +7 +-7 *68 +*76 '8 +*76 +8 +*76 -9 +*76 'B +*76 l +s17 +s51 +scv +sti +sti +*76 x21 +*76 x41 +*76 x42 +*76 x52 +*76 x74 +-7 -7 ++7 +7 +-7 *70 +-7 *73 +{ +7 *75 +-7 *76 +-7 -7 -7 ++7 +7 +7 +-7 -7 -7 -7 ++7 +7 +7 +7 +-7 -8 +*78 ++7 -8 ++7 +8 +*78 *31 +*78 -6 +*78 +6 +*78 *62 +-7 *87 +-7 -8 -9 +*78 '9 ++7 +8 +9 +*78 -A +s71 +*78 T3 +*78 x31 ++7 -8 x31 +*78 x53 +*78 x62 +-7 -9 +-7 +9 +*79 ++7 '9 +-7 -9 -8 +*79 -8 ++7 *98 ++7 +9 +8 +*79 -9 +*79 *95 +*79 'A +*79 x81 +-7 'A +*7A 'A +*7A x53 +*7A x62 +*7A x92 +*7B x64 +-7 c +'7 c ++7 c +'7 C +'7 C -5 +'7 c x01 +'7 D1 ++7 D1 ++7 D2 ++7 D2 x32 +'7 D3 +-7 D4 +'7 D4 +-7 D6 +-7 D8 +'7 f +-7 l +'7 l ++7 l ++7 L3 +sul ++7 r ++7 r [ +-7 R2 ++7 R3 +'7 R5 ++7 R5 +s0s ++7 R6 +s0s +s1- +s6/ +s7r +sat +saZ +scu +sdr +sfb +sfn +skD +srv +sul +sul +-7 t +'7 T1 ++7 T2 +'7 T3 +-7 T4 ++7 T4 +[ +7 T8 +-7 u +'7 u ++7 u +-7 u *06 +'7 x01 +'7 x01 c +-7 x03 ++7 x12 ++7 x24 ++7 x31 ++7 x32 D2 ++7 x41 ++7 x43 +'7 x51 ++7 x82 +-7 x94 ++7 xA2 +-8 +-8 [ +'8 +'8 [ +[ -8 +{ -8 ++8 ++8 } +-8 -0 +'8 +0 +} } *80 +'8 *02 +*80 *38 +*80 '6 +*80 D3 +*80 l x45 +*80 x04 +*80 x31 +*80 x36 +*80 x45 l +'8 *12 +*81 '7 +*81 '7 t +*81 '8 +*81 +8 +*81 t '7 +-8 +2 +'8 -2 ++8 -2 +*82 '6 +*82 'A +*82 l +*82 T5 ++8 -2 u +*82 x06 +*82 x45 +*82 x72 +*82 x81 +-8 -3 +'8 +3 +[ *83 +*83 +'8 *32 +*83 +3 +*83 *47 +*83 -7 +*83 D5 +s2+ +*83 x04 +*83 x54 +*83 x64 +*83 x64 -7 +*83 x81 +-8 +4 +*84 '6 +*84 '7 +*84 D8 +*84 u +*84 x23 +*84 x53 +'8 -5 +*85 +*85 +1 D8 +-8 *53 +*85 +5 +'8 *56 +*85 '6 +*85 *63 +-8 *57 +*85 '7 +*85 D8 +*85 D8 +1 +*85 x23 +*85 x42 +-8 +6 +'8 -6 +'8 +6 +*86 ++8 -6 ++8 +6 +*86 *50 +*86 +7 +*86 *70 +*86 -8 +*86 '8 +*86 *95 ++8 +6 C +*86 D3 +*86 R0 +sr5 +*86 x14 +*86 x34 +*86 x41 +*87 +*87 { ++8 -7 +'8 *70 ++8 *70 +*87 *36 +'8 *74 +-8 *76 +*87 +8 ++8 *78 +-8 *7B x62 +*87 c +s81 +sti +*87 { t +*87 t { +*87 t x26 +*87 x26 t +-8 -8 ++8 +8 +-8 -8 -8 ++8 +8 +8 +-8 -8 -8 -8 ++8 +8 +8 +8 +-8 *8A +-8 -9 +-8 +9 +*89 +*89 [ +*89 } ++8 -9 ++8 '9 ++8 +9 +*89 -7 +*89 +8 ++8 *9A +*89 D5 +*89 x63 x04 +*8A ++8 +A +*8A D8 +*8A t +*8A x54 +-8 *BA +] *8B x63 +*8B x64 +-8 c +'8 c ++8 c ++8 C +6 +-8 c x12 +-8 c x25 ++8 D0 ++8 D4 +'8 D4 +6 +-8 D6 +-8 D7 +-8 DA +'8 K +-8 l +-8 [ l +-8 l [ ++8 l +'8 L3 +'8 [ L6 +'8 R2 ++8 R6 +-8 R7 ++8 R7 +siz +sjs +spi +-8 t +'8 t ++8 T2 +-8 T4 ++8 T6 +'8 T7 +[ '8 T7 +'8 u ++8 u ++8 u -2 +-8 x04 +-8 x12 +-8 x12 c +-8 x16 ++8 x17 ++8 x21 +-8 x24 +-8 x25 +-8 x25 c ++8 x26 ++8 x42 +'8 x51 ++8 x52 +'8 x61 +-8 x62 +-8 x91 +-9 +-9 } +'9 +'9 [ +[ +9 +{ +9 +} -9 } +} '9 ++9 ++9 [ +-9 -0 +-9 +0 +'9 +0 ++9 +0 +-9 *01 +'9 *08 +*90 L0 +*90 x26 +} *91 +*91 +*91 *37 +*91 R8 +*91 x43 +'9 -2 +'9 +2 +*92 '9 +*92 x46 +*92 x56 +'9 -3 +*93 *43 +*93 '9 +*93 c +*93 x21 +-9 +3 x71 +-9 -4 +'9 -4 +*94 '8 +*94 u +*94 x34 +*94 x61 +*94 x73 +*95 *27 +'9 *53 +-9 *56 +*95 *87 +*95 D8 +*95 D8 u +*95 u D8 +'9 -6 +*96 ++9 +6 +*96 D7 +*96 x02 +*96 x15 +*96 x52 +*97 ++9 +7 +*97 *21 +*97 *32 x34 +'9 *76 +*97 c +*97 D1 +syi +*97 u +*97 x34 +*97 x36 +*98 +0 +*98 +1 +'9 *83 +*98 *32 D9 +'9 *85 +*98 *85 +*98 '9 +*98 +9 +*98 D9 +*98 D9 *32 +*98 x71 +-9 -9 ++9 +9 +-9 *98 +-9 -9 -9 ++9 +9 +9 +-9 -9 -9 -9 ++9 +9 +9 +9 +*9A ++9 'A +*9A *89 +s1c +*9A x73 +*9B *68 +*9B x62 ++9 c +-9 D0 ++9 D0 +6 +-9 D1 +'9 D2 ++9 D3 +'9 D4 ++9 D5 +-9 D7 ++9 D7 ++9 D8 +-9 l +'9 l ++9 l +-9 L9 +'9 r ++9 R1 +-9 R8 +'9 R8 +s09 +s25 +s91 +s9e +sah +se2 +sfg +[ -9 T0 +-9 T2 +'9 T2 +'9 T5 +'9 T8 +-9 t x02 +-9 u +'9 u ++9 u +-9 u x24 +-9 x02 t ++9 x15 ++9 x23 +-9 x24 +-9 x24 u +-9 x25 ++9 x34 +-9 x41 +'9 x43 ++9 x45 +-9 x53 +'9 x61 +-9 x71 +3 ++9 x72 +-9 xA2 +-A +[ -A +[ 'A +} 'A ++A +*A0 { +*A0 *5A x46 +*A0 x56 +'A +1 +*A1 D1 +*A1 RA +*A1 x16 +*A1 x83 +'A +2 +*A2 +*A2 '8 +'A +3 +*A3 '7 +*A3 x75 +*A3 x93 +*A4 -4 'A +*A4 '9 +*A4 'A -4 +*A4 x86 +*A5 '8 +*A5 *B7 '8 +*A5 x76 +*A6 '7 +*A6 *A9 +*A6 x26 +*A6 x42 +'A +7 +*A7 +*A7 [ +*A7 '8 +*A8 [ ++A *8A +*A8 l +s0x +*A8 x35 +-A +9 +*A9 ++A *9A +s05 +*A9 x52 +-A *A8 +*AB +'A c +'A C +'A D1 +'A D3 ++A D4 +'A D7 ++A DB +'A l +'A L1 +-A LA +'A R9 ++A R9 +s97 ++A t +'A T5 +'A T8 +-A x01 +-A x35 +'A x41 +'A x42 ++A x45 +'A x52 ++A x53 +'A x61 ++A x62 +'A x62 +6 ++A x82 ++A x91 +-B ++B [ +*B0 '8 +*B1 x03 +*B1 x45 +'B *23 +*B2 '7 +*B2 '8 +*B5 '8 +*B5 xA1 +*B7 '8 +*B8 'A +*B8 *A9 +-B -9 +*B9 +'B *97 +*B9 'A ++B *9A +s29 +*B9 x65 +-B -A +-B +A +'B +A +*BA +*BA c +*BA D9 +*BA x02 +*BA x83 +'B D3 +'B D4 +-B D7 +'B k +-B l +'B l ++B l +'B r +'B [ R1 +soa ++B x02 ++B x62 +'B x71 +'B x72 ++B x73 ++B x92 +c +[ [ c +[ [ [ c +[ [ [ [ c +] c +] ] c +] c ] +{ c +{ { c +{ { { c +{ { { { c +{ { { c { +{ { c { +{ { c { { +{ c { +{ c { { +{ c { { { +} c +} } c +} } } c +} } } } c +} } } c } +} } c } +} } c } } +} c } +} c } } +} c } } } +c { +c } +C +[ C +{ C +{ [ C +{ C [ +C [ +C } +c +0 +c *05 +c +0 *52 +c *05 D7 +C -1 +C +1 +c *14 K +c *16 +} c -2 +c -2 +C -2 +C +2 +c *21 +c *24 +c *24 T0 +c *25 x13 +c -3 +[ c *31 +c -3 '6 +{ c -4 +C -4 +c *40 t +c *46 +c -4 T4 +c +4 T5 +C -5 +C +5 +{ C *51 +C *52 +c *52 +0 +c *58 +C [ +6 +C +6 +C *61 +c '6 -3 +c *65 +c *68 +C -7 +C +7 +c *70 +C *71 +c +7 L3 +C '8 +c *83 +c *8A +c -9 +c '9 +C -9 +c '9 T3 +C +9 x34 +c +A +c -B +c +B T6 +C D0 +c D0 D8 +s5P +c D1 +C D3 +c D4 +C D4 +c D5 +C D5 +] c D6 +c D6 +C D6 +c D7 +c D7 *05 +c DA +c f +C f +c K +c [ K +c K [ +C k +c K *14 +c k *16 +c L0 +c L3 +7 +c L5 +C L5 +c r +C r +C R0 +C R1 +c R2 +[ c R3 +c R3 +C R5 +c R6 +c R7 +s0# +s04 +s0I +s1, +s10 +s2* +s29 +s2I +s2m +s49 +s4b +s4K +s5P +s62 +s8! +s87 +s8M +sa@ +sa4 +sa4 +sa@ +se3 +se3 +sa@ +sa@ +si1 +si1 +si! +sa@ +sa@ +so0 +so0 +so0 +sa@ +sa@ +sa@ +saw +sb1 +sb8 +sd1 +se3 +se3 +sa@ +sa@ +se3 +se3 +si1 +si1 +si! +se3 +se3 +so0 +so0 +so0 +se3 +se3 +se3 +se3 +se3 +se3 +se3 +se3 +seu +sex +sg5 +sg9 +sgV +si! +si1 +si1 +sa@ +sa@ +si1 +se3 +se3 +si1 +so0 +so0 +si1 +si1 +si1 +si1 +si1 +si1 +si1 +si2 +si! +sa@ +si! +se3 +si! +si! +so0 +so0 +si! +si! +si! +si! +si! +si! +siu +siU +siZ +sJw +sl1 +sl1 +sl1 +sl1 +sl1 +sl1 +sl1 +sl1 +sl1 +sl1 +sl1 +slt +slz +sMz +snj +so* +so0 +so0 +so0 +sa@ +sa@ +sa@ +so0 +se3 +se3 +se3 +so0 +so0 +si1 +si1 +si! +si! +so0 +so0 +so0 +so0 +so0 +so0 +so0 +so0 +spv +ss3 +ss4 +ss5 +ss5 +ss- +ssN +ssO +st7 +stC +sti +su[ +su4 +svz +sy1 +sy2 +c T0 *24 +c T1 +C T1 +c T1 f +se3 +si! +si1 +sl1 +so0 +c T1 T2 +c T1 T2 T3 +c T1 T2 T4 T5 +c T1 T2 T5 T4 +c T1 T3 +c T1 T3 T2 +c T1 T4 T2 T5 +c T1 T4 T5 T2 +c T1 T5 T2 T4 +c T1 T5 T4 T2 +c T2 +c T2 f +se3 +si! +si1 +sl1 +so0 +c T2 T1 +c T2 T1 T3 +c T2 T1 T4 T5 +c T2 T1 T5 T4 +c T2 T3 +c T2 T3 T1 +c T2 T4 +c T2 T4 T1 T5 +c T2 T4 T5 +c T2 T4 T5 T1 +c T2 T4 T6 +c T2 T4 T6 T8 +c T2 T4 T8 T6 +c T2 T5 T1 T4 +c T2 T5 T4 +c T2 T5 T4 T1 +c T2 T6 +c T2 T6 T4 +c T2 T6 T4 T8 +c T2 T6 T8 T4 +c T2 T8 T4 T6 +c T2 T8 T6 T4 +C T3 +c T3 '9 +se3 +si! +si1 +sJw +so0 +C T3 t +c T3 T1 +c T3 T1 T2 +c T3 T2 +c T3 T2 T1 +C T3 x42 +c T4 +C T4 +c t *40 +c T4 -4 +sa@ +se3 +si! +si1 +sl1 +so0 +c T4 T1 T2 T5 +c T4 T1 T5 T2 +c T4 T2 +c T4 T2 T1 T5 +c T4 T2 T5 +c T4 T2 T5 T1 +c T4 T2 T6 +c T4 T2 T6 T8 +c T4 T2 T8 T6 +c T4 T5 T1 T2 +c T4 T5 T2 +c T4 T5 T2 T1 +c T4 T6 T2 +c T4 T6 T2 T8 +c T4 T6 T8 T2 +c T4 T8 T2 T6 +c T4 T8 T6 T2 +c T5 +C T5 +c T5 +4 +sa@ +se3 +si! +si1 +sl1 +so0 +c T5 T1 T2 T4 +c T5 T1 T4 T2 +c T5 T2 T1 T4 +c T5 T2 T4 +c T5 T2 T4 T1 +c T5 T4 T1 T2 +c T5 T4 T2 +c T5 T4 T2 T1 +] c T6 +c T6 +c T6 +B +sa@ +se3 +si! +si1 +sl1 +so0 +c T6 T2 +c T6 T2 T4 +c T6 T2 T4 T8 +c T6 T2 T8 T4 +c T6 T4 T2 +c T6 T4 T2 T8 +c T6 T4 T8 T2 +c T6 T8 T2 T4 +c T6 T8 T4 T2 +C T7 +se3 +si1 +sl1 +so0 +c T8 +c T8 T2 T4 T6 +c T8 T2 T6 T4 +c T8 T4 T2 T6 +c T8 T4 T6 T2 +c T8 T6 T2 T4 +c T8 T6 T4 T2 +c T9 +C T9 +c TA +C TA +c TB +C t T3 +c x01 +C x01 +C x01 -5 +c x02 +C x02 +c x02 L1 +c x03 +c x12 +C x12 +C x14 +C x15 +{ c x21 +c x23 +c x24 +c x25 +c x26 +C x31 [ +c x32 +C x32 +c x35 +c x36 +C x42 +C x42 T3 +c x43 +C x43 +s87 +c x52 +C x52 +c x53 +C x53 +c x54 +c x64 +c x65 +C x73 +[ c x74 +c x84 +c x92 +C x92 +c xA2 +D0 +D0 } +D0 *01 } +D0 +1 +D0 *14 +D0 *30 +D0 *30 t +D0 } *54 +D0 'B +D0 c +D0 c x91 +D0 D1 +D0 D1 k +D0 D8 +D0 k t +D0 l +D0 L4 D1 +D0 R2 t +D0 R4 +s30 +s4C +s7b +s9A +se6 +seh +sfw +skv +smd +spe +D0 t *30 +D0 t k +] D0 u +D0 x13 +D0 x14 +D0 x43 +D0 x71 +D0 x91 +D0 x91 c +D1 +[ D1 +[ { D1 +[ D1 { +D1 +0 +D1 *12 +D1 *13 +D1 *21 +D1 -3 +D1 +3 +D1 -3 '6 +D1 +5 +D1 '6 -3 +D1 '8 +D1 'A +D1 C +D1 D2 +D1 D4 +D1 D5 +] D1 f +D1 f +D1 k +D1 K +D1 l +D1 L0 +D1 L1 +D1 L3 +D1 r { +D1 R0 +sD4 +sds +si6 +sih +sn_ +{ D1 t +D1 t +D1 T1 +D1 T2 +D1 T3 +D1 u +sD4 +D1 x35 +D1 x72 +D1 x91 +D2 +D2 { +D2 } +D2 *02 +D2 -1 +D2 -1 x34 +D2 +3 +D2 *46 +D2 -5 +D2 *53 +D2 -6 +D2 '6 +D2 '7 +D2 'A +D2 c +D2 C +D2 c L3 +D2 D3 +D2 l +D2 L2 +D2 L3 +D2 L3 c +D2 r +D2 R1 +D2 R4 +D2 R6 +s1` +s3- +sdy +sdy +sgc +sgy +sm1 +snq +soV +suq +D2 t +D2 T0 +D2 T3 +D2 T4 +D2 T5 +D2 T6 +sdy +D2 u +D2 x34 -1 +D2 x41 +D2 x45 +D2 x53 +D2 x73 +D3 +{ D3 +D3 [ +D3 ] +D3 } +D3 -1 +D3 +1 +D3 -1 c +D3 *34 +D3 *42 +D3 -5 +D3 *51 +D3 *56 +D3 *56 k +D3 -6 +D3 +6 +D3 +6 } +D3 '7 +D3 -8 +D3 +A x64 +D3 'B +D3 c +D3 c -1 +sAl +D3 C x32 +D3 D8 +D3 k +D3 k *56 +D3 l +[ D3 R0 +] D3 R0 +D3 R0 +D3 R2 +D3 R3 +s3 +s85 +s9D +sao +sbz +sck +sea +slW +D3 t +D3 T3 +D3 T5 +D3 x32 +D3 x32 C +D3 x47 +D3 x54 +D3 x65 +D3 x71 +D3 x72 +D3 x75 +D3 xA1 D4 +D3 xA1 x54 +D4 +[ D4 +} D4 +D4 { +D4 } +D4 *14 +D4 *15 +D4 *21 T4 +D4 *25 +D4 +3 +D4 '5 +D4 +6 +D4 *62 +D4 -7 +D4 '7 +D4 *85 +D4 *87 +D4 *87 l +D4 { D2 +D4 D2 { +D4 D7 +D4 f +D4 k +D4 K +D4 k L7 +sah +D4 l +D4 L5 +D4 L6 +D4 L7 +D4 L7 k +D4 l *87 +D4 r +D4 R3 +s3v +sah +sah +sg +sg- +sj# +skD +ssx +stG +sxp +D4 t +D4 { t +D4 t { +D4 T0 +D4 T1 +D4 T4 +D4 T4 *21 +D4 T5 +D4 T6 +D4 x02 +D4 { x21 +D5 +] D5 +D5 } +D5 -1 +D5 +1 +sa4 +D5 *24 +D5 +2 K +D5 *41 +D5 *43 K +D5 *45 +D5 *46 +D5 *57 +D5 +6 +D5 *65 +D5 -8 +D5 } D2 +D5 D2 } +D5 D7 +D5 DA +] D5 K +D5 K +D5 K +2 +D5 K *43 +D5 l +D5 L4 +D5 L8 +D5 R1 +D5 R4 +s0! +s13 +s2< +s63 +sa4 +sKp +spz +sr +sr +suw +D5 t +D5 T0 +D5 u +D5 x04 +D5 x12 +D5 x13 +D5 x54 +sr +D6 +] D6 +D6 [ +D6 { +D6 } +D6 *05 +D6 *15 +D6 +2 +D6 *24 +D6 +2 T6 +D6 +4 +D6 +6 +D6 +7 +D6 *9A +D6 +A +] D6 c +D6 l +D6 L1 +D6 L2 +D6 L5 +D6 L6 +D6 l x91 +D6 R1 +D6 R4 +D6 R6 +sKk +s2; +s82 +s9k +s9k +sbk +sby +seb +sGM +sKk +sKk +sn. +sng +D6 { T1 +D6 T6 +2 +D6 x03 +D6 x31 +D6 x41 +D6 x72 +D6 x91 +D6 x91 l +D6 x92 +D7 +D7 *05 +D7 *05 L2 +D7 *29 +D7 +6 [ +D7 *67 +D7 +A +D7 D0 +D7 D3 +D7 D6 +D7 D7 +D7 D8 +D7 D9 +D7 l +D7 L0 +D7 L2 +D7 L2 *05 +D7 l xA3 +D7 R5 +se2 +skt +D7 t +D7 T5 +D7 u +D7 x12 +D7 x23 +se2 +D7 xA3 l +D8 +} D8 +D8 *02 +D8 -4 +D8 *56 +D8 c +D8 k +D8 L0 +s3 +D8 u +D8 u x91 +D8 x23 +D8 x24 +D8 x91 +D8 x91 u +D9 +D9 *08 +D9 -5 +D9 +6 x81 +D9 -7 +D9 +7 +D9 -A +D9 'A +D9 c +D9 D4 +D9 D6 +D9 l +D9 L7 +D9 T3 +D9 x32 +D9 x81 +6 +DA +DA *86 +DA -A +DA -B +DA *BA +DA D4 +DA D5 +DA l +DA R7 x25 +DA x31 +DA x54 +DA x63 x21 +DB +A l +DB D0 +DB l +A +s68 +DB x13 +DB x62 +DB x64 +DB x71 +DB x91 +f +[ f +] f +{ f +} f +f [ { +f ] +f { +f { [ +f } +f +0 +f *06 x13 +f +0 x16 +f +0 x29 +f *15 +f *16 +f -2 +f +2 +f *20 +f *21 +f *24 +f ] *25 +f *25 ] +f -2 x3B +f *31 +f *36 +f *37 +f -4 +f +4 +f -5 +f +5 +f *50 +f ] *51 +f *51 ] +f *53 +f +6 +f *60 +f { *65 +f *67 +f *69 +f +6 x7A +f -7 +f '7 +f +7 +f *71 +f *75 +f *78 +f *79 +s!g +f '8 +f *86 +f '9 +f +9 +f *94 +f *98 x27 +f *9B +f '9 x61 +f 'A +f *A5 '8 +f 'B +f c +f C +f c D0 +f c x08 +f D0 +f D0 c +f D1 +f D1 x58 +f D2 +f D3 +f D4 +f D8 +f DB +f f +f k +f K +f K L3 +f k x3A +f K x69 +f l +f L2 +f L3 K +f R4 +f R6 +f R8 +s4k +s!g +sns +ss0 +sts +f t +f T0 +f T0 x34 +f T0 x5A +f T4 +f T4 D0 +f T6 x75 +f T9 x08 +f t x65 +f [ u +f u [ +f x02 +f x03 +f x04 +f x05 +f x06 +f x08 c +f x0A +f x13 +f x14 +f x15 +f x16 +f x16 +0 +f x17 +f x17 *0A +f x18 +f { x18 +f x18 { +f x18 *50 +f x19 +f ] x19 +f x19 ] +f x1A +f x1B +f x23 +f x24 +f x25 f +f x26 +f x27 +f x28 +f x29 +f x29 +0 +f x2A +f x2B +f x31 *31 +f x34 +f x34 T0 +f x36 *25 +f x37 +f x38 +f x39 +f x3A +f x3A k +f x3B -2 +f x42 +f x42 *40 +f x43 +f x45 +f x46 +f x47 +f x48 +f x48 D8 +f x49 +f x4A +f x4A L5 +f x4B +] f x51 +f x51 +f x52 +f x53 +f } x53 +f x53 } +f x54 +f x56 +f x57 +f x58 D1 +f x59 +f [ x59 +f x59 [ +f x5A +sts +f x5A T0 +f x5B +f x61 +f x63 +f x64 +f x64 L7 +f x65 +f x65 t +f x67 +f x68 +f x69 +f x69 K +f x6A +f x6B +f ] x72 +f x72 ] +f x73 +f x74 +f x75 +f [ x75 +f x75 [ +f x75 T6 +f x76 +f x78 +f x7A +f x7A +6 +f x7B +f x82 +f x83 +f x85 +f x86 +f x8A +f x8B +f x91 +f x93 +f x96 +f xA5 +f xA8 +f xB4 +k +] k +{ k +} k +k { +k } +K +{ K +} K +K [ +K [ } +K { +K } +K } } +k -0 +k +0 +k *04 +k *04 l +k *07 +K +1 +k *13 +K *13 +k -1 *34 +k *13 -5 +K *14 +[ K *15 +K +1 *50 +k *15 K +K *16 +k *17 +k +1 f +} k -2 +k +2 +K +2 +k *20 +K *25 +K *25 *30 +sir +k +3 +K *30 +K *30 *25 +K *32 +k *34 -1 +{ K *35 +K *35 +k -3 t +k +4 +K *43 +K *43 -5 +K *45 t +k *46 +K *46 r +k { *48 +k +5 +K -5 +K *50 +1 +k *51 +k -5 *13 +K *53 x41 +K *54 +K -5 *43 +k *54 R3 +k '6 +k *63 +k '6 l +k -7 [ +k '7 +k '8 +k '9 +k C +k { C +k C { +K c +k D4 +] K D5 +K f +k K +K k +k K *15 +k l *04 +k L2 +k l '6 +k r +k [ r +k } r +K r +k R1 x41 +k R2 +k R3 +K R3 +k R3 *54 +k R4 +K r *46 +[ k R5 +K R6 +k r R1 +K r u +s2c +s2o +s3H +s52 +s5b +s8k +saj +sbd +scr +sd; +sd3 +se6 +sej +ser +sex +sfp +sfp +sgd +si7 +sir +sjt +smO +snb +srJ +srp +ss2 +ssL +ssy +suz +sy6 +sy6 +szs +K t +K T0 +k t -3 +K t *45 +k T5 +} k u +k u +K u r +] K x01 +K x21 +k x23 +K x31 +K x32 +K x41 +k x41 R1 +K x42 +k x51 +k { x51 +k x51 { +k x62 +k x71 +sfp +l +l [ +l { +l } +l +0 +L0 +} L0 +L0 { +L0 } +L0 +1 +L0 *13 +L0 *13 T1 +l { *02 +L0 +2 +l *03 +L0 +4 +L0 -5 +L0 -7 +L0 '7 +L0 '7 l +L0 c +L0 l +L0 L0 +L0 L1 +L0 L5 +L0 l '7 +s67 +s6u +sdu +L0 T0 +s67 +L0 T2 +L0 u +L0 u x14 +L0 x12 +L0 x14 u +L0 x26 +L0 x31 +L0 x41 +L0 x51 +L0 x53 +l } -1 +l +1 [ +L1 +[ L1 +{ L1 +L1 [ +L1 { +L1 -0 +L1 +1 +L1 *15 +L1 -4 +L1 +4 +l { *15 +L1 '6 +L1 *62 +sw@ +L1 -7 +L1 +7 +L1 *79 D6 +L1 D5 +L1 R0 +L1 R5 +s61 +sa3 +sw@ +L1 t +L1 T0 +L1 T3 +L1 x23 +L1 x26 +L1 x35 +L1 x41 +L1 x42 +L1 x52 +l +2 +L2 +L2 } +l *20 +L2 *05 +L2 -1 +L2 *20 +L2 *3A x34 +l *23 L4 +spe +l *24 -5 +L2 +5 +L2 D0 +L2 D3 +L2 L3 +L2 L4 +L2 L7 D0 +L2 r +L2 R4 +s6u +s?9 +s9d +sd_ +sds +L2 t +L2 T1 +L2 u +L2 x02 +L2 x34 +L2 x62 +l +3 +L3 +L3 { +L3 } +L3 +0 +l *30 +4 +L3 +0 '6 +l *30 *64 +L3 +1 +L3 +3 +L3 *32 +L3 *35 +L3 -4 +L3 +4 +l *35 +L3 -5 +L3 +5 +l +3 -6 +L3 -6 +L3 '6 +L3 '6 +0 +l *37 +l +3 '7 +L3 D0 +L3 l +L3 L1 +sb` +slk +slk +L3 T0 +slk +L3 x02 +L3 x52 +L3 x62 +l +4 +L4 +l *41 +L4 *14 +L4 -3 +l +4 *30 +L4 -5 +L4 '5 +L4 -5 T1 +L4 -6 +L4 '6 +L4 D0 +L4 D2 t +L4 D3 +L4 l +L4 L4 +L4 r +L4 R0 +L4 T1 -5 +L4 t D2 +L4 u +L5 +L5 { +L5 *02 +L5 -2 +L5 *21 +L5 ] *23 +L5 *23 ] +l -5 *24 +L5 { *30 +L5 *34 +L5 -5 +L5 '7 +L5 c +L5 D0 +L5 l +L5 L5 +L5 R1 +seo +sgw +st6 +L5 t +L5 u +L5 x32 +L5 x62 +L6 +[ L6 +{ L6 +} L6 +L6 [ +l -6 +3 +l *63 +l *63 K +L6 +4 +l *64 *30 +L6 ] *50 +L6 *50 ] +L6 +6 +L6 +6 '9 +l *68 +L6 '9 +L6 '9 +6 +L6 c +L6 D3 +L6 D5 +{ L6 l +L6 L7 +L6 r +L6 R3 +s61 +sm2 +L6 t +L6 x71 +L6 x72 +L6 x74 +L7 +} L7 +L7 } +L7 *03 +L7 *03 *38 +L7 -1 +L7 *13 +L7 *17 +l '7 +3 +L7 *38 +l *74 +L7 *47 +l *75 +L7 *63 +L7 *67 +L7 '8 +l *79 +L7 c +L7 l +sj= +sj= +L7 t +L7 T1 t +L7 t T1 +L7 x15 +L7 x31 +L7 x52 +l '8 +L8 +{ L8 +L8 *08 +L8 *52 +l *86 +l *87 +L8 { *78 +L8 +8 +L8 '9 +s5s +L8 x21 +L8 x25 +L8 x31 +L8 x35 +L9 +L9 [ +l *92 +l *98 +L9 l +L9 x13 +L9 x24 +L9 x35 +L9 x42 +L9 x62 +L9 x72 +l -A +l +A +LA +l +A x45 +LA x82 +l *B9 +LB x53 +l D3 L5 +l D4 +l D8 +sns +l f x1B +l f x46 +l K +l K *63 +l L1 +l L2 +l L4 *23 +{ l L6 +l r +l R0 +l R1 +l R3 +l R6 +l R8 +s0? +s0i +s12 +s1a +s5d +s6- +s6p +s70 +s8p +sdk +sgc +sgj +shj +slh +smb +smb +smK +sns +so1 +sp1 +spe +spe +sr[ +ss& +ssd +ssf +ssi +suy +swn +sys +sz2 +l T1 +l T2 +l T3 +l T4 +l T5 +l T6 +[ l T7 +l T7 +l T8 +l T9 +l x12 +l x13 +l x14 +l x15 +{ l x21 +l x23 +l x24 +l x26 +l x27 +l x32 +l x35 -5 +l x42 +l x43 +l x52 +l x53 +l x63 +l x72 +l x75 +l x83 +l x83 D9 +l x92 +r +[ r +[ r } +] r +] { r +} r +r [ +r { +r } +r -0 +r +0 +R0 +{ R0 +} R0 +R0 } +R0 *06 +R0 *08 +R0 *09 +R0 *17 +r *02 +r *02 K +R0 +3 +R0 *37 +R0 } *38 +R0 +3 t +r { *04 +r *05 +r *06 +R0 -6 +R0 '6 +r *07 +R0 -7 +R0 '7 +r *07 *64 +r *09 +r -0 c +R0 c x12 +[ R0 D3 +] R0 D3 +R0 D3 +R0 D5 +R0 D6 +R0 K +R0 k *16 +R0 l +R0 L3 +R0 r +R0 R2 +shd +R0 T1 +R0 T1 *17 +R0 t +3 +R0 x12 +R0 x12 c +R0 x13 +R0 x24 +R0 x35 +r -1 +R1 +[ R1 +R1 [ ] +R1 ] +R1 ] [ +R1 } +r *10 +R1 *03 +R1 *12 +r *12 +r *12 *60 +R1 -3 +r *14 +R1 +4 +r *14 D5 +r *15 +R1 -5 +r *15 L2 +r -1 *61 +R1 -7 +R1 '7 +R1 c +R1 C +R1 D0 +R1 D3 +R1 D7 +R1 f +R1 } k +R1 r +R1 R0 { +R1 R2 +R1 R3 +R1 R4 +sed +sed +sfn +shc +R1 t +R1 x23 +R1 x35 +sed +R1 x41 +R1 x42 +R1 x53 +r -2 +R2 +{ R2 +} R2 +R2 } +[ r *20 +R2 +1 +R2 *24 +R2 +3 +R2 *61 +R2 *70 +R2 f +R2 l +R2 L1 +R2 L7 +R2 r +sob +R2 u +R2 x41 +R2 x51 +R2 x62 +R3 +{ R3 [ +R3 [ +r [ *30 +r *30 +R3 +1 +R3 +3 +R3 *30 +r *34 +r *34 [ +R3 -4 +R3 *40 +R3 *46 l +R3 -4 t +r *35 [ +R3 -5 +R3 +5 +r *36 +R3 '6 +R3 +6 +R3 -7 +[ R3 c +R3 D5 +R3 K +R3 l +R3 L0 +R3 L4 +R3 l *46 +s1j +shf +R3 t +R3 t -4 +R3 x21 +R3 x54 +R3 x73 +R4 +} R4 +R4 [ +R4 { +r *40 +r *40 t +R4 *30 +R4 +4 +R4 *41 +R4 *45 R4 +r ] *45 +r *45 +r *45 ] +R4 -5 +R4 -5 +6 +R4 *64 +R4 +6 -5 +R4 *87 [ +R4 D0 +R4 D1 +R4 D2 +R4 D2 T0 +R4 f +R4 r +r +4 R0 +R4 R2 +R4 R4 +R4 R5 +s20 +s9H +smD +ss) +R4 t +R4 T0 D2 +R4 T5 +R4 x02 +R4 x04 +R4 x21 +R4 x53 +R4 x63 +r '5 +r +5 +R5 +{ R5 +} r *50 +R5 +1 +r *52 +R5 +2 +r *53 +R5 *34 +r [ *54 +R5 -4 +R5 *53 +r *56 +R5 *64 +R5 *64 x63 +R5 *65 +R5 *65 K +R5 '8 +R5 '9 +R5 D4 +[ R5 k +R5 k +R5 K +R5 K *65 +R5 l +R5 R6 +s1n +s1z +s2* +s4a +sd4 +se7 +st3 +st9 +R5 u +R5 x31 +R5 x63 +r '6 +r +6 +R6 +R6 [ +r *60 *12 +R6 *07 -4 +R6 *23 +R6 -3 +R6 -4 +R6 +4 +r *64 *07 +R6 -4 *07 +R6 +5 +r *65 x32 +R6 *64 +R6 *64 C +R6 *65 +R6 -7 +r '6 C +R6 C +R6 C *64 +R6 D1 +R6 D2 +R6 D5 +R6 L2 +R6 r +R6 R0 +R6 R2 +R6 R7 +s2r +sc4 +ss8 +R6 t +R6 u +R6 x15 +R6 { x21 +R6 x21 { +R6 x24 +R6 x41 +R6 x75 +R7 +[ R7 +{ R7 +R7 [ +R7 *02 +R7 -0 x12 +R7 -5 +R7 *56 +R7 -6 +R7 *67 +R7 '8 +R7 +9 +R7 *97 +R7 [ D3 +R7 D3 [ +R7 l +sgs +syi +R7 t +R7 T4 +R7 x12 -0 +R7 x14 +R7 x34 +R7 x41 +R7 x43 +R7 x81 +r +8 +R8 +{ R8 +R8 -5 +R8 D1 +R8 R7 +R8 x04 +R8 x16 +R8 x31 +R8 x32 +R8 x34 +R9 +{ R9 +R9 [ +R9 *25 +R9 *36 +R9 -9 +R9 D7 +R9 u +R9 x02 +R9 x14 +R9 x24 +r 'A +RA [ +RA +1 x82 +RA x73 +RA x82 +1 +RB x02 +r c +r C +r c -0 +r C '6 +r c K +} r D0 +r D2 +r D3 +r { D3 +r D3 { +r D4 +r D5 +r D5 *14 +r D5 R8 +r D9 +r f +r } f +r f x3A +r K +r ] K +r K *02 +r K c +r K x51 +r L2 +r L2 *15 +r L5 +r L5 L5 +r L7 +r L9 +r R0 +r R0 +4 +r R2 +r R3 +r R4 } +r R4 C +r R6 +s0- +s0+ +s0l +s1 +sa3 +sb9 +sbq +sil +sJ7 +snw +so% +sra +stE +stk +stp +swg +r t +r T0 +r T1 +r t *40 +r T7 +r t x41 +] r u +r x04 +r x05 +r x15 +r x32 +r x37 +r x41 t +r x42 +r x51 +r x51 K +r x54 +r x62 +r x64 +s!- +s!. +s0- +s0` +s0< +s0 +s0_ +s0_ +s0- +s0, +s0! +s0? +s0/ +s0. +s0) +s0@ +s0* +s0\ +s0& +s0# +s0+ +s02 +s03 +s03 +s04 +s05 +s05 +s06 +s0- +s0* +s06 +s07 +s07 +s07 +s08 +s08 +s09 +s09 +s0a +s0a +s0A +s0a +s0b +s0B +s0c +s0+ +s0C +s0d +s0D +s0, +s0, +s0e +s0E +s0f +s0F +s0g +s0g +s0G +s0G +s0h +s0i +s0I +s0j +s0J +s0j +s0k +s0K +s0k +s0l +s0m +s0M +s0n +s0o +s0o +s0p +s0P +s0q +s0q +s0r +s0r +s0s +s0S +s0s +s0s +s0U +s0U +s0v +s0w +s0x +s0X +s0% +s0X +s0x +s0X +s0y +s0Y +s0z +s0Z +s1` +s1= +s1 +s1 +s1_ +s1- +s1, +s1; +s1! +s1? +s1/ +s1. +s1@ +s1& +s1# +s1+ +s10 +s12 +s13 +s13 +s14 +s15 +s15 +s15 +s16 +s16 +s17 +s18 +s1# +s18 +s18 +s18 +s18 +s19 +s19 +s1a +s1A +s1B +s1B +s1c +s1c +s1d +s1d +s1D +s1* +s1e +s1E +s1f +s1F +s1g +s1g +s1h +s1i +s1I +s1k +s1K +s1K +s1l +s1L +s1m +s1n +s1o +s1p +s1P +s1p +s1q +s1q +s1r +s1s +s1S +s1t +s1u +s1U +s1u +s1v +s1V +s1v +s1w +s1W +s1w +s1w +s1w +s1x +s1X +s1y +s1y +s1z +s2< +s2 +s2_ +s2- +s2, +s2; +s2? +s2. +s2@ +s2* +s2# +s2+ +s20 +s2@ +s21 +s21 +s21 +s23 +s24 +s25 +s26 +s26 +s27 +s27 +s28 +s28 +s29 +s2a +s2A +s2b +s2B +s2B +s2c +s2c +s2d +s2e +s2f +s2F +s2g +s2G +s2G +s2G +s2G +s2h +s2H +s2i +s2I +s2K +s2k +s2l +s2L +s2l +s2m +s2o +s2o +s2p +s2Q +s2r +s2* +s2s +s2t +s2T +s2u +s2v +s2v +s2w +s2W +s2x +s2* +s2y +s2y +s2y +s2y +s2z +s2Z +s2z +s3` +s3 +s3_ +s3, +s3! +s3. +s3" +s3[ +s3} +s3@ +s3* +s3# +s3% +s30 +s30 +s31 +s32 +s3+ +s34 +s34 +s35 +s36 +s36 +s37 +s37 +s38 +s38 +s38 +s38 +s38 +s39 +s3a +s3A +s3A +s3b +s3c +s3d +s3e +s3E +s3e +s3f +s3f +s3G +s3g +s3h +s3H +s3i +s3j +s3k +s3K +s3l +s3L +s3l +s3m +s3o +s3q +s3r +s3s +s3S +s3t +s3t +s3u +s3. +s3V +s3v +s3W +s3x +s3X +s3y +s3Y +s3z +s4 +s4_ +s4- +s4, +s4. +s4] +s4* +s4\ +s4& +s4+ +s40 +s41 +s42 +s43 +s45 +s45 +s46 +s47 +s48 +s49 +s49 +s4a +s4A +s4b +s4+ +s4C +s4d +s4d +s4e +s4f +s4F +s4G +s4i +s4I +s4j +s4j +s4k +s4K +s4k +s4n +s4o +s4O +s4p +s4q +s4r +s4R +s4r +s4s +s4s +s4t +s4T +s4* +s4w +s4w +s4x +s4X +s4_ +s4y +s4y +s4z +s!5 +s5= +s5 +s5_ +s5, +s5! +s5/ +s5* +s50 +s51 +s52 +s52 +s53 +s5% +s53 +s54 +s54 +s5% +s56 +s57 +s58 +s58 +s59 +s5a +s5b +s5b +s5c +s5d +s5e +s5e +s5f +s5g +s5G +s5g +s5i +s5k +s5m +s5n +s5o +s5o +s5s +s5t +s5w +s5x +s5. +s5% +s5= +s5Z +s6- +s6! +s6! +s6/ +s6. +s6@ +s6* +s6& +s6% +s60 +s61 +s62 +s63 +s63 +s64 +s64 +s65 +s67 +s68 +s68 +s69 +s69 +s6a +s6b +s6B +s6b +s6c +s6c +s6d +s6e +s6f +s6g +s6J +s6k +s6K +s6K +s6% +s6m +s6n +s6o +s6p +s6q +s6r +s6S +s6S +s6t +s6V +s6w +s6W +s6X +s6y +s6z +s6z +s7+ +s!7 +s7= +s7 +s7- +s7/ +s7. +s7" +s70 +s70 +s71 +s7- +s71 +s72 +s73 +s74 +s74 +s74 +s75 +s76 +s76 +s76 +s76 +s76 +s76 +s78 +s78 +s79 +s7a +s7A +s7b +s7c +s7C +s7d +s7D +s7e +s7f +s7g +s7i +s7j +s7K +s7l +s7L +s7M +s7n +s7N +s7P +s7q +s7r +s7r +s7s +s7W +s7x +s7 +s7Y +s!8 +s8- +s8/ +s8. +s8* +s8# +s80 +s8. +s80 +s8. +s80 +s81 +s82 +s84 +s84 +s84 +s85 +s86 +s87 +s87 +s87 +s89 +s89 +s8a +s8a +s8B +s8B +s8B +s8c +s8c +s8c +s8d +s8D +s8e +s8F +s8h +s8H +s8i +s8j +s8J +s8k +s8l +s8l +s8m +s8M +s8m +s8n +s8n +s8o +s8p +s8P +s8Q +s8R +s8S +s8t +s8t +s8T +s8u +s8U +s8u +s8v +s8W +s8x +s8y +s8z +s8z +s9 +s9_ +s9- +s9! +s9. +s9" +s9@ +s9* +s9\ +s9# +s9% +s90 +s90 +s91 +s92 +s92 +s92 +s93 +s94 +s94 +s95 +s97 +s97 +s97 +s97 +s98 +s9a +s9b +s9b +s9c +s9d +s9D +s9d +s9 +s9. +s9e +s9E +s9f +s9i +s9J +s9k +s9n +s9o +s9p +s9q +s9r +s9_ +s9S +s9" +s9T +s9u +s9u +s9u +s9w +s9x +s9 +s9y +s9z +s9Z +sa@ +sa= +sa +sa- +sa; +sa? +sa/ +sa. +sa' +sa@ +sa* +sa0 +sa0 +sa0 +sa1 +sa@ +sa2 +sa2 +sa3 +sa4 +sa4 +sa4 +sa5 +sa6 +sa7 +sa7 +sa8 +sA8 +sa9 +saA +saA +sac +sa@ +saC +sAC +sad +sad +sae +saE +sae +sae +saE +saf +saF +saF +sag +sah +saH +sai +saj +sak +sak +sa- +sam +sam +sam +san +san +sao +saO +sap +sap +saP +sAP +saq +saQ +sar +sa! +sar +sas +sa@ +se3 +se3 +se3 +sa@ +sa@ +si1 +si1 +si! +si! +sa@ +sa@ +so0 +so0 +so0 +so0 +saS +sat +sat +sa@ +sa@ +sa@ +sat +sau +sau +sa@ +saU +sAu +sau +sau +sav +sav +saw +saW +sax +sa. +sax +say +saY +sAy +saz +saZ +saZ +sb +sb- +sb# +sb0 +sb0 +sb0 +sb1 +sb2 +sb3 +sb4 +sb5 +sb5 +sb6 +sb7 +sb7 +sb7 +sb8 +sb8 +sb9 +sb9 +sb9 +sba +sbB +sbc +sb +sbC +sbd +sbd +sbe +sbf +sbf +sbg +sbg +sbH +sBH +sbi +sbj +sbJ +sbk +sbK +sbk +sbl +sbL +sbm +sbM +sBM +sbM +sbm +sbm +sbM +sbM +sbn +sbN +sbo +sbp +sbP +sbq +sbr +sbR +sbs +sbs +sbt +sbt +sbT +sbu +sbu +sbu +sbv +sBv +sbx +sbX +sby +sby +sby +sby +sbz +sbz +sc" +sc@ +sc/ +sc2 +sc3 +sc4 +sc5 +sC6 +sc7 +sc8 +sc9 +scb +scb +scD +sce +scf +scg +scg +sch +scH +sCh +sch +sci +scj +scj +scj +sck +scK +sck +sck +sck +scl +scL +scm +scn +sCN +sco +scp +scP +scq +scr +scr +scs +scs +scS +sCS +sct +scu +scv +scw +scx +scy +scz +scZ +scz +sd- +sd. +sd0 +sd1 +sd1 +sd3 +sd3 +sd5 +sd6 +sd7 +sd7 +sd9 +sdb +sdB +sdb +sdb +sdc +sdc +sdD +sdf +sdF +sDF +sdf +sdf +sdg +sdG +sdg +sdh +sdi +sdi +sdj +sdJ +sdJ +sdk +sdK +sdl +sdL +sDl +sDL +sdL +sdm +sdm +sdn +sdN +sdn +sdo +sdp +sdP +sdp +sdp +sdp +sdq +sdr +sds +sds +sDs +sdt +sdT +sdv +sdW +sDw +sdW +sdX +sdX +sdy +sdY +sdY +sdz +sdZ +sDz +se +se- +se; +se. +se@ +sE. +se0 +se1 +se1 +se2 +se3 +se3 +se3 +se3 +se3 +se3 +sl1 +se3 +se3 +se3 +se3 +se3 +se3 +se3 +se3 +se4 +se5 +se6 +se7 +se8 +se9 +sea +seb +sec +seC +sed +sed +seE +sef +seg +seg +seh +sei +sej +seJ +sEJ +sej +sej +sek +sem +sem +sem +sem +sen +seN +seo +seO +seO +sep +ser +ser +ser +sEs +ses +set +seu +seU +sEU +sev +sew +seW +sew +sex +sey +sez +sez +sf[ +sf1 +sf2 +sf5 +sf6 +sf7 +sfa +sfb +sfB +sfc +sFC +sfd +sfd +sfd +sfF +sfF +sfg +sfg +sfh +sfj +sfJ +sfk +sfk +sfK +sFl +sfm +sfn +sfn +sfN +sfo +sfp +sfq +sfr +sfr +sfs +ssB +sft +sfU +sfv +sfv +sfw +sfX +sfz +sg1 +sg2 +sg3 +sg4 +sg_ +sg4 +sg_ +sg5 +sg6 +s!g +sg8 +sg9 +sg9 +sga +sgb +sgc +sgC +sgd +sgD +sge +sgh +sgh +sgh +sgi +sgj +sgJ +sgj +sgk +sgK +sgl +sgl +sgm +sgm +sgn +sgn +sgo +sgo +sgO +sgo +sgp +sgq +sgq +sgr +sgR +sgs +sgS +sgt +sg +sgT +sgT +sGt +sgv +sgV +sgw +sGw +sgw +sGw +sgx +sgx +sgy +sgz +sh0 +sh1 +sh2 +sh2 +sh2 +sh2 +sh4 +sh6 +sh7 +sh7 +sh9 +sha +shb +shB +shb +shc +shC +shd +she +shf +shf +shf +shg +shG +shi +shj +shk +sh* +shk +shk +shk +shl +shL +sHL +shm +shM +shm +shn +shp +shq +shr +shr +shs +shS +sht +shT +shu +shU +shv +shW +shx +shx +shX +shy +shY +shy +shz +shz +shz +shz +si +si- +si; +si! +si. +si@ +si* +si1 +si1 +si1 +se3 +si1 +si1 +si1 +si1 +si1 +si1 +si1 +si2 +si3 +si3 +si5 +si6 +si7 +si7 +si7 +si9 +sia +sib +sib +siC +siC +siC +siC +siC +sid +sie +sIE +siE +sif +sif +sih +sij +sik +sil +si +sim +sin +siN +siN +sio +siO +sio +sio +sip +siq +siq +sir +sir +si! +sit +si! +si! +si! +si! +si! +si! +siu +si! +siU +siv +siw +siW +siw +siy +siy +siz +siZ +siz +sj= +sj- +sj. +s!J +sj1 +sj6 +sj6 +sj7 +sj8 +sj9 +sjb +sjB +sjd +sj +sjF +sjF +sjg +sJg +sjh +sjk +sjl +sjl +sjl +sjm +sjn +sjn +sjo +sjo +sjp +sjP +sjq +sjq +sjr +sjs +sjs +sjt +sjt +sjt +sjv +sjW +sjx +sjx +sjy +sjY +sjz +sk_ +sk- +sk0 +sk2 +sk4 +sk8 +sk9 +ska +skA +skb +skB +skb +skc +skc +skC +skd +skD +ske +skf +skg +skG +skg +skh +sKH +ski +skl +skL +skL +skm +skM +skm +skn +sko +skp +skP +skq +skr +sks +skt +skT +sku +skW +skW +skw +skx +sky +sKy +sky +sky +sky +skz +skz +skZ +sl +sl; +sl! +sl] +sl@ +sl1 +sl1 +sl1 +sl1 +sl1 +sl1 +sl1 +sl1 +sl1 +sl2 +sl4 +sl5 +sl6 +sl7 +sL7 +sl7 +sl8 +sl9 +sla +slA +slb +slB +slb +slc +slc +slc +sld +sld +sl; +sle +slf +slf +slF +slf +slf +slg +slh +slH +slh +sli +slj +slj +slk +slK +sl@ +slL +slm +slM +slM +sln +sln +slo +slo +slo +slp +slP +slq +slr +slR +slr +sls +slS +slt +slt +slT +slt +slt +slu +sLU +slv +slw +slW +slx +sl- +sly +sly +sly +slz +slz +slZ +sm_ +sm; +sm. +sm1 +sm1 +sm1 +sm2 +sm2 +sm3 +sm3 +sm4 +sm4 +sm5 +sm6 +sm8 +sm8 +sm9 +sma +smb +smb +smB +smB +smc +smd +smD +smd +sme +sme +smf +smg +smg +smG +smg +smH +sMH +smH +smj +smj +smj +smj +smk +smK +sml +sml +sml +smM +smn +smn +smo +smo +smo +smo +smo +smp +smp +smP +smP +smq +smQ +smr +smR +sms +sms +smt +sm@ +smu +smu +smu +smv +smw +smw +smW +smw +smy +smz +smZ +sMz +sn, +sn. +sn+ +sn0 +sn1 +sn2 +sn3 +sn4 +sn4 +sn4 +sn5 +sn6 +sn6 +sn7 +sn7 +sn8 +sn9 +sn9 +sna +snb +snB +snb +snc +snc +snd +snD +snd +sne +snf +sng +sng +snG +snG +sng +sng +snh +snh +snh +snj +snk +snl +snl +sn_ +snl +snL +snm +sNm +snm +snN +snp +snp +snq +snq +snr +sns +snt +snT +sNt +snt +snu +snu +snv +snw +snw +snw +snx +snX +sny +snz +snz +so +so, +so; +so! +so/ +so. +so] +so@ +so# +so0 +so0 +so0 +se3 +se3 +se3 +so0 +so0 +si1 +si1 +si! +si! +so0 +sl1 +so0 +so0 +so0 +so0 +so0 +so0 +so0 +so0 +sO0 +so1 +so3 +so3 +so3 +so3 +so4 +so5 +so6 +so8 +so9 +soa +soa +sob +soB +soc +soc +soC +sod +sod +sod +soe +sof +sog +sog +soh +soi +soi +soI +soK +sol +som +som +son +soN +soO +sop +soq +soQ +sor +sor +sos +soS +sot +soT +sou +sou +sov +sow +sOW +so' +soy +soY +soy +soz +sp` +sp= +sp. +sp) +sp[ +sp/ +sp1 +sp2 +sp6 +sp9 +spb +spb +spc +spc +spd +spD +spe +spf +spg +spG +sph +spi +spj +spk +spl +spm +spm +spr +spR +sps +spS +spt +spT +spT +spT +spT +spu +spv +spV +spW +spY +spz +spZ +sq +sq9 +sqb +sqc +sqg +sqg +sqk +sqm +sQn +sqt +sr +sr[ +sr0 +sr0 +sr1 +sr2 +sr2 +sr2 +sr4 +sr5 +sr5 +sr6 +sr +sr7 +sr8 +sra +sra +sra +srb +src +src +src +src +srd +srd +sre +srf +sRf +srf +srG +srg +srg +srh +srH +sri +srI +srj +srJ +srj +srj +srk +srk +srl +srm +sRm +srn +srN +sRN +srn +sro +sro +srR +srs +srS +srs +srt +srt +sru +sr& +sru +srv +srw +srW +srw +srx +sr +srx +srX +sry +sry +srY +srY +srz +srz +ss? +ss` +ss_ +ss- +ss, +ss. +ss' +ss* +ss\ +ss& +ss# +ss% +ss+ +ss0 +ss1 +ss1 +ss2 +ss2 +ss2 +ss3 +ss3 +ss4 +ss4 +ss5 +ss5 +ss5 +ss5 +ss6 +ss6 +ss7 +ss8 +ss9 +ssa +ssa +ssb +ssB +sSB +ssb +ssB +ssc +ssC +sS/ +ssd +ssd +ssE +ssf +sSf +ssF +ssg +ssg +ssh +ssH +ssh +ssH +soB +ssi +ssj +ssj +ssk +ssK +ssk +ssl +ss\ +ssL +ss- +ssm +ssn +ssn +sSn +sso +ssO +sso +ssp +ssp +ssr +ssS +sst +sst +ssT +ssu +ssv +ssv +ssw +ssW +ssx +ssy +ssy +ssy +ssz +ssZ +ssz +st +st` +st< +st +st, +st +st1 +s#_ +st1 +st1 +st1 +st3 +st4 +st4 +st5 +st5 +st6 +st7 +st7 +st8 +st9 +stA +stb +stB +stc +stC +sTC +std +stD +ste +ste +stf +stf +stF +stg +stg +stg +sth +sth +sth +sti +sti +stj +stk +stk +stl +stL +stl +stl +stl +stm +stn +stp +stq +str +st* +sts +sts +stS +st@ +stu +stu +stu +stv +stw +stw +stw +stx +sTX +stx +stx +stx +sty +stz +su_ +su. +su\ +su0 +su1 +su3 +su4 +su8 +su8 +suA +sub +sub +sub +sub +sub +sue +sue +sUe +suf +sug +suh +sui +suI +sui +sui +suk +sul +sul +sum +suM +sun +sun +suN +sun +sun +suo +suo +suo +sup +sup +sur +sus +sut +suw +suy +suY +suy +sv_ +sv3 +sv6 +sv7 +sv9 +svb +svd +svf +svg +svi +svj +svj +svl +svm +svm +svm +svm +svm +svn +svN +svp +svr +svr +svS +svt +svu +svw +svx +svy +svz +sw2 +sw2 +sw4 +sw6 +sw8 +sw9 +swA +swa +skz +swa +swa +swc +swd +sw; +swe +swe +swf +swf +swg +swh +swh +swj +swk +swL +swm +swn +swn +swp +swr +sw- +swr +sWt +swz +sx* +sx0 +sx1 +sx3 +sx4 +sx5 +sx6 +sx6 +sx7 +sx8 +sx8 +sx9 +sxa +sxb +sxc +sxf +sxh +sxh +sxh +sxh +sxk +sxl +sxm +sxo +sxp +sxq +sxr +sxr +sxs +sxt +sxv +sxw +sxy +sy. +sy1 +sy1 +sy3 +sy5 +sy6 +sy7 +syb +syb +syc +syd +sye +sye +sye +syf +syg +syh +syi +syI +syj +syl +syn +syo +syp +syq +sys +syt +syu +syU +syu +syv +syz +sz; +sz. +sz2 +sz6 +sz7 +sz8 +sz9 +sza +szb +szc +szd +sze +szf +szh +sZJ +szj +szk +szk +szl +szL +szl +szm +szm +szn +szp +szs +szt +sz +szu +szw +szw +szx +s.z +szy +szZ +t +[ } t +[ t } +{ t +t ] { +t { +t } +{ t -0 +T0 +{ T0 +T0 { +T0 *02 +T0 *03 +T0 *06 r +T0 *07 x21 +T0 *13 +T0 -4 +T0 *43 +t *04 k +t *05 +T0 +5 +t *05 R1 +T0 '6 +T0 +6 +T0 +7 +[ T0 -9 +T0 '9 +T0 *9A +T0 D3 +T0 D5 D8 +T0 D6 +T0 D8 D5 +T0 D9 +T0 L3 +T0 L4 +T0 L7 +T0 r +T0 r *06 +T0 R5 +s3! +se4 +sgK +sM +so +spR +sr@ +st5 +std +stj +T0 t +T0 T2 +T0 T3 +T0 T5 +T0 T9 +T0 x12 +T0 x63 +T0 x74 +T1 +[ T1 +{ T1 +T1 [ +T1 +0 +T1 +1 +sAP +T1 -3 +t *14 +T1 -4 +T1 +4 +t *15 +t -1 -6 +T1 +6 +T1 +7 +T1 '8 +T1 '9 +T1 f +T1 k +T1 K +T1 L0 +T1 r +sAP +sbN +T1 t +T1 T0 +T1 T2 +T1 T2 T3 +T1 T2 T4 +T1 T3 +T1 T3 T2 +T1 T3 T5 +T1 T3 T5 T7 +T1 T3 T7 T5 +T1 T4 +T1 T4 T2 +T1 T5 T3 +T1 T5 T3 T7 +T1 T5 T7 T3 +T1 T6 +T1 T7 +T1 T7 T3 T5 +T1 T7 T5 T3 +T1 x42 +T1 x54 +T2 +{ T2 +} T2 +t *20 +T2 -0 +T2 *03 +t *21 +T2 *57 +T2 '7 +T2 D7 +T2 k +T2 K +sA8 +T2 r +s03 +sA8 +sA8 +sx3 +T2 t +T2 T3 +T2 T4 +T2 T4 T5 +T2 T4 T6 +T2 T4 T6 T8 +T2 T4 T8 T6 +T2 T5 +T2 T5 T4 +T2 T6 +T2 T6 T4 +T2 T6 T4 T8 +T2 T6 T8 T4 +T2 T8 T4 T6 +T2 T8 T6 T4 +T2 x02 +T2 x41 +T2 x52 +T2 x52 *52 +T2 x57 +T2 x62 +t +3 +T3 +} T3 +T3 } +T3 +0 +t *31 +t *34 +T3 -5 x63 +t *37 +T3 '9 +T3 D0 +T3 D6 +T3 D7 +T3 L0 +T3 r +s0@ +si1 +sr2 +T3 t +T3 T5 +T3 T5 T7 +T3 T5 T7 T9 +T3 T5 T9 T7 +T3 T6 +T3 T7 T5 +T3 T7 T5 T9 +T3 T7 T9 T5 +T3 T8 +T3 T9 T5 T7 +T3 T9 T7 T5 +T3 x12 +T3 x43 +T3 x53 +T3 x63 -5 +T4 +] T4 +T4 [ +[ t *42 +T4 +2 +T4 +4 +T4 -5 +t *46 +T4 -6 +T4 '8 +T4 'A +T4 D5 +T4 D7 +T4 k +T4 r +T4 R4 +T4 R6 +T4 r t +s2u +s32 +s3H +sau +sIs +T4 T3 +T4 T5 +T4 T6 +T4 T6 T8 +T4 T8 T6 +T4 t r +T4 x02 +T4 x54 +t -5 +T5 +T5 [ +T5 { +T5 } +T5 -0 +t *51 +T5 -3 +t { *54 +T5 *43 +T5 *45 +T5 *45 T1 +T5 -6 +T5 '6 +T5 +6 +T5 *67 +T5 'B +T5 D1 +T5 D3 +s85 +T5 D4 +T5 K +s0+ +s2b +s85 +shn +T5 T1 *45 +T5 T7 +T5 x13 +T5 x61 +T5 x62 +t '6 +t +6 +T6 +] T6 +} [ T6 +T6 [ +t -6 -1 +T6 *23 +t *65 -7 +T6 -7 +T6 '7 +T6 '9 +T6 +B +T6 D1 +T6 D8 +T6 R0 +T6 T0 +T6 T5 +T6 T7 +T6 x02 +T6 x12 +T6 x14 +T6 x24 T3 +T6 x71 +T6 x72 +t '7 +T7 +[ T7 +t -7 *65 +T7 +6 t +t *78 +[ T7 '8 +T7 'A +T7 L6 +sg7 +sgp +T7 t +T7 T0 +T7 T2 +T7 T3 +T7 T4 +T7 t +6 +T7 T8 +t -7 x03 +T7 x24 +t +8 +T8 +[ T8 +} T8 +T8 } +[ T8 +7 +T8 -7 D4 +T8 D2 +T8 D5 +T8 D6 +sdn +T8 t +T8 } T8 +T8 x02 +T8 x31 +t '9 +T9 +T9 +B +T9 x13 +T9 x23 x51 +t -A +TA +TA RA +TA T0 +TA x53 x41 +{ t D1 +t D1 +t D1 -3 +sLA +t D8 +t D8 x24 +t DA x72 +t f x3B +t f x4A +t k +t } k +t L4 +t L5 +t r +t R0 +t R1 +t R1 *05 +t R3 +t R4 +t R7 +t r D2 +s0D +s0e +s0w +s0Y +s1n +s2) +s3e +s4f +s4U +s52 +s6b +s9! +sBZ +sF9 +sg7 +sgX +sJk +skC +sLA +sLH +sLN +sMD +sN8 +sNs +sS9 +sTc +sTk +sUe +sUR +sVF +sVG +sXB +t T1 +t T4 +t T5 +t T6 +t T9 +] t x02 +t x04 +t x12 +t x14 +t x23 +t x24 +t x27 +t x32 +t x32 [ +t x34 +t x36 +t x42 +t x45 +t x52 +t x54 +t x61 +t x62 +t x64 +t x72 +t x74 +t xA2 +u +[ { u +[ u { +{ u +} u +u { +u } +u +0 +u ] *04 +u *04 ] +u *05 -5 +u +1 +u +1 x21 +u -2 +u +2 +{ u *20 +u *21 +} u +3 +u -3 ] +u ] -3 +u *32 +u *32 +2 +[ u -4 +u +4 L1 +u -5 +u +5 +u *51 +u *52 +u -5 '6 +u *56 +u '6 +u '6 -5 +u *67 +u *68 +u *75 D5 +u *76 +u +7 R7 +u -8 +] u D0 +u D0 +u D0 +2 +u D0 D7 +u D3 +u D3 T1 +u D4 +u D4 x43 +u D6 +u D7 D0 +u D8 +u D9 +u f +} u k +u k x21 +u K x21 +u L0 +u L1 +4 +u L2 +} u L7 +] u r +u r +u { r +u R5 +u R7 +7 +sTz +s0b +s1c +s2n +s4T +s5R +s6& +s6c +s7t +s9_ +s94 +s9D +sBN +sCK +sCY +sEs +sG! +sLH +sMK +sP= +sPC +sRN +sTG +sTz +sVX +sYM +u T1 +u T1 D3 +u T2 +u [ T2 +u [ T3 +u T5 [ +u x01 +u x03 +u x13 +u x15 +u x21 +1 +u x21 k +u x21 K +u x24 +u x32 +u x35 +[ u x41 +u x41 +u x42 +u x43 D4 +u x47 +u x53 +u x56 +u x75 +u xA1 +x01 +] x01 } +x01 '9 +sRu +x01 f t +] x01 K +x01 r +sl. +sry +sta +swf +x01 t f +x01 x54 +x02 +{ x02 +} x02 +x02 { +x02 *02 +3 +x02 +1 +x02 +2 +x02 *20 +x02 *23 +x02 +3 +x02 +3 *02 +x02 *31 +x02 -4 +x02 *50 +x02 *50 t +x02 +6 +x02 -7 +x02 +8 +x02 C +x02 D3 +x02 D5 +x02 D6 +x02 f +x02 f +0 +x02 k +x02 K { +x02 L5 +x02 r +x02 R5 +x02 r k +str +syl +] x02 t +x02 t +x02 T5 +x02 t *50 +x02 x13 +x02 x53 +x02 x63 +x03 +{ x03 +x03 -0 +x03 +0 +x03 *12 +x03 *12 *50 +x03 +2 +x03 *30 +x03 *31 +x03 *34 +x03 *50 *12 +x03 +6 +x03 C +x03 D4 +x03 f +x03 f ] +x03 k +x03 { K +x03 L6 +x03 R1 +x03 R2 +x03 R3 +s2 +sld +x03 T6 +x04 +x04 *02 +x04 *78 +x04 l +x04 L4 +x04 L7 +x04 R5 +x05 +x05 -4 +x05 k +x06 +x12 +[ x12 +{ x12 +x12 } +x12 -1 +x12 +2 +x12 *21 +x12 *30 x31 +x12 *34 +x12 -4 +x12 *41 +x12 *41 *52 +x12 *52 +x12 *52 *41 +x12 '8 +x12 '8 u +x12 c +x12 D7 +x12 K +x12 K l +x12 L1 +x12 L3 +x12 l K +x12 r +x12 R3 +s2z +s81 +s9n +sf4 +sgo +shs +sna +x12 } t +x12 t } +x12 T3 +x12 u +x12 u '8 +x12 x23 +x12 x32 +x12 x91 +x13 +} x13 +x13 *03 +x13 *04 +x13 -0 K +x13 +1 r +x13 -2 +x13 *34 +x13 C +x13 D6 +x13 D7 +x13 f +x13 K -0 +x13 R2 +x13 R5 +s2f +s9i +sgP +spw +svz +x13 T1 +x13 T5 +x13 x32 +x13 x63 +x14 +} x14 +x14 { +x14 -1 +x14 *50 +x14 '8 +s28 +x14 D3 +x14 f +x14 f *10 +x14 L2 +x14 R0 +s28 +s28 +sam +x15 +x15 { +x15 +0 +x15 +1 +x15 +2 +x15 +3 +x15 c +x15 D5 L2 +x15 L2 D5 +sza +x15 T1 +x16 -1 +x16 +3 +x21 +[ x21 +{ x21 +} x21 +x21 { +x21 -0 +x21 +1 +x21 +2 +x21 *25 +x21 *30 +x21 *68 +x21 '9 +{ x21 c +x21 D7 +x21 f +x21 k R0 +{ x21 l +x21 { l +x21 l { +x21 R5 D4 +x21 R7 +s5 +scs +scZ +sm0 +stl +syu +x21 x52 +x23 +[ x23 +{ x23 +x23 { +x23 +0 +x23 *23 { +x23 '6 +x23 '7 +x23 -8 +x23 +9 +x23 C +x23 D3 +x23 f +x23 K +x23 L4 +x23 L5 +x23 r +x23 R3 +siL +x24 +{ x24 +x24 ] +x24 *13 +x24 *13 t +x24 +2 +x24 -3 +x24 *36 +x24 +5 +x24 '6 +x24 '8 +x24 f +x24 L2 +x24 L6 +sa. +sat +sod +sr4 +x24 T1 +x24 t *13 +x24 t R1 +[ x25 +x25 *13 +x25 -1 f +x25 +2 +x25 +2 x51 +x25 -5 +x25 D4 +x25 R6 +s5c +x25 u +x25 x51 +2 +x26 +x26 } +x26 +1 +x26 -5 +x26 R0 +skf +ssa +x27 +x28 +3 +x31 +[ x31 +x31 [ +x31 } +x31 -0 +ssr +x31 -2 +x31 *24 +x31 *26 +x31 *28 +x31 +4 +x31 +4 L7 +x31 ] *54 +x31 *54 ] +x31 '6 +x31 *61 +x31 *80 +x31 c +x31 f +x31 L7 +4 +x31 l R6 +x31 r +x31 R6 +x31 R6 l +s5X +sar +sar +si' +sl9 +smf +ssr +ssr +syT +x31 T2 +x31 x21 *45 +x31 x42 +x32 +[ x32 +} x32 +x32 [ +x32 } +x32 +1 +x32 +2 +x32 -3 +x32 *43 +x32 -5 +x32 +5 +x32 '6 +x32 +7 +x32 '8 +x32 D1 +x32 k +x32 l +x32 L4 +x32 R4 +x32 R5 +sg* +sG6 +sqc +srl +srs +sts +x32 T3 +x32 x42 +x32 x72 +x34 +[ x34 +x34 +1 +x34 *13 +x34 -4 +x34 -5 +x34 +6 D2 +x34 -7 +x34 +8 +x34 c +x34 D6 +x34 f u +x34 l +x34 L0 +s4d +seB +skz +som +ssm +ssp +svj +x34 [ t +x34 t [ +x34 u f +x34 x52 +x35 +x35 +0 +x35 -1 +x35 *43 +x35 -6 +x35 k +x35 l +x35 R1 +sr7 +x35 x63 +x36 +x36 +3 +x36 L3 +sbt +x36 T3 +x41 +x41 [ +x41 -0 +x41 +1 +x41 +1 *30 +x41 *30 +1 +x41 *36 r +x41 *40 +x41 *53 +x41 *56 C +x41 *78 +x41 C *56 +x41 D1 +x41 r *36 +sh5 +sjL +sku +snl +x41 T3 +[ x41 u +x41 x61 +x41 x82 +x41 x83 +x42 +[ x42 +x42 { +x42 } +x42 -0 +x42 +3 +x42 *34 +x42 -4 +x42 *52 +x42 *52 t +x42 *53 +x42 -6 +x42 '6 +x42 -7 +x42 -8 +x42 +9 +x42 c +x42 D1 +x42 D1 *21 +x42 D8 +sjb +x42 L4 +x42 L5 +x42 L6 +x42 r +x42 R5 +x42 R7 +sa +sdp +sjb +sjb +slL +srz +x42 T5 +x42 t *52 +x42 { T6 +x43 +] x43 +} x43 +x43 [ +x43 *04 +x43 *05 +x43 +2 +x43 +3 +x43 +4 +x43 *42 +x43 *43 +x43 +7 +x43 '8 +x43 C +x43 D4 +x43 D7 +x43 f +x43 L0 +x43 L4 +x43 R0 +s2i +s6n +sbh +sKi +x43 t +x43 u +x45 +[ x45 +x45 { +x45 -3 +x45 -5 +x45 c +x45 l +x45 R0 +x45 R1 +spb +x46 -1 +x46 -2 +x46 +2 +x46 l +x47 -4 +x47 c +x51 +[ x51 +{ x51 +x51 { +x51 *04 +x51 -1 +x51 -2 +x51 *68 +x51 -7 +x51 '9 +x51 K +se6 +x51 L1 L3 +x51 L3 +x51 L3 L1 +x51 r +x51 R1 r +x51 R8 +s9u +se6 +se6 +sh2 +sj4 +sjm +so +so +ssa +ssS +x51 T1 +x51 T3 +x51 x62 +x52 +x52 { +x52 } +x52 *02 +x52 *05 +x52 +2 +x52 *31 +x52 *32 +x52 +3 +3 +x52 *41 +x52 +5 +x52 -6 +x52 *67 +x52 'A +x52 D7 +x52 L4 +x52 r +x52 R2 +x52 R3 +x52 R4 +x52 T5 x13 +x52 u +x52 x02 +x53 +[ x53 +{ x53 +x53 *10 +x53 -4 +x53 -9 +x53 +9 +x53 *97 +x53 D7 +x53 L1 +x53 R5 +x53 R6 +s2r +x53 R7 +s0. +s1B +s2r +s2r +sln +x53 T0 +x53 T1 +x53 T4 +x53 x41 +x53 x62 +x54 +x54 -1 R0 +x54 +2 -4 +x54 -4 +x54 -4 +2 +x54 -5 +x54 c +x54 C +x54 D3 +x54 l +x54 R0 -1 +x54 R1 +x54 R5 +x54 R6 +x54 R7 +set +x54 T5 +x54 ] x21 +x54 x21 ] +x56 +x56 +5 +x56 -6 +x56 +6 +x57 R5 +x61 +{ x61 +x61 } -1 +x61 -2 +x61 *41 +x61 *53 +x61 +6 +x61 *64 +x61 *78 +x61 } -9 +sjx +x61 L0 +x61 r +x61 R5 +s8m +sjx +x61 t +x61 T4 +x61 x14 +x61 x21 +x62 +x62 [ +x62 +0 +x62 +2 +x62 +6 +x62 *65 +x62 +7 +x62 -8 +x62 '8 +x62 c +x62 D3 +s0l +x62 l +x62 r +s0l +s0l +s1i +sbd +x62 T3 +x62 T6 +x62 x02 +x62 x23 +x63 +] x63 +x63 -0 +x63 *34 +x63 *62 +x63 c +x63 r +x63 R1 +x63 R6 +x63 R7 +scH +x63 u +x64 +[ x64 +x64 +1 +] x64 +6 +x64 -6 +x64 -7 +x64 l +skL +x64 T0 +x64 x32 +x65 +x65 +6 +x65 *76 +x65 l +x65 R0 +slb +x71 +{ x71 +x71 } +x71 -0 c +x71 +5 +x71 *53 +x71 *56 +x71 *58 D5 +x71 *64 +x71 *64 k +x71 } c +x71 c } +x71 c -0 +x71 k *64 +x71 L7 +x71 r +x71 R6 +s2! +s2x +sh- +x71 T7 +x71 x21 +x71 [ x41 +x71 x41 [ +sh- +x72 +} x72 +x72 [ +x72 +1 +x72 *13 +x72 +2 +x72 +2 T6 +x72 -5 +x72 +8 +x72 c +x72 D1 +x72 [ l +x72 l [ +x72 R7 +sa1 +x72 T2 +x72 T6 +2 +x72 x31 +x72 x42 +x73 +[ x73 +x73 D8 +x73 l +x73 L1 +x73 R4 +x73 R6 +x73 R8 +s23 +x73 T6 +x73 u +x73 x51 +x74 +x74 +1 +[ x74 c +x74 c +x74 D2 +x74 l +x74 T1 +x75 +x75 T0 +x76 +7 +x81 +x81 -1 +x81 *34 +x81 -6 +x81 -8 +x81 -9 +x81 *91 +x81 -A +x81 D1 +x81 l x25 +x81 r +x81 R8 +s21 +seo +sry +x81 T3 +x81 T4 +x81 T5 +x81 x12 +x81 x25 +x81 x25 l +x81 x42 +x82 +x82 -0 +x82 -1 +x82 +1 +x82 -3 +x82 *86 +x82 -9 +x82 *94 '9 +x82 *96 +x82 *98 +x82 'A +x82 c +x82 D5 +x82 l +x82 L1 +x82 R7 +s45 +x82 T7 +x82 u +x82 x32 +x83 +x83 *57 +x83 c +x83 D9 +x83 l +x83 L8 +x83 x12 +x83 x42 +x83 x51 +x84 -7 +x84 +7 +x84 *78 +x84 -8 +x84 l +x85 D0 +x85 x43 +x91 +x91 +6 +x91 R8 +x91 R9 +s92 +x91 x24 +x91 x51 +x91 x51 'A +x92 +{ x92 +x92 +7 +x92 -8 +x92 -9 +x92 *98 +x92 +A +x92 c x71 +x92 D0 +x92 u +x92 x21 +x92 x71 c +x93 -7 +x93 c +x93 C +x93 D6 +x93 l +x93 L9 +x93 x41 +x93 x62 +xA1 +7 +xA1 -9 +xA1 +A +xA1 D8 +xA1 x04 +xA1 x15 +xA1 x21 +xA2 [ +xA2 *97 diff --git a/princeprocessor/rules/prince_optimized.rule b/princeprocessor/rules/prince_optimized.rule new file mode 100644 index 0000000..7c22f61 --- /dev/null +++ b/princeprocessor/rules/prince_optimized.rule @@ -0,0 +1,1255 @@ +## Name: prince.rule +## Version: 1.00 +## Compatibility: hashcat v0.48+, oclHashcat v1.31+ +## +## Description: +## +## Handwritten ruleset, documented, optimized rule for use with princeprocessor +## This rule can be used as amplifier in case you're attacking a fast hash + +: + +## Global cases + +l +u +c +C +t +E + +## Reverse the entire word, Duplicate entire word, Duplicate word reversed + +r +d +f + +## Duplicate every character, multiple times + +q +qq +qqq + +## Swaps first two characters, Swaps last two characters, both + +k +K +kK + +## Deletes character at position N, up to length 15 + +D1 +D2 +D3 +D4 +D5 +D6 +D7 +D8 +D9 +DA +DB +DC +DD +DE + +## Truncate word at position N, positions 8 - 15 + +'9 +'A +'B +'C +'D +'E + +## Duplicates first character N times, up to 6 times + +z1 +z2 +z3 +z4 +z5 +z6 + +## Duplicates last character N times, up to 6 times + +Z1 +Z2 +Z3 +Z4 +Z5 +Z6 + +## Duplicates first N characters, up to 6 times + +y1 +y2 +y3 +y4 +y5 +y6 + +## Duplicates last N characters, up to 6 times + +Y1 +Y2 +Y3 +Y4 +Y5 +Y6 + +## Deletes first character, Deletes last character, both combined (remove mutations) + +[ +[ [ +[ [ [ +] +] ] +] ] ] +[ ] +[ ] [ ] +[ ] [ ] [ ] + +## Deletes first character, Deletes last character, both combined (remove mutations), Capitalize word + +[ c +[ [ c +[ [ [ c +] c +] ] c +] ] ] c +[ ] c +[ ] [ ] c +[ ] [ ] [ ] c + +## Deletes first character, Deletes last character, both combined (remove mutations), Uppercase word + +[ u +[ [ u +[ [ [ u +] u +] ] u +] ] ] u +[ ] u +[ ] [ ] u +[ ] [ ] [ ] u + +## Rotates the word left and right + +{ +{ { +{ { { +} +} } +} } } + +## Rotates the word left and right, Capitalize word + +{ c +{ { c +{ { { c +} c +} } c +} } } c + +## Rotates the word left and right, Uppercase word + +[ u +[ [ u +[ [ [ u +] u +] ] u +] ] ] u +[ ] u +[ ] [ ] u +[ ] [ ] [ ] u + +## Increment character @ N by 1 ascii value, one char, up to length 15 + ++0 ++1 ++2 ++3 ++4 ++5 ++6 ++7 ++8 ++9 ++A ++B ++C ++D ++E + +## Increment character @ N by 1 ascii value, two chars, up to length 15 + ++0 +1 ++0 +2 ++0 +3 ++0 +4 ++0 +5 ++0 +6 ++0 +7 ++0 +8 ++0 +9 ++0 +A ++0 +B ++0 +C ++0 +D ++0 +E ++1 +2 ++1 +3 ++1 +4 ++1 +5 ++1 +6 ++1 +7 ++1 +8 ++1 +9 ++1 +A ++1 +B ++1 +C ++1 +D ++1 +E ++2 +3 ++2 +4 ++2 +5 ++2 +6 ++2 +7 ++2 +8 ++2 +9 ++2 +A ++2 +B ++2 +C ++2 +D ++2 +E ++3 +4 ++3 +5 ++3 +6 ++3 +7 ++3 +8 ++3 +9 ++3 +A ++3 +B ++3 +C ++3 +D ++3 +E ++4 +5 ++4 +6 ++4 +7 ++4 +8 ++4 +9 ++4 +A ++4 +B ++4 +C ++4 +D ++4 +E ++5 +6 ++5 +7 ++5 +8 ++5 +9 ++5 +A ++5 +B ++5 +C ++5 +D ++5 +E ++6 +7 ++6 +8 ++6 +9 ++6 +A ++6 +B ++6 +C ++6 +D ++6 +E ++7 +8 ++7 +9 ++7 +A ++7 +B ++7 +C ++7 +D ++7 +E ++8 +9 ++8 +A ++8 +B ++8 +C ++8 +D ++8 +E ++9 +A ++9 +B ++9 +C ++9 +D ++9 +E ++A +B ++A +C ++A +D ++A +E ++B +C ++B +D ++B +E ++C +D ++C +E ++D +E + +## Decrement character @ N by 1 ascii value, one char, up to length 15 + +-0 +-1 +-2 +-3 +-4 +-5 +-6 +-7 +-8 +-9 +-A +-B +-C +-D +-E + +## Decrement character @ N by 1 ascii value, two chars, up to length 15 + +-0 -1 +-0 -2 +-0 -3 +-0 -4 +-0 -5 +-0 -6 +-0 -7 +-0 -8 +-0 -9 +-0 -A +-0 -B +-0 -C +-0 -D +-0 -E +-1 -2 +-1 -3 +-1 -4 +-1 -5 +-1 -6 +-1 -7 +-1 -8 +-1 -9 +-1 -A +-1 -B +-1 -C +-1 -D +-1 -E +-2 -3 +-2 -4 +-2 -5 +-2 -6 +-2 -7 +-2 -8 +-2 -9 +-2 -A +-2 -B +-2 -C +-2 -D +-2 -E +-3 -4 +-3 -5 +-3 -6 +-3 -7 +-3 -8 +-3 -9 +-3 -A +-3 -B +-3 -C +-3 -D +-3 -E +-4 -5 +-4 -6 +-4 -7 +-4 -8 +-4 -9 +-4 -A +-4 -B +-4 -C +-4 -D +-4 -E +-5 -6 +-5 -7 +-5 -8 +-5 -9 +-5 -A +-5 -B +-5 -C +-5 -D +-5 -E +-6 -7 +-6 -8 +-6 -9 +-6 -A +-6 -B +-6 -C +-6 -D +-6 -E +-7 -8 +-7 -9 +-7 -A +-7 -B +-7 -C +-7 -D +-7 -E +-8 -9 +-8 -A +-8 -B +-8 -C +-8 -D +-8 -E +-9 -A +-9 -B +-9 -C +-9 -D +-9 -E +-A -B +-A -C +-A -D +-A -E +-B -C +-B -D +-B -E +-C -D +-C -E +-D -E + +## Swaps character X with Y, one char, up to length 15 + +*01 +*02 +*03 +*04 +*05 +*06 +*07 +*08 +*09 +*0A +*0B +*0C +*0D +*0E +*12 +*13 +*14 +*15 +*16 +*17 +*18 +*19 +*1A +*1B +*1C +*1D +*1E +*23 +*24 +*25 +*26 +*27 +*28 +*29 +*2A +*2B +*2C +*2D +*2E +*34 +*35 +*36 +*37 +*38 +*39 +*3A +*3B +*3C +*3D +*3E +*45 +*46 +*47 +*48 +*49 +*4A +*4B +*4C +*4D +*4E +*56 +*57 +*58 +*59 +*5A +*5B +*5C +*5D +*5E +*67 +*68 +*69 +*6A +*6B +*6C +*6D +*6E +*78 +*79 +*7A +*7B +*7C +*7D +*7E +*89 +*8A +*8B +*8C +*8D +*8E +*9A +*9B +*9C +*9D +*9E +*AB +*AC +*AD +*AE +*BC +*BD +*BE +*CD +*CE +*DE + +## Replace all instances of X with Y (most common leetspeak chars) + +se3 +so0 +si1 +si! +sa@ +se3 so0 +se3 si1 +se3 si! +se3 sa@ +so0 si1 +so0 si! +so0 sa@ +si1 sa@ +si! sa@ +se3 so0 si1 +se3 so0 si! +se3 so0 sa@ +se3 si1 sa@ +se3 si! sa@ +so0 si1 sa@ +so0 si! sa@ + +## Replace all instances of X with Y (less common leetspeak chars) + +sl1 +slk +sa4 +ss2 +ss5 +se6 +sun + +## Replace all instances of X with Y, toggle the case of characters at position N + +se3 T0 +se3 T1 +se3 T2 +se3 T3 +se3 T4 +se3 T5 +se3 T6 +se3 T7 +se3 T8 +se3 T9 +se3 TA +se3 TB +se3 TC +se3 TD +se3 TE +so0 T0 +so0 T1 +so0 T2 +so0 T3 +so0 T4 +so0 T5 +so0 T6 +so0 T7 +so0 T8 +so0 T9 +so0 TA +so0 TB +so0 TC +so0 TD +so0 TE +si1 T0 +si1 T1 +si1 T2 +si1 T3 +si1 T4 +si1 T5 +si1 T6 +si1 T7 +si1 T8 +si1 T9 +si1 TA +si1 TB +si1 TC +si1 TD +si1 TE +si! T0 +si! T1 +si! T2 +si! T3 +si! T4 +si! T5 +si! T6 +si! T7 +si! T8 +si! T9 +si! TA +si! TB +si! TC +si! TD +si! TE +sa@ T0 +sa@ T1 +sa@ T2 +sa@ T3 +sa@ T4 +sa@ T5 +sa@ T6 +sa@ T7 +sa@ T8 +sa@ T9 +sa@ TA +sa@ TB +sa@ TC +sa@ TD +sa@ TE +se3 so0 T0 +se3 so0 T1 +se3 so0 T2 +se3 so0 T3 +se3 so0 T4 +se3 so0 T5 +se3 so0 T6 +se3 so0 T7 +se3 so0 T8 +se3 so0 T9 +se3 so0 TA +se3 so0 TB +se3 so0 TC +se3 so0 TD +se3 so0 TE +se3 si1 T0 +se3 si1 T1 +se3 si1 T2 +se3 si1 T3 +se3 si1 T4 +se3 si1 T5 +se3 si1 T6 +se3 si1 T7 +se3 si1 T8 +se3 si1 T9 +se3 si1 TA +se3 si1 TB +se3 si1 TC +se3 si1 TD +se3 si1 TE +se3 si! T0 +se3 si! T1 +se3 si! T2 +se3 si! T3 +se3 si! T4 +se3 si! T5 +se3 si! T6 +se3 si! T7 +se3 si! T8 +se3 si! T9 +se3 si! TA +se3 si! TB +se3 si! TC +se3 si! TD +se3 si! TE +se3 sa@ T0 +se3 sa@ T1 +se3 sa@ T2 +se3 sa@ T3 +se3 sa@ T4 +se3 sa@ T5 +se3 sa@ T6 +se3 sa@ T7 +se3 sa@ T8 +se3 sa@ T9 +se3 sa@ TA +se3 sa@ TB +se3 sa@ TC +se3 sa@ TD +se3 sa@ TE +so0 si1 T0 +so0 si1 T1 +so0 si1 T2 +so0 si1 T3 +so0 si1 T4 +so0 si1 T5 +so0 si1 T6 +so0 si1 T7 +so0 si1 T8 +so0 si1 T9 +so0 si1 TA +so0 si1 TB +so0 si1 TC +so0 si1 TD +so0 si1 TE +so0 si! T0 +so0 si! T1 +so0 si! T2 +so0 si! T3 +so0 si! T4 +so0 si! T5 +so0 si! T6 +so0 si! T7 +so0 si! T8 +so0 si! T9 +so0 si! TA +so0 si! TB +so0 si! TC +so0 si! TD +so0 si! TE +so0 sa@ T0 +so0 sa@ T1 +so0 sa@ T2 +so0 sa@ T3 +so0 sa@ T4 +so0 sa@ T5 +so0 sa@ T6 +so0 sa@ T7 +so0 sa@ T8 +so0 sa@ T9 +so0 sa@ TA +so0 sa@ TB +so0 sa@ TC +so0 sa@ TD +so0 sa@ TE +si1 sa@ T0 +si1 sa@ T1 +si1 sa@ T2 +si1 sa@ T3 +si1 sa@ T4 +si1 sa@ T5 +si1 sa@ T6 +si1 sa@ T7 +si1 sa@ T8 +si1 sa@ T9 +si1 sa@ TA +si1 sa@ TB +si1 sa@ TC +si1 sa@ TD +si1 sa@ TE +si! sa@ T0 +si! sa@ T1 +si! sa@ T2 +si! sa@ T3 +si! sa@ T4 +si! sa@ T5 +si! sa@ T6 +si! sa@ T7 +si! sa@ T8 +si! sa@ T9 +si! sa@ TA +si! sa@ TB +si! sa@ TC +si! sa@ TD +si! sa@ TE +se3 so0 si1 T0 +se3 so0 si1 T1 +se3 so0 si1 T2 +se3 so0 si1 T3 +se3 so0 si1 T4 +se3 so0 si1 T5 +se3 so0 si1 T6 +se3 so0 si1 T7 +se3 so0 si1 T8 +se3 so0 si1 T9 +se3 so0 si1 TA +se3 so0 si1 TB +se3 so0 si1 TC +se3 so0 si1 TD +se3 so0 si1 TE +se3 so0 si! T0 +se3 so0 si! T1 +se3 so0 si! T2 +se3 so0 si! T3 +se3 so0 si! T4 +se3 so0 si! T5 +se3 so0 si! T6 +se3 so0 si! T7 +se3 so0 si! T8 +se3 so0 si! T9 +se3 so0 si! TA +se3 so0 si! TB +se3 so0 si! TC +se3 so0 si! TD +se3 so0 si! TE +se3 so0 sa@ T0 +se3 so0 sa@ T1 +se3 so0 sa@ T2 +se3 so0 sa@ T3 +se3 so0 sa@ T4 +se3 so0 sa@ T5 +se3 so0 sa@ T6 +se3 so0 sa@ T7 +se3 so0 sa@ T8 +se3 so0 sa@ T9 +se3 so0 sa@ TA +se3 so0 sa@ TB +se3 so0 sa@ TC +se3 so0 sa@ TD +se3 so0 sa@ TE +se3 si1 sa@ T0 +se3 si1 sa@ T1 +se3 si1 sa@ T2 +se3 si1 sa@ T3 +se3 si1 sa@ T4 +se3 si1 sa@ T5 +se3 si1 sa@ T6 +se3 si1 sa@ T7 +se3 si1 sa@ T8 +se3 si1 sa@ T9 +se3 si1 sa@ TA +se3 si1 sa@ TB +se3 si1 sa@ TC +se3 si1 sa@ TD +se3 si1 sa@ TE +se3 si! sa@ T0 +se3 si! sa@ T1 +se3 si! sa@ T2 +se3 si! sa@ T3 +se3 si! sa@ T4 +se3 si! sa@ T5 +se3 si! sa@ T6 +se3 si! sa@ T7 +se3 si! sa@ T8 +se3 si! sa@ T9 +se3 si! sa@ TA +se3 si! sa@ TB +se3 si! sa@ TC +se3 si! sa@ TD +se3 si! sa@ TE +so0 si1 sa@ T0 +so0 si1 sa@ T1 +so0 si1 sa@ T2 +so0 si1 sa@ T3 +so0 si1 sa@ T4 +so0 si1 sa@ T5 +so0 si1 sa@ T6 +so0 si1 sa@ T7 +so0 si1 sa@ T8 +so0 si1 sa@ T9 +so0 si1 sa@ TA +so0 si1 sa@ TB +so0 si1 sa@ TC +so0 si1 sa@ TD +so0 si1 sa@ TE +so0 si! sa@ T0 +so0 si! sa@ T1 +so0 si! sa@ T2 +so0 si! sa@ T3 +so0 si! sa@ T4 +so0 si! sa@ T5 +so0 si! sa@ T6 +so0 si! sa@ T7 +so0 si! sa@ T8 +so0 si! sa@ T9 +so0 si! sa@ TA +so0 si! sa@ TB +so0 si! sa@ TC +so0 si! sa@ TD +so0 si! sa@ TE + +## Toggle the case of characters at position N, one char, up to position 15 + +T0 +T1 +T2 +T3 +T4 +T5 +T6 +T7 +T8 +T9 +TA +TB +TC +TD +TE + +## Toggle the case of characters at position N, two chars, up to position 15 + +T0 T1 +T0 T2 +T0 T3 +T0 T4 +T0 T5 +T0 T6 +T0 T7 +T0 T8 +T0 T9 +T0 TA +T0 TB +T0 TC +T0 TD +T0 TE +T1 T2 +T1 T3 +T1 T4 +T1 T5 +T1 T6 +T1 T7 +T1 T8 +T1 T9 +T1 TA +T1 TB +T1 TC +T1 TD +T1 TE +T2 T3 +T2 T4 +T2 T5 +T2 T6 +T2 T7 +T2 T8 +T2 T9 +T2 TA +T2 TB +T2 TC +T2 TD +T2 TE +T3 T4 +T3 T5 +T3 T6 +T3 T7 +T3 T8 +T3 T9 +T3 TA +T3 TB +T3 TC +T3 TD +T3 TE +T4 T5 +T4 T6 +T4 T7 +T4 T8 +T4 T9 +T4 TA +T4 TB +T4 TC +T4 TD +T4 TE +T5 T6 +T5 T7 +T5 T8 +T5 T9 +T5 TA +T5 TB +T5 TC +T5 TD +T5 TE +T6 T7 +T6 T8 +T6 T9 +T6 TA +T6 TB +T6 TC +T6 TD +T6 TE +T7 T8 +T7 T9 +T7 TA +T7 TB +T7 TC +T7 TD +T7 TE +T8 T9 +T8 TA +T8 TB +T8 TC +T8 TD +T8 TE +T9 TA +T9 TB +T9 TC +T9 TD +T9 TE +TA TB +TA TC +TA TD +TA TE +TB TC +TB TD +TB TE +TC TD +TC TE +TD TE + +## Inserts character X at position N (passphrase maker), one char, up to length 15 + +i0 : +i1 : +i2 : +i3 : +i4 : +i5 : +i6 : +i7 : +i8 : +i9 : +iA : +iB : +iC : +iD : +iE : + +## Inserts character X at position N (passphrase maker), two chars, up to length 15 + +i0 i1 : +i0 i2 : +i0 i3 : +i0 i4 : +i0 i5 : +i0 i6 : +i0 i7 : +i0 i8 : +i0 i9 : +i0 iA : +i0 iB : +i0 iC : +i0 iD : +i0 iE : +i1 i2 : +i1 i3 : +i1 i4 : +i1 i5 : +i1 i6 : +i1 i7 : +i1 i8 : +i1 i9 : +i1 iA : +i1 iB : +i1 iC : +i1 iD : +i1 iE : +i2 i3 : +i2 i4 : +i2 i5 : +i2 i6 : +i2 i7 : +i2 i8 : +i2 i9 : +i2 iA : +i2 iB : +i2 iC : +i2 iD : +i2 iE : +i3 i4 : +i3 i5 : +i3 i6 : +i3 i7 : +i3 i8 : +i3 i9 : +i3 iA : +i3 iB : +i3 iC : +i3 iD : +i3 iE : +i4 i5 : +i4 i6 : +i4 i7 : +i4 i8 : +i4 i9 : +i4 iA : +i4 iB : +i4 iC : +i4 iD : +i4 iE : +i5 i6 : +i5 i7 : +i5 i8 : +i5 i9 : +i5 iA : +i5 iB : +i5 iC : +i5 iD : +i5 iE : +i6 i7 : +i6 i8 : +i6 i9 : +i6 iA : +i6 iB : +i6 iC : +i6 iD : +i6 iE : +i7 i8 : +i7 i9 : +i7 iA : +i7 iB : +i7 iC : +i7 iD : +i7 iE : +i8 i9 : +i8 iA : +i8 iB : +i8 iC : +i8 iD : +i8 iE : +i9 iA : +i9 iB : +i9 iC : +i9 iD : +i9 iE : +iA iB : +iA iC : +iA iD : +iA iE : +iB iC : +iB iD : +iB iE : +iC iD : +iC iE : +iD iE : + +## Inserts character X at position N (passphrase maker), one char, up to length 15 + +i0- : +i1- : +i2- : +i3- : +i4- : +i5- : +i6- : +i7- : +i8- : +i9- : +iA- : +iB- : +iC- : +iD- : +iE- : + +## Inserts character X at position N (passphrase maker), two chars, up to length 15 + +i0- i1- : +i0- i2- : +i0- i3- : +i0- i4- : +i0- i5- : +i0- i6- : +i0- i7- : +i0- i8- : +i0- i9- : +i0- iA- : +i0- iB- : +i0- iC- : +i0- iD- : +i0- iE- : +i1- i2- : +i1- i3- : +i1- i4- : +i1- i5- : +i1- i6- : +i1- i7- : +i1- i8- : +i1- i9- : +i1- iA- : +i1- iB- : +i1- iC- : +i1- iD- : +i1- iE- : +i2- i3- : +i2- i4- : +i2- i5- : +i2- i6- : +i2- i7- : +i2- i8- : +i2- i9- : +i2- iA- : +i2- iB- : +i2- iC- : +i2- iD- : +i2- iE- : +i3- i4- : +i3- i5- : +i3- i6- : +i3- i7- : +i3- i8- : +i3- i9- : +i3- iA- : +i3- iB- : +i3- iC- : +i3- iD- : +i3- iE- : +i4- i5- : +i4- i6- : +i4- i7- : +i4- i8- : +i4- i9- : +i4- iA- : +i4- iB- : +i4- iC- : +i4- iD- : +i4- iE- : +i5- i6- : +i5- i7- : +i5- i8- : +i5- i9- : +i5- iA- : +i5- iB- : +i5- iC- : +i5- iD- : +i5- iE- : +i6- i7- : +i6- i8- : +i6- i9- : +i6- iA- : +i6- iB- : +i6- iC- : +i6- iD- : +i6- iE- : +i7- i8- : +i7- i9- : +i7- iA- : +i7- iB- : +i7- iC- : +i7- iD- : +i7- iE- : +i8- i9- : +i8- iA- : +i8- iB- : +i8- iC- : +i8- iD- : +i8- iE- : +i9- iA- : +i9- iB- : +i9- iC- : +i9- iD- : +i9- iE- : +iA- iB- : +iA- iC- : +iA- iD- : +iA- iE- : +iB- iC- : +iB- iD- : +iB- iE- : +iC- iD- : +iC- iE- : +iD- iE- :