]> xmof Git - DeDRM.git/commitdiff
tools v5.4
authorApprentice Alf <apprenticealf@gmail.com>
Wed, 7 Nov 2012 13:14:25 +0000 (13:14 +0000)
committerApprentice Alf <apprenticealf@gmail.com>
Sat, 7 Mar 2015 13:14:37 +0000 (13:14 +0000)
119 files changed:
Calibre_Plugins/Ignobleepub ReadMe.txt
Calibre_Plugins/Ineptepub ReadMe.txt
Calibre_Plugins/Ineptpdf ReadMe.txt
Calibre_Plugins/K4MobiDeDRM ReadMe.txt
Calibre_Plugins/K4MobiDeDRM_plugin/__init__.py
Calibre_Plugins/K4MobiDeDRM_plugin/aescbc.py
Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto.dll
Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto.py
Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto64.dll
Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto_src.zip
Calibre_Plugins/K4MobiDeDRM_plugin/cmbtc_v2.2.py
Calibre_Plugins/K4MobiDeDRM_plugin/config.py
Calibre_Plugins/K4MobiDeDRM_plugin/convert2xml.py
Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2html.py
Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2svg.py
Calibre_Plugins/K4MobiDeDRM_plugin/genbook.py
Calibre_Plugins/K4MobiDeDRM_plugin/getk4pcpids.py
Calibre_Plugins/K4MobiDeDRM_plugin/k4mobidedrm_orig.py
Calibre_Plugins/K4MobiDeDRM_plugin/kgenpids.py
Calibre_Plugins/K4MobiDeDRM_plugin/libalfcrypto.dylib
Calibre_Plugins/K4MobiDeDRM_plugin/libalfcrypto32.so
Calibre_Plugins/K4MobiDeDRM_plugin/libalfcrypto64.so
Calibre_Plugins/K4MobiDeDRM_plugin/pbkdf2.py
Calibre_Plugins/K4MobiDeDRM_plugin/plugin-import-name-k4mobidedrm.txt
Calibre_Plugins/K4MobiDeDRM_plugin/scrolltextwidget.py
Calibre_Plugins/K4MobiDeDRM_plugin/stylexml2css.py
Calibre_Plugins/K4MobiDeDRM_plugin/subasyncio.py
Calibre_Plugins/K4MobiDeDRM_plugin/topazextract.py
Calibre_Plugins/eReaderPDB2PML ReadMe.txt
Calibre_Plugins/eReaderPDB2PML_plugin.zip
Calibre_Plugins/eReaderPDB2PML_plugin/__init__.py
Calibre_Plugins/eReaderPDB2PML_plugin/outputfix.py [new file with mode: 0644]
Calibre_Plugins/ignobleepub_plugin.zip
Calibre_Plugins/ignobleepub_plugin/Ignoble Epub DeDRM_Help.htm [new file with mode: 0644]
Calibre_Plugins/ignobleepub_plugin/__init__.py [moved from Calibre_Plugins/ignobleepub_plugin/ignobleepub_plugin.py with 50% similarity]
Calibre_Plugins/ignobleepub_plugin/config.py [new file with mode: 0644]
Calibre_Plugins/ignobleepub_plugin/outputfix.py [new file with mode: 0644]
Calibre_Plugins/ignobleepub_plugin/plugin-import-name-ignoble_epub.txt [new file with mode: 0644]
Calibre_Plugins/ignobleepub_plugin/utilities.py [new file with mode: 0644]
Calibre_Plugins/ignobleepub_plugin/zipfilerugged.py [new file with mode: 0644]
Calibre_Plugins/ignobleepub_plugin/zipfix.py
Calibre_Plugins/ineptepub_plugin.zip
Calibre_Plugins/ineptepub_plugin/__init__.py
Calibre_Plugins/ineptepub_plugin/ineptkey.py [moved from Calibre_Plugins/ineptepub_plugin/ade_key.py with 72% similarity]
Calibre_Plugins/ineptepub_plugin/outputfix.py [new file with mode: 0644]
Calibre_Plugins/ineptepub_plugin/zipfilerugged.py [new file with mode: 0644]
Calibre_Plugins/ineptepub_plugin/zipfix.py
Calibre_Plugins/ineptpdf_plugin.zip
Calibre_Plugins/ineptpdf_plugin/__init__.py
Calibre_Plugins/ineptpdf_plugin/ade_key.py
Calibre_Plugins/ineptpdf_plugin/plugin-import-name-ineptpdf.txt
Calibre_Plugins/k4mobidedrm_plugin.zip
Calibre_Plugins/k4mobidedrm_plugin/k4mutils.py
Calibre_Plugins/k4mobidedrm_plugin/k4pcutils.py
Calibre_Plugins/k4mobidedrm_plugin/mobidedrm.py
DeDRM_Macintosh_Application/DeDRM ReadMe.rtf
DeDRM_Macintosh_Application/DeDRM.app.txt
DeDRM_Macintosh_Application/DeDRM.app/Contents/Info.plist
DeDRM_Macintosh_Application/DeDRM.app/Contents/MacOS/droplet
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM Progress Source.zip
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM Progress.app/Contents/Info.plist
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM Progress.app/Contents/MacOS/DeDRM Progress
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/Scripts/main.scpt
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/cmbtc_v2.2.py [deleted file]
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/config.py
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/droplet.icns [deleted file]
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/droplet.rsrc
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignobleepub.py
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ignoblekeygen.py
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptepub.py
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/ineptkey.py
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mdumpkinfo.py [deleted file]
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mutils.py
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4pcutils.py
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/libalfcrypto src.zip [deleted file]
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/pbkdf2.py [deleted file]
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/pycrypto_des.py [new file with mode: 0644]
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/python_des.py [new file with mode: 0644]
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/topazextract.py
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfilerugged.py [new file with mode: 0644]
DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfix.py
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/DeDRM_app.pyw
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/alfcrypto.exp [deleted file]
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/cmbtc_v2.2.py [deleted file]
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/config.py
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignobleepub.py
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ignoblekeygen.py
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptepub.py
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/ineptkey.py
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4mutils.py
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/k4pcutils.py
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/libalfcrypto.dylib [new file with mode: 0644]
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/libalfcrypto32.so [new file with mode: 0644]
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/libalfcrypto64.so [new file with mode: 0644]
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/pbkdf2.py [deleted file]
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/topazextract.py
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/zipfilerugged.py [new file with mode: 0644]
DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/zipfix.py
DeDRM_Windows_Application/DeDRM_ReadMe.txt
Kindle_for_Android_Patches/kindle version 3.0.1.70/ReadMe_K4Android.txt [moved from Other_Tools/Kindle_for_Android_Patch/ReadMe_K4Android.txt with 100% similarity]
Kindle_for_Android_Patches/kindle version 3.0.1.70/kindle3.0.1.70.patch [moved from Other_Tools/Kindle_for_Android_Patch/kindle3.patch with 100% similarity]
Kindle_for_Android_Patches/kindle version 3.7.0.108/ReadMe_K4Android.txt [new file with mode: 0644]
Kindle_for_Android_Patches/kindle version 3.7.0.108/kindle3.7.0.108.patch [new file with mode: 0644]
Other_Tools/Adobe_PDF_Tools/ineptkey.pyw
Other_Tools/Adobe_ePub_Tools/ineptepub.pyw
Other_Tools/Adobe_ePub_Tools/ineptkey.pyw
Other_Tools/Barnes_and_Noble_EPUB_Tools/README_ignoble_epub.txt
Other_Tools/Barnes_and_Noble_EPUB_Tools/ignobleepub.pyw
Other_Tools/Barnes_and_Noble_EPUB_Tools/ignoblekeygen.pyw
Other_Tools/KindleBooks/README_KindleBooks.txt
Other_Tools/KindleBooks/lib/config.py
Other_Tools/KindleBooks/lib/k4mutils.py
Other_Tools/KindleBooks/lib/k4pcutils.py
Other_Tools/KindleBooks/lib/topazextract.py
Other_Tools/ePub_Fixer/lib/subasyncio.py
Other_Tools/ePub_Fixer/lib/zipfilerugged.py [new file with mode: 0644]
Other_Tools/ePub_Fixer/lib/zipfix.py
Other_Tools/eReader_PDB_Tools/README_eReaderPDB.txt
ReadMe_First.txt

index 697cbdf447f96b766f37a14689ab8a22cf8408bf..262ef402a6ddb4dc86fe756860e02307a032cefb 100644 (file)
@@ -1,4 +1,4 @@
-Ignoble Epub DeDRM - ignobleepub_v01.6_plugin.zip
+Ignoble Epub DeDRM - ignobleepub_v02.2_plugin.zip
 
 All credit given to I♥Cabbages for the original standalone scripts.
 I had the much easier job of converting them to a calibre plugin.
@@ -8,31 +8,60 @@ This plugin is meant to decrypt Barnes & Noble Epubs that are protected with Ado
 
 Installation:
 
-Go to calibre's Preferences page.  Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file  (ignobleepub_vXX_plugin.zip) and click the 'Add' button. you're done.
-
-Please note:  calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added.
+Go to calibre's Preferences page.  Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file  (ignobleepub_v02.2_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog.
 
 
 Configuration:
 
-1) The easiest way to configure the plugin is to enter your name (Barnes & Noble account name) and credit card number (the one used to purchase the books) into the plugin's customization window. It's the same info you would enter into the ignoblekeygen script. Highlight the plugin (Ignoble Epub DeDRM) and click the "Customize Plugin" button on calibre's Preferences->Plugins page. Enter the name and credit card number separated by a comma: Your Name,1234123412341234
+Upon first installing the plugin (or upgrading from a version earlier than 0.2.0), the plugin will be unconfigured. Until you create at least one B&N key—or migrate your existing key(s)/data from an earlier version of the plugin—the plugin will not function. When unconfigured (no saved keys)... an error message will occur whenever ePubs are imported to calibre. To eliminate the error message, open the plugin's customization dialog and create/import/migrate a key (or disable/uninstall the plugin). You can get to the plugin's customization dialog by opening calibre's Preferences dialog, and clicking Plugins (under the Advanced section). Once in the Plugin Preferences, expand the "File type plugins" section and look for the "Ignoble Epub DeDRM" plugin. Highlight that plugin and click the "Customize plugin" button.
+
+Upgrading from old keys
+
+If you are upgrading from an earlier version of this plugin and have provided your name(s) and credit card number(s) as part of the old plugin's customization string, you will be prompted to migrate this data to the plugin's new, more secure, key storage method when you open the customization dialog for the first time. If you choose NOT to migrate that data, you will be prompted to save that data as a text file in a location of your choosing. Either way, this plugin will no longer be storing names and credit card numbers in plain sight (or anywhere for that matter) on your computer or in calibre. If you don't choose to migrate OR save the data, that data will be lost. You have been warned!!
+
+Upon configuring for the first time, you may also be asked if you wish to import your existing *.b64 keyfiles (if you use them) to the plugin's new key storage method. The new plugin no longer looks for keyfiles in calibre's configuration directory, so it's highly recommended that you import any existing keyfiles when prompted ... but you always have the ability to import existing keyfiles anytime you might need/want to.
+
+If you have upgraded from an earlier version of the plugin, the above instructions may be all you need to do to get the new plugin up and running. Continue reading for new-key generation and existing-key management instructions.
+
+Creating New Keys:
 
-If you've purchased books with more than one credit card, separate that other info with a colon: Your Name,1234123412341234:Other Name,2345234523452345
+On the right-hand side of the plugin's customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering the necessary data to generate a new key.
 
-** NOTE ** The above method is your only option if you don't have/can't run the original I♥Cabbages scripts on your particular machine. Your credit card number will be on display in calibre's Plugin configuration page when using the above method. If other people have access to your computer, you may want to use the second configuration method below.
+* Unique Key Name: this is a unique name you choose to help you identify the key after it's created. This name will show in the list of configured keys. Choose something that will help you remember the data (name, cc#) it was created with.
+* Your Name: Your name as set in your Barnes & Noble account, My Account page, directly under PERSONAL INFORMATION. It is usually just your first name and last name separated by a space. This name will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key that's stored in the preferences.
+* Credit Card number: this is the credit card number that was set as default with Barnes & Noble at the time of download. Nothing fancy here; no dashes or spaces ... just the 16 (15?) digits. Again... this number will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key that's stored in the preferences.
+Click the 'OK" button to create and store the generated key. Or Cancel if you didn't want to create a key.
 
+Deleting Keys:
 
-2) If you already have keyfiles generated with I <3 Cabbages' ignoblekeygen.pyw script, you can put those keyfiles into calibre's configuration directory. The easiest way to find the correct directory is to go to calibre's Preferences page... click on the 'Miscellaneous' button (looks like a gear),  and then click the 'Open calibre configuration directory' button. Paste your keyfiles in there. Just make sure that they have different names and are saved with the '.b64' extension (like the ignoblekeygen script produces). This directory isn't touched when upgrading calibre, so it's quite safe to leave them there.
+On the right-hand side of the plugin's customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure that's what you truly mean to do. Once gone, it's permanently gone.
 
-All keyfiles from method 2 and all data entered from method 1 will be used to attempt to decrypt a book. You can use method 1 or method 2, or a combination of both.
+Exporting Keys:
 
+On the right-hand side of the plugin's customization dialog, you will see a button with an icon that looks like a computer's hard-drive. Use this button to export the highlighted key to a file (*.b64). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.
+
+Importing Existing Keyfiles:
+
+At the bottom-left of the plugin's customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing *.b64 keyfiles. Used for migrating keyfiles from older versions of the plugin (or keys generated with the original I <3 Cabbages script), or moving keyfiles from computer to computer, or restoring a backup. Some very basic validation is done to try to avoid overwriting already configured keys with incoming, imported keyfiles with the same base file name, but I'm sure that could be broken if someone tried hard. Just take care when importing.
+
+Once done creating/importing/exporting/deleting decryption keys; click "OK" to exit the customization dialogue (the cancel button will actually work the same way here ... at this point all data/changes are committed already, so take your pick).
 
 Troubleshooting:
 
-If you find that it's not working for you (imported epubs still have DRM), you can save a lot of time and trouble by trying to add the epub to calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might as well get used to it. ;)
+If you find that it's not working for you (imported ebooks still have DRM), you can save a lot of time and trouble by first deleting the DRMed ebook from calibre and then trying to add the ebook to calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might as well get used to it. ;)
+
+On Macintosh only you must first run calibre, open Preferences, open Miscellaneous, and click on the “Install command line tools” button. (On Windows and Linux the command line tools are installed automatically.)
+
+On Windows, open a terminal/command window. (Start/Run… and then type 'cmd' (without the 's) as the program to run).
+On Macintosh, open the Terminal application (in your Utilities folder).
+On Linux open a command window. Hopefully all Linux users know how to do this, as I do not.
 
-Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook.epub". Don't type the quotes and obviously change the 'your_ebook.epub' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make.
+You should now have a text-based command-line window open. Also have open the folder containing the ebook to be imported. Make sure that book isn’t already in calibre, and that calibre isn’t running.
 
-** Note: the Mac version of calibre doesn't install the command line tools by default. If you go to the 'Preferences' page and click on the miscellaneous button, you'll see the option to install the command line tools.
+Now type in "calibredb add " (without the " but don’t miss that final space) and now drag the book to be imported onto the window. The full path to the book should be inserted into the command line. Now press the return/enter key. The import routines will run and produce some logging information.
 
+Now copy the output from the terminal window.
+On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it.
+On Macintosh and Linux, just use the normal text select and copy commands.
 
+Paste the information into a comment at my blog, describing your problem.
\ No newline at end of file
index 52bece487709a6160a4564cc066eebda5dfbc7ef..9dfdf579b003be5af9e1d1f2fa0f5baa493d63eb 100644 (file)
@@ -1,4 +1,4 @@
-Inept Epub DeDRM - ineptepub_v01.7_plugin.zip
+Inept Epub DeDRM - ineptepub_v01.9_plugin.zip
 
 All credit given to I♥Cabbages for the original standalone scripts.
 I had the much easier job of converting them to a Calibre plugin.
@@ -8,14 +8,14 @@ This plugin is meant to decrypt Adobe Digital Edition Epubs that are protected w
 
 Installation:
 
-Go to Calibre's Preferences page. Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Cahnge calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ineptepub_vXX_plugin.zip) and click the 'Add' button. you're done.
+Go to Calibre's Preferences page. Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Cahnge calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ineptepub_v01.9_plugin.zip) and click the 'Add' button. you're done.
 
 Please note:  Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added.
 
 
 Configuration:
 
-When first run, the plugin will attempt to find your Adobe Digital Editions installation (on Windows and Mac OS's). If successful, it will create an 'adeptkey.der' file and save it in Calibre's configuration directory. It will use that file on subsequent runs. If there are already '*.der' files in the directory, the plugin won't attempt to find the Adobe Digital Editions installation installation.
+When first run, the plugin will attempt to find your Adobe Digital Editions installation (on Windows and Mac OS). If successful, it will create 'calibre-adeptkey[number].der' file(s) and save them in Calibre's configuration directory. It will use those files and any other '*.der' files in any decryption attempts. If there is already at least one 'calibre-adept*.der' file in the directory, the plugin won't attempt to find the Adobe Digital Editions installation keys again.
 
 So if you have Adobe Digital Editions installation installed on the same machine as Calibre... you are ready to go. If not... keep reading.
 
@@ -31,9 +31,20 @@ All keyfiles with a '.der' extension found in Calibre's configuration directory
 
 Troubleshooting:
 
-If you find that it's not working for you (imported epubs still have DRM), you can save a lot of time and trouble by trying to add the epub to Calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might as well get used to it. ;)
+If you find that it's not working for you (imported ebooks still have DRM), you can save a lot of time and trouble by first deleting the DRMed ebook from calibre and then trying to add the ebook to calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might as well get used to it. ;)
 
-Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook.epub". Don't type the quotes and obviously change the 'your_ebook.epub' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make.
+On Macintosh only you must first run calibre, open Preferences, open Miscellaneous, and click on the “Install command line tools” button. (On Windows and Linux the command line tools are installed automatically.)
 
-** Note: the Mac version of Calibre doesn't install the command line tools by default. If you go to the 'Preferences' page and click on the miscellaneous button, you'll see the option to install the command line tools.
+On Windows, open a terminal/command window. (Start/Run… and then type ‘cmd’ (without the ‘s) as the program to run).
+On Macintosh, open the Terminal application (in your Utilities folder).
+On Linux open a command window. Hopefully all Linux users know how to do this, as I do not.
 
+You should now have a text-based command-line window open. Also have open the folder containing the ebook to be imported. Make sure that book isn’t already in calibre, and that calibre isn’t running.
+
+Now type in "calibredb add " (without the " but don’t miss that final space) and now drag the book to be imported onto the window. The full path to the book should be inserted into the command line. Now press the return/enter key. The import routines will run and produce some logging information.
+
+Now copy the output from the terminal window.
+On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it.
+On Macintosh and Linux, just use the normal text select and copy commands.
+
+Paste the information into a comment at my blog, describing your problem.
\ No newline at end of file
index f3b15a06419b1f58c76385a0e12f1bf962ba8f47..9fcb58d181855c6e18b36c37e1c94839df622304 100644 (file)
@@ -1,4 +1,4 @@
-Inept PDF Plugin - ineptpdf_v01.5_plugin.zip
+Inept PDF Plugin - ineptpdf_v01.6_plugin.zip
 
 All credit given to I♥Cabbages for the original standalone scripts.
 I had the much easier job of converting them to a Calibre plugin.
@@ -8,18 +8,16 @@ This plugin is meant to decrypt Adobe Digital Edition PDFs that are protected wi
 
 Installation:
 
-Go to Calibre's Preferences page.  Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" plugins, instead select "Change calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ineptpdf_vXX_plugin.zip) and click the 'Add' button. you're done.
-
-Please note:  Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added.
+Go to calibre's Preferences page.  Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (ineptpdf_v01.6_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog.
 
 
 Configuration:
 
-When first run, the plugin will attempt to find your Adobe Digital Editions installation (on Windows and Mac OS's). If successful, it will create an 'adeptkey.der' file and save it in Calibre's configuration directory. It will use that file on subsequent runs. If there are already '*.der' files in the directory, the plugin won't attempt to find the Adobe Digital Editions installation installation.
+When first run, the plugin will attempt to find your Adobe Digital Editions installation (on Windows and Mac OS). If successful, it will create 'calibre-adeptkey[number].der' file(s) and save them in Calibre's configuration directory. It will use those files and any other '*.der' files in any decryption attempts. If there is already at least one 'calibre-adept*.der' file in the directory, the plugin won't attempt to find the Adobe Digital Editions installation keys again.
 
 So if you have Adobe Digital Editions installation installed on the same machine as Calibre... you are ready to go. If not... keep reading.
 
-If you already have keyfiles generated with I <3 Cabbages' ineptkey.pyw script, you can put those keyfiles in Calibre's configuration directory. The easiest way to find the correct directory is to go to Calibre's Preferences page... click on the 'Miscellaneous' button (looks like a gear),  and then click the 'Open Calibre configuration directory' button. Paste your keyfiles in there. Just make sure that
+If you already have keyfiles generated with ICabbages' ineptkey.pyw script, you can put those keyfiles in Calibre's configuration directory. The easiest way to find the correct directory is to go to Calibre's Preferences page... click on the 'Miscellaneous' button (looks like a gear),  and then click the 'Open Calibre configuration directory' button. Paste your keyfiles in there. Just make sure that
 they have different names and are saved with the '.der' extension (like the ineptkey script produces). This directory isn't touched when upgrading Calibre, so it's quite safe to leave them there.
 
 Since there is no Linux version of Adobe Digital Editions, Linux users will have to obtain a keyfile through other methods and put the file in Calibre's configuration directory.
@@ -31,9 +29,20 @@ All keyfiles with a '.der' extension found in Calibre's configuration directory
 
 Troubleshooting:
 
-If you find that it's not working for you (imported PDFs still have DRM), you can save a lot of time and trouble by trying to add the PDF to Calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might as well get used to it. ;)
+If you find that it's not working for you (imported ebooks still have DRM), you can save a lot of time and trouble by first deleting the DRMed ebook from calibre and then trying to add the ebook to calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might as well get used to it. ;)
+
+On Macintosh only you must first run calibre, open Preferences, open Miscellaneous, and click on the “Install command line tools” button. (On Windows and Linux the command line tools are installed automatically.)
+
+On Windows, open a terminal/command window. (Start/Run… and then type ‘cmd’ (without the ‘s) as the program to run).
+On Macintosh, open the Terminal application (in your Utilities folder).
+On Linux open a command window. Hopefully all Linux users know how to do this, as I do not.
+
+You should now have a text-based command-line window open. Also have open the folder containing the ebook to be imported. Make sure that book isn’t already in calibre, and that calibre isn’t running.
 
-Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook.pdf". Don't type the quotes and obviously change the 'your_ebook.pdf' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make.
+Now type in "calibredb add " (without the " but don’t miss that final space) and now drag the book to be imported onto the window. The full path to the book should be inserted into the command line. Now press the return/enter key. The import routines will run and produce some logging information.
 
-** Note: the Mac version of Calibre doesn't install the command line tools by default. If you go to the 'Preferences' page and click on the miscellaneous button, you'll see the option to install the command line tools.
+Now copy the output from the terminal window.
+On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it.
+On Macintosh and Linux, just use the normal text select and copy commands.
 
+Paste the information into a comment at my blog, describing your problem.
\ No newline at end of file
index e892d1191d5507dd6470f73edc7588129d95ef88..022f8abe2dcbd53003603d1c61f93324a211e48f 100644 (file)
@@ -1,24 +1,30 @@
-K4MobiDeDRM_v04.5_plugin.zip
+K4MobiDeDRM_v04.6_plugin.zip
 
 Credit given to The Dark Reverser for the original standalone script. Credit also to the many people who have updated and expanded that script since then.
 
 Plugin for K4PC, K4Mac, eInk Kindles and Mobipocket.
 
-This plugin supersedes MobiDeDRM, K4DeDRM, and K4PCDeDRM and K4X plugins.  If you install this plugin, those plugins can be safely removed.
+This plugin supersedes MobiDeDRM, K4DeDRM, and K4PCDeDRM and K4X plugins. If you install this plugin, those plugins should be removed.
 
-This plugin is meant to remove the DRM from .prc, .mobi, .azw, .azw1, .azw3, .azw4 and .tpz ebooks.   Calibre can then convert them to whatever format you desire. It is meant to function without having to install any  dependencies except for Calibre being on your same machine and in the same account as your "Kindle for PC" or "Kindle for Mac" application if you are going to remove the DRM from those types of books.
+This plugin is meant to remove the DRM from .prc, .mobi, .azw, .azw1, .azw3, .azw4 and .tpz ebooks. Calibre can then convert them to whatever format you desire. It is meant to function without having to install any dependencies except for Calibre being on your same machine and in the same account as your "Kindle for PC" or "Kindle for Mac" application if you are going to remove the DRM from those types of books.
 
 
 Installation:
 
-Go to Calibre's Preferences page.  Do **NOT** select "Get Plugins to enhance calibre" as this is reserved for official calibre plugins", instead select "Change calibre behavior". Under "Advanced" click on the on the Plugins button. Click on the "Load plugin from file" button at the bottom of the screen.  Use the file dialog button to select the plugin's zip file  (K4MobiDeDRM_vXX_plugin.zip) and click the "Add" (or it may say "Open" button.  Then click on the "Yes" button in the warning dialog that appears.  A Confirmation dialog appears that says the plugin has been installed.
+Go to calibre's Preferences page.  Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file (K4MobiDeDRM_v04.6_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog.
+
+Make sure that you delete any old versions of the plugin. They might interfere with the operation of the new one.
 
 
 Configuration:
 
-Highlight the plugin (K4MobiDeDRM under the "File type plugins" category) and click the "Customize Plugin" button on Calibre's Preferences->Plugins page. If you have an eInk Kindle enter the 16 digit serial number (these typically begin "B0..."). If you have more than one eInk Kindle, you can enter multiple serial numbers separated by commas (no spaces). If you have Mobipocket books, enter your 10 digit PID.  If you have more than one PID, separate them with commax (no spaces).
+Highlight the plugin (K4MobiDeDRM under the "File type plugins" category) and click the "Customize Plugin" button on Calibre's Preferences->Plugins page.
+
+If you have an eInk Kindle enter the 16 character serial number (these all begin a "B") in the serial numbers field. The easiest way to make sure that you have the serial number right is to copy it from your Amazon account pages (the "Manage Your Devices" page). If you have more than one eInk Kindle, you can enter multiple serial numbers separated by commas.
 
-This configuration step is not needed if you only want to decode "Kindle for PC" or "Kindle for Mac" books. 
+If you have Mobipocket books, enter your 8 or 10 digit PID in the Mobipocket PIDs field. If you have more than one PID, separate them with commas.
+
+These configuration steps are not needed if you only want to decode "Kindle for PC" or "Kindle for Mac" books. 
 
 
 Linux Systems Only:
@@ -28,10 +34,22 @@ If you install Kindle for PC in Wine, the plugin should be able to decode files
 
 Troubleshooting:
 
-If you find that it's not working for you, you can save a lot of time and trouble by trying to add the DRMed ebook to Calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might as well get used to it. ;)
+If you find that it's not working for you (imported ebooks still have DRM), you can save a lot of time and trouble by first deleting the DRMed ebook from calibre and then trying to add the ebook to calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might as well get used to it. ;)
+
+On Macintosh only you must first run calibre, open Preferences, open Miscellaneous, and click on the “Install command line tools” button. (On Windows and Linux the command line tools are installed automatically.)
+
+On Windows, open a terminal/command window. (Start/Run… and then type 'cmd' (without the 's) as the program to run).
+On Macintosh, open the Terminal application (in your Utilities folder).
+On Linux open a command window. Hopefully all Linux users know how to do this, as I do not.
+
+You should now have a text-based command-line window open. Also have open the folder containing the ebook to be imported. Make sure that book isn’t already in calibre, and that calibre isn’t running.
+
+Now type in "calibredb add " (without the " but don’t miss that final space) and now drag the book to be imported onto the window. The full path to the book should be inserted into the command line. Now press the return/enter key. The import routines will run and produce some logging information.
 
-Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook_file". Don't type the quotes and obviously change the 'your_ebook_file' to whatever the filename of your book is (including any file name extension like .azw). Copy the resulting output and paste it into any online help request you make.
+Now copy the output from the terminal window.
+On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it.
+On Macintosh and Linux, just use the normal text select and copy commands.
 
-** Note: the Mac version of Calibre doesn't install the command line tools by default. If you go to the 'Preferences' page and click on the miscellaneous button, you'll see the option to install the command line tools.
+Paste the information into a comment at my blog, describing your problem.
 
 
index 48a3d232b4bfbf7050aa393d87c65afea86396d6..ba11adfdabedd5a7862c7d062e4bde5e7845388c 100644 (file)
@@ -1,4 +1,5 @@
 #!/usr/bin/env python
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
 
 from __future__ import with_statement
 
@@ -19,7 +20,7 @@ class K4DeDRM(FileTypePlugin):
     description         = 'Removes DRM from eInk Kindle, Kindle 4 Mac and Kindle 4 PC ebooks, and from Mobipocket ebooks. Provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, mdlnx, ApprenticeAlf, etc.'
     supported_platforms = ['osx', 'windows', 'linux'] # Platforms this plugin will run on
     author              = 'DiapDealer, SomeUpdates, mdlnx, Apprentice Alf' # The author of this plugin
-    version             = (0, 4, 5)   # The version number of this plugin
+    version             = (0, 4, 6)   # The version number of this plugin
     file_types          = set(['prc','mobi','azw','azw1','azw3','azw4','tpz']) # The file types that this plugin will be applied to
     on_import           = True # Run this plugin during the import
     priority            = 520  # run this plugin before earlier versions
@@ -39,7 +40,7 @@ class K4DeDRM(FileTypePlugin):
         elif isosx:
             names = ['libalfcrypto.dylib']
         else:
-            names = ['libalfcrypto32.so','libalfcrypto64.so','alfcrypto.py','alfcrypto.dll','alfcrypto64.dll','getk4pcpids.py','mobidedrm.py','kgenpids.py','k4pcutils.py','topazextract.py']
+            names = ['libalfcrypto32.so','libalfcrypto64.so','alfcrypto.py','alfcrypto.dll','alfcrypto64.dll','getk4pcpids.py','mobidedrm.py','kgenpids.py','k4pcutils.py','topazextract.py','outputfix.py']
         lib_dict = self.load_resources(names)
         self.alfdir = os.path.join(config_dir, 'alfcrypto')
         if not os.path.exists(self.alfdir):
@@ -56,9 +57,16 @@ class K4DeDRM(FileTypePlugin):
         # Had to move these imports here so the custom libs can be
         # extracted to the appropriate places beforehand these routines
         # look for them.
-        from calibre_plugins.k4mobidedrm import kgenpids
-        from calibre_plugins.k4mobidedrm import topazextract
-        from calibre_plugins.k4mobidedrm import mobidedrm
+        from calibre_plugins.k4mobidedrm import kgenpids, topazextract, mobidedrm, outputfix
+
+        if sys.stdout.encoding == None:
+            sys.stdout = outputfix.getwriter('utf-8')(sys.stdout)
+        else:
+            sys.stdout = outputfix.getwriter(sys.stdout.encoding)(sys.stdout)
+        if sys.stderr.encoding == None:
+            sys.stderr = outputfix.getwriter('utf-8')(sys.stderr)
+        else:
+            sys.stderr = outputfix.getwriter(sys.stderr.encoding)(sys.stderr)
 
         plug_ver = '.'.join(str(self.version).strip('()').replace(' ', '').split(','))
         k4 = True
index 566751130103e0bd0b7a6961c8a9638c8fc3a5be..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 (file)
@@ -1,568 +0,0 @@
-#! /usr/bin/env python
-
-"""
-    Routines for doing AES CBC in one file
-
-    Modified by some_updates to extract
-    and combine only those parts needed for AES CBC
-    into one simple to add python file
-
-    Original Version
-    Copyright (c) 2002 by Paul A. Lambert
-    Under:
-    CryptoPy Artisitic License Version 1.0
-    See the wonderful pure python package cryptopy-1.2.5
-    and read its LICENSE.txt for complete license details.
-"""
-
-class CryptoError(Exception):
-    """ Base class for crypto exceptions """
-    def __init__(self,errorMessage='Error!'):
-        self.message = errorMessage
-    def __str__(self):
-        return self.message
-
-class InitCryptoError(CryptoError):
-    """ Crypto errors during algorithm initialization """
-class BadKeySizeError(InitCryptoError):
-    """ Bad key size error """
-class EncryptError(CryptoError):
-    """ Error in encryption processing """
-class DecryptError(CryptoError):
-    """ Error in decryption processing """
-class DecryptNotBlockAlignedError(DecryptError):
-    """ Error in decryption processing """
-
-def xorS(a,b):
-    """ XOR two strings """
-    assert len(a)==len(b)
-    x = []
-    for i in range(len(a)):
-        x.append( chr(ord(a[i])^ord(b[i])))
-    return ''.join(x)
-
-def xor(a,b):
-    """ XOR two strings """
-    x = []
-    for i in range(min(len(a),len(b))):
-        x.append( chr(ord(a[i])^ord(b[i])))
-    return ''.join(x)
-
-"""
-    Base 'BlockCipher' and Pad classes for cipher instances.
-    BlockCipher supports automatic padding and type conversion. The BlockCipher
-    class was written to make the actual algorithm code more readable and
-    not for performance.
-"""
-
-class BlockCipher:
-    """ Block ciphers """
-    def __init__(self):
-        self.reset()
-
-    def reset(self):
-        self.resetEncrypt()
-        self.resetDecrypt()
-    def resetEncrypt(self):
-        self.encryptBlockCount = 0
-        self.bytesToEncrypt = ''
-    def resetDecrypt(self):
-        self.decryptBlockCount = 0
-        self.bytesToDecrypt = ''
-
-    def encrypt(self, plainText, more = None):
-        """ Encrypt a string and return a binary string """
-        self.bytesToEncrypt += plainText  # append plainText to any bytes from prior encrypt
-        numBlocks, numExtraBytes = divmod(len(self.bytesToEncrypt), self.blockSize)
-        cipherText = ''
-        for i in range(numBlocks):
-            bStart = i*self.blockSize
-            ctBlock = self.encryptBlock(self.bytesToEncrypt[bStart:bStart+self.blockSize])
-            self.encryptBlockCount += 1
-            cipherText += ctBlock
-        if numExtraBytes > 0:        # save any bytes that are not block aligned
-            self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:]
-        else:
-            self.bytesToEncrypt = ''
-
-        if more == None:   # no more data expected from caller
-            finalBytes = self.padding.addPad(self.bytesToEncrypt,self.blockSize)
-            if len(finalBytes) > 0:
-                ctBlock = self.encryptBlock(finalBytes)
-                self.encryptBlockCount += 1
-                cipherText += ctBlock
-            self.resetEncrypt()
-        return cipherText
-
-    def decrypt(self, cipherText, more = None):
-        """ Decrypt a string and return a string """
-        self.bytesToDecrypt += cipherText  # append to any bytes from prior decrypt
-
-        numBlocks, numExtraBytes = divmod(len(self.bytesToDecrypt), self.blockSize)
-        if more == None:  # no more calls to decrypt, should have all the data
-            if numExtraBytes  != 0:
-                raise DecryptNotBlockAlignedError, 'Data not block aligned on decrypt'
-
-        # hold back some bytes in case last decrypt has zero len
-        if (more != None) and (numExtraBytes == 0) and (numBlocks >0) :
-            numBlocks -= 1
-            numExtraBytes = self.blockSize
-
-        plainText = ''
-        for i in range(numBlocks):
-            bStart = i*self.blockSize
-            ptBlock = self.decryptBlock(self.bytesToDecrypt[bStart : bStart+self.blockSize])
-            self.decryptBlockCount += 1
-            plainText += ptBlock
-
-        if numExtraBytes > 0:        # save any bytes that are not block aligned
-            self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:]
-        else:
-            self.bytesToEncrypt = ''
-
-        if more == None:         # last decrypt remove padding
-            plainText = self.padding.removePad(plainText, self.blockSize)
-            self.resetDecrypt()
-        return plainText
-
-
-class Pad:
-    def __init__(self):
-        pass              # eventually could put in calculation of min and max size extension
-
-class padWithPadLen(Pad):
-    """ Pad a binary string with the length of the padding """
-
-    def addPad(self, extraBytes, blockSize):
-        """ Add padding to a binary string to make it an even multiple
-            of the block size """
-        blocks, numExtraBytes = divmod(len(extraBytes), blockSize)
-        padLength = blockSize - numExtraBytes
-        return extraBytes + padLength*chr(padLength)
-
-    def removePad(self, paddedBinaryString, blockSize):
-        """ Remove padding from a binary string """
-        if not(0<len(paddedBinaryString)):
-            raise DecryptNotBlockAlignedError, 'Expected More Data'
-        return paddedBinaryString[:-ord(paddedBinaryString[-1])]
-
-class noPadding(Pad):
-    """ No padding. Use this to get ECB behavior from encrypt/decrypt """
-
-    def addPad(self, extraBytes, blockSize):
-        """ Add no padding """
-        return extraBytes
-
-    def removePad(self, paddedBinaryString, blockSize):
-        """ Remove no padding """
-        return paddedBinaryString
-
-"""
-    Rijndael encryption algorithm
-    This byte oriented implementation is intended to closely
-    match FIPS specification for readability.  It is not implemented
-    for performance.
-"""
-
-class Rijndael(BlockCipher):
-    """ Rijndael encryption algorithm """
-    def __init__(self, key = None, padding = padWithPadLen(), keySize=16, blockSize=16 ):
-        self.name       = 'RIJNDAEL'
-        self.keySize    = keySize
-        self.strength   = keySize*8
-        self.blockSize  = blockSize  # blockSize is in bytes
-        self.padding    = padding    # change default to noPadding() to get normal ECB behavior
-
-        assert( keySize%4==0 and NrTable[4].has_key(keySize/4)),'key size must be 16,20,24,29 or 32 bytes'
-        assert( blockSize%4==0 and NrTable.has_key(blockSize/4)), 'block size must be 16,20,24,29 or 32 bytes'
-
-        self.Nb = self.blockSize/4          # Nb is number of columns of 32 bit words
-        self.Nk = keySize/4                 # Nk is the key length in 32-bit words
-        self.Nr = NrTable[self.Nb][self.Nk] # The number of rounds (Nr) is a function of
-                                            # the block (Nb) and key (Nk) sizes.
-        if key != None:
-            self.setKey(key)
-
-    def setKey(self, key):
-        """ Set a key and generate the expanded key """
-        assert( len(key) == (self.Nk*4) ), 'Key length must be same as keySize parameter'
-        self.__expandedKey = keyExpansion(self, key)
-        self.reset()                   # BlockCipher.reset()
-
-    def encryptBlock(self, plainTextBlock):
-        """ Encrypt a block, plainTextBlock must be a array of bytes [Nb by 4] """
-        self.state = self._toBlock(plainTextBlock)
-        AddRoundKey(self, self.__expandedKey[0:self.Nb])
-        for round in range(1,self.Nr):          #for round = 1 step 1 to Nr
-            SubBytes(self)
-            ShiftRows(self)
-            MixColumns(self)
-            AddRoundKey(self, self.__expandedKey[round*self.Nb:(round+1)*self.Nb])
-        SubBytes(self)
-        ShiftRows(self)
-        AddRoundKey(self, self.__expandedKey[self.Nr*self.Nb:(self.Nr+1)*self.Nb])
-        return self._toBString(self.state)
-
-
-    def decryptBlock(self, encryptedBlock):
-        """ decrypt a block (array of bytes) """
-        self.state = self._toBlock(encryptedBlock)
-        AddRoundKey(self, self.__expandedKey[self.Nr*self.Nb:(self.Nr+1)*self.Nb])
-        for round in range(self.Nr-1,0,-1):
-            InvShiftRows(self)
-            InvSubBytes(self)
-            AddRoundKey(self, self.__expandedKey[round*self.Nb:(round+1)*self.Nb])
-            InvMixColumns(self)
-        InvShiftRows(self)
-        InvSubBytes(self)
-        AddRoundKey(self, self.__expandedKey[0:self.Nb])
-        return self._toBString(self.state)
-
-    def _toBlock(self, bs):
-        """ Convert binary string to array of bytes, state[col][row]"""
-        assert ( len(bs) == 4*self.Nb ), 'Rijndarl blocks must be of size blockSize'
-        return [[ord(bs[4*i]),ord(bs[4*i+1]),ord(bs[4*i+2]),ord(bs[4*i+3])] for i in range(self.Nb)]
-
-    def _toBString(self, block):
-        """ Convert block (array of bytes) to binary string """
-        l = []
-        for col in block:
-            for rowElement in col:
-                l.append(chr(rowElement))
-        return ''.join(l)
-#-------------------------------------
-"""    Number of rounds Nr = NrTable[Nb][Nk]
-
-            Nb  Nk=4   Nk=5   Nk=6   Nk=7   Nk=8
-            -------------------------------------   """
-NrTable =  {4: {4:10,  5:11,  6:12,  7:13,  8:14},
-            5: {4:11,  5:11,  6:12,  7:13,  8:14},
-            6: {4:12,  5:12,  6:12,  7:13,  8:14},
-            7: {4:13,  5:13,  6:13,  7:13,  8:14},
-            8: {4:14,  5:14,  6:14,  7:14,  8:14}}
-#-------------------------------------
-def keyExpansion(algInstance, keyString):
-    """ Expand a string of size keySize into a larger array """
-    Nk, Nb, Nr = algInstance.Nk, algInstance.Nb, algInstance.Nr # for readability
-    key = [ord(byte) for byte in keyString]  # convert string to list
-    w = [[key[4*i],key[4*i+1],key[4*i+2],key[4*i+3]] for i in range(Nk)]
-    for i in range(Nk,Nb*(Nr+1)):
-        temp = w[i-1]        # a four byte column
-        if (i%Nk) == 0 :
-            temp     = temp[1:]+[temp[0]]  # RotWord(temp)
-            temp     = [ Sbox[byte] for byte in temp ]
-            temp[0] ^= Rcon[i/Nk]
-        elif Nk > 6 and  i%Nk == 4 :
-            temp     = [ Sbox[byte] for byte in temp ]  # SubWord(temp)
-        w.append( [ w[i-Nk][byte]^temp[byte] for byte in range(4) ] )
-    return w
-
-Rcon = (0,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36,     # note extra '0' !!!
-        0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,
-        0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91)
-
-#-------------------------------------
-def AddRoundKey(algInstance, keyBlock):
-    """ XOR the algorithm state with a block of key material """
-    for column in range(algInstance.Nb):
-        for row in range(4):
-            algInstance.state[column][row] ^= keyBlock[column][row]
-#-------------------------------------
-
-def SubBytes(algInstance):
-    for column in range(algInstance.Nb):
-        for row in range(4):
-            algInstance.state[column][row] = Sbox[algInstance.state[column][row]]
-
-def InvSubBytes(algInstance):
-    for column in range(algInstance.Nb):
-        for row in range(4):
-            algInstance.state[column][row] = InvSbox[algInstance.state[column][row]]
-
-Sbox =    (0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,
-           0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
-           0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,
-           0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
-           0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,
-           0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
-           0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,
-           0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
-           0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,
-           0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
-           0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,
-           0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
-           0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,
-           0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
-           0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,
-           0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
-           0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,
-           0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
-           0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,
-           0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
-           0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,
-           0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
-           0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,
-           0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
-           0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,
-           0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
-           0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,
-           0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
-           0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,
-           0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
-           0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,
-           0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16)
-
-InvSbox = (0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38,
-           0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb,
-           0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87,
-           0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb,
-           0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d,
-           0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e,
-           0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2,
-           0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25,
-           0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16,
-           0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92,
-           0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda,
-           0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84,
-           0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a,
-           0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06,
-           0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02,
-           0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b,
-           0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea,
-           0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73,
-           0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85,
-           0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e,
-           0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89,
-           0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b,
-           0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20,
-           0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4,
-           0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31,
-           0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f,
-           0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d,
-           0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef,
-           0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0,
-           0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61,
-           0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26,
-           0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d)
-
-#-------------------------------------
-""" For each block size (Nb), the ShiftRow operation shifts row i
-    by the amount Ci.  Note that row 0 is not shifted.
-                 Nb      C1 C2 C3
-               -------------------  """
-shiftOffset  = { 4 : ( 0, 1, 2, 3),
-                 5 : ( 0, 1, 2, 3),
-                 6 : ( 0, 1, 2, 3),
-                 7 : ( 0, 1, 2, 4),
-                 8 : ( 0, 1, 3, 4) }
-def ShiftRows(algInstance):
-    tmp = [0]*algInstance.Nb   # list of size Nb
-    for r in range(1,4):       # row 0 reamains unchanged and can be skipped
-        for c in range(algInstance.Nb):
-            tmp[c] = algInstance.state[(c+shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r]
-        for c in range(algInstance.Nb):
-            algInstance.state[c][r] = tmp[c]
-def InvShiftRows(algInstance):
-    tmp = [0]*algInstance.Nb   # list of size Nb
-    for r in range(1,4):       # row 0 reamains unchanged and can be skipped
-        for c in range(algInstance.Nb):
-            tmp[c] = algInstance.state[(c+algInstance.Nb-shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r]
-        for c in range(algInstance.Nb):
-            algInstance.state[c][r] = tmp[c]
-#-------------------------------------
-def MixColumns(a):
-    Sprime = [0,0,0,0]
-    for j in range(a.Nb):    # for each column
-        Sprime[0] = mul(2,a.state[j][0])^mul(3,a.state[j][1])^mul(1,a.state[j][2])^mul(1,a.state[j][3])
-        Sprime[1] = mul(1,a.state[j][0])^mul(2,a.state[j][1])^mul(3,a.state[j][2])^mul(1,a.state[j][3])
-        Sprime[2] = mul(1,a.state[j][0])^mul(1,a.state[j][1])^mul(2,a.state[j][2])^mul(3,a.state[j][3])
-        Sprime[3] = mul(3,a.state[j][0])^mul(1,a.state[j][1])^mul(1,a.state[j][2])^mul(2,a.state[j][3])
-        for i in range(4):
-            a.state[j][i] = Sprime[i]
-
-def InvMixColumns(a):
-    """ Mix the four bytes of every column in a linear way
-        This is the opposite operation of Mixcolumn """
-    Sprime = [0,0,0,0]
-    for j in range(a.Nb):    # for each column
-        Sprime[0] = mul(0x0E,a.state[j][0])^mul(0x0B,a.state[j][1])^mul(0x0D,a.state[j][2])^mul(0x09,a.state[j][3])
-        Sprime[1] = mul(0x09,a.state[j][0])^mul(0x0E,a.state[j][1])^mul(0x0B,a.state[j][2])^mul(0x0D,a.state[j][3])
-        Sprime[2] = mul(0x0D,a.state[j][0])^mul(0x09,a.state[j][1])^mul(0x0E,a.state[j][2])^mul(0x0B,a.state[j][3])
-        Sprime[3] = mul(0x0B,a.state[j][0])^mul(0x0D,a.state[j][1])^mul(0x09,a.state[j][2])^mul(0x0E,a.state[j][3])
-        for i in range(4):
-            a.state[j][i] = Sprime[i]
-
-#-------------------------------------
-def mul(a, b):
-    """ Multiply two elements of GF(2^m)
-        needed for MixColumn and InvMixColumn """
-    if (a !=0 and  b!=0):
-        return Alogtable[(Logtable[a] + Logtable[b])%255]
-    else:
-        return 0
-
-Logtable = ( 0,   0,  25,   1,  50,   2,  26, 198,  75, 199,  27, 104,  51, 238, 223,   3,
-           100,   4, 224,  14,  52, 141, 129, 239,  76, 113,   8, 200, 248, 105,  28, 193,
-           125, 194,  29, 181, 249, 185,  39, 106,  77, 228, 166, 114, 154, 201,   9, 120,
-           101,  47, 138,   5,  33,  15, 225,  36,  18, 240, 130,  69,  53, 147, 218, 142,
-           150, 143, 219, 189,  54, 208, 206, 148,  19,  92, 210, 241,  64,  70, 131,  56,
-           102, 221, 253,  48, 191,   6, 139,  98, 179,  37, 226, 152,  34, 136, 145,  16,
-           126, 110,  72, 195, 163, 182,  30,  66,  58, 107,  40,  84, 250, 133,  61, 186,
-            43, 121,  10,  21, 155, 159,  94, 202,  78, 212, 172, 229, 243, 115, 167,  87,
-           175,  88, 168,  80, 244, 234, 214, 116,  79, 174, 233, 213, 231, 230, 173, 232,
-            44, 215, 117, 122, 235,  22,  11, 245,  89, 203,  95, 176, 156, 169,  81, 160,
-           127,  12, 246, 111,  23, 196,  73, 236, 216,  67,  31,  45, 164, 118, 123, 183,
-           204, 187,  62,  90, 251,  96, 177, 134,  59,  82, 161, 108, 170,  85,  41, 157,
-           151, 178, 135, 144,  97, 190, 220, 252, 188, 149, 207, 205,  55,  63,  91, 209,
-            83,  57, 132,  60,  65, 162, 109,  71,  20,  42, 158,  93,  86, 242, 211, 171,
-            68,  17, 146, 217,  35,  32,  46, 137, 180, 124, 184,  38, 119, 153, 227, 165,
-           103,  74, 237, 222, 197,  49, 254,  24,  13,  99, 140, 128, 192, 247, 112,   7)
-
-Alogtable= ( 1,   3,   5,  15,  17,  51,  85, 255,  26,  46, 114, 150, 161, 248,  19,  53,
-            95, 225,  56,  72, 216, 115, 149, 164, 247,   2,   6,  10,  30,  34, 102, 170,
-           229,  52,  92, 228,  55,  89, 235,  38, 106, 190, 217, 112, 144, 171, 230,  49,
-            83, 245,   4,  12,  20,  60,  68, 204,  79, 209, 104, 184, 211, 110, 178, 205,
-            76, 212, 103, 169, 224,  59,  77, 215,  98, 166, 241,   8,  24,  40, 120, 136,
-           131, 158, 185, 208, 107, 189, 220, 127, 129, 152, 179, 206,  73, 219, 118, 154,
-           181, 196,  87, 249,  16,  48,  80, 240,  11,  29,  39, 105, 187, 214,  97, 163,
-           254,  25,  43, 125, 135, 146, 173, 236,  47, 113, 147, 174, 233,  32,  96, 160,
-           251,  22,  58,  78, 210, 109, 183, 194,  93, 231,  50,  86, 250,  21,  63,  65,
-           195,  94, 226,  61,  71, 201,  64, 192,  91, 237,  44, 116, 156, 191, 218, 117,
-           159, 186, 213, 100, 172, 239,  42, 126, 130, 157, 188, 223, 122, 142, 137, 128,
-           155, 182, 193,  88, 232,  35, 101, 175, 234,  37, 111, 177, 200,  67, 197,  84,
-           252,  31,  33,  99, 165, 244,   7,   9,  27,  45, 119, 153, 176, 203,  70, 202,
-            69, 207,  74, 222, 121, 139, 134, 145, 168, 227,  62,  66, 198,  81, 243,  14,
-            18,  54,  90, 238,  41, 123, 141, 140, 143, 138, 133, 148, 167, 242,  13,  23,
-            57,  75, 221, 124, 132, 151, 162, 253,  28,  36, 108, 180, 199,  82, 246,   1)
-
-
-
-
-"""
-    AES Encryption Algorithm
-    The AES algorithm is just Rijndael algorithm restricted to the default
-    blockSize of 128 bits.
-"""
-
-class AES(Rijndael):
-    """ The AES algorithm is the Rijndael block cipher restricted to block
-        sizes of 128 bits and key sizes of 128, 192 or 256 bits
-    """
-    def __init__(self, key = None, padding = padWithPadLen(), keySize=16):
-        """ Initialize AES, keySize is in bytes """
-        if  not (keySize == 16 or keySize == 24 or keySize == 32) :
-            raise BadKeySizeError, 'Illegal AES key size, must be 16, 24, or 32 bytes'
-
-        Rijndael.__init__( self, key, padding=padding, keySize=keySize, blockSize=16 )
-
-        self.name       = 'AES'
-
-
-"""
-    CBC mode of encryption for block ciphers.
-    This algorithm mode wraps any BlockCipher to make a
-    Cipher Block Chaining mode.
-"""
-from random             import Random  # should change to crypto.random!!!
-
-
-class CBC(BlockCipher):
-    """ The CBC class wraps block ciphers to make cipher block chaining (CBC) mode
-        algorithms.  The initialization (IV) is automatic if set to None.  Padding
-        is also automatic based on the Pad class used to initialize the algorithm
-    """
-    def __init__(self, blockCipherInstance, padding = padWithPadLen()):
-        """ CBC algorithms are created by initializing with a BlockCipher instance """
-        self.baseCipher = blockCipherInstance
-        self.name       = self.baseCipher.name + '_CBC'
-        self.blockSize  = self.baseCipher.blockSize
-        self.keySize    = self.baseCipher.keySize
-        self.padding    = padding
-        self.baseCipher.padding = noPadding()   # baseCipher should NOT pad!!
-        self.r          = Random()            # for IV generation, currently uses
-                                              # mediocre standard distro version     <----------------
-        import time
-        newSeed = time.ctime()+str(self.r)    # seed with instance location
-        self.r.seed(newSeed)                  # to make unique
-        self.reset()
-
-    def setKey(self, key):
-        self.baseCipher.setKey(key)
-
-    # Overload to reset both CBC state and the wrapped baseCipher
-    def resetEncrypt(self):
-        BlockCipher.resetEncrypt(self)  # reset CBC encrypt state (super class)
-        self.baseCipher.resetEncrypt()  # reset base cipher encrypt state
-
-    def resetDecrypt(self):
-        BlockCipher.resetDecrypt(self)  # reset CBC state (super class)
-        self.baseCipher.resetDecrypt()  # reset base cipher decrypt state
-
-    def encrypt(self, plainText, iv=None, more=None):
-        """ CBC encryption - overloads baseCipher to allow optional explicit IV
-            when iv=None, iv is auto generated!
-        """
-        if self.encryptBlockCount == 0:
-            self.iv = iv
-        else:
-            assert(iv==None), 'IV used only on first call to encrypt'
-
-        return BlockCipher.encrypt(self,plainText, more=more)
-
-    def decrypt(self, cipherText, iv=None, more=None):
-        """ CBC decryption - overloads baseCipher to allow optional explicit IV
-            when iv=None, iv is auto generated!
-        """
-        if self.decryptBlockCount == 0:
-            self.iv = iv
-        else:
-            assert(iv==None), 'IV used only on first call to decrypt'
-
-        return BlockCipher.decrypt(self, cipherText, more=more)
-
-    def encryptBlock(self, plainTextBlock):
-        """ CBC block encryption, IV is set with 'encrypt' """
-        auto_IV = ''
-        if self.encryptBlockCount == 0:
-            if self.iv == None:
-                # generate IV and use
-                self.iv = ''.join([chr(self.r.randrange(256)) for i in range(self.blockSize)])
-                self.prior_encr_CT_block = self.iv
-                auto_IV = self.prior_encr_CT_block    # prepend IV if it's automatic
-            else:                       # application provided IV
-                assert(len(self.iv) == self.blockSize ),'IV must be same length as block'
-                self.prior_encr_CT_block = self.iv
-        """ encrypt the prior CT XORed with the PT """
-        ct = self.baseCipher.encryptBlock( xor(self.prior_encr_CT_block, plainTextBlock) )
-        self.prior_encr_CT_block = ct
-        return auto_IV+ct
-
-    def decryptBlock(self, encryptedBlock):
-        """ Decrypt a single block """
-
-        if self.decryptBlockCount == 0:   # first call, process IV
-            if self.iv == None:    # auto decrypt IV?
-                self.prior_CT_block = encryptedBlock
-                return ''
-            else:
-                assert(len(self.iv)==self.blockSize),"Bad IV size on CBC decryption"
-                self.prior_CT_block = self.iv
-
-        dct = self.baseCipher.decryptBlock(encryptedBlock)
-        """ XOR the prior decrypted CT with the prior CT """
-        dct_XOR_priorCT = xor( self.prior_CT_block, dct )
-
-        self.prior_CT_block = encryptedBlock
-
-        return dct_XOR_priorCT
-
-
-"""
-    AES_CBC Encryption Algorithm
-"""
-
-class AES_CBC(CBC):
-    """ AES encryption in CBC feedback mode """
-    def __init__(self, key=None, padding=padWithPadLen(), keySize=16):
-        CBC.__init__( self, AES(key, noPadding(), keySize), padding)
-        self.name       = 'AES_CBC'
index 26d740ddb0ed3be82128b3fbe0e45471b0e04f4b..1b0ee71c84e93901faa8c1b41e325e32be46a06c 100644 (file)
Binary files a/Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto.dll and b/Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto.dll differ
index e25a0c8227ff869069f615b6cd5ad985c1164a7e..566751130103e0bd0b7a6961c8a9638c8fc3a5be 100644 (file)
 #! /usr/bin/env python
 
-import sys, os
-import hmac
-from struct import pack
-import hashlib
-
-
-# interface to needed routines libalfcrypto
-def _load_libalfcrypto():
-    import ctypes
-    from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \
-        Structure, c_ulong, create_string_buffer, addressof, string_at, cast, sizeof
-
-    pointer_size = ctypes.sizeof(ctypes.c_voidp)
-    name_of_lib = None
-    if sys.platform.startswith('darwin'):
-        name_of_lib = 'libalfcrypto.dylib'
-    elif sys.platform.startswith('win'):
-        if pointer_size == 4:
-            name_of_lib = 'alfcrypto.dll'
+"""
+    Routines for doing AES CBC in one file
+
+    Modified by some_updates to extract
+    and combine only those parts needed for AES CBC
+    into one simple to add python file
+
+    Original Version
+    Copyright (c) 2002 by Paul A. Lambert
+    Under:
+    CryptoPy Artisitic License Version 1.0
+    See the wonderful pure python package cryptopy-1.2.5
+    and read its LICENSE.txt for complete license details.
+"""
+
+class CryptoError(Exception):
+    """ Base class for crypto exceptions """
+    def __init__(self,errorMessage='Error!'):
+        self.message = errorMessage
+    def __str__(self):
+        return self.message
+
+class InitCryptoError(CryptoError):
+    """ Crypto errors during algorithm initialization """
+class BadKeySizeError(InitCryptoError):
+    """ Bad key size error """
+class EncryptError(CryptoError):
+    """ Error in encryption processing """
+class DecryptError(CryptoError):
+    """ Error in decryption processing """
+class DecryptNotBlockAlignedError(DecryptError):
+    """ Error in decryption processing """
+
+def xorS(a,b):
+    """ XOR two strings """
+    assert len(a)==len(b)
+    x = []
+    for i in range(len(a)):
+        x.append( chr(ord(a[i])^ord(b[i])))
+    return ''.join(x)
+
+def xor(a,b):
+    """ XOR two strings """
+    x = []
+    for i in range(min(len(a),len(b))):
+        x.append( chr(ord(a[i])^ord(b[i])))
+    return ''.join(x)
+
+"""
+    Base 'BlockCipher' and Pad classes for cipher instances.
+    BlockCipher supports automatic padding and type conversion. The BlockCipher
+    class was written to make the actual algorithm code more readable and
+    not for performance.
+"""
+
+class BlockCipher:
+    """ Block ciphers """
+    def __init__(self):
+        self.reset()
+
+    def reset(self):
+        self.resetEncrypt()
+        self.resetDecrypt()
+    def resetEncrypt(self):
+        self.encryptBlockCount = 0
+        self.bytesToEncrypt = ''
+    def resetDecrypt(self):
+        self.decryptBlockCount = 0
+        self.bytesToDecrypt = ''
+
+    def encrypt(self, plainText, more = None):
+        """ Encrypt a string and return a binary string """
+        self.bytesToEncrypt += plainText  # append plainText to any bytes from prior encrypt
+        numBlocks, numExtraBytes = divmod(len(self.bytesToEncrypt), self.blockSize)
+        cipherText = ''
+        for i in range(numBlocks):
+            bStart = i*self.blockSize
+            ctBlock = self.encryptBlock(self.bytesToEncrypt[bStart:bStart+self.blockSize])
+            self.encryptBlockCount += 1
+            cipherText += ctBlock
+        if numExtraBytes > 0:        # save any bytes that are not block aligned
+            self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:]
         else:
-            name_of_lib = 'alfcrypto64.dll'
+            self.bytesToEncrypt = ''
+
+        if more == None:   # no more data expected from caller
+            finalBytes = self.padding.addPad(self.bytesToEncrypt,self.blockSize)
+            if len(finalBytes) > 0:
+                ctBlock = self.encryptBlock(finalBytes)
+                self.encryptBlockCount += 1
+                cipherText += ctBlock
+            self.resetEncrypt()
+        return cipherText
+
+    def decrypt(self, cipherText, more = None):
+        """ Decrypt a string and return a string """
+        self.bytesToDecrypt += cipherText  # append to any bytes from prior decrypt
+
+        numBlocks, numExtraBytes = divmod(len(self.bytesToDecrypt), self.blockSize)
+        if more == None:  # no more calls to decrypt, should have all the data
+            if numExtraBytes  != 0:
+                raise DecryptNotBlockAlignedError, 'Data not block aligned on decrypt'
+
+        # hold back some bytes in case last decrypt has zero len
+        if (more != None) and (numExtraBytes == 0) and (numBlocks >0) :
+            numBlocks -= 1
+            numExtraBytes = self.blockSize
+
+        plainText = ''
+        for i in range(numBlocks):
+            bStart = i*self.blockSize
+            ptBlock = self.decryptBlock(self.bytesToDecrypt[bStart : bStart+self.blockSize])
+            self.decryptBlockCount += 1
+            plainText += ptBlock
+
+        if numExtraBytes > 0:        # save any bytes that are not block aligned
+            self.bytesToEncrypt = self.bytesToEncrypt[-numExtraBytes:]
+        else:
+            self.bytesToEncrypt = ''
+
+        if more == None:         # last decrypt remove padding
+            plainText = self.padding.removePad(plainText, self.blockSize)
+            self.resetDecrypt()
+        return plainText
+
+
+class Pad:
+    def __init__(self):
+        pass              # eventually could put in calculation of min and max size extension
+
+class padWithPadLen(Pad):
+    """ Pad a binary string with the length of the padding """
+
+    def addPad(self, extraBytes, blockSize):
+        """ Add padding to a binary string to make it an even multiple
+            of the block size """
+        blocks, numExtraBytes = divmod(len(extraBytes), blockSize)
+        padLength = blockSize - numExtraBytes
+        return extraBytes + padLength*chr(padLength)
+
+    def removePad(self, paddedBinaryString, blockSize):
+        """ Remove padding from a binary string """
+        if not(0<len(paddedBinaryString)):
+            raise DecryptNotBlockAlignedError, 'Expected More Data'
+        return paddedBinaryString[:-ord(paddedBinaryString[-1])]
+
+class noPadding(Pad):
+    """ No padding. Use this to get ECB behavior from encrypt/decrypt """
+
+    def addPad(self, extraBytes, blockSize):
+        """ Add no padding """
+        return extraBytes
+
+    def removePad(self, paddedBinaryString, blockSize):
+        """ Remove no padding """
+        return paddedBinaryString
+
+"""
+    Rijndael encryption algorithm
+    This byte oriented implementation is intended to closely
+    match FIPS specification for readability.  It is not implemented
+    for performance.
+"""
+
+class Rijndael(BlockCipher):
+    """ Rijndael encryption algorithm """
+    def __init__(self, key = None, padding = padWithPadLen(), keySize=16, blockSize=16 ):
+        self.name       = 'RIJNDAEL'
+        self.keySize    = keySize
+        self.strength   = keySize*8
+        self.blockSize  = blockSize  # blockSize is in bytes
+        self.padding    = padding    # change default to noPadding() to get normal ECB behavior
+
+        assert( keySize%4==0 and NrTable[4].has_key(keySize/4)),'key size must be 16,20,24,29 or 32 bytes'
+        assert( blockSize%4==0 and NrTable.has_key(blockSize/4)), 'block size must be 16,20,24,29 or 32 bytes'
+
+        self.Nb = self.blockSize/4          # Nb is number of columns of 32 bit words
+        self.Nk = keySize/4                 # Nk is the key length in 32-bit words
+        self.Nr = NrTable[self.Nb][self.Nk] # The number of rounds (Nr) is a function of
+                                            # the block (Nb) and key (Nk) sizes.
+        if key != None:
+            self.setKey(key)
+
+    def setKey(self, key):
+        """ Set a key and generate the expanded key """
+        assert( len(key) == (self.Nk*4) ), 'Key length must be same as keySize parameter'
+        self.__expandedKey = keyExpansion(self, key)
+        self.reset()                   # BlockCipher.reset()
+
+    def encryptBlock(self, plainTextBlock):
+        """ Encrypt a block, plainTextBlock must be a array of bytes [Nb by 4] """
+        self.state = self._toBlock(plainTextBlock)
+        AddRoundKey(self, self.__expandedKey[0:self.Nb])
+        for round in range(1,self.Nr):          #for round = 1 step 1 to Nr
+            SubBytes(self)
+            ShiftRows(self)
+            MixColumns(self)
+            AddRoundKey(self, self.__expandedKey[round*self.Nb:(round+1)*self.Nb])
+        SubBytes(self)
+        ShiftRows(self)
+        AddRoundKey(self, self.__expandedKey[self.Nr*self.Nb:(self.Nr+1)*self.Nb])
+        return self._toBString(self.state)
+
+
+    def decryptBlock(self, encryptedBlock):
+        """ decrypt a block (array of bytes) """
+        self.state = self._toBlock(encryptedBlock)
+        AddRoundKey(self, self.__expandedKey[self.Nr*self.Nb:(self.Nr+1)*self.Nb])
+        for round in range(self.Nr-1,0,-1):
+            InvShiftRows(self)
+            InvSubBytes(self)
+            AddRoundKey(self, self.__expandedKey[round*self.Nb:(round+1)*self.Nb])
+            InvMixColumns(self)
+        InvShiftRows(self)
+        InvSubBytes(self)
+        AddRoundKey(self, self.__expandedKey[0:self.Nb])
+        return self._toBString(self.state)
+
+    def _toBlock(self, bs):
+        """ Convert binary string to array of bytes, state[col][row]"""
+        assert ( len(bs) == 4*self.Nb ), 'Rijndarl blocks must be of size blockSize'
+        return [[ord(bs[4*i]),ord(bs[4*i+1]),ord(bs[4*i+2]),ord(bs[4*i+3])] for i in range(self.Nb)]
+
+    def _toBString(self, block):
+        """ Convert block (array of bytes) to binary string """
+        l = []
+        for col in block:
+            for rowElement in col:
+                l.append(chr(rowElement))
+        return ''.join(l)
+#-------------------------------------
+"""    Number of rounds Nr = NrTable[Nb][Nk]
+
+            Nb  Nk=4   Nk=5   Nk=6   Nk=7   Nk=8
+            -------------------------------------   """
+NrTable =  {4: {4:10,  5:11,  6:12,  7:13,  8:14},
+            5: {4:11,  5:11,  6:12,  7:13,  8:14},
+            6: {4:12,  5:12,  6:12,  7:13,  8:14},
+            7: {4:13,  5:13,  6:13,  7:13,  8:14},
+            8: {4:14,  5:14,  6:14,  7:14,  8:14}}
+#-------------------------------------
+def keyExpansion(algInstance, keyString):
+    """ Expand a string of size keySize into a larger array """
+    Nk, Nb, Nr = algInstance.Nk, algInstance.Nb, algInstance.Nr # for readability
+    key = [ord(byte) for byte in keyString]  # convert string to list
+    w = [[key[4*i],key[4*i+1],key[4*i+2],key[4*i+3]] for i in range(Nk)]
+    for i in range(Nk,Nb*(Nr+1)):
+        temp = w[i-1]        # a four byte column
+        if (i%Nk) == 0 :
+            temp     = temp[1:]+[temp[0]]  # RotWord(temp)
+            temp     = [ Sbox[byte] for byte in temp ]
+            temp[0] ^= Rcon[i/Nk]
+        elif Nk > 6 and  i%Nk == 4 :
+            temp     = [ Sbox[byte] for byte in temp ]  # SubWord(temp)
+        w.append( [ w[i-Nk][byte]^temp[byte] for byte in range(4) ] )
+    return w
+
+Rcon = (0,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36,     # note extra '0' !!!
+        0x6c,0xd8,0xab,0x4d,0x9a,0x2f,0x5e,0xbc,0x63,0xc6,
+        0x97,0x35,0x6a,0xd4,0xb3,0x7d,0xfa,0xef,0xc5,0x91)
+
+#-------------------------------------
+def AddRoundKey(algInstance, keyBlock):
+    """ XOR the algorithm state with a block of key material """
+    for column in range(algInstance.Nb):
+        for row in range(4):
+            algInstance.state[column][row] ^= keyBlock[column][row]
+#-------------------------------------
+
+def SubBytes(algInstance):
+    for column in range(algInstance.Nb):
+        for row in range(4):
+            algInstance.state[column][row] = Sbox[algInstance.state[column][row]]
+
+def InvSubBytes(algInstance):
+    for column in range(algInstance.Nb):
+        for row in range(4):
+            algInstance.state[column][row] = InvSbox[algInstance.state[column][row]]
+
+Sbox =    (0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,
+           0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
+           0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,
+           0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
+           0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,
+           0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
+           0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,
+           0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
+           0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,
+           0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
+           0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,
+           0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
+           0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,
+           0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
+           0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,
+           0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
+           0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,
+           0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
+           0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,
+           0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
+           0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,
+           0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
+           0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,
+           0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
+           0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,
+           0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
+           0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,
+           0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
+           0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,
+           0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
+           0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,
+           0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16)
+
+InvSbox = (0x52,0x09,0x6a,0xd5,0x30,0x36,0xa5,0x38,
+           0xbf,0x40,0xa3,0x9e,0x81,0xf3,0xd7,0xfb,
+           0x7c,0xe3,0x39,0x82,0x9b,0x2f,0xff,0x87,
+           0x34,0x8e,0x43,0x44,0xc4,0xde,0xe9,0xcb,
+           0x54,0x7b,0x94,0x32,0xa6,0xc2,0x23,0x3d,
+           0xee,0x4c,0x95,0x0b,0x42,0xfa,0xc3,0x4e,
+           0x08,0x2e,0xa1,0x66,0x28,0xd9,0x24,0xb2,
+           0x76,0x5b,0xa2,0x49,0x6d,0x8b,0xd1,0x25,
+           0x72,0xf8,0xf6,0x64,0x86,0x68,0x98,0x16,
+           0xd4,0xa4,0x5c,0xcc,0x5d,0x65,0xb6,0x92,
+           0x6c,0x70,0x48,0x50,0xfd,0xed,0xb9,0xda,
+           0x5e,0x15,0x46,0x57,0xa7,0x8d,0x9d,0x84,
+           0x90,0xd8,0xab,0x00,0x8c,0xbc,0xd3,0x0a,
+           0xf7,0xe4,0x58,0x05,0xb8,0xb3,0x45,0x06,
+           0xd0,0x2c,0x1e,0x8f,0xca,0x3f,0x0f,0x02,
+           0xc1,0xaf,0xbd,0x03,0x01,0x13,0x8a,0x6b,
+           0x3a,0x91,0x11,0x41,0x4f,0x67,0xdc,0xea,
+           0x97,0xf2,0xcf,0xce,0xf0,0xb4,0xe6,0x73,
+           0x96,0xac,0x74,0x22,0xe7,0xad,0x35,0x85,
+           0xe2,0xf9,0x37,0xe8,0x1c,0x75,0xdf,0x6e,
+           0x47,0xf1,0x1a,0x71,0x1d,0x29,0xc5,0x89,
+           0x6f,0xb7,0x62,0x0e,0xaa,0x18,0xbe,0x1b,
+           0xfc,0x56,0x3e,0x4b,0xc6,0xd2,0x79,0x20,
+           0x9a,0xdb,0xc0,0xfe,0x78,0xcd,0x5a,0xf4,
+           0x1f,0xdd,0xa8,0x33,0x88,0x07,0xc7,0x31,
+           0xb1,0x12,0x10,0x59,0x27,0x80,0xec,0x5f,
+           0x60,0x51,0x7f,0xa9,0x19,0xb5,0x4a,0x0d,
+           0x2d,0xe5,0x7a,0x9f,0x93,0xc9,0x9c,0xef,
+           0xa0,0xe0,0x3b,0x4d,0xae,0x2a,0xf5,0xb0,
+           0xc8,0xeb,0xbb,0x3c,0x83,0x53,0x99,0x61,
+           0x17,0x2b,0x04,0x7e,0xba,0x77,0xd6,0x26,
+           0xe1,0x69,0x14,0x63,0x55,0x21,0x0c,0x7d)
+
+#-------------------------------------
+""" For each block size (Nb), the ShiftRow operation shifts row i
+    by the amount Ci.  Note that row 0 is not shifted.
+                 Nb      C1 C2 C3
+               -------------------  """
+shiftOffset  = { 4 : ( 0, 1, 2, 3),
+                 5 : ( 0, 1, 2, 3),
+                 6 : ( 0, 1, 2, 3),
+                 7 : ( 0, 1, 2, 4),
+                 8 : ( 0, 1, 3, 4) }
+def ShiftRows(algInstance):
+    tmp = [0]*algInstance.Nb   # list of size Nb
+    for r in range(1,4):       # row 0 reamains unchanged and can be skipped
+        for c in range(algInstance.Nb):
+            tmp[c] = algInstance.state[(c+shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r]
+        for c in range(algInstance.Nb):
+            algInstance.state[c][r] = tmp[c]
+def InvShiftRows(algInstance):
+    tmp = [0]*algInstance.Nb   # list of size Nb
+    for r in range(1,4):       # row 0 reamains unchanged and can be skipped
+        for c in range(algInstance.Nb):
+            tmp[c] = algInstance.state[(c+algInstance.Nb-shiftOffset[algInstance.Nb][r]) % algInstance.Nb][r]
+        for c in range(algInstance.Nb):
+            algInstance.state[c][r] = tmp[c]
+#-------------------------------------
+def MixColumns(a):
+    Sprime = [0,0,0,0]
+    for j in range(a.Nb):    # for each column
+        Sprime[0] = mul(2,a.state[j][0])^mul(3,a.state[j][1])^mul(1,a.state[j][2])^mul(1,a.state[j][3])
+        Sprime[1] = mul(1,a.state[j][0])^mul(2,a.state[j][1])^mul(3,a.state[j][2])^mul(1,a.state[j][3])
+        Sprime[2] = mul(1,a.state[j][0])^mul(1,a.state[j][1])^mul(2,a.state[j][2])^mul(3,a.state[j][3])
+        Sprime[3] = mul(3,a.state[j][0])^mul(1,a.state[j][1])^mul(1,a.state[j][2])^mul(2,a.state[j][3])
+        for i in range(4):
+            a.state[j][i] = Sprime[i]
+
+def InvMixColumns(a):
+    """ Mix the four bytes of every column in a linear way
+        This is the opposite operation of Mixcolumn """
+    Sprime = [0,0,0,0]
+    for j in range(a.Nb):    # for each column
+        Sprime[0] = mul(0x0E,a.state[j][0])^mul(0x0B,a.state[j][1])^mul(0x0D,a.state[j][2])^mul(0x09,a.state[j][3])
+        Sprime[1] = mul(0x09,a.state[j][0])^mul(0x0E,a.state[j][1])^mul(0x0B,a.state[j][2])^mul(0x0D,a.state[j][3])
+        Sprime[2] = mul(0x0D,a.state[j][0])^mul(0x09,a.state[j][1])^mul(0x0E,a.state[j][2])^mul(0x0B,a.state[j][3])
+        Sprime[3] = mul(0x0B,a.state[j][0])^mul(0x0D,a.state[j][1])^mul(0x09,a.state[j][2])^mul(0x0E,a.state[j][3])
+        for i in range(4):
+            a.state[j][i] = Sprime[i]
+
+#-------------------------------------
+def mul(a, b):
+    """ Multiply two elements of GF(2^m)
+        needed for MixColumn and InvMixColumn """
+    if (a !=0 and  b!=0):
+        return Alogtable[(Logtable[a] + Logtable[b])%255]
     else:
-        if pointer_size == 4:
-            name_of_lib = 'libalfcrypto32.so'
+        return 0
+
+Logtable = ( 0,   0,  25,   1,  50,   2,  26, 198,  75, 199,  27, 104,  51, 238, 223,   3,
+           100,   4, 224,  14,  52, 141, 129, 239,  76, 113,   8, 200, 248, 105,  28, 193,
+           125, 194,  29, 181, 249, 185,  39, 106,  77, 228, 166, 114, 154, 201,   9, 120,
+           101,  47, 138,   5,  33,  15, 225,  36,  18, 240, 130,  69,  53, 147, 218, 142,
+           150, 143, 219, 189,  54, 208, 206, 148,  19,  92, 210, 241,  64,  70, 131,  56,
+           102, 221, 253,  48, 191,   6, 139,  98, 179,  37, 226, 152,  34, 136, 145,  16,
+           126, 110,  72, 195, 163, 182,  30,  66,  58, 107,  40,  84, 250, 133,  61, 186,
+            43, 121,  10,  21, 155, 159,  94, 202,  78, 212, 172, 229, 243, 115, 167,  87,
+           175,  88, 168,  80, 244, 234, 214, 116,  79, 174, 233, 213, 231, 230, 173, 232,
+            44, 215, 117, 122, 235,  22,  11, 245,  89, 203,  95, 176, 156, 169,  81, 160,
+           127,  12, 246, 111,  23, 196,  73, 236, 216,  67,  31,  45, 164, 118, 123, 183,
+           204, 187,  62,  90, 251,  96, 177, 134,  59,  82, 161, 108, 170,  85,  41, 157,
+           151, 178, 135, 144,  97, 190, 220, 252, 188, 149, 207, 205,  55,  63,  91, 209,
+            83,  57, 132,  60,  65, 162, 109,  71,  20,  42, 158,  93,  86, 242, 211, 171,
+            68,  17, 146, 217,  35,  32,  46, 137, 180, 124, 184,  38, 119, 153, 227, 165,
+           103,  74, 237, 222, 197,  49, 254,  24,  13,  99, 140, 128, 192, 247, 112,   7)
+
+Alogtable= ( 1,   3,   5,  15,  17,  51,  85, 255,  26,  46, 114, 150, 161, 248,  19,  53,
+            95, 225,  56,  72, 216, 115, 149, 164, 247,   2,   6,  10,  30,  34, 102, 170,
+           229,  52,  92, 228,  55,  89, 235,  38, 106, 190, 217, 112, 144, 171, 230,  49,
+            83, 245,   4,  12,  20,  60,  68, 204,  79, 209, 104, 184, 211, 110, 178, 205,
+            76, 212, 103, 169, 224,  59,  77, 215,  98, 166, 241,   8,  24,  40, 120, 136,
+           131, 158, 185, 208, 107, 189, 220, 127, 129, 152, 179, 206,  73, 219, 118, 154,
+           181, 196,  87, 249,  16,  48,  80, 240,  11,  29,  39, 105, 187, 214,  97, 163,
+           254,  25,  43, 125, 135, 146, 173, 236,  47, 113, 147, 174, 233,  32,  96, 160,
+           251,  22,  58,  78, 210, 109, 183, 194,  93, 231,  50,  86, 250,  21,  63,  65,
+           195,  94, 226,  61,  71, 201,  64, 192,  91, 237,  44, 116, 156, 191, 218, 117,
+           159, 186, 213, 100, 172, 239,  42, 126, 130, 157, 188, 223, 122, 142, 137, 128,
+           155, 182, 193,  88, 232,  35, 101, 175, 234,  37, 111, 177, 200,  67, 197,  84,
+           252,  31,  33,  99, 165, 244,   7,   9,  27,  45, 119, 153, 176, 203,  70, 202,
+            69, 207,  74, 222, 121, 139, 134, 145, 168, 227,  62,  66, 198,  81, 243,  14,
+            18,  54,  90, 238,  41, 123, 141, 140, 143, 138, 133, 148, 167, 242,  13,  23,
+            57,  75, 221, 124, 132, 151, 162, 253,  28,  36, 108, 180, 199,  82, 246,   1)
+
+
+
+
+"""
+    AES Encryption Algorithm
+    The AES algorithm is just Rijndael algorithm restricted to the default
+    blockSize of 128 bits.
+"""
+
+class AES(Rijndael):
+    """ The AES algorithm is the Rijndael block cipher restricted to block
+        sizes of 128 bits and key sizes of 128, 192 or 256 bits
+    """
+    def __init__(self, key = None, padding = padWithPadLen(), keySize=16):
+        """ Initialize AES, keySize is in bytes """
+        if  not (keySize == 16 or keySize == 24 or keySize == 32) :
+            raise BadKeySizeError, 'Illegal AES key size, must be 16, 24, or 32 bytes'
+
+        Rijndael.__init__( self, key, padding=padding, keySize=keySize, blockSize=16 )
+
+        self.name       = 'AES'
+
+
+"""
+    CBC mode of encryption for block ciphers.
+    This algorithm mode wraps any BlockCipher to make a
+    Cipher Block Chaining mode.
+"""
+from random             import Random  # should change to crypto.random!!!
+
+
+class CBC(BlockCipher):
+    """ The CBC class wraps block ciphers to make cipher block chaining (CBC) mode
+        algorithms.  The initialization (IV) is automatic if set to None.  Padding
+        is also automatic based on the Pad class used to initialize the algorithm
+    """
+    def __init__(self, blockCipherInstance, padding = padWithPadLen()):
+        """ CBC algorithms are created by initializing with a BlockCipher instance """
+        self.baseCipher = blockCipherInstance
+        self.name       = self.baseCipher.name + '_CBC'
+        self.blockSize  = self.baseCipher.blockSize
+        self.keySize    = self.baseCipher.keySize
+        self.padding    = padding
+        self.baseCipher.padding = noPadding()   # baseCipher should NOT pad!!
+        self.r          = Random()            # for IV generation, currently uses
+                                              # mediocre standard distro version     <----------------
+        import time
+        newSeed = time.ctime()+str(self.r)    # seed with instance location
+        self.r.seed(newSeed)                  # to make unique
+        self.reset()
+
+    def setKey(self, key):
+        self.baseCipher.setKey(key)
+
+    # Overload to reset both CBC state and the wrapped baseCipher
+    def resetEncrypt(self):
+        BlockCipher.resetEncrypt(self)  # reset CBC encrypt state (super class)
+        self.baseCipher.resetEncrypt()  # reset base cipher encrypt state
+
+    def resetDecrypt(self):
+        BlockCipher.resetDecrypt(self)  # reset CBC state (super class)
+        self.baseCipher.resetDecrypt()  # reset base cipher decrypt state
+
+    def encrypt(self, plainText, iv=None, more=None):
+        """ CBC encryption - overloads baseCipher to allow optional explicit IV
+            when iv=None, iv is auto generated!
+        """
+        if self.encryptBlockCount == 0:
+            self.iv = iv
         else:
-            name_of_lib = 'libalfcrypto64.so'
-    
-    libalfcrypto = sys.path[0] + os.sep + name_of_lib
-
-    if not os.path.isfile(libalfcrypto):
-        raise Exception('libalfcrypto not found')
-
-    libalfcrypto = CDLL(libalfcrypto)
-
-    c_char_pp = POINTER(c_char_p)
-    c_int_p = POINTER(c_int)
-
-
-    def F(restype, name, argtypes):
-        func = getattr(libalfcrypto, name)
-        func.restype = restype
-        func.argtypes = argtypes
-        return func
-
-    # aes cbc decryption
-    #
-    # struct aes_key_st {
-    # unsigned long rd_key[4 *(AES_MAXNR + 1)];
-    # int rounds;
-    # };
-    #
-    # typedef struct aes_key_st AES_KEY;
-    #
-    # int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key);
-    #
-    # 
-    # void AES_cbc_encrypt(const unsigned char *in, unsigned char *out,
-    # const unsigned long length, const AES_KEY *key,
-    # unsigned char *ivec, const int enc);
-
-    AES_MAXNR = 14
-
-    class AES_KEY(Structure):
-        _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)]
-
-    AES_KEY_p = POINTER(AES_KEY)
-    AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p, c_int])
-    AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p])
-
-
-
-    # Pukall 1 Cipher
-    # unsigned char *PC1(const unsigned char *key, unsigned int klen, const unsigned char *src,
-    #                unsigned char *dest, unsigned int len, int decryption);
-
-    PC1 = F(c_char_p, 'PC1', [c_char_p, c_ulong, c_char_p, c_char_p, c_ulong, c_ulong])
-
-    # Topaz Encryption
-    # typedef struct _TpzCtx {
-    #    unsigned int v[2];
-    # } TpzCtx;
-    #
-    # void topazCryptoInit(TpzCtx *ctx, const unsigned char *key, int klen);
-    # void topazCryptoDecrypt(const TpzCtx *ctx, const unsigned char *in, unsigned char *out, int len);
-
-    class TPZ_CTX(Structure):
-        _fields_ = [('v', c_long * 2)]
-
-    TPZ_CTX_p = POINTER(TPZ_CTX)
-    topazCryptoInit = F(None, 'topazCryptoInit', [TPZ_CTX_p, c_char_p, c_ulong])
-    topazCryptoDecrypt = F(None, 'topazCryptoDecrypt', [TPZ_CTX_p, c_char_p, c_char_p, c_ulong])
-
-
-    class AES_CBC(object):
-        def __init__(self):
-            self._blocksize = 0
-            self._keyctx = None
-            self._iv = 0
-
-        def set_decrypt_key(self, userkey, iv):
-            self._blocksize = len(userkey)
-            if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
-                raise Exception('AES CBC improper key used')
-                return
-            keyctx = self._keyctx = AES_KEY()
-            self._iv = iv
-            rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
-            if rv < 0:
-                raise Exception('Failed to initialize AES CBC key')
-
-        def decrypt(self, data):
-            out = create_string_buffer(len(data))
-            mutable_iv = create_string_buffer(self._iv, len(self._iv))
-            rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, mutable_iv, 0)
-            if rv == 0:
-                raise Exception('AES CBC decryption failed')
-            return out.raw
-
-    class Pukall_Cipher(object):
-        def __init__(self):
-            self.key = None
-
-        def PC1(self, key, src, decryption=True):
-            self.key = key
-            out = create_string_buffer(len(src))
-            de = 0
-            if decryption:
-                de = 1
-            rv = PC1(key, len(key), src, out, len(src), de)
-            return out.raw
-
-    class Topaz_Cipher(object):
-        def __init__(self):
-            self._ctx = None
-
-        def ctx_init(self, key):
-            tpz_ctx = self._ctx = TPZ_CTX()
-            topazCryptoInit(tpz_ctx, key, len(key))
-            return tpz_ctx
-
-        def decrypt(self, data,  ctx=None):
-            if ctx == None:
-                ctx = self._ctx
-            out = create_string_buffer(len(data))
-            topazCryptoDecrypt(ctx, data, out, len(data))
-            return out.raw
-
-    print "Using Library AlfCrypto DLL/DYLIB/SO"
-    return (AES_CBC, Pukall_Cipher, Topaz_Cipher)
-
-
-def _load_python_alfcrypto():
-
-    import aescbc
-
-    class Pukall_Cipher(object):
-        def __init__(self):
-            self.key = None
-
-        def PC1(self, key, src, decryption=True):
-            sum1 = 0;
-            sum2 = 0;
-            keyXorVal = 0;
-            if len(key)!=16:
-                print "Bad key length!"
-                return None
-            wkey = []
-            for i in xrange(8):
-                wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
-            dst = ""
-            for i in xrange(len(src)):
-                temp1 = 0;
-                byteXorVal = 0;
-                for j in xrange(8):
-                    temp1 ^= wkey[j]
-                    sum2  = (sum2+j)*20021 + sum1
-                    sum1  = (temp1*346)&0xFFFF
-                    sum2  = (sum2+sum1)&0xFFFF
-                    temp1 = (temp1*20021+1)&0xFFFF
-                    byteXorVal ^= temp1 ^ sum2
-                curByte = ord(src[i])
-                if not decryption:
-                    keyXorVal = curByte * 257;
-                curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
-                if decryption:
-                    keyXorVal = curByte * 257;
-                for j in xrange(8):
-                    wkey[j] ^= keyXorVal;
-                dst+=chr(curByte)
-            return dst
-
-    class Topaz_Cipher(object):
-        def __init__(self):
-            self._ctx = None
-
-        def ctx_init(self, key):
-            ctx1 = 0x0CAFFE19E
-            for keyChar in key:
-                keyByte = ord(keyChar)
-                ctx2 = ctx1
-                ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF )
-            self._ctx = [ctx1, ctx2]
-            return [ctx1,ctx2]
-
-        def decrypt(self, data,  ctx=None):
-            if ctx == None:
-                ctx = self._ctx
-            ctx1 = ctx[0]
-            ctx2 = ctx[1]
-            plainText = ""
-            for dataChar in data:
-                dataByte = ord(dataChar)
-                m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF
-                ctx2 = ctx1
-                ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF)
-                plainText += chr(m)
-            return plainText
-
-    class AES_CBC(object):
-        def __init__(self):
-            self._key = None
-            self._iv = None
-            self.aes = None
-
-        def set_decrypt_key(self, userkey, iv):
-            self._key = userkey
-            self._iv = iv
-            self.aes = aescbc.AES_CBC(userkey, aescbc.noPadding(), len(userkey))
-
-        def decrypt(self, data):
-            iv = self._iv
-            cleartext = self.aes.decrypt(iv + data)
-            return cleartext
-
-    return (AES_CBC, Pukall_Cipher, Topaz_Cipher)
-
-
-def _load_crypto():
-    AES_CBC = Pukall_Cipher = Topaz_Cipher = None
-    cryptolist = (_load_libalfcrypto, _load_python_alfcrypto)
-    for loader in cryptolist:
-        try:
-            AES_CBC, Pukall_Cipher, Topaz_Cipher = loader()
-            break
-        except (ImportError, Exception):
-            pass
-    return AES_CBC, Pukall_Cipher, Topaz_Cipher
-
-AES_CBC, Pukall_Cipher, Topaz_Cipher = _load_crypto()
-
-
-class KeyIVGen(object):
-    # this only exists in openssl so we will use pure python implementation instead
-    # PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1',
-    #                             [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
-    def pbkdf2(self, passwd, salt, iter, keylen):
-
-        def xorstr( a, b ):
-            if len(a) != len(b):
-                raise Exception("xorstr(): lengths differ")
-            return ''.join((chr(ord(x)^ord(y)) for x, y in zip(a, b)))
-
-        def prf( h, data ):
-            hm = h.copy()
-            hm.update( data )
-            return hm.digest()
-
-        def pbkdf2_F( h, salt, itercount, blocknum ):
-            U = prf( h, salt + pack('>i',blocknum ) )
-            T = U
-            for i in range(2, itercount+1):
-                U = prf( h, U )
-                T = xorstr( T, U )
-            return T
-
-        sha = hashlib.sha1
-        digest_size = sha().digest_size
-        # l - number of output blocks to produce
-        l = keylen / digest_size
-        if keylen % digest_size != 0:
-            l += 1
-        h = hmac.new( passwd, None, sha )
-        T = ""
-        for i in range(1, l+1):
-            T += pbkdf2_F( h, salt, iter, i )
-        return T[0: keylen]
+            assert(iv==None), 'IV used only on first call to encrypt'
 
+        return BlockCipher.encrypt(self,plainText, more=more)
 
+    def decrypt(self, cipherText, iv=None, more=None):
+        """ CBC decryption - overloads baseCipher to allow optional explicit IV
+            when iv=None, iv is auto generated!
+        """
+        if self.decryptBlockCount == 0:
+            self.iv = iv
+        else:
+            assert(iv==None), 'IV used only on first call to decrypt'
+
+        return BlockCipher.decrypt(self, cipherText, more=more)
+
+    def encryptBlock(self, plainTextBlock):
+        """ CBC block encryption, IV is set with 'encrypt' """
+        auto_IV = ''
+        if self.encryptBlockCount == 0:
+            if self.iv == None:
+                # generate IV and use
+                self.iv = ''.join([chr(self.r.randrange(256)) for i in range(self.blockSize)])
+                self.prior_encr_CT_block = self.iv
+                auto_IV = self.prior_encr_CT_block    # prepend IV if it's automatic
+            else:                       # application provided IV
+                assert(len(self.iv) == self.blockSize ),'IV must be same length as block'
+                self.prior_encr_CT_block = self.iv
+        """ encrypt the prior CT XORed with the PT """
+        ct = self.baseCipher.encryptBlock( xor(self.prior_encr_CT_block, plainTextBlock) )
+        self.prior_encr_CT_block = ct
+        return auto_IV+ct
+
+    def decryptBlock(self, encryptedBlock):
+        """ Decrypt a single block """
+
+        if self.decryptBlockCount == 0:   # first call, process IV
+            if self.iv == None:    # auto decrypt IV?
+                self.prior_CT_block = encryptedBlock
+                return ''
+            else:
+                assert(len(self.iv)==self.blockSize),"Bad IV size on CBC decryption"
+                self.prior_CT_block = self.iv
+
+        dct = self.baseCipher.decryptBlock(encryptedBlock)
+        """ XOR the prior decrypted CT with the prior CT """
+        dct_XOR_priorCT = xor( self.prior_CT_block, dct )
+
+        self.prior_CT_block = encryptedBlock
+
+        return dct_XOR_priorCT
+
+
+"""
+    AES_CBC Encryption Algorithm
+"""
+
+class AES_CBC(CBC):
+    """ AES encryption in CBC feedback mode """
+    def __init__(self, key=None, padding=padWithPadLen(), keySize=16):
+        CBC.__init__( self, AES(key, noPadding(), keySize), padding)
+        self.name       = 'AES_CBC'
index 7bef68eac0d0a751b0c7090f5b5427a1d5ab1a59..26d740ddb0ed3be82128b3fbe0e45471b0e04f4b 100644 (file)
Binary files a/Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto64.dll and b/Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto64.dll differ
index 269810cfa24541f8be10050e7192fb6211a45a98..e25a0c8227ff869069f615b6cd5ad985c1164a7e 100644 (file)
Binary files a/Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto_src.zip and b/Calibre_Plugins/K4MobiDeDRM_plugin/alfcrypto_src.zip differ
index 7be7a8a82b0c345412bd3ec93ac440c6dfd4851d..7bef68eac0d0a751b0c7090f5b5427a1d5ab1a59 100644 (file)
Binary files a/Calibre_Plugins/K4MobiDeDRM_plugin/cmbtc_v2.2.py and b/Calibre_Plugins/K4MobiDeDRM_plugin/cmbtc_v2.2.py differ
index c029760955bd4d6bed120397667051ddfdde3f1d..269810cfa24541f8be10050e7192fb6211a45a98 100644 (file)
Binary files a/Calibre_Plugins/K4MobiDeDRM_plugin/config.py and b/Calibre_Plugins/K4MobiDeDRM_plugin/config.py differ
index c412d7b1ba2e38eb8c61ab119fd2b2877cccde2a..98258788c86dbb40a3f2e773d185baad750d4cdf 100644 (file)
-#! /usr/bin/python
-# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
-# For use with Topaz Scripts Version 2.6
+from PyQt4.Qt import QWidget, QVBoxLayout, QLabel, QLineEdit
 
-class Unbuffered:
-    def __init__(self, stream):
-        self.stream = stream
-    def write(self, data):
-        self.stream.write(data)
-        self.stream.flush()
-    def __getattr__(self, attr):
-        return getattr(self.stream, attr)
+from calibre.utils.config import JSONConfig
 
-import sys
-sys.stdout=Unbuffered(sys.stdout)
+# This is where all preferences for this plugin will be stored
+# You should always prefix your config file name with plugins/,
+# so as to ensure you dont accidentally clobber a calibre config file
+prefs = JSONConfig('plugins/K4MobiDeDRM')
 
-import csv
-import os
-import getopt
-from struct import pack
-from struct import unpack
+# Set defaults
+prefs.defaults['pids'] = ""
+prefs.defaults['serials'] = ""
+prefs.defaults['WINEPREFIX'] = None
 
-class TpzDRMError(Exception):
-    pass
 
-# Get a 7 bit encoded number from string. The most
-# significant byte comes first and has the high bit (8th) set
+class ConfigWidget(QWidget):
 
-def readEncodedNumber(file):
-    flag = False
-    c = file.read(1)
-    if (len(c) == 0):
-        return None
-    data = ord(c)
+    def __init__(self):
+        QWidget.__init__(self)
+        self.l = QVBoxLayout()
+        self.setLayout(self.l)
 
-    if data == 0xFF:
-        flag = True
-        c = file.read(1)
-        if (len(c) == 0):
-            return None
-        data = ord(c)
+        self.serialLabel = QLabel('eInk Kindle Serial numbers (First character B, 16 characters, use commas if more than one)')
+        self.l.addWidget(self.serialLabel)
 
-    if data >= 0x80:
-        datax = (data & 0x7F)
-        while data >= 0x80 :
-            c = file.read(1)
-            if (len(c) == 0):
-                return None
-            data = ord(c)
-            datax = (datax <<7) + (data & 0x7F)
-        data = datax
+        self.serials = QLineEdit(self)
+        self.serials.setText(prefs['serials'])
+        self.l.addWidget(self.serials)
+        self.serialLabel.setBuddy(self.serials)
 
-    if flag:
-        data = -data
-    return data
+        self.pidLabel = QLabel('Mobipocket PIDs (8 or 10 characters, use commas if more than one)')
+        self.l.addWidget(self.pidLabel)
 
+        self.pids = QLineEdit(self)
+        self.pids.setText(prefs['pids'])
+        self.l.addWidget(self.pids)
+        self.pidLabel.setBuddy(self.serials)
 
-# returns a binary string that encodes a number into 7 bits
-# most significant byte first which has the high bit set
+        self.wpLabel = QLabel('For Linux only: WINEPREFIX (enter absolute path)')
+        self.l.addWidget(self.wpLabel)
 
-def encodeNumber(number):
-    result = ""
-    negative = False
-    flag = 0
-
-    if number < 0 :
-        number = -number + 1
-        negative = True
-
-    while True:
-        byte = number & 0x7F
-        number = number >> 7
-        byte += flag
-        result += chr(byte)
-        flag = 0x80
-        if number == 0 :
-            if (byte == 0xFF and negative == False) :
-                result += chr(0x80)
-            break
-
-    if negative:
-        result += chr(0xFF)
-
-    return result[::-1]
-
-
-
-# create / read  a length prefixed string from the file
-
-def lengthPrefixString(data):
-    return encodeNumber(len(data))+data
-
-def readString(file):
-    stringLength = readEncodedNumber(file)
-    if (stringLength == None):
-        return ""
-    sv = file.read(stringLength)
-    if (len(sv)  != stringLength):
-        return ""
-    return unpack(str(stringLength)+"s",sv)[0]
-
-
-# convert a binary string generated by encodeNumber (7 bit encoded number)
-# to the value you would find inside the page*.dat files to be processed
-
-def convert(i):
-    result = ''
-    val = encodeNumber(i)
-    for j in xrange(len(val)):
-        c = ord(val[j:j+1])
-        result += '%02x' % c
-    return result
-
-
-
-# the complete string table used to store all book text content
-# as well as the xml tokens and values that make sense out of it
-
-class Dictionary(object):
-    def __init__(self, dictFile):
-        self.filename = dictFile
-        self.size = 0
-        self.fo = file(dictFile,'rb')
-        self.stable = []
-        self.size = readEncodedNumber(self.fo)
-        for i in xrange(self.size):
-            self.stable.append(self.escapestr(readString(self.fo)))
-        self.pos = 0
-
-    def escapestr(self, str):
-        str = str.replace('&','&amp;')
-        str = str.replace('<','&lt;')
-        str = str.replace('>','&gt;')
-        str = str.replace('=','&#61;')
-        return str
-
-    def lookup(self,val):
-        if ((val >= 0) and (val < self.size)) :
-            self.pos = val
-            return self.stable[self.pos]
-        else:
-            print "Error - %d outside of string table limits" % val
-            raise TpzDRMError('outside of string table limits')
-            # sys.exit(-1)
-
-    def getSize(self):
-        return self.size
-
-    def getPos(self):
-        return self.pos
-
-    def dumpDict(self):
-        for i in xrange(self.size):
-            print "%d %s %s" % (i, convert(i), self.stable[i])
-        return
-
-# parses the xml snippets that are represented by each page*.dat file.
-# also parses the other0.dat file - the main stylesheet
-# and information used to inject the xml snippets into page*.dat files
-
-class PageParser(object):
-    def __init__(self, filename, dict, debug, flat_xml):
-        self.fo = file(filename,'rb')
-        self.id = os.path.basename(filename).replace('.dat','')
-        self.dict = dict
-        self.debug = debug
-        self.flat_xml = flat_xml
-        self.tagpath = []
-        self.doc = []
-        self.snippetList = []
-
-
-    # hash table used to enable the decoding process
-    # This has all been developed by trial and error so it may still have omissions or
-    # contain errors
-    # Format:
-    # tag : (number of arguments, argument type, subtags present, special case of subtags presents when escaped)
-
-    token_tags = {
-        'x'            : (1, 'scalar_number', 0, 0),
-        'y'            : (1, 'scalar_number', 0, 0),
-        'h'            : (1, 'scalar_number', 0, 0),
-        'w'            : (1, 'scalar_number', 0, 0),
-        'firstWord'    : (1, 'scalar_number', 0, 0),
-        'lastWord'     : (1, 'scalar_number', 0, 0),
-        'rootID'       : (1, 'scalar_number', 0, 0),
-        'stemID'       : (1, 'scalar_number', 0, 0),
-        'type'         : (1, 'scalar_text', 0, 0),
-
-        'info'            : (0, 'number', 1, 0),
-
-        'info.word'            : (0, 'number', 1, 1),
-        'info.word.ocrText'    : (1, 'text', 0, 0),
-        'info.word.firstGlyph' : (1, 'raw', 0, 0),
-        'info.word.lastGlyph'  : (1, 'raw', 0, 0),
-        'info.word.bl'         : (1, 'raw', 0, 0),
-        'info.word.link_id'    : (1, 'number', 0, 0),
-
-        'glyph'           : (0, 'number', 1, 1),
-        'glyph.x'         : (1, 'number', 0, 0),
-        'glyph.y'         : (1, 'number', 0, 0),
-        'glyph.glyphID'   : (1, 'number', 0, 0),
-
-        'dehyphen'          : (0, 'number', 1, 1),
-        'dehyphen.rootID'   : (1, 'number', 0, 0),
-        'dehyphen.stemID'   : (1, 'number', 0, 0),
-        'dehyphen.stemPage' : (1, 'number', 0, 0),
-        'dehyphen.sh'       : (1, 'number', 0, 0),
-
-        'links'        : (0, 'number', 1, 1),
-        'links.page'   : (1, 'number', 0, 0),
-        'links.rel'    : (1, 'number', 0, 0),
-        'links.row'    : (1, 'number', 0, 0),
-        'links.title'  : (1, 'text', 0, 0),
-        'links.href'   : (1, 'text', 0, 0),
-        'links.type'   : (1, 'text', 0, 0),
-        'links.id'     : (1, 'number', 0, 0),
-
-        'paraCont'          : (0, 'number', 1, 1),
-        'paraCont.rootID'   : (1, 'number', 0, 0),
-        'paraCont.stemID'   : (1, 'number', 0, 0),
-        'paraCont.stemPage' : (1, 'number', 0, 0),
-
-        'paraStems'        : (0, 'number', 1, 1),
-        'paraStems.stemID' : (1, 'number', 0, 0),
-
-        'wordStems'          : (0, 'number', 1, 1),
-        'wordStems.stemID'   : (1, 'number', 0, 0),
-
-        'empty'          : (1, 'snippets', 1, 0),
-
-        'page'           : (1, 'snippets', 1, 0),
-        'page.pageid'    : (1, 'scalar_text', 0, 0),
-        'page.pagelabel' : (1, 'scalar_text', 0, 0),
-        'page.type'      : (1, 'scalar_text', 0, 0),
-        'page.h'         : (1, 'scalar_number', 0, 0),
-        'page.w'         : (1, 'scalar_number', 0, 0),
-        'page.startID' : (1, 'scalar_number', 0, 0),
-
-        'group'           : (1, 'snippets', 1, 0),
-        'group.type'      : (1, 'scalar_text', 0, 0),
-        'group._tag'      : (1, 'scalar_text', 0, 0),
-        'group.orientation': (1, 'scalar_text', 0, 0),
-
-        'region'           : (1, 'snippets', 1, 0),
-        'region.type'      : (1, 'scalar_text', 0, 0),
-        'region.x'         : (1, 'scalar_number', 0, 0),
-        'region.y'         : (1, 'scalar_number', 0, 0),
-        'region.h'         : (1, 'scalar_number', 0, 0),
-        'region.w'         : (1, 'scalar_number', 0, 0),
-        'region.orientation' : (1, 'scalar_text', 0, 0),
-
-        'empty_text_region' : (1, 'snippets', 1, 0),
-
-        'img'           : (1, 'snippets', 1, 0),
-        'img.x'         : (1, 'scalar_number', 0, 0),
-        'img.y'         : (1, 'scalar_number', 0, 0),
-        'img.h'         : (1, 'scalar_number', 0, 0),
-        'img.w'         : (1, 'scalar_number', 0, 0),
-        'img.src'       : (1, 'scalar_number', 0, 0),
-        'img.color_src' : (1, 'scalar_number', 0, 0),
-
-        'paragraph'           : (1, 'snippets', 1, 0),
-        'paragraph.class'     : (1, 'scalar_text', 0, 0),
-        'paragraph.firstWord' : (1, 'scalar_number', 0, 0),
-        'paragraph.lastWord'  : (1, 'scalar_number', 0, 0),
-        'paragraph.lastWord'  : (1, 'scalar_number', 0, 0),
-        'paragraph.gridSize'  : (1, 'scalar_number', 0, 0),
-        'paragraph.gridBottomCenter'  : (1, 'scalar_number', 0, 0),
-        'paragraph.gridTopCenter' : (1, 'scalar_number', 0, 0),
-        'paragraph.gridBeginCenter' : (1, 'scalar_number', 0, 0),
-        'paragraph.gridEndCenter' : (1, 'scalar_number', 0, 0),
-
-
-        'word_semantic'           : (1, 'snippets', 1, 1),
-        'word_semantic.type'      : (1, 'scalar_text', 0, 0),
-        'word_semantic.firstWord' : (1, 'scalar_number', 0, 0),
-        'word_semantic.lastWord'  : (1, 'scalar_number', 0, 0),
-
-        'word'            : (1, 'snippets', 1, 0),
-        'word.type'       : (1, 'scalar_text', 0, 0),
-        'word.class'      : (1, 'scalar_text', 0, 0),
-        'word.firstGlyph' : (1, 'scalar_number', 0, 0),
-        'word.lastGlyph'  : (1, 'scalar_number', 0, 0),
-
-        '_span'           : (1, 'snippets', 1, 0),
-        '_span.firstWord' : (1, 'scalar_number', 0, 0),
-        '_span.lastWord'  : (1, 'scalar_number', 0, 0),
-        '_span.gridSize'  : (1, 'scalar_number', 0, 0),
-        '_span.gridBottomCenter'  : (1, 'scalar_number', 0, 0),
-        '_span.gridTopCenter' : (1, 'scalar_number', 0, 0),
-        '_span.gridBeginCenter' : (1, 'scalar_number', 0, 0),
-        '_span.gridEndCenter' : (1, 'scalar_number', 0, 0),
-
-        'span'           : (1, 'snippets', 1, 0),
-        'span.firstWord' : (1, 'scalar_number', 0, 0),
-        'span.lastWord'  : (1, 'scalar_number', 0, 0),
-        'span.gridSize'  : (1, 'scalar_number', 0, 0),
-        'span.gridBottomCenter'  : (1, 'scalar_number', 0, 0),
-        'span.gridTopCenter' : (1, 'scalar_number', 0, 0),
-        'span.gridBeginCenter' : (1, 'scalar_number', 0, 0),
-        'span.gridEndCenter' : (1, 'scalar_number', 0, 0),
-
-        'extratokens'            : (1, 'snippets', 1, 0),
-        'extratokens.type'       : (1, 'scalar_text', 0, 0),
-        'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0),
-        'extratokens.lastGlyph'  : (1, 'scalar_number', 0, 0),
-
-        'glyph.h'      : (1, 'number', 0, 0),
-        'glyph.w'      : (1, 'number', 0, 0),
-        'glyph.use'    : (1, 'number', 0, 0),
-        'glyph.vtx'    : (1, 'number', 0, 1),
-        'glyph.len'    : (1, 'number', 0, 1),
-        'glyph.dpi'    : (1, 'number', 0, 0),
-        'vtx'          : (0, 'number', 1, 1),
-        'vtx.x'        : (1, 'number', 0, 0),
-        'vtx.y'        : (1, 'number', 0, 0),
-        'len'          : (0, 'number', 1, 1),
-        'len.n'        : (1, 'number', 0, 0),
-
-        'book'         : (1, 'snippets', 1, 0),
-        'version'      : (1, 'snippets', 1, 0),
-        'version.FlowEdit_1_id'            : (1, 'scalar_text', 0, 0),
-        'version.FlowEdit_1_version'       : (1, 'scalar_text', 0, 0),
-        'version.Schema_id'                : (1, 'scalar_text', 0, 0),
-        'version.Schema_version'           : (1, 'scalar_text', 0, 0),
-        'version.Topaz_version'            : (1, 'scalar_text', 0, 0),
-        'version.WordDetailEdit_1_id'      : (1, 'scalar_text', 0, 0),
-        'version.WordDetailEdit_1_version' : (1, 'scalar_text', 0, 0),
-        'version.ZoneEdit_1_id'            : (1, 'scalar_text', 0, 0),
-        'version.ZoneEdit_1_version'       : (1, 'scalar_text', 0, 0),
-        'version.chapterheaders'           : (1, 'scalar_text', 0, 0),
-        'version.creation_date'            : (1, 'scalar_text', 0, 0),
-        'version.header_footer'            : (1, 'scalar_text', 0, 0),
-        'version.init_from_ocr'            : (1, 'scalar_text', 0, 0),
-        'version.letter_insertion'         : (1, 'scalar_text', 0, 0),
-        'version.xmlinj_convert'           : (1, 'scalar_text', 0, 0),
-        'version.xmlinj_reflow'            : (1, 'scalar_text', 0, 0),
-        'version.xmlinj_transform'         : (1, 'scalar_text', 0, 0),
-        'version.findlists'                : (1, 'scalar_text', 0, 0),
-        'version.page_num'                 : (1, 'scalar_text', 0, 0),
-        'version.page_type'                : (1, 'scalar_text', 0, 0),
-        'version.bad_text'                 : (1, 'scalar_text', 0, 0),
-        'version.glyph_mismatch'           : (1, 'scalar_text', 0, 0),
-        'version.margins'                  : (1, 'scalar_text', 0, 0),
-        'version.staggered_lines'          : (1, 'scalar_text', 0, 0),
-        'version.paragraph_continuation'   : (1, 'scalar_text', 0, 0),
-        'version.toc'                      : (1, 'scalar_text', 0, 0),
-
-        'stylesheet'   : (1, 'snippets', 1, 0),
-        'style'              : (1, 'snippets', 1, 0),
-        'style._tag'         : (1, 'scalar_text', 0, 0),
-        'style.type'         : (1, 'scalar_text', 0, 0),
-        'style._parent_type' : (1, 'scalar_text', 0, 0),
-        'style.class'        : (1, 'scalar_text', 0, 0),
-        'style._after_class' : (1, 'scalar_text', 0, 0),
-        'rule'               : (1, 'snippets', 1, 0),
-        'rule.attr'          : (1, 'scalar_text', 0, 0),
-        'rule.value'         : (1, 'scalar_text', 0, 0),
-
-        'original'      : (0, 'number', 1, 1),
-        'original.pnum' : (1, 'number', 0, 0),
-        'original.pid'  : (1, 'text', 0, 0),
-        'pages'        : (0, 'number', 1, 1),
-        'pages.ref'    : (1, 'number', 0, 0),
-        'pages.id'     : (1, 'number', 0, 0),
-        'startID'      : (0, 'number', 1, 1),
-        'startID.page' : (1, 'number', 0, 0),
-        'startID.id'   : (1, 'number', 0, 0),
-
-     }
-
-
-    # full tag path record keeping routines
-    def tag_push(self, token):
-        self.tagpath.append(token)
-    def tag_pop(self):
-        if len(self.tagpath) > 0 :
-            self.tagpath.pop()
-    def tagpath_len(self):
-        return len(self.tagpath)
-    def get_tagpath(self, i):
-        cnt = len(self.tagpath)
-        if i < cnt : result = self.tagpath[i]
-        for j in xrange(i+1, cnt) :
-            result += '.' + self.tagpath[j]
-        return result
-
-
-    # list of absolute command byte values values that indicate
-    # various types of loop meachanisms typically used to generate vectors
-
-    cmd_list = (0x76, 0x76)
-
-    # peek at and return 1 byte that is ahead by i bytes
-    def peek(self, aheadi):
-        c = self.fo.read(aheadi)
-        if (len(c) == 0):
-            return None
-        self.fo.seek(-aheadi,1)
-        c = c[-1:]
-        return ord(c)
-
-
-    # get the next value from the file being processed
-    def getNext(self):
-        nbyte = self.peek(1);
-        if (nbyte == None):
-            return None
-        val = readEncodedNumber(self.fo)
-        return val
-
-
-    # format an arg by argtype
-    def formatArg(self, arg, argtype):
-        if (argtype == 'text') or (argtype == 'scalar_text') :
-            result = self.dict.lookup(arg)
-        elif (argtype == 'raw') or (argtype == 'number') or (argtype == 'scalar_number') :
-            result = arg
-        elif (argtype == 'snippets') :
-            result = arg
-        else :
-            print "Error Unknown argtype %s" % argtype
-            sys.exit(-2)
-        return result
-
-
-    # process the next tag token, recursively handling subtags,
-    # arguments, and commands
-    def procToken(self, token):
-
-        known_token = False
-        self.tag_push(token)
-
-        if self.debug : print 'Processing: ', self.get_tagpath(0)
-        cnt = self.tagpath_len()
-        for j in xrange(cnt):
-            tkn = self.get_tagpath(j)
-            if tkn in self.token_tags :
-                num_args = self.token_tags[tkn][0]
-                argtype = self.token_tags[tkn][1]
-                subtags = self.token_tags[tkn][2]
-                splcase = self.token_tags[tkn][3]
-                ntags = -1
-                known_token = True
-                break
-
-        if known_token :
-
-            # handle subtags if present
-            subtagres = []
-            if (splcase == 1):
-                # this type of tag uses of escape marker 0x74 indicate subtag count
-                if self.peek(1) == 0x74:
-                    skip = readEncodedNumber(self.fo)
-                    subtags = 1
-                    num_args = 0
-
-            if (subtags == 1):
-                ntags = readEncodedNumber(self.fo)
-                if self.debug : print 'subtags: ' + token + ' has ' + str(ntags)
-                for j in xrange(ntags):
-                    val = readEncodedNumber(self.fo)
-                    subtagres.append(self.procToken(self.dict.lookup(val)))
-
-            # arguments can be scalars or vectors of text or numbers
-            argres = []
-            if num_args > 0 :
-                firstarg = self.peek(1)
-                if (firstarg in self.cmd_list) and (argtype != 'scalar_number') and (argtype != 'scalar_text'):
-                    # single argument is a variable length vector of data
-                    arg = readEncodedNumber(self.fo)
-                    argres = self.decodeCMD(arg,argtype)
-                else :
-                    # num_arg scalar arguments
-                    for i in xrange(num_args):
-                        argres.append(self.formatArg(readEncodedNumber(self.fo), argtype))
-
-            # build the return tag
-            result = []
-            tkn = self.get_tagpath(0)
-            result.append(tkn)
-            result.append(subtagres)
-            result.append(argtype)
-            result.append(argres)
-            self.tag_pop()
-            return result
-
-        # all tokens that need to be processed should be in the hash
-        # table if it may indicate a problem, either new token
-        # or an out of sync condition
+        self.wineprefix = QLineEdit(self)
+        wineprefix = prefs['WINEPREFIX']
+        if wineprefix is not None:
+            self.wineprefix.setText(wineprefix)
         else:
-            result = []
-            if (self.debug):
-                print 'Unknown Token:', token
-            self.tag_pop()
-            return result
-
-
-    # special loop used to process code snippets
-    # it is NEVER used to format arguments.
-    # builds the snippetList
-    def doLoop72(self, argtype):
-        cnt = readEncodedNumber(self.fo)
-        if self.debug :
-            result = 'Set of '+ str(cnt) + ' xml snippets. The overall structure \n'
-            result += 'of the document is indicated by snippet number sets at the\n'
-            result += 'end of each snippet. \n'
-            print result
-        for i in xrange(cnt):
-            if self.debug: print 'Snippet:',str(i)
-            snippet = []
-            snippet.append(i)
-            val = readEncodedNumber(self.fo)
-            snippet.append(self.procToken(self.dict.lookup(val)))
-            self.snippetList.append(snippet)
-        return
-
-
-
-    # general loop code gracisouly submitted by "skindle" - thank you!
-    def doLoop76Mode(self, argtype, cnt, mode):
-        result = []
-        adj = 0
-        if mode & 1:
-            adj = readEncodedNumber(self.fo)
-        mode = mode >> 1
-        x = []
-        for i in xrange(cnt):
-            x.append(readEncodedNumber(self.fo) - adj)
-        for i in xrange(mode):
-            for j in xrange(1, cnt):
-                x[j] = x[j] + x[j - 1]
-        for i in xrange(cnt):
-            result.append(self.formatArg(x[i],argtype))
-        return result
-
-
-    # dispatches loop commands bytes with various modes
-    # The 0x76 style loops are used to build vectors
+            self.wineprefix.setText('')
 
-    # This was all derived by trial and error and
-    # new loop types may exist that are not handled here
-    # since they did not appear in the test cases
+        self.l.addWidget(self.wineprefix)
+        self.wpLabel.setBuddy(self.wineprefix)
 
-    def decodeCMD(self, cmd, argtype):
-        if (cmd == 0x76):
-
-            # loop with cnt, and mode to control loop styles
-            cnt = readEncodedNumber(self.fo)
-            mode = readEncodedNumber(self.fo)
-
-            if self.debug : print 'Loop for', cnt, 'with  mode', mode,  ':  '
-            return self.doLoop76Mode(argtype, cnt, mode)
-
-        if self.dbug: print  "Unknown command", cmd
-        result = []
-        return result
-
-
-
-    # add full tag path to injected snippets
-    def updateName(self, tag, prefix):
-        name = tag[0]
-        subtagList = tag[1]
-        argtype = tag[2]
-        argList = tag[3]
-        nname = prefix + '.' + name
-        nsubtaglist = []
-        for j in subtagList:
-            nsubtaglist.append(self.updateName(j,prefix))
-        ntag = []
-        ntag.append(nname)
-        ntag.append(nsubtaglist)
-        ntag.append(argtype)
-        ntag.append(argList)
-        return ntag
-
-
-
-    # perform depth first injection of specified snippets into this one
-    def injectSnippets(self, snippet):
-        snipno, tag = snippet
-        name = tag[0]
-        subtagList = tag[1]
-        argtype = tag[2]
-        argList = tag[3]
-        nsubtagList = []
-        if len(argList) > 0 :
-            for j in argList:
-                asnip = self.snippetList[j]
-                aso, atag = self.injectSnippets(asnip)
-                atag = self.updateName(atag, name)
-                nsubtagList.append(atag)
-        argtype='number'
-        argList=[]
-        if len(nsubtagList) > 0 :
-            subtagList.extend(nsubtagList)
-        tag = []
-        tag.append(name)
-        tag.append(subtagList)
-        tag.append(argtype)
-        tag.append(argList)
-        snippet = []
-        snippet.append(snipno)
-        snippet.append(tag)
-        return snippet
-
-
-
-    # format the tag for output
-    def formatTag(self, node):
-        name = node[0]
-        subtagList = node[1]
-        argtype = node[2]
-        argList = node[3]
-        fullpathname = name.split('.')
-        nodename = fullpathname.pop()
-        ilvl = len(fullpathname)
-        indent = ' ' * (3 * ilvl)
-        rlst = []
-        rlst.append(indent + '<' + nodename + '>')
-        if len(argList) > 0:
-            alst = []
-            for j in argList:
-                if (argtype == 'text') or (argtype == 'scalar_text') :
-                    alst.append(j + '|')
-                else :
-                    alst.append(str(j) + ',')
-            argres = "".join(alst)
-            argres = argres[0:-1]
-            if argtype == 'snippets' :
-                rlst.append('snippets:' + argres)
-            else :
-                rlst.append(argres)
-        if len(subtagList) > 0 :
-            rlst.append('\n')
-            for j in subtagList:
-                if len(j) > 0 :
-                    rlst.append(self.formatTag(j))
-            rlst.append(indent + '</' + nodename + '>\n')
+    def save_settings(self):
+       prefs['pids'] = str(self.pids.text()).replace(" ","")
+        prefs['serials'] = str(self.serials.text()).replace(" ","")
+        winepref=str(self.wineprefix.text())
+        if winepref.strip() != '':
+            prefs['WINEPREFIX'] = winepref
         else:
-            rlst.append('</' + nodename + '>\n')
-        return "".join(rlst)
-
-
-    # flatten tag
-    def flattenTag(self, node):
-        name = node[0]
-        subtagList = node[1]
-        argtype = node[2]
-        argList = node[3]
-        rlst = []
-        rlst.append(name)
-        if (len(argList) > 0):
-            alst = []
-            for j in argList:
-                if (argtype == 'text') or (argtype == 'scalar_text') :
-                    alst.append(j + '|')
-                else :
-                    alst.append(str(j) + '|')
-            argres = "".join(alst)
-            argres = argres[0:-1]
-            if argtype == 'snippets' :
-                rlst.append('.snippets=' + argres)
-            else :
-                rlst.append('=' + argres)
-        rlst.append('\n')
-        for j in subtagList:
-            if len(j) > 0 :
-                rlst.append(self.flattenTag(j))
-        return "".join(rlst)
-
-
-    # reduce create xml output
-    def formatDoc(self, flat_xml):
-        rlst = []
-        for j in self.doc :
-            if len(j) > 0:
-                if flat_xml:
-                    rlst.append(self.flattenTag(j))
-                else:
-                    rlst.append(self.formatTag(j))
-        result = "".join(rlst)
-        if self.debug : print result
-        return result
-
-
-
-    # main loop - parse the page.dat files
-    # to create structured document and snippets
-
-    # FIXME: value at end of magic appears to be a subtags count
-    # but for what?  For now, inject an 'info" tag as it is in
-    # every dictionary and seems close to what is meant
-    # The alternative is to special case the last _ "0x5f" to mean something
-
-    def process(self):
-
-        # peek at the first bytes to see what type of file it is
-        magic = self.fo.read(9)
-        if (magic[0:1] == 'p') and (magic[2:9] == 'marker_'):
-            first_token = 'info'
-        elif (magic[0:1] == 'p') and (magic[2:9] == '__PAGE_'):
-            skip = self.fo.read(2)
-            first_token = 'info'
-        elif (magic[0:1] == 'p') and (magic[2:8] == '_PAGE_'):
-            first_token = 'info'
-        elif (magic[0:1] == 'g') and (magic[2:9] == '__GLYPH'):
-            skip = self.fo.read(3)
-            first_token = 'info'
-        else :
-            # other0.dat file
-            first_token = None
-            self.fo.seek(-9,1)
-
-
-        # main loop to read and build the document tree
-        while True:
-
-            if first_token != None :
-                # use "inserted" first token 'info' for page and glyph files
-                tag = self.procToken(first_token)
-                if len(tag) > 0 :
-                    self.doc.append(tag)
-                first_token = None
-
-            v = self.getNext()
-            if (v == None):
-                break
-
-            if (v == 0x72):
-                self.doLoop72('number')
-            elif (v > 0) and (v < self.dict.getSize()) :
-                tag = self.procToken(self.dict.lookup(v))
-                if len(tag) > 0 :
-                    self.doc.append(tag)
-            else:
-                if self.debug:
-                    print "Main Loop:  Unknown value: %x" % v
-                if (v == 0):
-                    if (self.peek(1) == 0x5f):
-                        skip = self.fo.read(1)
-                        first_token = 'info'
-
-        # now do snippet injection
-        if len(self.snippetList) > 0 :
-            if self.debug : print 'Injecting Snippets:'
-            snippet = self.injectSnippets(self.snippetList[0])
-            snipno = snippet[0]
-            tag_add = snippet[1]
-            if self.debug : print self.formatTag(tag_add)
-            if len(tag_add) > 0:
-                self.doc.append(tag_add)
-
-        # handle generation of xml output
-        xmlpage = self.formatDoc(self.flat_xml)
-
-        return xmlpage
-
-
-def fromData(dict, fname):
-    flat_xml = True
-    debug = False
-    pp = PageParser(fname, dict, debug, flat_xml)
-    xmlpage = pp.process()
-    return xmlpage
-
-def getXML(dict, fname):
-    flat_xml = False
-    debug = False
-    pp = PageParser(fname, dict, debug, flat_xml)
-    xmlpage = pp.process()
-    return xmlpage
-
-def usage():
-    print 'Usage: '
-    print '    convert2xml.py dict0000.dat infile.dat '
-    print ' '
-    print ' Options:'
-    print '   -h            print this usage help message '
-    print '   -d            turn on debug output to check for potential errors '
-    print '   --flat-xml    output the flattened xml page description only '
-    print ' '
-    print '     This program will attempt to convert a page*.dat file or '
-    print ' glyphs*.dat file, using the dict0000.dat file, to its xml description. '
-    print ' '
-    print ' Use "cmbtc_dump.py" first to unencrypt, uncompress, and dump '
-    print ' the *.dat files from a Topaz format e-book.'
-
-#
-# Main
-#
-
-def main(argv):
-    dictFile = ""
-    pageFile = ""
-    debug = False
-    flat_xml = False
-    printOutput = False
-    if len(argv) == 0:
-        printOutput = True
-        argv = sys.argv
-
-    try:
-        opts, args = getopt.getopt(argv[1:], "hd", ["flat-xml"])
-
-    except getopt.GetoptError, err:
-
-        # print help information and exit:
-        print str(err) # will print something like "option -a not recognized"
-        usage()
-        sys.exit(2)
-
-    if len(opts) == 0 and len(args) == 0 :
-        usage()
-        sys.exit(2)
-
-    for o, a in opts:
-        if o =="-d":
-            debug=True
-        if o =="-h":
-            usage()
-            sys.exit(0)
-        if o =="--flat-xml":
-            flat_xml = True
-
-    dictFile, pageFile = args[0], args[1]
-
-    # read in the string table dictionary
-    dict = Dictionary(dictFile)
-    # dict.dumpDict()
-
-    # create a page parser
-    pp = PageParser(pageFile, dict, debug, flat_xml)
-
-    xmlpage = pp.process()
-
-    if printOutput:
-        print xmlpage
-        return 0
-
-    return xmlpage
-
-if __name__ == '__main__':
-    sys.exit(main(''))
+            prefs['WINEPREFIX'] = None
index e5647f4bc3842abb1546088d41b2c899fa88c36a..3cdc820035684d0d3dfe606b54de768977ec6652 100644 (file)
Binary files a/Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2html.py and b/Calibre_Plugins/K4MobiDeDRM_plugin/flatxml2html.py differ
index 4dfd6c7bbfae633633100610805ddbb13334fd46..c412d7b1ba2e38eb8c61ab119fd2b2877cccde2a 100644 (file)
 #! /usr/bin/python
 # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
+# For use with Topaz Scripts Version 2.6
+
+class Unbuffered:
+    def __init__(self, stream):
+        self.stream = stream
+    def write(self, data):
+        self.stream.write(data)
+        self.stream.flush()
+    def __getattr__(self, attr):
+        return getattr(self.stream, attr)
 
 import sys
+sys.stdout=Unbuffered(sys.stdout)
+
 import csv
 import os
 import getopt
 from struct import pack
 from struct import unpack
 
+class TpzDRMError(Exception):
+    pass
 
-class PParser(object):
-    def __init__(self, gd, flatxml, meta_array):
-        self.gd = gd
-        self.flatdoc = flatxml.split('\n')
-        self.docSize = len(self.flatdoc)
-        self.temp = []
-
-        self.ph = -1
-        self.pw = -1
-        startpos = self.posinDoc('page.h') or self.posinDoc('book.h')
-        for p in startpos:
-            (name, argres) = self.lineinDoc(p)
-            self.ph = max(self.ph, int(argres))
-        startpos = self.posinDoc('page.w') or self.posinDoc('book.w')
-        for p in startpos:
-            (name, argres) = self.lineinDoc(p)
-            self.pw = max(self.pw, int(argres))
-
-        if self.ph <= 0:
-            self.ph = int(meta_array.get('pageHeight', '11000'))
-        if self.pw <= 0:
-            self.pw = int(meta_array.get('pageWidth', '8500'))
-
-        res = []
-        startpos = self.posinDoc('info.glyph.x')
-        for p in startpos:
-            argres = self.getDataatPos('info.glyph.x', p)
-            res.extend(argres)
-        self.gx = res
-
-        res = []
-        startpos = self.posinDoc('info.glyph.y')
-        for p in startpos:
-            argres = self.getDataatPos('info.glyph.y', p)
-            res.extend(argres)
-        self.gy = res
-
-        res = []
-        startpos = self.posinDoc('info.glyph.glyphID')
-        for p in startpos:
-            argres = self.getDataatPos('info.glyph.glyphID', p)
-            res.extend(argres)
-        self.gid = res
-
-
-    # return tag at line pos in document
-    def lineinDoc(self, pos) :
-        if (pos >= 0) and (pos < self.docSize) :
-            item = self.flatdoc[pos]
-            if item.find('=') >= 0:
-                (name, argres) = item.split('=',1)
-            else :
-                name = item
-                argres = ''
-        return name, argres
-
-    # find tag in doc if within pos to end inclusive
-    def findinDoc(self, tagpath, pos, end) :
-        result = None
-        if end == -1 :
-            end = self.docSize
+# Get a 7 bit encoded number from string. The most
+# significant byte comes first and has the high bit (8th) set
+
+def readEncodedNumber(file):
+    flag = False
+    c = file.read(1)
+    if (len(c) == 0):
+        return None
+    data = ord(c)
+
+    if data == 0xFF:
+        flag = True
+        c = file.read(1)
+        if (len(c) == 0):
+            return None
+        data = ord(c)
+
+    if data >= 0x80:
+        datax = (data & 0x7F)
+        while data >= 0x80 :
+            c = file.read(1)
+            if (len(c) == 0):
+                return None
+            data = ord(c)
+            datax = (datax <<7) + (data & 0x7F)
+        data = datax
+
+    if flag:
+        data = -data
+    return data
+
+
+# returns a binary string that encodes a number into 7 bits
+# most significant byte first which has the high bit set
+
+def encodeNumber(number):
+    result = ""
+    negative = False
+    flag = 0
+
+    if number < 0 :
+        number = -number + 1
+        negative = True
+
+    while True:
+        byte = number & 0x7F
+        number = number >> 7
+        byte += flag
+        result += chr(byte)
+        flag = 0x80
+        if number == 0 :
+            if (byte == 0xFF and negative == False) :
+                result += chr(0x80)
+            break
+
+    if negative:
+        result += chr(0xFF)
+
+    return result[::-1]
+
+
+
+# create / read  a length prefixed string from the file
+
+def lengthPrefixString(data):
+    return encodeNumber(len(data))+data
+
+def readString(file):
+    stringLength = readEncodedNumber(file)
+    if (stringLength == None):
+        return ""
+    sv = file.read(stringLength)
+    if (len(sv)  != stringLength):
+        return ""
+    return unpack(str(stringLength)+"s",sv)[0]
+
+
+# convert a binary string generated by encodeNumber (7 bit encoded number)
+# to the value you would find inside the page*.dat files to be processed
+
+def convert(i):
+    result = ''
+    val = encodeNumber(i)
+    for j in xrange(len(val)):
+        c = ord(val[j:j+1])
+        result += '%02x' % c
+    return result
+
+
+
+# the complete string table used to store all book text content
+# as well as the xml tokens and values that make sense out of it
+
+class Dictionary(object):
+    def __init__(self, dictFile):
+        self.filename = dictFile
+        self.size = 0
+        self.fo = file(dictFile,'rb')
+        self.stable = []
+        self.size = readEncodedNumber(self.fo)
+        for i in xrange(self.size):
+            self.stable.append(self.escapestr(readString(self.fo)))
+        self.pos = 0
+
+    def escapestr(self, str):
+        str = str.replace('&','&amp;')
+        str = str.replace('<','&lt;')
+        str = str.replace('>','&gt;')
+        str = str.replace('=','&#61;')
+        return str
+
+    def lookup(self,val):
+        if ((val >= 0) and (val < self.size)) :
+            self.pos = val
+            return self.stable[self.pos]
         else:
-            end = min(self.docSize, end)
-        foundat = -1
-        for j in xrange(pos, end):
-            item = self.flatdoc[j]
-            if item.find('=') >= 0:
-                (name, argres) = item.split('=',1)
-            else :
-                name = item
-                argres = ''
-            if name.endswith(tagpath) :
-                result = argres
-                foundat = j
-                break
-        return foundat, result
-
-    # return list of start positions for the tagpath
-    def posinDoc(self, tagpath):
-        startpos = []
-        pos = 0
-        res = ""
-        while res != None :
-            (foundpos, res) = self.findinDoc(tagpath, pos, -1)
-            if res != None :
-                startpos.append(foundpos)
-            pos = foundpos + 1
-        return startpos
-
-    def getData(self, path):
-        result = None
-        cnt = len(self.flatdoc)
-        for j in xrange(cnt):
-            item = self.flatdoc[j]
-            if item.find('=') >= 0:
-                (name, argt) = item.split('=')
-                argres = argt.split('|')
-            else:
-                name = item
-                argres = []
-            if (name.endswith(path)):
-                result = argres
-                break
-        if (len(argres) > 0) :
-            for j in xrange(0,len(argres)):
-                argres[j] = int(argres[j])
+            print "Error - %d outside of string table limits" % val
+            raise TpzDRMError('outside of string table limits')
+            # sys.exit(-1)
+
+    def getSize(self):
+        return self.size
+
+    def getPos(self):
+        return self.pos
+
+    def dumpDict(self):
+        for i in xrange(self.size):
+            print "%d %s %s" % (i, convert(i), self.stable[i])
+        return
+
+# parses the xml snippets that are represented by each page*.dat file.
+# also parses the other0.dat file - the main stylesheet
+# and information used to inject the xml snippets into page*.dat files
+
+class PageParser(object):
+    def __init__(self, filename, dict, debug, flat_xml):
+        self.fo = file(filename,'rb')
+        self.id = os.path.basename(filename).replace('.dat','')
+        self.dict = dict
+        self.debug = debug
+        self.flat_xml = flat_xml
+        self.tagpath = []
+        self.doc = []
+        self.snippetList = []
+
+
+    # hash table used to enable the decoding process
+    # This has all been developed by trial and error so it may still have omissions or
+    # contain errors
+    # Format:
+    # tag : (number of arguments, argument type, subtags present, special case of subtags presents when escaped)
+
+    token_tags = {
+        'x'            : (1, 'scalar_number', 0, 0),
+        'y'            : (1, 'scalar_number', 0, 0),
+        'h'            : (1, 'scalar_number', 0, 0),
+        'w'            : (1, 'scalar_number', 0, 0),
+        'firstWord'    : (1, 'scalar_number', 0, 0),
+        'lastWord'     : (1, 'scalar_number', 0, 0),
+        'rootID'       : (1, 'scalar_number', 0, 0),
+        'stemID'       : (1, 'scalar_number', 0, 0),
+        'type'         : (1, 'scalar_text', 0, 0),
+
+        'info'            : (0, 'number', 1, 0),
+
+        'info.word'            : (0, 'number', 1, 1),
+        'info.word.ocrText'    : (1, 'text', 0, 0),
+        'info.word.firstGlyph' : (1, 'raw', 0, 0),
+        'info.word.lastGlyph'  : (1, 'raw', 0, 0),
+        'info.word.bl'         : (1, 'raw', 0, 0),
+        'info.word.link_id'    : (1, 'number', 0, 0),
+
+        'glyph'           : (0, 'number', 1, 1),
+        'glyph.x'         : (1, 'number', 0, 0),
+        'glyph.y'         : (1, 'number', 0, 0),
+        'glyph.glyphID'   : (1, 'number', 0, 0),
+
+        'dehyphen'          : (0, 'number', 1, 1),
+        'dehyphen.rootID'   : (1, 'number', 0, 0),
+        'dehyphen.stemID'   : (1, 'number', 0, 0),
+        'dehyphen.stemPage' : (1, 'number', 0, 0),
+        'dehyphen.sh'       : (1, 'number', 0, 0),
+
+        'links'        : (0, 'number', 1, 1),
+        'links.page'   : (1, 'number', 0, 0),
+        'links.rel'    : (1, 'number', 0, 0),
+        'links.row'    : (1, 'number', 0, 0),
+        'links.title'  : (1, 'text', 0, 0),
+        'links.href'   : (1, 'text', 0, 0),
+        'links.type'   : (1, 'text', 0, 0),
+        'links.id'     : (1, 'number', 0, 0),
+
+        'paraCont'          : (0, 'number', 1, 1),
+        'paraCont.rootID'   : (1, 'number', 0, 0),
+        'paraCont.stemID'   : (1, 'number', 0, 0),
+        'paraCont.stemPage' : (1, 'number', 0, 0),
+
+        'paraStems'        : (0, 'number', 1, 1),
+        'paraStems.stemID' : (1, 'number', 0, 0),
+
+        'wordStems'          : (0, 'number', 1, 1),
+        'wordStems.stemID'   : (1, 'number', 0, 0),
+
+        'empty'          : (1, 'snippets', 1, 0),
+
+        'page'           : (1, 'snippets', 1, 0),
+        'page.pageid'    : (1, 'scalar_text', 0, 0),
+        'page.pagelabel' : (1, 'scalar_text', 0, 0),
+        'page.type'      : (1, 'scalar_text', 0, 0),
+        'page.h'         : (1, 'scalar_number', 0, 0),
+        'page.w'         : (1, 'scalar_number', 0, 0),
+        'page.startID' : (1, 'scalar_number', 0, 0),
+
+        'group'           : (1, 'snippets', 1, 0),
+        'group.type'      : (1, 'scalar_text', 0, 0),
+        'group._tag'      : (1, 'scalar_text', 0, 0),
+        'group.orientation': (1, 'scalar_text', 0, 0),
+
+        'region'           : (1, 'snippets', 1, 0),
+        'region.type'      : (1, 'scalar_text', 0, 0),
+        'region.x'         : (1, 'scalar_number', 0, 0),
+        'region.y'         : (1, 'scalar_number', 0, 0),
+        'region.h'         : (1, 'scalar_number', 0, 0),
+        'region.w'         : (1, 'scalar_number', 0, 0),
+        'region.orientation' : (1, 'scalar_text', 0, 0),
+
+        'empty_text_region' : (1, 'snippets', 1, 0),
+
+        'img'           : (1, 'snippets', 1, 0),
+        'img.x'         : (1, 'scalar_number', 0, 0),
+        'img.y'         : (1, 'scalar_number', 0, 0),
+        'img.h'         : (1, 'scalar_number', 0, 0),
+        'img.w'         : (1, 'scalar_number', 0, 0),
+        'img.src'       : (1, 'scalar_number', 0, 0),
+        'img.color_src' : (1, 'scalar_number', 0, 0),
+
+        'paragraph'           : (1, 'snippets', 1, 0),
+        'paragraph.class'     : (1, 'scalar_text', 0, 0),
+        'paragraph.firstWord' : (1, 'scalar_number', 0, 0),
+        'paragraph.lastWord'  : (1, 'scalar_number', 0, 0),
+        'paragraph.lastWord'  : (1, 'scalar_number', 0, 0),
+        'paragraph.gridSize'  : (1, 'scalar_number', 0, 0),
+        'paragraph.gridBottomCenter'  : (1, 'scalar_number', 0, 0),
+        'paragraph.gridTopCenter' : (1, 'scalar_number', 0, 0),
+        'paragraph.gridBeginCenter' : (1, 'scalar_number', 0, 0),
+        'paragraph.gridEndCenter' : (1, 'scalar_number', 0, 0),
+
+
+        'word_semantic'           : (1, 'snippets', 1, 1),
+        'word_semantic.type'      : (1, 'scalar_text', 0, 0),
+        'word_semantic.firstWord' : (1, 'scalar_number', 0, 0),
+        'word_semantic.lastWord'  : (1, 'scalar_number', 0, 0),
+
+        'word'            : (1, 'snippets', 1, 0),
+        'word.type'       : (1, 'scalar_text', 0, 0),
+        'word.class'      : (1, 'scalar_text', 0, 0),
+        'word.firstGlyph' : (1, 'scalar_number', 0, 0),
+        'word.lastGlyph'  : (1, 'scalar_number', 0, 0),
+
+        '_span'           : (1, 'snippets', 1, 0),
+        '_span.firstWord' : (1, 'scalar_number', 0, 0),
+        '_span.lastWord'  : (1, 'scalar_number', 0, 0),
+        '_span.gridSize'  : (1, 'scalar_number', 0, 0),
+        '_span.gridBottomCenter'  : (1, 'scalar_number', 0, 0),
+        '_span.gridTopCenter' : (1, 'scalar_number', 0, 0),
+        '_span.gridBeginCenter' : (1, 'scalar_number', 0, 0),
+        '_span.gridEndCenter' : (1, 'scalar_number', 0, 0),
+
+        'span'           : (1, 'snippets', 1, 0),
+        'span.firstWord' : (1, 'scalar_number', 0, 0),
+        'span.lastWord'  : (1, 'scalar_number', 0, 0),
+        'span.gridSize'  : (1, 'scalar_number', 0, 0),
+        'span.gridBottomCenter'  : (1, 'scalar_number', 0, 0),
+        'span.gridTopCenter' : (1, 'scalar_number', 0, 0),
+        'span.gridBeginCenter' : (1, 'scalar_number', 0, 0),
+        'span.gridEndCenter' : (1, 'scalar_number', 0, 0),
+
+        'extratokens'            : (1, 'snippets', 1, 0),
+        'extratokens.type'       : (1, 'scalar_text', 0, 0),
+        'extratokens.firstGlyph' : (1, 'scalar_number', 0, 0),
+        'extratokens.lastGlyph'  : (1, 'scalar_number', 0, 0),
+
+        'glyph.h'      : (1, 'number', 0, 0),
+        'glyph.w'      : (1, 'number', 0, 0),
+        'glyph.use'    : (1, 'number', 0, 0),
+        'glyph.vtx'    : (1, 'number', 0, 1),
+        'glyph.len'    : (1, 'number', 0, 1),
+        'glyph.dpi'    : (1, 'number', 0, 0),
+        'vtx'          : (0, 'number', 1, 1),
+        'vtx.x'        : (1, 'number', 0, 0),
+        'vtx.y'        : (1, 'number', 0, 0),
+        'len'          : (0, 'number', 1, 1),
+        'len.n'        : (1, 'number', 0, 0),
+
+        'book'         : (1, 'snippets', 1, 0),
+        'version'      : (1, 'snippets', 1, 0),
+        'version.FlowEdit_1_id'            : (1, 'scalar_text', 0, 0),
+        'version.FlowEdit_1_version'       : (1, 'scalar_text', 0, 0),
+        'version.Schema_id'                : (1, 'scalar_text', 0, 0),
+        'version.Schema_version'           : (1, 'scalar_text', 0, 0),
+        'version.Topaz_version'            : (1, 'scalar_text', 0, 0),
+        'version.WordDetailEdit_1_id'      : (1, 'scalar_text', 0, 0),
+        'version.WordDetailEdit_1_version' : (1, 'scalar_text', 0, 0),
+        'version.ZoneEdit_1_id'            : (1, 'scalar_text', 0, 0),
+        'version.ZoneEdit_1_version'       : (1, 'scalar_text', 0, 0),
+        'version.chapterheaders'           : (1, 'scalar_text', 0, 0),
+        'version.creation_date'            : (1, 'scalar_text', 0, 0),
+        'version.header_footer'            : (1, 'scalar_text', 0, 0),
+        'version.init_from_ocr'            : (1, 'scalar_text', 0, 0),
+        'version.letter_insertion'         : (1, 'scalar_text', 0, 0),
+        'version.xmlinj_convert'           : (1, 'scalar_text', 0, 0),
+        'version.xmlinj_reflow'            : (1, 'scalar_text', 0, 0),
+        'version.xmlinj_transform'         : (1, 'scalar_text', 0, 0),
+        'version.findlists'                : (1, 'scalar_text', 0, 0),
+        'version.page_num'                 : (1, 'scalar_text', 0, 0),
+        'version.page_type'                : (1, 'scalar_text', 0, 0),
+        'version.bad_text'                 : (1, 'scalar_text', 0, 0),
+        'version.glyph_mismatch'           : (1, 'scalar_text', 0, 0),
+        'version.margins'                  : (1, 'scalar_text', 0, 0),
+        'version.staggered_lines'          : (1, 'scalar_text', 0, 0),
+        'version.paragraph_continuation'   : (1, 'scalar_text', 0, 0),
+        'version.toc'                      : (1, 'scalar_text', 0, 0),
+
+        'stylesheet'   : (1, 'snippets', 1, 0),
+        'style'              : (1, 'snippets', 1, 0),
+        'style._tag'         : (1, 'scalar_text', 0, 0),
+        'style.type'         : (1, 'scalar_text', 0, 0),
+        'style._parent_type' : (1, 'scalar_text', 0, 0),
+        'style.class'        : (1, 'scalar_text', 0, 0),
+        'style._after_class' : (1, 'scalar_text', 0, 0),
+        'rule'               : (1, 'snippets', 1, 0),
+        'rule.attr'          : (1, 'scalar_text', 0, 0),
+        'rule.value'         : (1, 'scalar_text', 0, 0),
+
+        'original'      : (0, 'number', 1, 1),
+        'original.pnum' : (1, 'number', 0, 0),
+        'original.pid'  : (1, 'text', 0, 0),
+        'pages'        : (0, 'number', 1, 1),
+        'pages.ref'    : (1, 'number', 0, 0),
+        'pages.id'     : (1, 'number', 0, 0),
+        'startID'      : (0, 'number', 1, 1),
+        'startID.page' : (1, 'number', 0, 0),
+        'startID.id'   : (1, 'number', 0, 0),
+
+     }
+
+
+    # full tag path record keeping routines
+    def tag_push(self, token):
+        self.tagpath.append(token)
+    def tag_pop(self):
+        if len(self.tagpath) > 0 :
+            self.tagpath.pop()
+    def tagpath_len(self):
+        return len(self.tagpath)
+    def get_tagpath(self, i):
+        cnt = len(self.tagpath)
+        if i < cnt : result = self.tagpath[i]
+        for j in xrange(i+1, cnt) :
+            result += '.' + self.tagpath[j]
         return result
 
-    def getDataatPos(self, path, pos):
-        result = None
-        item = self.flatdoc[pos]
-        if item.find('=') >= 0:
-            (name, argt) = item.split('=')
-            argres = argt.split('|')
-        else:
-            name = item
-            argres = []
-        if (len(argres) > 0) :
-            for j in xrange(0,len(argres)):
-                argres[j] = int(argres[j])
-        if (name.endswith(path)):
-            result = argres
+
+    # list of absolute command byte values values that indicate
+    # various types of loop meachanisms typically used to generate vectors
+
+    cmd_list = (0x76, 0x76)
+
+    # peek at and return 1 byte that is ahead by i bytes
+    def peek(self, aheadi):
+        c = self.fo.read(aheadi)
+        if (len(c) == 0):
+            return None
+        self.fo.seek(-aheadi,1)
+        c = c[-1:]
+        return ord(c)
+
+
+    # get the next value from the file being processed
+    def getNext(self):
+        nbyte = self.peek(1);
+        if (nbyte == None):
+            return None
+        val = readEncodedNumber(self.fo)
+        return val
+
+
+    # format an arg by argtype
+    def formatArg(self, arg, argtype):
+        if (argtype == 'text') or (argtype == 'scalar_text') :
+            result = self.dict.lookup(arg)
+        elif (argtype == 'raw') or (argtype == 'number') or (argtype == 'scalar_number') :
+            result = arg
+        elif (argtype == 'snippets') :
+            result = arg
+        else :
+            print "Error Unknown argtype %s" % argtype
+            sys.exit(-2)
         return result
 
-    def getDataTemp(self, path):
-        result = None
-        cnt = len(self.temp)
+
+    # process the next tag token, recursively handling subtags,
+    # arguments, and commands
+    def procToken(self, token):
+
+        known_token = False
+        self.tag_push(token)
+
+        if self.debug : print 'Processing: ', self.get_tagpath(0)
+        cnt = self.tagpath_len()
         for j in xrange(cnt):
-            item = self.temp[j]
-            if item.find('=') >= 0:
-                (name, argt) = item.split('=')
-                argres = argt.split('|')
-            else:
-                name = item
-                argres = []
-            if (name.endswith(path)):
-                result = argres
-                self.temp.pop(j)
+            tkn = self.get_tagpath(j)
+            if tkn in self.token_tags :
+                num_args = self.token_tags[tkn][0]
+                argtype = self.token_tags[tkn][1]
+                subtags = self.token_tags[tkn][2]
+                splcase = self.token_tags[tkn][3]
+                ntags = -1
+                known_token = True
                 break
-        if (len(argres) > 0) :
-            for j in xrange(0,len(argres)):
-                argres[j] = int(argres[j])
-        return result
 
-    def getImages(self):
+        if known_token :
+
+            # handle subtags if present
+            subtagres = []
+            if (splcase == 1):
+                # this type of tag uses of escape marker 0x74 indicate subtag count
+                if self.peek(1) == 0x74:
+                    skip = readEncodedNumber(self.fo)
+                    subtags = 1
+                    num_args = 0
+
+            if (subtags == 1):
+                ntags = readEncodedNumber(self.fo)
+                if self.debug : print 'subtags: ' + token + ' has ' + str(ntags)
+                for j in xrange(ntags):
+                    val = readEncodedNumber(self.fo)
+                    subtagres.append(self.procToken(self.dict.lookup(val)))
+
+            # arguments can be scalars or vectors of text or numbers
+            argres = []
+            if num_args > 0 :
+                firstarg = self.peek(1)
+                if (firstarg in self.cmd_list) and (argtype != 'scalar_number') and (argtype != 'scalar_text'):
+                    # single argument is a variable length vector of data
+                    arg = readEncodedNumber(self.fo)
+                    argres = self.decodeCMD(arg,argtype)
+                else :
+                    # num_arg scalar arguments
+                    for i in xrange(num_args):
+                        argres.append(self.formatArg(readEncodedNumber(self.fo), argtype))
+
+            # build the return tag
+            result = []
+            tkn = self.get_tagpath(0)
+            result.append(tkn)
+            result.append(subtagres)
+            result.append(argtype)
+            result.append(argres)
+            self.tag_pop()
+            return result
+
+        # all tokens that need to be processed should be in the hash
+        # table if it may indicate a problem, either new token
+        # or an out of sync condition
+        else:
+            result = []
+            if (self.debug):
+                print 'Unknown Token:', token
+            self.tag_pop()
+            return result
+
+
+    # special loop used to process code snippets
+    # it is NEVER used to format arguments.
+    # builds the snippetList
+    def doLoop72(self, argtype):
+        cnt = readEncodedNumber(self.fo)
+        if self.debug :
+            result = 'Set of '+ str(cnt) + ' xml snippets. The overall structure \n'
+            result += 'of the document is indicated by snippet number sets at the\n'
+            result += 'end of each snippet. \n'
+            print result
+        for i in xrange(cnt):
+            if self.debug: print 'Snippet:',str(i)
+            snippet = []
+            snippet.append(i)
+            val = readEncodedNumber(self.fo)
+            snippet.append(self.procToken(self.dict.lookup(val)))
+            self.snippetList.append(snippet)
+        return
+
+
+
+    # general loop code gracisouly submitted by "skindle" - thank you!
+    def doLoop76Mode(self, argtype, cnt, mode):
         result = []
-        self.temp = self.flatdoc
-        while (self.getDataTemp('img') != None):
-            h = self.getDataTemp('img.h')[0]
-            w = self.getDataTemp('img.w')[0]
-            x = self.getDataTemp('img.x')[0]
-            y = self.getDataTemp('img.y')[0]
-            src = self.getDataTemp('img.src')[0]
-            result.append('<image xlink:href="../img/img%04d.jpg" x="%d" y="%d" width="%d" height="%d" />\n' % (src, x, y, w, h))
+        adj = 0
+        if mode & 1:
+            adj = readEncodedNumber(self.fo)
+        mode = mode >> 1
+        x = []
+        for i in xrange(cnt):
+            x.append(readEncodedNumber(self.fo) - adj)
+        for i in xrange(mode):
+            for j in xrange(1, cnt):
+                x[j] = x[j] + x[j - 1]
+        for i in xrange(cnt):
+            result.append(self.formatArg(x[i],argtype))
         return result
 
-    def getGlyphs(self):
+
+    # dispatches loop commands bytes with various modes
+    # The 0x76 style loops are used to build vectors
+
+    # This was all derived by trial and error and
+    # new loop types may exist that are not handled here
+    # since they did not appear in the test cases
+
+    def decodeCMD(self, cmd, argtype):
+        if (cmd == 0x76):
+
+            # loop with cnt, and mode to control loop styles
+            cnt = readEncodedNumber(self.fo)
+            mode = readEncodedNumber(self.fo)
+
+            if self.debug : print 'Loop for', cnt, 'with  mode', mode,  ':  '
+            return self.doLoop76Mode(argtype, cnt, mode)
+
+        if self.dbug: print  "Unknown command", cmd
         result = []
-        if (self.gid != None) and (len(self.gid) > 0):
-            glyphs = []
-            for j in set(self.gid):
-                glyphs.append(j)
-            glyphs.sort()
-            for gid in glyphs:
-                id='id="gl%d"' % gid
-                path = self.gd.lookup(id)
-                if path:
-                    result.append(id + ' ' + path)
         return result
 
 
-def convert2SVG(gdict, flat_xml, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi):
-    mlst = []
-    pp = PParser(gdict, flat_xml, meta_array)
-    mlst.append('<?xml version="1.0" standalone="no"?>\n')
-    if (raw):
-        mlst.append('<!DOCTYPE svg PUBLIC "-//W3C/DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
-        mlst.append('<svg width="%fin" height="%fin" viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">\n' % (pp.pw / scaledpi, pp.ph / scaledpi, pp.pw -1, pp.ph -1))
-        mlst.append('<title>Page %d - %s by %s</title>\n' % (pageid, meta_array['Title'],meta_array['Authors']))
-    else:
-        mlst.append('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n')
-        mlst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" ><head>\n')
-        mlst.append('<title>Page %d - %s by %s</title>\n' % (pageid, meta_array['Title'],meta_array['Authors']))
-        mlst.append('<script><![CDATA[\n')
-        mlst.append('function gd(){var p=window.location.href.replace(/^.*\?dpi=(\d+).*$/i,"$1");return p;}\n')
-        mlst.append('var dpi=%d;\n' % scaledpi)
-        if (previd) :
-            mlst.append('var prevpage="page%04d.xhtml";\n' % (previd))
-        if (nextid) :
-            mlst.append('var nextpage="page%04d.xhtml";\n' % (nextid))
-        mlst.append('var pw=%d;var ph=%d;' % (pp.pw, pp.ph))
-        mlst.append('function zoomin(){dpi=dpi*(0.8);setsize();}\n')
-        mlst.append('function zoomout(){dpi=dpi*1.25;setsize();}\n')
-        mlst.append('function setsize(){var svg=document.getElementById("svgimg");var prev=document.getElementById("prevsvg");var next=document.getElementById("nextsvg");var width=(pw/dpi)+"in";var height=(ph/dpi)+"in";svg.setAttribute("width",width);svg.setAttribute("height",height);prev.setAttribute("height",height);prev.setAttribute("width","50px");next.setAttribute("height",height);next.setAttribute("width","50px");}\n')
-        mlst.append('function ppage(){window.location.href=prevpage+"?dpi="+Math.round(dpi);}\n')
-        mlst.append('function npage(){window.location.href=nextpage+"?dpi="+Math.round(dpi);}\n')
-        mlst.append('var gt=gd();if(gt>0){dpi=gt;}\n')
-        mlst.append('window.onload=setsize;\n')
-        mlst.append(']]></script>\n')
-        mlst.append('</head>\n')
-        mlst.append('<body onLoad="setsize();" style="background-color:#777;text-align:center;">\n')
-        mlst.append('<div style="white-space:nowrap;">\n')
-        if previd == None:
-            mlst.append('<a href="javascript:ppage();"><svg id="prevsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"></svg></a>\n')
+
+    # add full tag path to injected snippets
+    def updateName(self, tag, prefix):
+        name = tag[0]
+        subtagList = tag[1]
+        argtype = tag[2]
+        argList = tag[3]
+        nname = prefix + '.' + name
+        nsubtaglist = []
+        for j in subtagList:
+            nsubtaglist.append(self.updateName(j,prefix))
+        ntag = []
+        ntag.append(nname)
+        ntag.append(nsubtaglist)
+        ntag.append(argtype)
+        ntag.append(argList)
+        return ntag
+
+
+
+    # perform depth first injection of specified snippets into this one
+    def injectSnippets(self, snippet):
+        snipno, tag = snippet
+        name = tag[0]
+        subtagList = tag[1]
+        argtype = tag[2]
+        argList = tag[3]
+        nsubtagList = []
+        if len(argList) > 0 :
+            for j in argList:
+                asnip = self.snippetList[j]
+                aso, atag = self.injectSnippets(asnip)
+                atag = self.updateName(atag, name)
+                nsubtagList.append(atag)
+        argtype='number'
+        argList=[]
+        if len(nsubtagList) > 0 :
+            subtagList.extend(nsubtagList)
+        tag = []
+        tag.append(name)
+        tag.append(subtagList)
+        tag.append(argtype)
+        tag.append(argList)
+        snippet = []
+        snippet.append(snipno)
+        snippet.append(tag)
+        return snippet
+
+
+
+    # format the tag for output
+    def formatTag(self, node):
+        name = node[0]
+        subtagList = node[1]
+        argtype = node[2]
+        argList = node[3]
+        fullpathname = name.split('.')
+        nodename = fullpathname.pop()
+        ilvl = len(fullpathname)
+        indent = ' ' * (3 * ilvl)
+        rlst = []
+        rlst.append(indent + '<' + nodename + '>')
+        if len(argList) > 0:
+            alst = []
+            for j in argList:
+                if (argtype == 'text') or (argtype == 'scalar_text') :
+                    alst.append(j + '|')
+                else :
+                    alst.append(str(j) + ',')
+            argres = "".join(alst)
+            argres = argres[0:-1]
+            if argtype == 'snippets' :
+                rlst.append('snippets:' + argres)
+            else :
+                rlst.append(argres)
+        if len(subtagList) > 0 :
+            rlst.append('\n')
+            for j in subtagList:
+                if len(j) > 0 :
+                    rlst.append(self.formatTag(j))
+            rlst.append(indent + '</' + nodename + '>\n')
         else:
-            mlst.append('<a href="javascript:ppage();"><svg id="prevsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"><polygon points="5,150,95,5,95,295" fill="#AAAAAA" /></svg></a>\n')
-
-        mlst.append('<a href="javascript:npage();"><svg id="svgimg" viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" style="background-color:#FFF;border:1px solid black;">' % (pp.pw, pp.ph))
-    if (pp.gid != None):
-        mlst.append('<defs>\n')
-        gdefs = pp.getGlyphs()
-        for j in xrange(0,len(gdefs)):
-            mlst.append(gdefs[j])
-        mlst.append('</defs>\n')
-    img = pp.getImages()
-    if (img != None):
-        for j in xrange(0,len(img)):
-            mlst.append(img[j])
-    if (pp.gid != None):
-        for j in xrange(0,len(pp.gid)):
-            mlst.append('<use xlink:href="#gl%d" x="%d" y="%d" />\n' % (pp.gid[j], pp.gx[j], pp.gy[j]))
-    if (img == None or len(img) == 0) and (pp.gid == None or len(pp.gid) == 0):
-        xpos = "%d" % (pp.pw // 3)
-        ypos = "%d" % (pp.ph // 3)
-        mlst.append('<text x="' + xpos + '" y="' + ypos + '" font-size="' + meta_array['fontSize'] + '" font-family="Helvetica" stroke="black">This page intentionally left blank.</text>\n')
-    if (raw) :
-        mlst.append('</svg>')
-    else :
-        mlst.append('</svg></a>\n')
-        if nextid == None:
-            mlst.append('<a href="javascript:npage();"><svg id="nextsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"></svg></a>\n')
+            rlst.append('</' + nodename + '>\n')
+        return "".join(rlst)
+
+
+    # flatten tag
+    def flattenTag(self, node):
+        name = node[0]
+        subtagList = node[1]
+        argtype = node[2]
+        argList = node[3]
+        rlst = []
+        rlst.append(name)
+        if (len(argList) > 0):
+            alst = []
+            for j in argList:
+                if (argtype == 'text') or (argtype == 'scalar_text') :
+                    alst.append(j + '|')
+                else :
+                    alst.append(str(j) + '|')
+            argres = "".join(alst)
+            argres = argres[0:-1]
+            if argtype == 'snippets' :
+                rlst.append('.snippets=' + argres)
+            else :
+                rlst.append('=' + argres)
+        rlst.append('\n')
+        for j in subtagList:
+            if len(j) > 0 :
+                rlst.append(self.flattenTag(j))
+        return "".join(rlst)
+
+
+    # reduce create xml output
+    def formatDoc(self, flat_xml):
+        rlst = []
+        for j in self.doc :
+            if len(j) > 0:
+                if flat_xml:
+                    rlst.append(self.flattenTag(j))
+                else:
+                    rlst.append(self.formatTag(j))
+        result = "".join(rlst)
+        if self.debug : print result
+        return result
+
+
+
+    # main loop - parse the page.dat files
+    # to create structured document and snippets
+
+    # FIXME: value at end of magic appears to be a subtags count
+    # but for what?  For now, inject an 'info" tag as it is in
+    # every dictionary and seems close to what is meant
+    # The alternative is to special case the last _ "0x5f" to mean something
+
+    def process(self):
+
+        # peek at the first bytes to see what type of file it is
+        magic = self.fo.read(9)
+        if (magic[0:1] == 'p') and (magic[2:9] == 'marker_'):
+            first_token = 'info'
+        elif (magic[0:1] == 'p') and (magic[2:9] == '__PAGE_'):
+            skip = self.fo.read(2)
+            first_token = 'info'
+        elif (magic[0:1] == 'p') and (magic[2:8] == '_PAGE_'):
+            first_token = 'info'
+        elif (magic[0:1] == 'g') and (magic[2:9] == '__GLYPH'):
+            skip = self.fo.read(3)
+            first_token = 'info'
         else :
-            mlst.append('<a href="javascript:npage();"><svg id="nextsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"><polygon points="5,5,5,295,95,150" fill="#AAAAAA" /></svg></a>\n')
-        mlst.append('</div>\n')
-        mlst.append('<div><a href="javascript:zoomin();">zoom in</a> - <a href="javascript:zoomout();">zoom out</a></div>\n')
-        mlst.append('</body>\n')
-        mlst.append('</html>\n')
-    return "".join(mlst)
+            # other0.dat file
+            first_token = None
+            self.fo.seek(-9,1)
+
+
+        # main loop to read and build the document tree
+        while True:
+
+            if first_token != None :
+                # use "inserted" first token 'info' for page and glyph files
+                tag = self.procToken(first_token)
+                if len(tag) > 0 :
+                    self.doc.append(tag)
+                first_token = None
+
+            v = self.getNext()
+            if (v == None):
+                break
+
+            if (v == 0x72):
+                self.doLoop72('number')
+            elif (v > 0) and (v < self.dict.getSize()) :
+                tag = self.procToken(self.dict.lookup(v))
+                if len(tag) > 0 :
+                    self.doc.append(tag)
+            else:
+                if self.debug:
+                    print "Main Loop:  Unknown value: %x" % v
+                if (v == 0):
+                    if (self.peek(1) == 0x5f):
+                        skip = self.fo.read(1)
+                        first_token = 'info'
+
+        # now do snippet injection
+        if len(self.snippetList) > 0 :
+            if self.debug : print 'Injecting Snippets:'
+            snippet = self.injectSnippets(self.snippetList[0])
+            snipno = snippet[0]
+            tag_add = snippet[1]
+            if self.debug : print self.formatTag(tag_add)
+            if len(tag_add) > 0:
+                self.doc.append(tag_add)
+
+        # handle generation of xml output
+        xmlpage = self.formatDoc(self.flat_xml)
+
+        return xmlpage
+
+
+def fromData(dict, fname):
+    flat_xml = True
+    debug = False
+    pp = PageParser(fname, dict, debug, flat_xml)
+    xmlpage = pp.process()
+    return xmlpage
+
+def getXML(dict, fname):
+    flat_xml = False
+    debug = False
+    pp = PageParser(fname, dict, debug, flat_xml)
+    xmlpage = pp.process()
+    return xmlpage
+
+def usage():
+    print 'Usage: '
+    print '    convert2xml.py dict0000.dat infile.dat '
+    print ' '
+    print ' Options:'
+    print '   -h            print this usage help message '
+    print '   -d            turn on debug output to check for potential errors '
+    print '   --flat-xml    output the flattened xml page description only '
+    print ' '
+    print '     This program will attempt to convert a page*.dat file or '
+    print ' glyphs*.dat file, using the dict0000.dat file, to its xml description. '
+    print ' '
+    print ' Use "cmbtc_dump.py" first to unencrypt, uncompress, and dump '
+    print ' the *.dat files from a Topaz format e-book.'
+
+#
+# Main
+#
+
+def main(argv):
+    dictFile = ""
+    pageFile = ""
+    debug = False
+    flat_xml = False
+    printOutput = False
+    if len(argv) == 0:
+        printOutput = True
+        argv = sys.argv
+
+    try:
+        opts, args = getopt.getopt(argv[1:], "hd", ["flat-xml"])
+
+    except getopt.GetoptError, err:
+
+        # print help information and exit:
+        print str(err) # will print something like "option -a not recognized"
+        usage()
+        sys.exit(2)
+
+    if len(opts) == 0 and len(args) == 0 :
+        usage()
+        sys.exit(2)
+
+    for o, a in opts:
+        if o =="-d":
+            debug=True
+        if o =="-h":
+            usage()
+            sys.exit(0)
+        if o =="--flat-xml":
+            flat_xml = True
+
+    dictFile, pageFile = args[0], args[1]
+
+    # read in the string table dictionary
+    dict = Dictionary(dictFile)
+    # dict.dumpDict()
+
+    # create a page parser
+    pp = PageParser(pageFile, dict, debug, flat_xml)
+
+    xmlpage = pp.process()
+
+    if printOutput:
+        print xmlpage
+        return 0
+
+    return xmlpage
+
+if __name__ == '__main__':
+    sys.exit(main(''))
index 97338872be00259f25164d5efa82a6970f24758b..e5647f4bc3842abb1546088d41b2c899fa88c36a 100644 (file)
 #! /usr/bin/python
 # vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
-
-class Unbuffered:
-    def __init__(self, stream):
-        self.stream = stream
-    def write(self, data):
-        self.stream.write(data)
-        self.stream.flush()
-    def __getattr__(self, attr):
-        return getattr(self.stream, attr)
+# For use with Topaz Scripts Version 2.6
 
 import sys
-sys.stdout=Unbuffered(sys.stdout)
-
 import csv
 import os
+import math
 import getopt
 from struct import pack
 from struct import unpack
 
-class TpzDRMError(Exception):
-    pass
-
-# local support routines
-if 'calibre' in sys.modules:
-    inCalibre = True
-else:
-    inCalibre = False
-
-if inCalibre :
-    from calibre_plugins.k4mobidedrm import convert2xml
-    from calibre_plugins.k4mobidedrm import flatxml2html
-    from calibre_plugins.k4mobidedrm import flatxml2svg
-    from calibre_plugins.k4mobidedrm import stylexml2css
-else :
-    import convert2xml
-    import flatxml2html
-    import flatxml2svg
-    import stylexml2css
-
-# global switch
-buildXML = False
-
-# Get a 7 bit encoded number from a file
-def readEncodedNumber(file):
-    flag = False
-    c = file.read(1)
-    if (len(c) == 0):
-        return None
-    data = ord(c)
-    if data == 0xFF:
-        flag = True
-        c = file.read(1)
-        if (len(c) == 0):
-            return None
-        data = ord(c)
-    if data >= 0x80:
-        datax = (data & 0x7F)
-        while data >= 0x80 :
-            c = file.read(1)
-            if (len(c) == 0):
-                return None
-            data = ord(c)
-            datax = (datax <<7) + (data & 0x7F)
-        data = datax
-    if flag:
-        data = -data
-    return data
-
-# Get a length prefixed string from the file
-def lengthPrefixString(data):
-    return encodeNumber(len(data))+data
-
-def readString(file):
-    stringLength = readEncodedNumber(file)
-    if (stringLength == None):
-        return None
-    sv = file.read(stringLength)
-    if (len(sv)  != stringLength):
-        return ""
-    return unpack(str(stringLength)+"s",sv)[0]
-
-def getMetaArray(metaFile):
-    # parse the meta file
-    result = {}
-    fo = file(metaFile,'rb')
-    size = readEncodedNumber(fo)
-    for i in xrange(size):
-        tag = readString(fo)
-        value = readString(fo)
-        result[tag] = value
-        # print tag, value
-    fo.close()
-    return result
-
-
-# dictionary of all text strings by index value
-class Dictionary(object):
-    def __init__(self, dictFile):
-        self.filename = dictFile
-        self.size = 0
-        self.fo = file(dictFile,'rb')
-        self.stable = []
-        self.size = readEncodedNumber(self.fo)
-        for i in xrange(self.size):
-            self.stable.append(self.escapestr(readString(self.fo)))
-        self.pos = 0
-    def escapestr(self, str):
-        str = str.replace('&','&amp;')
-        str = str.replace('<','&lt;')
-        str = str.replace('>','&gt;')
-        str = str.replace('=','&#61;')
-        return str
-    def lookup(self,val):
-        if ((val >= 0) and (val < self.size)) :
-            self.pos = val
-            return self.stable[self.pos]
-        else:
-            print "Error - %d outside of string table limits" % val
-            raise TpzDRMError('outside or string table limits')
-            # sys.exit(-1)
-    def getSize(self):
-        return self.size
-    def getPos(self):
-        return self.pos
-
-
-class PageDimParser(object):
-    def __init__(self, flatxml):
-        self.flatdoc = flatxml.split('\n')
-    # find tag if within pos to end inclusive
+
+class DocParser(object):
+    def __init__(self, flatxml, classlst, fileid, bookDir, gdict, fixedimage):
+        self.id = os.path.basename(fileid).replace('.dat','')
+        self.svgcount = 0
+        self.docList = flatxml.split('\n')
+        self.docSize = len(self.docList)
+        self.classList = {}
+        self.bookDir = bookDir
+        self.gdict = gdict
+        tmpList = classlst.split('\n')
+        for pclass in tmpList:
+            if pclass != '':
+                # remove the leading period from the css name
+                cname = pclass[1:]
+            self.classList[cname] = True
+        self.fixedimage = fixedimage
+        self.ocrtext = []
+        self.link_id = []
+        self.link_title = []
+        self.link_page = []
+        self.link_href = []
+        self.link_type = []
+        self.dehyphen_rootid = []
+        self.paracont_stemid = []
+        self.parastems_stemid = []
+
+
+    def getGlyph(self, gid):
+        result = ''
+        id='id="gl%d"' % gid
+        return self.gdict.lookup(id)
+
+    def glyphs_to_image(self, glyphList):
+
+        def extract(path, key):
+            b = path.find(key) + len(key)
+            e = path.find(' ',b)
+            return int(path[b:e])
+
+        svgDir = os.path.join(self.bookDir,'svg')
+
+        imgDir = os.path.join(self.bookDir,'img')
+        imgname = self.id + '_%04d.svg' % self.svgcount
+        imgfile = os.path.join(imgDir,imgname)
+
+        # get glyph information
+        gxList = self.getData('info.glyph.x',0,-1)
+        gyList = self.getData('info.glyph.y',0,-1)
+        gidList = self.getData('info.glyph.glyphID',0,-1)
+
+        gids = []
+        maxws = []
+        maxhs = []
+        xs = []
+        ys = []
+        gdefs = []
+
+        # get path defintions, positions, dimensions for each glyph
+        # that makes up the image, and find min x and min y to reposition origin
+        minx = -1
+        miny = -1
+        for j in glyphList:
+            gid = gidList[j]
+            gids.append(gid)
+
+            xs.append(gxList[j])
+            if minx == -1: minx = gxList[j]
+            else : minx = min(minx, gxList[j])
+
+            ys.append(gyList[j])
+            if miny == -1: miny = gyList[j]
+            else : miny = min(miny, gyList[j])
+
+            path = self.getGlyph(gid)
+            gdefs.append(path)
+
+            maxws.append(extract(path,'width='))
+            maxhs.append(extract(path,'height='))
+
+
+        # change the origin to minx, miny and calc max height and width
+        maxw = maxws[0] + xs[0] - minx
+        maxh = maxhs[0] + ys[0] - miny
+        for j in xrange(0, len(xs)):
+            xs[j] = xs[j] - minx
+            ys[j] = ys[j] - miny
+            maxw = max( maxw, (maxws[j] + xs[j]) )
+            maxh = max( maxh, (maxhs[j] + ys[j]) )
+
+        # open the image file for output
+        ifile = open(imgfile,'w')
+        ifile.write('<?xml version="1.0" standalone="no"?>\n')
+        ifile.write('<!DOCTYPE svg PUBLIC "-//W3C/DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
+        ifile.write('<svg width="%dpx" height="%dpx" viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">\n' % (math.floor(maxw/10), math.floor(maxh/10), maxw, maxh))
+        ifile.write('<defs>\n')
+        for j in xrange(0,len(gdefs)):
+            ifile.write(gdefs[j])
+        ifile.write('</defs>\n')
+        for j in xrange(0,len(gids)):
+            ifile.write('<use xlink:href="#gl%d" x="%d" y="%d" />\n' % (gids[j], xs[j], ys[j]))
+        ifile.write('</svg>')
+        ifile.close()
+
+        return 0
+
+
+
+    # return tag at line pos in document
+    def lineinDoc(self, pos) :
+        if (pos >= 0) and (pos < self.docSize) :
+            item = self.docList[pos]
+            if item.find('=') >= 0:
+                (name, argres) = item.split('=',1)
+            else :
+                name = item
+                argres = ''
+        return name, argres
+
+
+    # find tag in doc if within pos to end inclusive
     def findinDoc(self, tagpath, pos, end) :
         result = None
-        docList = self.flatdoc
-        cnt = len(docList)
         if end == -1 :
-            end = cnt
+            end = self.docSize
         else:
-            end = min(cnt,end)
+            end = min(self.docSize, end)
         foundat = -1
         for j in xrange(pos, end):
-            item = docList[j]
+            item = self.docList[j]
             if item.find('=') >= 0:
-                (name, argres) = item.split('=')
+                (name, argres) = item.split('=',1)
             else :
                 name = item
                 argres = ''
@@ -151,571 +150,644 @@ class PageDimParser(object):
                 foundat = j
                 break
         return foundat, result
-    def process(self):
-        (pos, sph) = self.findinDoc('page.h',0,-1)
-        (pos, spw) = self.findinDoc('page.w',0,-1)
-        if (sph == None): sph = '-1'
-        if (spw == None): spw = '-1'
-        return sph, spw
 
-def getPageDim(flatxml):
-    # create a document parser
-    dp = PageDimParser(flatxml)
-    (ph, pw) = dp.process()
-    return ph, pw
-
-class GParser(object):
-    def __init__(self, flatxml):
-        self.flatdoc = flatxml.split('\n')
-        self.dpi = 1440
-        self.gh = self.getData('info.glyph.h')
-        self.gw = self.getData('info.glyph.w')
-        self.guse = self.getData('info.glyph.use')
-        if self.guse :
-            self.count = len(self.guse)
-        else :
-            self.count = 0
-        self.gvtx = self.getData('info.glyph.vtx')
-        self.glen = self.getData('info.glyph.len')
-        self.gdpi = self.getData('info.glyph.dpi')
-        self.vx = self.getData('info.vtx.x')
-        self.vy = self.getData('info.vtx.y')
-        self.vlen = self.getData('info.len.n')
-        if self.vlen :
-            self.glen.append(len(self.vlen))
-        elif self.glen:
-            self.glen.append(0)
-        if self.vx :
-            self.gvtx.append(len(self.vx))
-        elif self.gvtx :
-            self.gvtx.append(0)
-    def getData(self, path):
-        result = None
-        cnt = len(self.flatdoc)
-        for j in xrange(cnt):
-            item = self.flatdoc[j]
-            if item.find('=') >= 0:
-                (name, argt) = item.split('=')
-                argres = argt.split('|')
-            else:
-                name = item
-                argres = []
-            if (name == path):
-                result = argres
-                break
-        if (len(argres) > 0) :
-            for j in xrange(0,len(argres)):
-                argres[j] = int(argres[j])
-        return result
-    def getGlyphDim(self, gly):
-        if self.gdpi[gly] == 0:
-            return 0, 0
-        maxh = (self.gh[gly] * self.dpi) / self.gdpi[gly]
-        maxw = (self.gw[gly] * self.dpi) / self.gdpi[gly]
-        return maxh, maxw
-    def getPath(self, gly):
-        path = ''
-        if (gly < 0) or (gly >= self.count):
-            return path
-        tx = self.vx[self.gvtx[gly]:self.gvtx[gly+1]]
-        ty = self.vy[self.gvtx[gly]:self.gvtx[gly+1]]
-        p = 0
-        for k in xrange(self.glen[gly], self.glen[gly+1]):
-            if (p == 0):
-                zx = tx[0:self.vlen[k]+1]
-                zy = ty[0:self.vlen[k]+1]
+
+    # return list of start positions for the tagpath
+    def posinDoc(self, tagpath):
+        startpos = []
+        pos = 0
+        res = ""
+        while res != None :
+            (foundpos, res) = self.findinDoc(tagpath, pos, -1)
+            if res != None :
+                startpos.append(foundpos)
+            pos = foundpos + 1
+        return startpos
+
+
+    # returns a vector of integers for the tagpath
+    def getData(self, tagpath, pos, end):
+        argres=[]
+        (foundat, argt) = self.findinDoc(tagpath, pos, end)
+        if (argt != None) and (len(argt) > 0) :
+            argList = argt.split('|')
+            argres = [ int(strval) for strval in argList]
+        return argres
+
+
+    # get the class
+    def getClass(self, pclass):
+        nclass = pclass
+
+        # class names are an issue given topaz may start them with numerals (not allowed),
+        # use a mix of cases (which cause some browsers problems), and actually
+        # attach numbers after "_reclustered*" to the end to deal classeses that inherit
+        # from a base class (but then not actually provide all of these _reclustereed
+        # classes in the stylesheet!
+
+        # so we clean this up by lowercasing, prepend 'cl-', and getting any baseclass
+        # that exists in the stylesheet first, and then adding this specific class
+        # after
+
+        # also some class names have spaces in them so need to convert to dashes
+        if nclass != None :
+            nclass = nclass.replace(' ','-')
+            classres = ''
+            nclass = nclass.lower()
+            nclass = 'cl-' + nclass
+            baseclass = ''
+            # graphic is the base class for captions
+            if nclass.find('cl-cap-') >=0 :
+                classres = 'graphic' + ' '
+            else :
+                # strip to find baseclass
+                p = nclass.find('_')
+                if p > 0 :
+                    baseclass = nclass[0:p]
+                    if baseclass in self.classList:
+                        classres += baseclass + ' '
+            classres += nclass
+            nclass = classres
+        return nclass
+
+
+    # develop a sorted description of the starting positions of
+    # groups and regions on the page, as well as the page type
+    def PageDescription(self):
+
+        def compare(x, y):
+            (xtype, xval) = x
+            (ytype, yval) = y
+            if xval > yval:
+                return 1
+            if xval == yval:
+                return 0
+            return -1
+
+        result = []
+        (pos, pagetype) = self.findinDoc('page.type',0,-1)
+
+        groupList = self.posinDoc('page.group')
+        groupregionList = self.posinDoc('page.group.region')
+        pageregionList = self.posinDoc('page.region')
+        # integrate into one list
+        for j in groupList:
+            result.append(('grpbeg',j))
+        for j in groupregionList:
+            result.append(('gregion',j))
+        for j in pageregionList:
+            result.append(('pregion',j))
+        result.sort(compare)
+
+        # insert group end and page end indicators
+        inGroup = False
+        j = 0
+        while True:
+            if j == len(result): break
+            rtype = result[j][0]
+            rval = result[j][1]
+            if not inGroup and (rtype == 'grpbeg') :
+                inGroup = True
+                j = j + 1
+            elif inGroup and (rtype in ('grpbeg', 'pregion')):
+                result.insert(j,('grpend',rval))
+                inGroup = False
             else:
-                zx = tx[self.vlen[k-1]+1:self.vlen[k]+1]
-                zy = ty[self.vlen[k-1]+1:self.vlen[k]+1]
-            p += 1
-            j = 0
-            while ( j  < len(zx) ):
-                if (j == 0):
-                    # Start Position.
-                    path += 'M %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly])
-                elif (j <= len(zx)-3):
-                    # Cubic Bezier Curve
-                    path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[j+2] * self.dpi / self.gdpi[gly], zy[j+2] * self.dpi / self.gdpi[gly])
-                    j += 2
-                elif (j == len(zx)-2):
-                    # Cubic Bezier Curve to Start Position
-                    path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly])
-                    j += 1
-                elif (j == len(zx)-1):
-                    # Quadratic Bezier Curve to Start Position
-                    path += 'Q %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly])
-
-                j += 1
-        path += 'z'
-        return path
-
-
-
-# dictionary of all text strings by index value
-class GlyphDict(object):
-    def __init__(self):
-        self.gdict = {}
-    def lookup(self, id):
-        # id='id="gl%d"' % val
-        if id in self.gdict:
-            return self.gdict[id]
-        return None
-    def addGlyph(self, val, path):
-        id='id="gl%d"' % val
-        self.gdict[id] = path
-
-
-def generateBook(bookDir, raw, fixedimage):
-    # sanity check Topaz file extraction
-    if not os.path.exists(bookDir) :
-        print "Can not find directory with unencrypted book"
-        return 1
-
-    dictFile = os.path.join(bookDir,'dict0000.dat')
-    if not os.path.exists(dictFile) :
-        print "Can not find dict0000.dat file"
-        return 1
-
-    pageDir = os.path.join(bookDir,'page')
-    if not os.path.exists(pageDir) :
-        print "Can not find page directory in unencrypted book"
-        return 1
-
-    imgDir = os.path.join(bookDir,'img')
-    if not os.path.exists(imgDir) :
-        print "Can not find image directory in unencrypted book"
-        return 1
-
-    glyphsDir = os.path.join(bookDir,'glyphs')
-    if not os.path.exists(glyphsDir) :
-        print "Can not find glyphs directory in unencrypted book"
-        return 1
-
-    metaFile = os.path.join(bookDir,'metadata0000.dat')
-    if not os.path.exists(metaFile) :
-        print "Can not find metadata0000.dat in unencrypted book"
-        return 1
-
-    svgDir = os.path.join(bookDir,'svg')
-    if not os.path.exists(svgDir) :
-        os.makedirs(svgDir)
-
-    if buildXML:
-        xmlDir = os.path.join(bookDir,'xml')
-        if not os.path.exists(xmlDir) :
-            os.makedirs(xmlDir)
-
-    otherFile = os.path.join(bookDir,'other0000.dat')
-    if not os.path.exists(otherFile) :
-        print "Can not find other0000.dat in unencrypted book"
-        return 1
-
-    print "Updating to color images if available"
-    spath = os.path.join(bookDir,'color_img')
-    dpath = os.path.join(bookDir,'img')
-    filenames = os.listdir(spath)
-    filenames = sorted(filenames)
-    for filename in filenames:
-        imgname = filename.replace('color','img')
-        sfile = os.path.join(spath,filename)
-        dfile = os.path.join(dpath,imgname)
-        imgdata = file(sfile,'rb').read()
-        file(dfile,'wb').write(imgdata)
-
-    print "Creating cover.jpg"
-    isCover = False
-    cpath = os.path.join(bookDir,'img')
-    cpath = os.path.join(cpath,'img0000.jpg')
-    if os.path.isfile(cpath):
-        cover = file(cpath, 'rb').read()
-        cpath = os.path.join(bookDir,'cover.jpg')
-        file(cpath, 'wb').write(cover)
-        isCover = True
-
-
-    print 'Processing Dictionary'
-    dict = Dictionary(dictFile)
-
-    print 'Processing Meta Data and creating OPF'
-    meta_array = getMetaArray(metaFile)
-
-    # replace special chars in title and authors like & < >
-    title = meta_array.get('Title','No Title Provided')
-    title = title.replace('&','&amp;')
-    title = title.replace('<','&lt;')
-    title = title.replace('>','&gt;')
-    meta_array['Title'] = title
-    authors = meta_array.get('Authors','No Authors Provided')
-    authors = authors.replace('&','&amp;')
-    authors = authors.replace('<','&lt;')
-    authors = authors.replace('>','&gt;')
-    meta_array['Authors'] = authors
-
-    if buildXML:
-        xname = os.path.join(xmlDir, 'metadata.xml')
-        mlst = []
-        for key in meta_array:
-            mlst.append('<meta name="' + key + '" content="' + meta_array[key] + '" />\n')
-        metastr = "".join(mlst)
-        mlst = None
-        file(xname, 'wb').write(metastr)
-
-    print 'Processing StyleSheet'
-
-    # get some scaling info from metadata to use while processing styles
-    # and first page info
-
-    fontsize = '135'
-    if 'fontSize' in meta_array:
-        fontsize = meta_array['fontSize']
-
-    # also get the size of a normal text page
-    # get the total number of pages unpacked as a safety check
-    filenames = os.listdir(pageDir)
-    numfiles = len(filenames)
-
-    spage = '1'
-    if 'firstTextPage' in meta_array:
-        spage = meta_array['firstTextPage']
-    pnum = int(spage)
-    if pnum >= numfiles or pnum < 0:
-        # metadata is wrong so just select a page near the front
-        # 10% of the book to get a normal text page
-        pnum = int(0.10 * numfiles)
-    # print "first normal text page is", spage
-
-    # get page height and width from first text page for use in stylesheet scaling
-    pname = 'page%04d.dat' % (pnum + 1)
-    fname = os.path.join(pageDir,pname)
-    flat_xml = convert2xml.fromData(dict, fname)
-
-    (ph, pw) = getPageDim(flat_xml)
-    if (ph == '-1') or (ph == '0') : ph = '11000'
-    if (pw == '-1') or (pw == '0') : pw = '8500'
-    meta_array['pageHeight'] = ph
-    meta_array['pageWidth'] = pw
-    if 'fontSize' not in meta_array.keys():
-        meta_array['fontSize'] = fontsize
-
-    # process other.dat for css info and for map of page files to svg images
-    # this map is needed because some pages actually are made up of multiple
-    # pageXXXX.xml files
-    xname = os.path.join(bookDir, 'style.css')
-    flat_xml = convert2xml.fromData(dict, otherFile)
-
-    # extract info.original.pid to get original page information
-    pageIDMap = {}
-    pageidnums = stylexml2css.getpageIDMap(flat_xml)
-    if len(pageidnums) == 0:
-        filenames = os.listdir(pageDir)
-        numfiles = len(filenames)
-        for k in range(numfiles):
-            pageidnums.append(k)
-    # create a map from page ids to list of page file nums to process for that page
-    for i in range(len(pageidnums)):
-        id = pageidnums[i]
-        if id in pageIDMap.keys():
-            pageIDMap[id].append(i)
-        else:
-            pageIDMap[id] = [i]
-
-    # now get the css info
-    cssstr , classlst = stylexml2css.convert2CSS(flat_xml, fontsize, ph, pw)
-    file(xname, 'wb').write(cssstr)
-    if buildXML:
-        xname = os.path.join(xmlDir, 'other0000.xml')
-        file(xname, 'wb').write(convert2xml.getXML(dict, otherFile))
-
-    print 'Processing Glyphs'
-    gd = GlyphDict()
-    filenames = os.listdir(glyphsDir)
-    filenames = sorted(filenames)
-    glyfname = os.path.join(svgDir,'glyphs.svg')
-    glyfile = open(glyfname, 'w')
-    glyfile.write('<?xml version="1.0" standalone="no"?>\n')
-    glyfile.write('<!DOCTYPE svg PUBLIC "-//W3C/DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
-    glyfile.write('<svg width="512" height="512" viewBox="0 0 511 511" xmlns="http://www.w3.org/2000/svg" version="1.1">\n')
-    glyfile.write('<title>Glyphs for %s</title>\n' % meta_array['Title'])
-    glyfile.write('<defs>\n')
-    counter = 0
-    for filename in filenames:
-        # print '     ', filename
-        print '.',
-        fname = os.path.join(glyphsDir,filename)
-        flat_xml = convert2xml.fromData(dict, fname)
-
-        if buildXML:
-            xname = os.path.join(xmlDir, filename.replace('.dat','.xml'))
-            file(xname, 'wb').write(convert2xml.getXML(dict, fname))
-
-        gp = GParser(flat_xml)
-        for i in xrange(0, gp.count):
-            path = gp.getPath(i)
-            maxh, maxw = gp.getGlyphDim(i)
-            fullpath = '<path id="gl%d" d="%s" fill="black" /><!-- width=%d height=%d -->\n' % (counter * 256 + i, path, maxw, maxh)
-            glyfile.write(fullpath)
-            gd.addGlyph(counter * 256 + i, fullpath)
-        counter += 1
-    glyfile.write('</defs>\n')
-    glyfile.write('</svg>\n')
-    glyfile.close()
-    print " "
-
-
-    # start up the html
-    # also build up tocentries while processing html
-    htmlFileName = "book.html"
-    hlst = []
-    hlst.append('<?xml version="1.0" encoding="utf-8"?>\n')
-    hlst.append('<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.1 Strict//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11-strict.dtd">\n')
-    hlst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">\n')
-    hlst.append('<head>\n')
-    hlst.append('<meta http-equiv="content-type" content="text/html; charset=utf-8"/>\n')
-    hlst.append('<title>' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '</title>\n')
-    hlst.append('<meta name="Author" content="' + meta_array['Authors'] + '" />\n')
-    hlst.append('<meta name="Title" content="' + meta_array['Title'] + '" />\n')
-    if 'ASIN' in meta_array:
-        hlst.append('<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n')
-    if 'GUID' in meta_array:
-        hlst.append('<meta name="GUID" content="' + meta_array['GUID'] + '" />\n')
-    hlst.append('<link href="style.css" rel="stylesheet" type="text/css" />\n')
-    hlst.append('</head>\n<body>\n')
-
-    print 'Processing Pages'
-    # Books are at 1440 DPI.  This is rendering at twice that size for
-    # readability when rendering to the screen.
-    scaledpi = 1440.0
-
-    filenames = os.listdir(pageDir)
-    filenames = sorted(filenames)
-    numfiles = len(filenames)
-
-    xmllst = []
-    elst = []
-
-    for filename in filenames:
-        # print '     ', filename
-        print ".",
-        fname = os.path.join(pageDir,filename)
-        flat_xml = convert2xml.fromData(dict, fname)
-
-        # keep flat_xml for later svg processing
-        xmllst.append(flat_xml)
-
-        if buildXML:
-            xname = os.path.join(xmlDir, filename.replace('.dat','.xml'))
-            file(xname, 'wb').write(convert2xml.getXML(dict, fname))
-
-        # first get the html
-        pagehtml, tocinfo = flatxml2html.convert2HTML(flat_xml, classlst, fname, bookDir, gd, fixedimage)
-        elst.append(tocinfo)
-        hlst.append(pagehtml)
-
-    # finish up the html string and output it
-    hlst.append('</body>\n</html>\n')
-    htmlstr = "".join(hlst)
-    hlst = None
-    file(os.path.join(bookDir, htmlFileName), 'wb').write(htmlstr)
-
-    print " "
-    print 'Extracting Table of Contents from Amazon OCR'
-
-    # first create a table of contents file for the svg images
-    tlst = []
-    tlst.append('<?xml version="1.0" encoding="utf-8"?>\n')
-    tlst.append('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n')
-    tlst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >')
-    tlst.append('<head>\n')
-    tlst.append('<title>' + meta_array['Title'] + '</title>\n')
-    tlst.append('<meta name="Author" content="' + meta_array['Authors'] + '" />\n')
-    tlst.append('<meta name="Title" content="' + meta_array['Title'] + '" />\n')
-    if 'ASIN' in meta_array:
-        tlst.append('<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n')
-    if 'GUID' in meta_array:
-        tlst.append('<meta name="GUID" content="' + meta_array['GUID'] + '" />\n')
-    tlst.append('</head>\n')
-    tlst.append('<body>\n')
-
-    tlst.append('<h2>Table of Contents</h2>\n')
-    start = pageidnums[0]
-    if (raw):
-        startname = 'page%04d.svg' % start
-    else:
-        startname = 'page%04d.xhtml' % start
-
-    tlst.append('<h3><a href="' + startname + '">Start of Book</a></h3>\n')
-    # build up a table of contents for the svg xhtml output
-    tocentries = "".join(elst)
-    elst = None
-    toclst = tocentries.split('\n')
-    toclst.pop()
-    for entry in toclst:
-        print entry
-        title, pagenum = entry.split('|')
-        id = pageidnums[int(pagenum)]
-        if (raw):
-            fname = 'page%04d.svg' % id
-        else:
-            fname = 'page%04d.xhtml' % id
-        tlst.append('<h3><a href="'+ fname + '">' + title + '</a></h3>\n')
-    tlst.append('</body>\n')
-    tlst.append('</html>\n')
-    tochtml = "".join(tlst)
-    file(os.path.join(svgDir, 'toc.xhtml'), 'wb').write(tochtml)
-
-
-    # now create index_svg.xhtml that points to all required files
-    slst = []
-    slst.append('<?xml version="1.0" encoding="utf-8"?>\n')
-    slst.append('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n')
-    slst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >')
-    slst.append('<head>\n')
-    slst.append('<title>' + meta_array['Title'] + '</title>\n')
-    slst.append('<meta name="Author" content="' + meta_array['Authors'] + '" />\n')
-    slst.append('<meta name="Title" content="' + meta_array['Title'] + '" />\n')
-    if 'ASIN' in meta_array:
-        slst.append('<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n')
-    if 'GUID' in meta_array:
-        slst.append('<meta name="GUID" content="' + meta_array['GUID'] + '" />\n')
-    slst.append('</head>\n')
-    slst.append('<body>\n')
-
-    print "Building svg images of each book page"
-    slst.append('<h2>List of Pages</h2>\n')
-    slst.append('<div>\n')
-    idlst = sorted(pageIDMap.keys())
-    numids = len(idlst)
-    cnt = len(idlst)
-    previd = None
-    for j in range(cnt):
-        pageid = idlst[j]
-        if j < cnt - 1:
-            nextid = idlst[j+1]
+                j = j + 1
+        if inGroup:
+            result.append(('grpend',-1))
+        result.append(('pageend', -1))
+        return pagetype, result
+
+
+
+    # build a description of the paragraph
+    def getParaDescription(self, start, end, regtype):
+
+        result = []
+
+        # paragraph
+        (pos, pclass) = self.findinDoc('paragraph.class',start,end)
+
+        pclass = self.getClass(pclass)
+
+        # if paragraph uses extratokens (extra glyphs) then make it fixed
+        (pos, extraglyphs) = self.findinDoc('paragraph.extratokens',start,end)
+
+        # build up a description of the paragraph in result and return it
+        # first check for the  basic - all words paragraph
+        (pos, sfirst) = self.findinDoc('paragraph.firstWord',start,end)
+        (pos, slast) = self.findinDoc('paragraph.lastWord',start,end)
+        if (sfirst != None) and (slast != None) :
+            first = int(sfirst)
+            last = int(slast)
+
+            makeImage = (regtype == 'vertical') or (regtype == 'table')
+            makeImage = makeImage or (extraglyphs != None)
+            if self.fixedimage:
+                makeImage = makeImage or (regtype == 'fixed')
+
+            if (pclass != None):
+                makeImage = makeImage or (pclass.find('.inverted') >= 0)
+                if self.fixedimage :
+                    makeImage = makeImage or (pclass.find('cl-f-') >= 0)
+
+            # before creating an image make sure glyph info exists
+            gidList = self.getData('info.glyph.glyphID',0,-1)
+
+            makeImage = makeImage & (len(gidList) > 0)
+
+            if not makeImage :
+                # standard all word paragraph
+                for wordnum in xrange(first, last):
+                    result.append(('ocr', wordnum))
+                return pclass, result
+
+            # convert paragraph to svg image
+            # translate first and last word into first and last glyphs
+            # and generate inline image and include it
+            glyphList = []
+            firstglyphList = self.getData('word.firstGlyph',0,-1)
+            gidList = self.getData('info.glyph.glyphID',0,-1)
+            firstGlyph = firstglyphList[first]
+            if last < len(firstglyphList):
+                lastGlyph = firstglyphList[last]
+            else :
+                lastGlyph = len(gidList)
+
+            # handle case of white sapce paragraphs with no actual glyphs in them
+            # by reverting to text based paragraph
+            if firstGlyph >= lastGlyph:
+                # revert to standard text based paragraph
+                for wordnum in xrange(first, last):
+                    result.append(('ocr', wordnum))
+                return pclass, result
+
+            for glyphnum in xrange(firstGlyph, lastGlyph):
+                glyphList.append(glyphnum)
+            # include any extratokens if they exist
+            (pos, sfg) = self.findinDoc('extratokens.firstGlyph',start,end)
+            (pos, slg) = self.findinDoc('extratokens.lastGlyph',start,end)
+            if (sfg != None) and (slg != None):
+                for glyphnum in xrange(int(sfg), int(slg)):
+                    glyphList.append(glyphnum)
+            num = self.svgcount
+            self.glyphs_to_image(glyphList)
+            self.svgcount += 1
+            result.append(('svg', num))
+            return pclass, result
+
+        # this type of paragraph may be made up of multiple spans, inline
+        # word monograms (images), and words with semantic meaning,
+        # plus glyphs used to form starting letter of first word
+
+        # need to parse this type line by line
+        line = start + 1
+        word_class = ''
+
+        # if end is -1 then we must search to end of document
+        if end == -1 :
+            end = self.docSize
+
+        # seems some xml has last* coming before first* so we have to
+        # handle any order
+        sp_first = -1
+        sp_last = -1
+
+        gl_first = -1
+        gl_last = -1
+
+        ws_first = -1
+        ws_last = -1
+
+        word_class = ''
+
+        word_semantic_type = ''
+
+        while (line < end) :
+
+            (name, argres) = self.lineinDoc(line)
+
+            if name.endswith('span.firstWord') :
+                sp_first = int(argres)
+
+            elif name.endswith('span.lastWord') :
+                sp_last = int(argres)
+
+            elif name.endswith('word.firstGlyph') :
+                gl_first = int(argres)
+
+            elif name.endswith('word.lastGlyph') :
+                gl_last = int(argres)
+
+            elif name.endswith('word_semantic.firstWord'):
+                ws_first = int(argres)
+
+            elif name.endswith('word_semantic.lastWord'):
+                ws_last = int(argres)
+
+            elif name.endswith('word.class'):
+                (cname, space) = argres.split('-',1)
+                if space == '' : space = '0'
+                if (cname == 'spaceafter') and (int(space) > 0) :
+                    word_class = 'sa'
+
+            elif name.endswith('word.img.src'):
+                result.append(('img' + word_class, int(argres)))
+                word_class = ''
+
+            elif name.endswith('region.img.src'):
+                result.append(('img' + word_class, int(argres)))
+
+            if (sp_first != -1) and (sp_last != -1):
+                for wordnum in xrange(sp_first, sp_last):
+                    result.append(('ocr', wordnum))
+                sp_first = -1
+                sp_last = -1
+
+            if (gl_first != -1) and (gl_last != -1):
+                glyphList = []
+                for glyphnum in xrange(gl_first, gl_last):
+                    glyphList.append(glyphnum)
+                num = self.svgcount
+                self.glyphs_to_image(glyphList)
+                self.svgcount += 1
+                result.append(('svg', num))
+                gl_first = -1
+                gl_last = -1
+
+            if (ws_first != -1) and (ws_last != -1):
+                for wordnum in xrange(ws_first, ws_last):
+                    result.append(('ocr', wordnum))
+                ws_first = -1
+                ws_last = -1
+
+            line += 1
+
+        return pclass, result
+
+
+    def buildParagraph(self, pclass, pdesc, type, regtype) :
+        parares = ''
+        sep =''
+
+        classres = ''
+        if pclass :
+            classres = ' class="' + pclass + '"'
+
+        br_lb = (regtype == 'fixed') or (regtype == 'chapterheading') or (regtype == 'vertical')
+
+        handle_links = len(self.link_id) > 0
+
+        if (type == 'full') or (type == 'begin') :
+            parares += '<p' + classres + '>'
+
+        if (type == 'end'):
+            parares += ' '
+
+        lstart = len(parares)
+
+        cnt = len(pdesc)
+
+        for j in xrange( 0, cnt) :
+
+            (wtype, num) = pdesc[j]
+
+            if wtype == 'ocr' :
+                word = self.ocrtext[num]
+                sep = ' '
+
+                if handle_links:
+                    link = self.link_id[num]
+                    if (link > 0):
+                        linktype = self.link_type[link-1]
+                        title = self.link_title[link-1]
+                        if (title == "") or (parares.rfind(title) < 0):
+                            title=parares[lstart:]
+                        if linktype == 'external' :
+                            linkhref = self.link_href[link-1]
+                            linkhtml = '<a href="%s">' % linkhref
+                        else :
+                            if len(self.link_page) >= link :
+                                ptarget = self.link_page[link-1] - 1
+                                linkhtml = '<a href="#page%04d">' % ptarget
+                            else :
+                                # just link to the current page
+                                linkhtml = '<a href="#' + self.id + '">'
+                        linkhtml += title + '</a>'
+                        pos = parares.rfind(title)
+                        if pos >= 0:
+                            parares = parares[0:pos] + linkhtml + parares[pos+len(title):]
+                        else :
+                            parares += linkhtml
+                        lstart = len(parares)
+                        if word == '_link_' : word = ''
+                    elif (link < 0) :
+                        if word == '_link_' : word = ''
+
+                if word == '_lb_':
+                    if ((num-1) in self.dehyphen_rootid ) or handle_links:
+                        word = ''
+                        sep = ''
+                    elif br_lb :
+                        word = '<br />\n'
+                        sep = ''
+                    else :
+                        word = '\n'
+                        sep = ''
+
+                if num in self.dehyphen_rootid :
+                    word = word[0:-1]
+                    sep = ''
+
+                parares += word + sep
+
+            elif wtype == 'img' :
+                sep = ''
+                parares += '<img src="img/img%04d.jpg" alt="" />' % num
+                parares += sep
+
+            elif wtype == 'imgsa' :
+                sep = ' '
+                parares += '<img src="img/img%04d.jpg" alt="" />' % num
+                parares += sep
+
+            elif wtype == 'svg' :
+                sep = ''
+                parares += '<img src="img/' + self.id + '_%04d.svg" alt="" />' % num
+                parares += sep
+
+        if len(sep) > 0 : parares = parares[0:-1]
+        if (type == 'full') or (type == 'end') :
+            parares += '</p>'
+        return parares
+
+
+    def buildTOCEntry(self, pdesc) :
+        parares = ''
+        sep =''
+        tocentry = ''
+        handle_links = len(self.link_id) > 0
+
+        lstart = 0
+
+        cnt = len(pdesc)
+        for j in xrange( 0, cnt) :
+
+            (wtype, num) = pdesc[j]
+
+            if wtype == 'ocr' :
+                word = self.ocrtext[num]
+                sep = ' '
+
+                if handle_links:
+                    link = self.link_id[num]
+                    if (link > 0):
+                        linktype = self.link_type[link-1]
+                        title = self.link_title[link-1]
+                        title = title.rstrip('. ')
+                        alt_title = parares[lstart:]
+                        alt_title = alt_title.strip()
+                        # now strip off the actual printed page number
+                        alt_title = alt_title.rstrip('01234567890ivxldIVXLD-.')
+                        alt_title = alt_title.rstrip('. ')
+                        # skip over any external links - can't have them in a books toc
+                        if linktype == 'external' :
+                            title = ''
+                            alt_title = ''
+                            linkpage = ''
+                        else :
+                            if len(self.link_page) >= link :
+                                ptarget = self.link_page[link-1] - 1
+                                linkpage = '%04d' % ptarget
+                            else :
+                                # just link to the current page
+                                linkpage = self.id[4:]
+                        if len(alt_title) >= len(title):
+                            title = alt_title
+                        if title != '' and linkpage != '':
+                            tocentry += title + '|' + linkpage + '\n'
+                        lstart = len(parares)
+                        if word == '_link_' : word = ''
+                    elif (link < 0) :
+                        if word == '_link_' : word = ''
+
+                if word == '_lb_':
+                    word = ''
+                    sep = ''
+
+                if num in self.dehyphen_rootid :
+                    word = word[0:-1]
+                    sep = ''
+
+                parares += word + sep
+
+            else :
+                continue
+
+        return tocentry
+
+
+
+
+    # walk the document tree collecting the information needed
+    # to build an html page using the ocrText
+
+    def process(self):
+
+        tocinfo = ''
+        hlst = []
+
+        # get the ocr text
+        (pos, argres) = self.findinDoc('info.word.ocrText',0,-1)
+        if argres :  self.ocrtext = argres.split('|')
+
+        # get information to dehyphenate the text
+        self.dehyphen_rootid = self.getData('info.dehyphen.rootID',0,-1)
+
+        # determine if first paragraph is continued from previous page
+        (pos, self.parastems_stemid) = self.findinDoc('info.paraStems.stemID',0,-1)
+        first_para_continued = (self.parastems_stemid  != None)
+
+        # determine if last paragraph is continued onto the next page
+        (pos, self.paracont_stemid) = self.findinDoc('info.paraCont.stemID',0,-1)
+        last_para_continued = (self.paracont_stemid != None)
+
+        # collect link ids
+        self.link_id = self.getData('info.word.link_id',0,-1)
+
+        # collect link destination page numbers
+        self.link_page = self.getData('info.links.page',0,-1)
+
+        # collect link types (container versus external)
+        (pos, argres) = self.findinDoc('info.links.type',0,-1)
+        if argres :  self.link_type = argres.split('|')
+
+        # collect link destinations
+        (pos, argres) = self.findinDoc('info.links.href',0,-1)
+        if argres :  self.link_href = argres.split('|')
+
+        # collect link titles
+        (pos, argres) = self.findinDoc('info.links.title',0,-1)
+        if argres :
+            self.link_title = argres.split('|')
         else:
-            nextid = None
-        print '.',
-        pagelst = pageIDMap[pageid]
-        flst = []
-        for page in pagelst:
-            flst.append(xmllst[page])
-        flat_svg = "".join(flst)
-        flst=None
-        svgxml = flatxml2svg.convert2SVG(gd, flat_svg, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi)
-        if (raw) :
-            pfile = open(os.path.join(svgDir,'page%04d.svg' % pageid),'w')
-            slst.append('<a href="svg/page%04d.svg">Page %d</a>\n' % (pageid, pageid))
-        else :
-            pfile = open(os.path.join(svgDir,'page%04d.xhtml' % pageid), 'w')
-            slst.append('<a href="svg/page%04d.xhtml">Page %d</a>\n' % (pageid, pageid))
-        previd = pageid
-        pfile.write(svgxml)
-        pfile.close()
-        counter += 1
-    slst.append('</div>\n')
-    slst.append('<h2><a href="svg/toc.xhtml">Table of Contents</a></h2>\n')
-    slst.append('</body>\n</html>\n')
-    svgindex = "".join(slst)
-    slst = None
-    file(os.path.join(bookDir, 'index_svg.xhtml'), 'wb').write(svgindex)
-
-    print " "
-
-    # build the opf file
-    opfname = os.path.join(bookDir, 'book.opf')
-    olst = []
-    olst.append('<?xml version="1.0" encoding="utf-8"?>\n')
-    olst.append('<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="guid_id">\n')
-    # adding metadata
-    olst.append('   <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">\n')
-    if 'GUID' in meta_array:
-        olst.append('      <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n')
-    if 'ASIN' in meta_array:
-        olst.append('      <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n')
-    if 'oASIN' in meta_array:
-        olst.append('      <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n')
-    olst.append('      <dc:title>' + meta_array['Title'] + '</dc:title>\n')
-    olst.append('      <dc:creator opf:role="aut">' + meta_array['Authors'] + '</dc:creator>\n')
-    olst.append('      <dc:language>en</dc:language>\n')
-    olst.append('      <dc:date>' + meta_array['UpdateTime'] + '</dc:date>\n')
-    if isCover:
-        olst.append('      <meta name="cover" content="bookcover"/>\n')
-    olst.append('   </metadata>\n')
-    olst.append('<manifest>\n')
-    olst.append('   <item id="book" href="book.html" media-type="application/xhtml+xml"/>\n')
-    olst.append('   <item id="stylesheet" href="style.css" media-type="text/css"/>\n')
-    # adding image files to manifest
-    filenames = os.listdir(imgDir)
-    filenames = sorted(filenames)
-    for filename in filenames:
-        imgname, imgext = os.path.splitext(filename)
-        if imgext == '.jpg':
-            imgext = 'jpeg'
-        if imgext == '.svg':
-            imgext = 'svg+xml'
-        olst.append('   <item id="' + imgname + '" href="img/' + filename + '" media-type="image/' + imgext + '"/>\n')
-    if isCover:
-        olst.append('   <item id="bookcover" href="cover.jpg" media-type="image/jpeg" />\n')
-    olst.append('</manifest>\n')
-    # adding spine
-    olst.append('<spine>\n   <itemref idref="book" />\n</spine>\n')
-    if isCover:
-        olst.append('   <guide>\n')
-        olst.append('      <reference href="cover.jpg" type="cover" title="Cover"/>\n')
-        olst.append('   </guide>\n')
-    olst.append('</package>\n')
-    opfstr = "".join(olst)
-    olst = None
-    file(opfname, 'wb').write(opfstr)
-
-    print 'Processing Complete'
-
-    return 0
-
-def usage():
-    print "genbook.py generates a book from the extract Topaz Files"
-    print "Usage:"
-    print "    genbook.py [-r] [-h [--fixed-image] <bookDir>  "
-    print "  "
-    print "Options:"
-    print "  -h            :  help - print this usage message"
-    print "  -r            :  generate raw svg files (not wrapped in xhtml)"
-    print "  --fixed-image :  genearate any Fixed Area as an svg image in the html"
-    print "  "
-
-
-def main(argv):
-    bookDir = ''
-    if len(argv) == 0:
-        argv = sys.argv
-
-    try:
-        opts, args = getopt.getopt(argv[1:], "rh:",["fixed-image"])
-
-    except getopt.GetoptError, err:
-        print str(err)
-        usage()
-        return 1
-
-    if len(opts) == 0 and len(args) == 0 :
-        usage()
-        return 1
-
-    raw = 0
-    fixedimage = True
-    for o, a in opts:
-        if o =="-h":
-            usage()
-            return 0
-        if o =="-r":
-            raw = 1
-        if o =="--fixed-image":
-            fixedimage = True
-
-    bookDir = args[0]
-
-    rv = generateBook(bookDir, raw, fixedimage)
-    return rv
-
-
-if __name__ == '__main__':
-    sys.exit(main(''))
+            self.link_title.append('')
+
+        # get a descriptions of the starting points of the regions
+        # and groups on the page
+        (pagetype, pageDesc) = self.PageDescription()
+        regcnt = len(pageDesc) - 1
+
+        anchorSet = False
+        breakSet = False
+        inGroup = False
+
+        # process each region on the page and convert what you can to html
+
+        for j in xrange(regcnt):
+
+            (etype, start) = pageDesc[j]
+            (ntype, end) = pageDesc[j+1]
+
+
+            # set anchor for link target on this page
+            if not anchorSet and not first_para_continued:
+                hlst.append('<div style="visibility: hidden; height: 0; width: 0;" id="')
+                hlst.append(self.id + '" title="pagetype_' + pagetype + '"></div>\n')
+                anchorSet = True
+
+            # handle groups of graphics with text captions
+            if (etype == 'grpbeg'):
+                (pos, grptype) = self.findinDoc('group.type', start, end)
+                if grptype != None:
+                    if grptype == 'graphic':
+                        gcstr = ' class="' + grptype + '"'
+                        hlst.append('<div' + gcstr + '>')
+                        inGroup = True
+
+            elif (etype == 'grpend'):
+                if inGroup:
+                    hlst.append('</div>\n')
+                    inGroup = False
+
+            else:
+                (pos, regtype) = self.findinDoc('region.type',start,end)
+
+                if regtype == 'graphic' :
+                    (pos, simgsrc) = self.findinDoc('img.src',start,end)
+                    if simgsrc:
+                        if inGroup:
+                            hlst.append('<img src="img/img%04d.jpg" alt="" />' % int(simgsrc))
+                        else:
+                            hlst.append('<div class="graphic"><img src="img/img%04d.jpg" alt="" /></div>' % int(simgsrc))
+
+                elif regtype == 'chapterheading' :
+                    (pclass, pdesc) = self.getParaDescription(start,end, regtype)
+                    if not breakSet:
+                        hlst.append('<div style="page-break-after: always;">&nbsp;</div>\n')
+                        breakSet = True
+                    tag = 'h1'
+                    if pclass and (len(pclass) >= 7):
+                        if pclass[3:7] == 'ch1-' : tag = 'h1'
+                        if pclass[3:7] == 'ch2-' : tag = 'h2'
+                        if pclass[3:7] == 'ch3-' : tag = 'h3'
+                        hlst.append('<' + tag + ' class="' + pclass + '">')
+                    else:
+                        hlst.append('<' + tag + '>')
+                    hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype))
+                    hlst.append('</' + tag + '>')
+
+                elif (regtype == 'text') or (regtype == 'fixed') or (regtype == 'insert') or (regtype == 'listitem'):
+                    ptype = 'full'
+                    # check to see if this is a continution from the previous page
+                    if first_para_continued :
+                        ptype = 'end'
+                        first_para_continued = False
+                    (pclass, pdesc) = self.getParaDescription(start,end, regtype)
+                    if pclass and (len(pclass) >= 6) and (ptype == 'full'):
+                        tag = 'p'
+                        if pclass[3:6] == 'h1-' : tag = 'h4'
+                        if pclass[3:6] == 'h2-' : tag = 'h5'
+                        if pclass[3:6] == 'h3-' : tag = 'h6'
+                        hlst.append('<' + tag + ' class="' + pclass + '">')
+                        hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype))
+                        hlst.append('</' + tag + '>')
+                    else :
+                        hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
+
+                elif (regtype == 'tocentry') :
+                    ptype = 'full'
+                    if first_para_continued :
+                        ptype = 'end'
+                        first_para_continued = False
+                    (pclass, pdesc) = self.getParaDescription(start,end, regtype)
+                    tocinfo += self.buildTOCEntry(pdesc)
+                    hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
+
+                elif (regtype == 'vertical') or (regtype == 'table') :
+                    ptype = 'full'
+                    if inGroup:
+                        ptype = 'middle'
+                    if first_para_continued :
+                        ptype = 'end'
+                        first_para_continued = False
+                    (pclass, pdesc) = self.getParaDescription(start, end, regtype)
+                    hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
+
+
+                elif (regtype == 'synth_fcvr.center'):
+                    (pos, simgsrc) = self.findinDoc('img.src',start,end)
+                    if simgsrc:
+                        hlst.append('<div class="graphic"><img src="img/img%04d.jpg" alt="" /></div>' % int(simgsrc))
+
+                else :
+                    print '          Making region type', regtype,
+                    (pos, temp) = self.findinDoc('paragraph',start,end)
+                    (pos2, temp) = self.findinDoc('span',start,end)
+                    if pos != -1 or pos2 != -1:
+                        print ' a "text" region'
+                        orig_regtype = regtype
+                        regtype = 'fixed'
+                        ptype = 'full'
+                        # check to see if this is a continution from the previous page
+                        if first_para_continued :
+                            ptype = 'end'
+                            first_para_continued = False
+                        (pclass, pdesc) = self.getParaDescription(start,end, regtype)
+                        if not pclass:
+                            if orig_regtype.endswith('.right')     : pclass = 'cl-right'
+                            elif orig_regtype.endswith('.center')  : pclass = 'cl-center'
+                            elif orig_regtype.endswith('.left')    : pclass = 'cl-left'
+                            elif orig_regtype.endswith('.justify') : pclass = 'cl-justify'
+                        if pclass and (ptype == 'full') and (len(pclass) >= 6):
+                            tag = 'p'
+                            if pclass[3:6] == 'h1-' : tag = 'h4'
+                            if pclass[3:6] == 'h2-' : tag = 'h5'
+                            if pclass[3:6] == 'h3-' : tag = 'h6'
+                            hlst.append('<' + tag + ' class="' + pclass + '">')
+                            hlst.append(self.buildParagraph(pclass, pdesc, 'middle', regtype))
+                            hlst.append('</' + tag + '>')
+                        else :
+                            hlst.append(self.buildParagraph(pclass, pdesc, ptype, regtype))
+                    else :
+                        print ' a "graphic" region'
+                        (pos, simgsrc) = self.findinDoc('img.src',start,end)
+                        if simgsrc:
+                            hlst.append('<div class="graphic"><img src="img/img%04d.jpg" alt="" /></div>' % int(simgsrc))
+
+
+        htmlpage = "".join(hlst)
+        if last_para_continued :
+            if htmlpage[-4:] == '</p>':
+                htmlpage = htmlpage[0:-4]
+            last_para_continued = False
+
+        return htmlpage, tocinfo
+
+
+def convert2HTML(flatxml, classlst, fileid, bookDir, gdict, fixedimage):
+    # create a document parser
+    dp = DocParser(flatxml, classlst, fileid, bookDir, gdict, fixedimage)
+    htmlpage, tocinfo = dp.process()
+    return htmlpage, tocinfo
index c4716bd7f820d22eaf8b6577d898afc566721006..4dfd6c7bbfae633633100610805ddbb13334fd46 100644 (file)
-#!/usr/bin/python
-#
-# This is a python script. You need a Python interpreter to run it.
-# For example, ActiveState Python, which exists for windows.
-#
-# Changelog
-#  1.00 - Initial version
-
-__version__ = '1.00'
+#! /usr/bin/python
+# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
 
 import sys
+import csv
+import os
+import getopt
+from struct import pack
+from struct import unpack
 
-class Unbuffered:
-    def __init__(self, stream):
-        self.stream = stream
-    def write(self, data):
-        self.stream.write(data)
-        self.stream.flush()
-    def __getattr__(self, attr):
-        return getattr(self.stream, attr)
-sys.stdout=Unbuffered(sys.stdout)
 
-import os
-import struct
-import binascii
-import kgenpids
-import topazextract
-import mobidedrm
-from alfcrypto import Pukall_Cipher
-
-class DrmException(Exception):
-    pass
-
-def getK4PCpids(path_to_ebook):
-    # Return Kindle4PC PIDs. Assumes that the caller checked that we are not on Linux, which will raise an exception
-
-    mobi = True
-    magic3 = file(path_to_ebook,'rb').read(3)
-    if magic3 == 'TPZ':
-        mobi = False
-
-    if mobi:
-        mb = mobidedrm.MobiBook(path_to_ebook,False)
-    else:
-        mb = topazextract.TopazBook(path_to_ebook)
-    
-    md1, md2 = mb.getPIDMetaInfo()
+class PParser(object):
+    def __init__(self, gd, flatxml, meta_array):
+        self.gd = gd
+        self.flatdoc = flatxml.split('\n')
+        self.docSize = len(self.flatdoc)
+        self.temp = []
+
+        self.ph = -1
+        self.pw = -1
+        startpos = self.posinDoc('page.h') or self.posinDoc('book.h')
+        for p in startpos:
+            (name, argres) = self.lineinDoc(p)
+            self.ph = max(self.ph, int(argres))
+        startpos = self.posinDoc('page.w') or self.posinDoc('book.w')
+        for p in startpos:
+            (name, argres) = self.lineinDoc(p)
+            self.pw = max(self.pw, int(argres))
+
+        if self.ph <= 0:
+            self.ph = int(meta_array.get('pageHeight', '11000'))
+        if self.pw <= 0:
+            self.pw = int(meta_array.get('pageWidth', '8500'))
+
+        res = []
+        startpos = self.posinDoc('info.glyph.x')
+        for p in startpos:
+            argres = self.getDataatPos('info.glyph.x', p)
+            res.extend(argres)
+        self.gx = res
+
+        res = []
+        startpos = self.posinDoc('info.glyph.y')
+        for p in startpos:
+            argres = self.getDataatPos('info.glyph.y', p)
+            res.extend(argres)
+        self.gy = res
+
+        res = []
+        startpos = self.posinDoc('info.glyph.glyphID')
+        for p in startpos:
+            argres = self.getDataatPos('info.glyph.glyphID', p)
+            res.extend(argres)
+        self.gid = res
 
-    return kgenpids.getPidList(md1, md2, True, [], [], []) 
 
+    # return tag at line pos in document
+    def lineinDoc(self, pos) :
+        if (pos >= 0) and (pos < self.docSize) :
+            item = self.flatdoc[pos]
+            if item.find('=') >= 0:
+                (name, argres) = item.split('=',1)
+            else :
+                name = item
+                argres = ''
+        return name, argres
 
-def main(argv=sys.argv):
-    print ('getk4pcpids.py v%(__version__)s. '
-        'Copyright 2012 Apprentic Alf' % globals())
+    # find tag in doc if within pos to end inclusive
+    def findinDoc(self, tagpath, pos, end) :
+        result = None
+        if end == -1 :
+            end = self.docSize
+        else:
+            end = min(self.docSize, end)
+        foundat = -1
+        for j in xrange(pos, end):
+            item = self.flatdoc[j]
+            if item.find('=') >= 0:
+                (name, argres) = item.split('=',1)
+            else :
+                name = item
+                argres = ''
+            if name.endswith(tagpath) :
+                result = argres
+                foundat = j
+                break
+        return foundat, result
 
-    if len(argv)<2 or len(argv)>3:
-        print "Gets the possible book-specific PIDs from K4PC for a particular book"
-        print "Usage:"
-        print "    %s <bookfile> [<outfile>]" % sys.argv[0]
-        return 1
+    # return list of start positions for the tagpath
+    def posinDoc(self, tagpath):
+        startpos = []
+        pos = 0
+        res = ""
+        while res != None :
+            (foundpos, res) = self.findinDoc(tagpath, pos, -1)
+            if res != None :
+                startpos.append(foundpos)
+            pos = foundpos + 1
+        return startpos
+
+    def getData(self, path):
+        result = None
+        cnt = len(self.flatdoc)
+        for j in xrange(cnt):
+            item = self.flatdoc[j]
+            if item.find('=') >= 0:
+                (name, argt) = item.split('=')
+                argres = argt.split('|')
+            else:
+                name = item
+                argres = []
+            if (name.endswith(path)):
+                result = argres
+                break
+        if (len(argres) > 0) :
+            for j in xrange(0,len(argres)):
+                argres[j] = int(argres[j])
+        return result
+
+    def getDataatPos(self, path, pos):
+        result = None
+        item = self.flatdoc[pos]
+        if item.find('=') >= 0:
+            (name, argt) = item.split('=')
+            argres = argt.split('|')
+        else:
+            name = item
+            argres = []
+        if (len(argres) > 0) :
+            for j in xrange(0,len(argres)):
+                argres[j] = int(argres[j])
+        if (name.endswith(path)):
+            result = argres
+        return result
+
+    def getDataTemp(self, path):
+        result = None
+        cnt = len(self.temp)
+        for j in xrange(cnt):
+            item = self.temp[j]
+            if item.find('=') >= 0:
+                (name, argt) = item.split('=')
+                argres = argt.split('|')
+            else:
+                name = item
+                argres = []
+            if (name.endswith(path)):
+                result = argres
+                self.temp.pop(j)
+                break
+        if (len(argres) > 0) :
+            for j in xrange(0,len(argres)):
+                argres[j] = int(argres[j])
+        return result
+
+    def getImages(self):
+        result = []
+        self.temp = self.flatdoc
+        while (self.getDataTemp('img') != None):
+            h = self.getDataTemp('img.h')[0]
+            w = self.getDataTemp('img.w')[0]
+            x = self.getDataTemp('img.x')[0]
+            y = self.getDataTemp('img.y')[0]
+            src = self.getDataTemp('img.src')[0]
+            result.append('<image xlink:href="../img/img%04d.jpg" x="%d" y="%d" width="%d" height="%d" />\n' % (src, x, y, w, h))
+        return result
+
+    def getGlyphs(self):
+        result = []
+        if (self.gid != None) and (len(self.gid) > 0):
+            glyphs = []
+            for j in set(self.gid):
+                glyphs.append(j)
+            glyphs.sort()
+            for gid in glyphs:
+                id='id="gl%d"' % gid
+                path = self.gd.lookup(id)
+                if path:
+                    result.append(id + ' ' + path)
+        return result
+
+
+def convert2SVG(gdict, flat_xml, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi):
+    mlst = []
+    pp = PParser(gdict, flat_xml, meta_array)
+    mlst.append('<?xml version="1.0" standalone="no"?>\n')
+    if (raw):
+        mlst.append('<!DOCTYPE svg PUBLIC "-//W3C/DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
+        mlst.append('<svg width="%fin" height="%fin" viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">\n' % (pp.pw / scaledpi, pp.ph / scaledpi, pp.pw -1, pp.ph -1))
+        mlst.append('<title>Page %d - %s by %s</title>\n' % (pageid, meta_array['Title'],meta_array['Authors']))
     else:
-        infile = argv[1]
-        try:
-            pidlist = getK4PCpids(infile)
-        except DrmException, e:
-            print "Error: %s" % e
-            return 1
-        pidstring = ','.join(pidlist)
-        print "Possible PIDs are: ", pidstring
-        if len(argv) is 3:
-            outfile = argv[2]
-            file(outfile, 'w').write(pidstring)
-        
-    return 0
-
-if __name__ == "__main__":
-    sys.exit(main())
+        mlst.append('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n')
+        mlst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" ><head>\n')
+        mlst.append('<title>Page %d - %s by %s</title>\n' % (pageid, meta_array['Title'],meta_array['Authors']))
+        mlst.append('<script><![CDATA[\n')
+        mlst.append('function gd(){var p=window.location.href.replace(/^.*\?dpi=(\d+).*$/i,"$1");return p;}\n')
+        mlst.append('var dpi=%d;\n' % scaledpi)
+        if (previd) :
+            mlst.append('var prevpage="page%04d.xhtml";\n' % (previd))
+        if (nextid) :
+            mlst.append('var nextpage="page%04d.xhtml";\n' % (nextid))
+        mlst.append('var pw=%d;var ph=%d;' % (pp.pw, pp.ph))
+        mlst.append('function zoomin(){dpi=dpi*(0.8);setsize();}\n')
+        mlst.append('function zoomout(){dpi=dpi*1.25;setsize();}\n')
+        mlst.append('function setsize(){var svg=document.getElementById("svgimg");var prev=document.getElementById("prevsvg");var next=document.getElementById("nextsvg");var width=(pw/dpi)+"in";var height=(ph/dpi)+"in";svg.setAttribute("width",width);svg.setAttribute("height",height);prev.setAttribute("height",height);prev.setAttribute("width","50px");next.setAttribute("height",height);next.setAttribute("width","50px");}\n')
+        mlst.append('function ppage(){window.location.href=prevpage+"?dpi="+Math.round(dpi);}\n')
+        mlst.append('function npage(){window.location.href=nextpage+"?dpi="+Math.round(dpi);}\n')
+        mlst.append('var gt=gd();if(gt>0){dpi=gt;}\n')
+        mlst.append('window.onload=setsize;\n')
+        mlst.append(']]></script>\n')
+        mlst.append('</head>\n')
+        mlst.append('<body onLoad="setsize();" style="background-color:#777;text-align:center;">\n')
+        mlst.append('<div style="white-space:nowrap;">\n')
+        if previd == None:
+            mlst.append('<a href="javascript:ppage();"><svg id="prevsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"></svg></a>\n')
+        else:
+            mlst.append('<a href="javascript:ppage();"><svg id="prevsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"><polygon points="5,150,95,5,95,295" fill="#AAAAAA" /></svg></a>\n')
+
+        mlst.append('<a href="javascript:npage();"><svg id="svgimg" viewBox="0 0 %d %d" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" style="background-color:#FFF;border:1px solid black;">' % (pp.pw, pp.ph))
+    if (pp.gid != None):
+        mlst.append('<defs>\n')
+        gdefs = pp.getGlyphs()
+        for j in xrange(0,len(gdefs)):
+            mlst.append(gdefs[j])
+        mlst.append('</defs>\n')
+    img = pp.getImages()
+    if (img != None):
+        for j in xrange(0,len(img)):
+            mlst.append(img[j])
+    if (pp.gid != None):
+        for j in xrange(0,len(pp.gid)):
+            mlst.append('<use xlink:href="#gl%d" x="%d" y="%d" />\n' % (pp.gid[j], pp.gx[j], pp.gy[j]))
+    if (img == None or len(img) == 0) and (pp.gid == None or len(pp.gid) == 0):
+        xpos = "%d" % (pp.pw // 3)
+        ypos = "%d" % (pp.ph // 3)
+        mlst.append('<text x="' + xpos + '" y="' + ypos + '" font-size="' + meta_array['fontSize'] + '" font-family="Helvetica" stroke="black">This page intentionally left blank.</text>\n')
+    if (raw) :
+        mlst.append('</svg>')
+    else :
+        mlst.append('</svg></a>\n')
+        if nextid == None:
+            mlst.append('<a href="javascript:npage();"><svg id="nextsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"></svg></a>\n')
+        else :
+            mlst.append('<a href="javascript:npage();"><svg id="nextsvg" viewBox="0 0 100 300" xmlns="http://www.w3.org/2000/svg" version="1.1" style="background-color:#777"><polygon points="5,5,5,295,95,150" fill="#AAAAAA" /></svg></a>\n')
+        mlst.append('</div>\n')
+        mlst.append('<div><a href="javascript:zoomin();">zoom in</a> - <a href="javascript:zoomout();">zoom out</a></div>\n')
+        mlst.append('</body>\n')
+        mlst.append('</html>\n')
+    return "".join(mlst)
index b5c2804286b3d8102729ac9e6cf8ea110345fdde..97338872be00259f25164d5efa82a6970f24758b 100644 (file)
@@ -1,23 +1,5 @@
-#!/usr/bin/env python
-
-from __future__ import with_statement
-
-# engine to remove drm from Kindle for Mac and Kindle for PC books
-# for personal use for archiving and converting your ebooks
-
-# PLEASE DO NOT PIRATE EBOOKS!
-
-# We want all authors and publishers, and eBook stores to live
-# long and prosperous lives but at the same time  we just want to
-# be able to read OUR books on whatever device we want and to keep
-# readable for a long, long time
-
-#  This borrows very heavily from works by CMBDTC, IHeartCabbages, skindle,
-#    unswindle, DarkReverser, ApprenticeAlf, DiapDealer, some_updates
-#    and many many others
-
-
-__version__ = '4.3'
+#! /usr/bin/python
+# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
 
 class Unbuffered:
     def __init__(self, stream):
@@ -29,195 +11,711 @@ class Unbuffered:
         return getattr(self.stream, attr)
 
 import sys
-import os, csv, getopt
-import string
-import re
-import traceback
+sys.stdout=Unbuffered(sys.stdout)
 
-buildXML = False
+import csv
+import os
+import getopt
+from struct import pack
+from struct import unpack
 
-class DrmException(Exception):
+class TpzDRMError(Exception):
     pass
 
+# local support routines
 if 'calibre' in sys.modules:
     inCalibre = True
 else:
     inCalibre = False
 
-if inCalibre:
-    from calibre_plugins.k4mobidedrm import mobidedrm
-    from calibre_plugins.k4mobidedrm import topazextract
-    from calibre_plugins.k4mobidedrm import kgenpids
-else:
-    import mobidedrm
-    import topazextract
-    import kgenpids
-
-
-# cleanup bytestring filenames
-# borrowed from calibre from calibre/src/calibre/__init__.py
-# added in removal of non-printing chars
-# and removal of . at start
-# convert underscores to spaces (we're OK with spaces in file names)
-def cleanup_name(name):
-    _filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]')
-    substitute='_'
-    one = ''.join(char for char in name if char in string.printable)
-    one = _filename_sanitize.sub(substitute, one)
-    one = re.sub(r'\s', ' ', one).strip()
-    one = re.sub(r'^\.+$', '_', one)
-    one = one.replace('..', substitute)
-    # Windows doesn't like path components that end with a period
-    if one.endswith('.'):
-        one = one[:-1]+substitute
-    # Mac and Unix don't like file names that begin with a full stop
-    if len(one) > 0 and one[0] == '.':
-        one = substitute+one[1:]
-    one = one.replace('_',' ')
-    return one
-
-def decryptBook(infile, outdir, k4, kInfoFiles, serials, pids):
-    global buildXML
-
-    # handle the obvious cases at the beginning
-    if not os.path.isfile(infile):
-        print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: Input file does not exist"
-        return 1
-
-    mobi = True
-    magic3 = file(infile,'rb').read(3)
-    if magic3 == 'TPZ':
-        mobi = False
-
-    bookname = os.path.splitext(os.path.basename(infile))[0]
+if inCalibre :
+    from calibre_plugins.k4mobidedrm import convert2xml
+    from calibre_plugins.k4mobidedrm import flatxml2html
+    from calibre_plugins.k4mobidedrm import flatxml2svg
+    from calibre_plugins.k4mobidedrm import stylexml2css
+else :
+    import convert2xml
+    import flatxml2html
+    import flatxml2svg
+    import stylexml2css
+
+# global switch
+buildXML = False
 
-    if mobi:
-        mb = mobidedrm.MobiBook(infile)
-    else:
-        mb = topazextract.TopazBook(infile)
-
-    title = mb.getBookTitle()
-    print "Processing Book: ", title
-    filenametitle = cleanup_name(title)
-    outfilename = cleanup_name(bookname)
-    
-    # generate 'sensible' filename, that will sort with the original name,
-    # but is close to the name from the file.
-    outlength = len(outfilename)
-    comparelength = min(8,min(outlength,len(filenametitle)))
-    copylength = min(max(outfilename.find(' '),8),len(outfilename))
-    if outlength==0:
-        outfilename = filenametitle
-    elif comparelength > 0:
-       if outfilename[:comparelength] == filenametitle[:comparelength]:
-               outfilename = filenametitle
+# Get a 7 bit encoded number from a file
+def readEncodedNumber(file):
+    flag = False
+    c = file.read(1)
+    if (len(c) == 0):
+        return None
+    data = ord(c)
+    if data == 0xFF:
+        flag = True
+        c = file.read(1)
+        if (len(c) == 0):
+            return None
+        data = ord(c)
+    if data >= 0x80:
+        datax = (data & 0x7F)
+        while data >= 0x80 :
+            c = file.read(1)
+            if (len(c) == 0):
+                return None
+            data = ord(c)
+            datax = (datax <<7) + (data & 0x7F)
+        data = datax
+    if flag:
+        data = -data
+    return data
+
+# Get a length prefixed string from the file
+def lengthPrefixString(data):
+    return encodeNumber(len(data))+data
+
+def readString(file):
+    stringLength = readEncodedNumber(file)
+    if (stringLength == None):
+        return None
+    sv = file.read(stringLength)
+    if (len(sv)  != stringLength):
+        return ""
+    return unpack(str(stringLength)+"s",sv)[0]
+
+def getMetaArray(metaFile):
+    # parse the meta file
+    result = {}
+    fo = file(metaFile,'rb')
+    size = readEncodedNumber(fo)
+    for i in xrange(size):
+        tag = readString(fo)
+        value = readString(fo)
+        result[tag] = value
+        # print tag, value
+    fo.close()
+    return result
+
+
+# dictionary of all text strings by index value
+class Dictionary(object):
+    def __init__(self, dictFile):
+        self.filename = dictFile
+        self.size = 0
+        self.fo = file(dictFile,'rb')
+        self.stable = []
+        self.size = readEncodedNumber(self.fo)
+        for i in xrange(self.size):
+            self.stable.append(self.escapestr(readString(self.fo)))
+        self.pos = 0
+    def escapestr(self, str):
+        str = str.replace('&','&amp;')
+        str = str.replace('<','&lt;')
+        str = str.replace('>','&gt;')
+        str = str.replace('=','&#61;')
+        return str
+    def lookup(self,val):
+        if ((val >= 0) and (val < self.size)) :
+            self.pos = val
+            return self.stable[self.pos]
         else:
-               outfilename = outfilename[:copylength] + " " + filenametitle
-
-    # avoid excessively long file names
-    if len(outfilename)>150:
-        outfilename = outfilename[:150]
+            print "Error - %d outside of string table limits" % val
+            raise TpzDRMError('outside or string table limits')
+            # sys.exit(-1)
+    def getSize(self):
+        return self.size
+    def getPos(self):
+        return self.pos
+
+
+class PageDimParser(object):
+    def __init__(self, flatxml):
+        self.flatdoc = flatxml.split('\n')
+    # find tag if within pos to end inclusive
+    def findinDoc(self, tagpath, pos, end) :
+        result = None
+        docList = self.flatdoc
+        cnt = len(docList)
+        if end == -1 :
+            end = cnt
+        else:
+            end = min(cnt,end)
+        foundat = -1
+        for j in xrange(pos, end):
+            item = docList[j]
+            if item.find('=') >= 0:
+                (name, argres) = item.split('=')
+            else :
+                name = item
+                argres = ''
+            if name.endswith(tagpath) :
+                result = argres
+                foundat = j
+                break
+        return foundat, result
+    def process(self):
+        (pos, sph) = self.findinDoc('page.h',0,-1)
+        (pos, spw) = self.findinDoc('page.w',0,-1)
+        if (sph == None): sph = '-1'
+        if (spw == None): spw = '-1'
+        return sph, spw
+
+def getPageDim(flatxml):
+    # create a document parser
+    dp = PageDimParser(flatxml)
+    (ph, pw) = dp.process()
+    return ph, pw
+
+class GParser(object):
+    def __init__(self, flatxml):
+        self.flatdoc = flatxml.split('\n')
+        self.dpi = 1440
+        self.gh = self.getData('info.glyph.h')
+        self.gw = self.getData('info.glyph.w')
+        self.guse = self.getData('info.glyph.use')
+        if self.guse :
+            self.count = len(self.guse)
+        else :
+            self.count = 0
+        self.gvtx = self.getData('info.glyph.vtx')
+        self.glen = self.getData('info.glyph.len')
+        self.gdpi = self.getData('info.glyph.dpi')
+        self.vx = self.getData('info.vtx.x')
+        self.vy = self.getData('info.vtx.y')
+        self.vlen = self.getData('info.len.n')
+        if self.vlen :
+            self.glen.append(len(self.vlen))
+        elif self.glen:
+            self.glen.append(0)
+        if self.vx :
+            self.gvtx.append(len(self.vx))
+        elif self.gvtx :
+            self.gvtx.append(0)
+    def getData(self, path):
+        result = None
+        cnt = len(self.flatdoc)
+        for j in xrange(cnt):
+            item = self.flatdoc[j]
+            if item.find('=') >= 0:
+                (name, argt) = item.split('=')
+                argres = argt.split('|')
+            else:
+                name = item
+                argres = []
+            if (name == path):
+                result = argres
+                break
+        if (len(argres) > 0) :
+            for j in xrange(0,len(argres)):
+                argres[j] = int(argres[j])
+        return result
+    def getGlyphDim(self, gly):
+        if self.gdpi[gly] == 0:
+            return 0, 0
+        maxh = (self.gh[gly] * self.dpi) / self.gdpi[gly]
+        maxw = (self.gw[gly] * self.dpi) / self.gdpi[gly]
+        return maxh, maxw
+    def getPath(self, gly):
+        path = ''
+        if (gly < 0) or (gly >= self.count):
+            return path
+        tx = self.vx[self.gvtx[gly]:self.gvtx[gly+1]]
+        ty = self.vy[self.gvtx[gly]:self.gvtx[gly+1]]
+        p = 0
+        for k in xrange(self.glen[gly], self.glen[gly+1]):
+            if (p == 0):
+                zx = tx[0:self.vlen[k]+1]
+                zy = ty[0:self.vlen[k]+1]
+            else:
+                zx = tx[self.vlen[k-1]+1:self.vlen[k]+1]
+                zy = ty[self.vlen[k-1]+1:self.vlen[k]+1]
+            p += 1
+            j = 0
+            while ( j  < len(zx) ):
+                if (j == 0):
+                    # Start Position.
+                    path += 'M %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly])
+                elif (j <= len(zx)-3):
+                    # Cubic Bezier Curve
+                    path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[j+2] * self.dpi / self.gdpi[gly], zy[j+2] * self.dpi / self.gdpi[gly])
+                    j += 2
+                elif (j == len(zx)-2):
+                    # Cubic Bezier Curve to Start Position
+                    path += 'C %d %d %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[j+1] * self.dpi / self.gdpi[gly], zy[j+1] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly])
+                    j += 1
+                elif (j == len(zx)-1):
+                    # Quadratic Bezier Curve to Start Position
+                    path += 'Q %d %d %d %d ' % (zx[j] * self.dpi / self.gdpi[gly], zy[j] * self.dpi / self.gdpi[gly], zx[0] * self.dpi / self.gdpi[gly], zy[0] * self.dpi / self.gdpi[gly])
+
+                j += 1
+        path += 'z'
+        return path
+
+
+
+# dictionary of all text strings by index value
+class GlyphDict(object):
+    def __init__(self):
+        self.gdict = {}
+    def lookup(self, id):
+        # id='id="gl%d"' % val
+        if id in self.gdict:
+            return self.gdict[id]
+        return None
+    def addGlyph(self, val, path):
+        id='id="gl%d"' % val
+        self.gdict[id] = path
+
+
+def generateBook(bookDir, raw, fixedimage):
+    # sanity check Topaz file extraction
+    if not os.path.exists(bookDir) :
+        print "Can not find directory with unencrypted book"
+        return 1
 
-    # build pid list
-    md1, md2 = mb.getPIDMetaInfo()
-    pidlst = kgenpids.getPidList(md1, md2, k4, pids, serials, kInfoFiles)
+    dictFile = os.path.join(bookDir,'dict0000.dat')
+    if not os.path.exists(dictFile) :
+        print "Can not find dict0000.dat file"
+        return 1
 
-    try:
-        mb.processBook(pidlst)
+    pageDir = os.path.join(bookDir,'page')
+    if not os.path.exists(pageDir) :
+        print "Can not find page directory in unencrypted book"
+        return 1
 
-    except mobidedrm.DrmException, e:
-        print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: " + str(e) + "\nDRM Removal Failed.\n"
+    imgDir = os.path.join(bookDir,'img')
+    if not os.path.exists(imgDir) :
+        print "Can not find image directory in unencrypted book"
         return 1
-    except topazextract.TpzDRMError, e:
-        print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: " + str(e) + "\nDRM Removal Failed.\n"
+
+    glyphsDir = os.path.join(bookDir,'glyphs')
+    if not os.path.exists(glyphsDir) :
+        print "Can not find glyphs directory in unencrypted book"
         return 1
-    except Exception, e:
-        print >>sys.stderr, ('K4MobiDeDrm v%(__version__)s\n' % globals()) + "Error: " + str(e) + "\nDRM Removal Failed.\n"
+
+    metaFile = os.path.join(bookDir,'metadata0000.dat')
+    if not os.path.exists(metaFile) :
+        print "Can not find metadata0000.dat in unencrypted book"
         return 1
 
-    if mobi:
-        if mb.getPrintReplica():
-            outfile = os.path.join(outdir, outfilename + '_nodrm' + '.azw4')
-        elif mb.getMobiVersion() >= 8:
-            outfile = os.path.join(outdir, outfilename + '_nodrm' + '.azw3')
-        else:
-            outfile = os.path.join(outdir, outfilename + '_nodrm' + '.mobi')
-        mb.getMobiFile(outfile)
-        return 0
+    svgDir = os.path.join(bookDir,'svg')
+    if not os.path.exists(svgDir) :
+        os.makedirs(svgDir)
+
+    if buildXML:
+        xmlDir = os.path.join(bookDir,'xml')
+        if not os.path.exists(xmlDir) :
+            os.makedirs(xmlDir)
 
-    # topaz:
-    print "   Creating NoDRM HTMLZ Archive"
-    zipname = os.path.join(outdir, outfilename + '_nodrm' + '.htmlz')
-    mb.getHTMLZip(zipname)
+    otherFile = os.path.join(bookDir,'other0000.dat')
+    if not os.path.exists(otherFile) :
+        print "Can not find other0000.dat in unencrypted book"
+        return 1
 
-    print "   Creating SVG ZIP Archive"
-    zipname = os.path.join(outdir, outfilename + '_SVG' + '.zip')
-    mb.getSVGZip(zipname)
+    print "Updating to color images if available"
+    spath = os.path.join(bookDir,'color_img')
+    dpath = os.path.join(bookDir,'img')
+    filenames = os.listdir(spath)
+    filenames = sorted(filenames)
+    for filename in filenames:
+        imgname = filename.replace('color','img')
+        sfile = os.path.join(spath,filename)
+        dfile = os.path.join(dpath,imgname)
+        imgdata = file(sfile,'rb').read()
+        file(dfile,'wb').write(imgdata)
+
+    print "Creating cover.jpg"
+    isCover = False
+    cpath = os.path.join(bookDir,'img')
+    cpath = os.path.join(cpath,'img0000.jpg')
+    if os.path.isfile(cpath):
+        cover = file(cpath, 'rb').read()
+        cpath = os.path.join(bookDir,'cover.jpg')
+        file(cpath, 'wb').write(cover)
+        isCover = True
+
+
+    print 'Processing Dictionary'
+    dict = Dictionary(dictFile)
+
+    print 'Processing Meta Data and creating OPF'
+    meta_array = getMetaArray(metaFile)
+
+    # replace special chars in title and authors like & < >
+    title = meta_array.get('Title','No Title Provided')
+    title = title.replace('&','&amp;')
+    title = title.replace('<','&lt;')
+    title = title.replace('>','&gt;')
+    meta_array['Title'] = title
+    authors = meta_array.get('Authors','No Authors Provided')
+    authors = authors.replace('&','&amp;')
+    authors = authors.replace('<','&lt;')
+    authors = authors.replace('>','&gt;')
+    meta_array['Authors'] = authors
 
     if buildXML:
-        print "   Creating XML ZIP Archive"
-        zipname = os.path.join(outdir, outfilename + '_XML' + '.zip')
-        mb.getXMLZip(zipname)
+        xname = os.path.join(xmlDir, 'metadata.xml')
+        mlst = []
+        for key in meta_array:
+            mlst.append('<meta name="' + key + '" content="' + meta_array[key] + '" />\n')
+        metastr = "".join(mlst)
+        mlst = None
+        file(xname, 'wb').write(metastr)
+
+    print 'Processing StyleSheet'
+
+    # get some scaling info from metadata to use while processing styles
+    # and first page info
+
+    fontsize = '135'
+    if 'fontSize' in meta_array:
+        fontsize = meta_array['fontSize']
+
+    # also get the size of a normal text page
+    # get the total number of pages unpacked as a safety check
+    filenames = os.listdir(pageDir)
+    numfiles = len(filenames)
+
+    spage = '1'
+    if 'firstTextPage' in meta_array:
+        spage = meta_array['firstTextPage']
+    pnum = int(spage)
+    if pnum >= numfiles or pnum < 0:
+        # metadata is wrong so just select a page near the front
+        # 10% of the book to get a normal text page
+        pnum = int(0.10 * numfiles)
+    # print "first normal text page is", spage
+
+    # get page height and width from first text page for use in stylesheet scaling
+    pname = 'page%04d.dat' % (pnum + 1)
+    fname = os.path.join(pageDir,pname)
+    flat_xml = convert2xml.fromData(dict, fname)
+
+    (ph, pw) = getPageDim(flat_xml)
+    if (ph == '-1') or (ph == '0') : ph = '11000'
+    if (pw == '-1') or (pw == '0') : pw = '8500'
+    meta_array['pageHeight'] = ph
+    meta_array['pageWidth'] = pw
+    if 'fontSize' not in meta_array.keys():
+        meta_array['fontSize'] = fontsize
+
+    # process other.dat for css info and for map of page files to svg images
+    # this map is needed because some pages actually are made up of multiple
+    # pageXXXX.xml files
+    xname = os.path.join(bookDir, 'style.css')
+    flat_xml = convert2xml.fromData(dict, otherFile)
+
+    # extract info.original.pid to get original page information
+    pageIDMap = {}
+    pageidnums = stylexml2css.getpageIDMap(flat_xml)
+    if len(pageidnums) == 0:
+        filenames = os.listdir(pageDir)
+        numfiles = len(filenames)
+        for k in range(numfiles):
+            pageidnums.append(k)
+    # create a map from page ids to list of page file nums to process for that page
+    for i in range(len(pageidnums)):
+        id = pageidnums[i]
+        if id in pageIDMap.keys():
+            pageIDMap[id].append(i)
+        else:
+            pageIDMap[id] = [i]
 
-    # remove internal temporary directory of Topaz pieces
-    mb.cleanup()
+    # now get the css info
+    cssstr , classlst = stylexml2css.convert2CSS(flat_xml, fontsize, ph, pw)
+    file(xname, 'wb').write(cssstr)
+    if buildXML:
+        xname = os.path.join(xmlDir, 'other0000.xml')
+        file(xname, 'wb').write(convert2xml.getXML(dict, otherFile))
+
+    print 'Processing Glyphs'
+    gd = GlyphDict()
+    filenames = os.listdir(glyphsDir)
+    filenames = sorted(filenames)
+    glyfname = os.path.join(svgDir,'glyphs.svg')
+    glyfile = open(glyfname, 'w')
+    glyfile.write('<?xml version="1.0" standalone="no"?>\n')
+    glyfile.write('<!DOCTYPE svg PUBLIC "-//W3C/DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
+    glyfile.write('<svg width="512" height="512" viewBox="0 0 511 511" xmlns="http://www.w3.org/2000/svg" version="1.1">\n')
+    glyfile.write('<title>Glyphs for %s</title>\n' % meta_array['Title'])
+    glyfile.write('<defs>\n')
+    counter = 0
+    for filename in filenames:
+        # print '     ', filename
+        print '.',
+        fname = os.path.join(glyphsDir,filename)
+        flat_xml = convert2xml.fromData(dict, fname)
+
+        if buildXML:
+            xname = os.path.join(xmlDir, filename.replace('.dat','.xml'))
+            file(xname, 'wb').write(convert2xml.getXML(dict, fname))
+
+        gp = GParser(flat_xml)
+        for i in xrange(0, gp.count):
+            path = gp.getPath(i)
+            maxh, maxw = gp.getGlyphDim(i)
+            fullpath = '<path id="gl%d" d="%s" fill="black" /><!-- width=%d height=%d -->\n' % (counter * 256 + i, path, maxw, maxh)
+            glyfile.write(fullpath)
+            gd.addGlyph(counter * 256 + i, fullpath)
+        counter += 1
+    glyfile.write('</defs>\n')
+    glyfile.write('</svg>\n')
+    glyfile.close()
+    print " "
+
+
+    # start up the html
+    # also build up tocentries while processing html
+    htmlFileName = "book.html"
+    hlst = []
+    hlst.append('<?xml version="1.0" encoding="utf-8"?>\n')
+    hlst.append('<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.1 Strict//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11-strict.dtd">\n')
+    hlst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">\n')
+    hlst.append('<head>\n')
+    hlst.append('<meta http-equiv="content-type" content="text/html; charset=utf-8"/>\n')
+    hlst.append('<title>' + meta_array['Title'] + ' by ' + meta_array['Authors'] + '</title>\n')
+    hlst.append('<meta name="Author" content="' + meta_array['Authors'] + '" />\n')
+    hlst.append('<meta name="Title" content="' + meta_array['Title'] + '" />\n')
+    if 'ASIN' in meta_array:
+        hlst.append('<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n')
+    if 'GUID' in meta_array:
+        hlst.append('<meta name="GUID" content="' + meta_array['GUID'] + '" />\n')
+    hlst.append('<link href="style.css" rel="stylesheet" type="text/css" />\n')
+    hlst.append('</head>\n<body>\n')
+
+    print 'Processing Pages'
+    # Books are at 1440 DPI.  This is rendering at twice that size for
+    # readability when rendering to the screen.
+    scaledpi = 1440.0
+
+    filenames = os.listdir(pageDir)
+    filenames = sorted(filenames)
+    numfiles = len(filenames)
+
+    xmllst = []
+    elst = []
+
+    for filename in filenames:
+        # print '     ', filename
+        print ".",
+        fname = os.path.join(pageDir,filename)
+        flat_xml = convert2xml.fromData(dict, fname)
+
+        # keep flat_xml for later svg processing
+        xmllst.append(flat_xml)
+
+        if buildXML:
+            xname = os.path.join(xmlDir, filename.replace('.dat','.xml'))
+            file(xname, 'wb').write(convert2xml.getXML(dict, fname))
+
+        # first get the html
+        pagehtml, tocinfo = flatxml2html.convert2HTML(flat_xml, classlst, fname, bookDir, gd, fixedimage)
+        elst.append(tocinfo)
+        hlst.append(pagehtml)
+
+    # finish up the html string and output it
+    hlst.append('</body>\n</html>\n')
+    htmlstr = "".join(hlst)
+    hlst = None
+    file(os.path.join(bookDir, htmlFileName), 'wb').write(htmlstr)
+
+    print " "
+    print 'Extracting Table of Contents from Amazon OCR'
+
+    # first create a table of contents file for the svg images
+    tlst = []
+    tlst.append('<?xml version="1.0" encoding="utf-8"?>\n')
+    tlst.append('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n')
+    tlst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >')
+    tlst.append('<head>\n')
+    tlst.append('<title>' + meta_array['Title'] + '</title>\n')
+    tlst.append('<meta name="Author" content="' + meta_array['Authors'] + '" />\n')
+    tlst.append('<meta name="Title" content="' + meta_array['Title'] + '" />\n')
+    if 'ASIN' in meta_array:
+        tlst.append('<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n')
+    if 'GUID' in meta_array:
+        tlst.append('<meta name="GUID" content="' + meta_array['GUID'] + '" />\n')
+    tlst.append('</head>\n')
+    tlst.append('<body>\n')
+
+    tlst.append('<h2>Table of Contents</h2>\n')
+    start = pageidnums[0]
+    if (raw):
+        startname = 'page%04d.svg' % start
+    else:
+        startname = 'page%04d.xhtml' % start
+
+    tlst.append('<h3><a href="' + startname + '">Start of Book</a></h3>\n')
+    # build up a table of contents for the svg xhtml output
+    tocentries = "".join(elst)
+    elst = None
+    toclst = tocentries.split('\n')
+    toclst.pop()
+    for entry in toclst:
+        print entry
+        title, pagenum = entry.split('|')
+        id = pageidnums[int(pagenum)]
+        if (raw):
+            fname = 'page%04d.svg' % id
+        else:
+            fname = 'page%04d.xhtml' % id
+        tlst.append('<h3><a href="'+ fname + '">' + title + '</a></h3>\n')
+    tlst.append('</body>\n')
+    tlst.append('</html>\n')
+    tochtml = "".join(tlst)
+    file(os.path.join(svgDir, 'toc.xhtml'), 'wb').write(tochtml)
+
+
+    # now create index_svg.xhtml that points to all required files
+    slst = []
+    slst.append('<?xml version="1.0" encoding="utf-8"?>\n')
+    slst.append('<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n')
+    slst.append('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >')
+    slst.append('<head>\n')
+    slst.append('<title>' + meta_array['Title'] + '</title>\n')
+    slst.append('<meta name="Author" content="' + meta_array['Authors'] + '" />\n')
+    slst.append('<meta name="Title" content="' + meta_array['Title'] + '" />\n')
+    if 'ASIN' in meta_array:
+        slst.append('<meta name="ASIN" content="' + meta_array['ASIN'] + '" />\n')
+    if 'GUID' in meta_array:
+        slst.append('<meta name="GUID" content="' + meta_array['GUID'] + '" />\n')
+    slst.append('</head>\n')
+    slst.append('<body>\n')
+
+    print "Building svg images of each book page"
+    slst.append('<h2>List of Pages</h2>\n')
+    slst.append('<div>\n')
+    idlst = sorted(pageIDMap.keys())
+    numids = len(idlst)
+    cnt = len(idlst)
+    previd = None
+    for j in range(cnt):
+        pageid = idlst[j]
+        if j < cnt - 1:
+            nextid = idlst[j+1]
+        else:
+            nextid = None
+        print '.',
+        pagelst = pageIDMap[pageid]
+        flst = []
+        for page in pagelst:
+            flst.append(xmllst[page])
+        flat_svg = "".join(flst)
+        flst=None
+        svgxml = flatxml2svg.convert2SVG(gd, flat_svg, pageid, previd, nextid, svgDir, raw, meta_array, scaledpi)
+        if (raw) :
+            pfile = open(os.path.join(svgDir,'page%04d.svg' % pageid),'w')
+            slst.append('<a href="svg/page%04d.svg">Page %d</a>\n' % (pageid, pageid))
+        else :
+            pfile = open(os.path.join(svgDir,'page%04d.xhtml' % pageid), 'w')
+            slst.append('<a href="svg/page%04d.xhtml">Page %d</a>\n' % (pageid, pageid))
+        previd = pageid
+        pfile.write(svgxml)
+        pfile.close()
+        counter += 1
+    slst.append('</div>\n')
+    slst.append('<h2><a href="svg/toc.xhtml">Table of Contents</a></h2>\n')
+    slst.append('</body>\n</html>\n')
+    svgindex = "".join(slst)
+    slst = None
+    file(os.path.join(bookDir, 'index_svg.xhtml'), 'wb').write(svgindex)
+
+    print " "
+
+    # build the opf file
+    opfname = os.path.join(bookDir, 'book.opf')
+    olst = []
+    olst.append('<?xml version="1.0" encoding="utf-8"?>\n')
+    olst.append('<package xmlns="http://www.idpf.org/2007/opf" unique-identifier="guid_id">\n')
+    # adding metadata
+    olst.append('   <metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">\n')
+    if 'GUID' in meta_array:
+        olst.append('      <dc:identifier opf:scheme="GUID" id="guid_id">' + meta_array['GUID'] + '</dc:identifier>\n')
+    if 'ASIN' in meta_array:
+        olst.append('      <dc:identifier opf:scheme="ASIN">' + meta_array['ASIN'] + '</dc:identifier>\n')
+    if 'oASIN' in meta_array:
+        olst.append('      <dc:identifier opf:scheme="oASIN">' + meta_array['oASIN'] + '</dc:identifier>\n')
+    olst.append('      <dc:title>' + meta_array['Title'] + '</dc:title>\n')
+    olst.append('      <dc:creator opf:role="aut">' + meta_array['Authors'] + '</dc:creator>\n')
+    olst.append('      <dc:language>en</dc:language>\n')
+    olst.append('      <dc:date>' + meta_array['UpdateTime'] + '</dc:date>\n')
+    if isCover:
+        olst.append('      <meta name="cover" content="bookcover"/>\n')
+    olst.append('   </metadata>\n')
+    olst.append('<manifest>\n')
+    olst.append('   <item id="book" href="book.html" media-type="application/xhtml+xml"/>\n')
+    olst.append('   <item id="stylesheet" href="style.css" media-type="text/css"/>\n')
+    # adding image files to manifest
+    filenames = os.listdir(imgDir)
+    filenames = sorted(filenames)
+    for filename in filenames:
+        imgname, imgext = os.path.splitext(filename)
+        if imgext == '.jpg':
+            imgext = 'jpeg'
+        if imgext == '.svg':
+            imgext = 'svg+xml'
+        olst.append('   <item id="' + imgname + '" href="img/' + filename + '" media-type="image/' + imgext + '"/>\n')
+    if isCover:
+        olst.append('   <item id="bookcover" href="cover.jpg" media-type="image/jpeg" />\n')
+    olst.append('</manifest>\n')
+    # adding spine
+    olst.append('<spine>\n   <itemref idref="book" />\n</spine>\n')
+    if isCover:
+        olst.append('   <guide>\n')
+        olst.append('      <reference href="cover.jpg" type="cover" title="Cover"/>\n')
+        olst.append('   </guide>\n')
+    olst.append('</package>\n')
+    opfstr = "".join(olst)
+    olst = None
+    file(opfname, 'wb').write(opfstr)
+
+    print 'Processing Complete'
 
     return 0
 
-
-def usage(progname):
-    print "Removes DRM protection from K4PC/M, Kindle, Mobi and Topaz ebooks"
+def usage():
+    print "genbook.py generates a book from the extract Topaz Files"
     print "Usage:"
-    print "    %s [-k <kindle.info>] [-p <pidnums>] [-s <kindleSerialNumbers>] <infile> <outdir>  " % progname
-
-#
-# Main
-#
-def main(argv=sys.argv):
-    progname = os.path.basename(argv[0])
+    print "    genbook.py [-r] [-h [--fixed-image] <bookDir>  "
+    print "  "
+    print "Options:"
+    print "  -h            :  help - print this usage message"
+    print "  -r            :  generate raw svg files (not wrapped in xhtml)"
+    print "  --fixed-image :  genearate any Fixed Area as an svg image in the html"
+    print "  "
 
-    k4 = False
-    kInfoFiles = []
-    serials = []
-    pids = []
 
-    print ('K4MobiDeDrm v%(__version__)s '
-           'provided by the work of many including DiapDealer, SomeUpdates, IHeartCabbages, CMBDTC, Skindle, DarkReverser, ApprenticeAlf, etc .' % globals())
+def main(argv):
+    bookDir = ''
+    if len(argv) == 0:
+        argv = sys.argv
 
     try:
-        opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
+        opts, args = getopt.getopt(argv[1:], "rh:",["fixed-image"])
+
     except getopt.GetoptError, err:
         print str(err)
-        usage(progname)
-        sys.exit(2)
-    if len(args)<2:
-        usage(progname)
-        sys.exit(2)
+        usage()
+        return 1
 
+    if len(opts) == 0 and len(args) == 0 :
+        usage()
+        return 1
+
+    raw = 0
+    fixedimage = True
     for o, a in opts:
-        if o == "-k":
-            if a == None :
-                raise DrmException("Invalid parameter for -k")
-            kInfoFiles.append(a)
-        if o == "-p":
-            if a == None :
-                raise DrmException("Invalid parameter for -p")
-            pids = a.split(',')
-        if o == "-s":
-            if a == None :
-                raise DrmException("Invalid parameter for -s")
-            serials = a.split(',')
-
-    # try with built in Kindle Info files
-    k4 = True
-    if sys.platform.startswith('linux'):
-        k4 = False
-        kInfoFiles = None
-    infile = args[0]
-    outdir = args[1]
-    return decryptBook(infile, outdir, k4, kInfoFiles, serials, pids)
+        if o =="-h":
+            usage()
+            return 0
+        if o =="-r":
+            raw = 1
+        if o =="--fixed-image":
+            fixedimage = True
+
+    bookDir = args[0]
+
+    rv = generateBook(bookDir, raw, fixedimage)
+    return rv
 
 
 if __name__ == '__main__':
-    sys.stdout=Unbuffered(sys.stdout)
-    sys.exit(main())
+    sys.exit(main(''))
index 40d84ad713c427faa5b25b18a9a4a2cdcf100a6e..813f8cee7eb399ddee5fe522813a953e0f77a81c 100644 (file)
Binary files a/Calibre_Plugins/K4MobiDeDRM_plugin/kgenpids.py and b/Calibre_Plugins/K4MobiDeDRM_plugin/kgenpids.py differ
index 01c348cc8a638e243754aea2cd2681ad30e629a4..d491d7d8dd7e52c8fc98b1c3ad2b197e441521b9 100644 (file)
Binary files a/Calibre_Plugins/K4MobiDeDRM_plugin/libalfcrypto.dylib and b/Calibre_Plugins/K4MobiDeDRM_plugin/libalfcrypto.dylib differ
index 9a5a442617a2046fb9b7050d339b18bca8993e7b..dd60706d4171c74ab1ba403fb62b9984bce8c94e 100644 (file)
Binary files a/Calibre_Plugins/K4MobiDeDRM_plugin/libalfcrypto32.so and b/Calibre_Plugins/K4MobiDeDRM_plugin/libalfcrypto32.so differ
index a08ac28930fdd54e0eb4e8973a398e5e951d3825..40d84ad713c427faa5b25b18a9a4a2cdcf100a6e 100644 (file)
Binary files a/Calibre_Plugins/K4MobiDeDRM_plugin/libalfcrypto64.so and b/Calibre_Plugins/K4MobiDeDRM_plugin/libalfcrypto64.so differ
index 65220a950808997c8598098d76a14cfe610e229f..9a5a442617a2046fb9b7050d339b18bca8993e7b 100644 (file)
Binary files a/Calibre_Plugins/K4MobiDeDRM_plugin/pbkdf2.py and b/Calibre_Plugins/K4MobiDeDRM_plugin/pbkdf2.py differ
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a08ac28930fdd54e0eb4e8973a398e5e951d3825 100644 (file)
Binary files a/Calibre_Plugins/K4MobiDeDRM_plugin/plugin-import-name-k4mobidedrm.txt and b/Calibre_Plugins/K4MobiDeDRM_plugin/plugin-import-name-k4mobidedrm.txt differ
index 98b41476f65c6afdeaade1120dc16f1d7d38c252..1ad2bacca76e28c4a5197e245bc203dcec390f5b 100644 (file)
-#!/usr/bin/env python
-# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
-
-import Tkinter
-import Tkconstants
-
-# basic scrolled text widget
-class ScrolledText(Tkinter.Text):
-    def __init__(self, master=None, **kw):
-        self.frame = Tkinter.Frame(master)
-        self.vbar = Tkinter.Scrollbar(self.frame)
-        self.vbar.pack(side=Tkconstants.RIGHT, fill=Tkconstants.Y)
-        kw.update({'yscrollcommand': self.vbar.set})
-        Tkinter.Text.__init__(self, self.frame, **kw)
-        self.pack(side=Tkconstants.LEFT, fill=Tkconstants.BOTH, expand=True)
-        self.vbar['command'] = self.yview
-        # Copy geometry methods of self.frame without overriding Text
-        # methods = hack!
-        text_meths = vars(Tkinter.Text).keys()
-        methods = vars(Tkinter.Pack).keys() + vars(Tkinter.Grid).keys() + vars(Tkinter.Place).keys()
-        methods = set(methods).difference(text_meths)
-        for m in methods:
-            if m[0] != '_' and m != 'config' and m != 'configure':
-                setattr(self, m, getattr(self.frame, m))
-
-    def __str__(self):
-        return str(self.frame)
+#!/usr/bin/python
+#
+# This is a python script. You need a Python interpreter to run it.
+# For example, ActiveState Python, which exists for windows.
+#
+# Changelog
+#  0.01 - Initial version
+#  0.02 - Huffdic compressed books were not properly decrypted
+#  0.03 - Wasn't checking MOBI header length
+#  0.04 - Wasn't sanity checking size of data record
+#  0.05 - It seems that the extra data flags take two bytes not four
+#  0.06 - And that low bit does mean something after all :-)
+#  0.07 - The extra data flags aren't present in MOBI header < 0xE8 in size
+#  0.08 - ...and also not in Mobi header version < 6
+#  0.09 - ...but they are there with Mobi header version 6, header size 0xE4!
+#  0.10 - Outputs unencrypted files as-is, so that when run as a Calibre
+#         import filter it works when importing unencrypted files.
+#         Also now handles encrypted files that don't need a specific PID.
+#  0.11 - use autoflushed stdout and proper return values
+#  0.12 - Fix for problems with metadata import as Calibre plugin, report errors
+#  0.13 - Formatting fixes: retabbed file, removed trailing whitespace
+#         and extra blank lines, converted CR/LF pairs at ends of each line,
+#         and other cosmetic fixes.
+#  0.14 - Working out when the extra data flags are present has been problematic
+#         Versions 7 through 9 have tried to tweak the conditions, but have been
+#         only partially successful. Closer examination of lots of sample
+#         files reveals that a confusion has arisen because trailing data entries
+#         are not encrypted, but it turns out that the multibyte entries
+#         in utf8 file are encrypted. (Although neither kind gets compressed.)
+#         This knowledge leads to a simplification of the test for the
+#         trailing data byte flags - version 5 and higher AND header size >= 0xE4.
+#  0.15 - Now outputs 'heartbeat', and is also quicker for long files.
+#  0.16 - And reverts to 'done' not 'done.' at the end for unswindle compatibility.
+#  0.17 - added modifications to support its use as an imported python module
+#         both inside calibre and also in other places (ie K4DeDRM tools)
+#  0.17a- disabled the standalone plugin feature since a plugin can not import
+#         a plugin
+#  0.18 - It seems that multibyte entries aren't encrypted in a v7 file...
+#         Removed the disabled Calibre plug-in code
+#         Permit use of 8-digit PIDs
+#  0.19 - It seems that multibyte entries aren't encrypted in a v6 file either.
+#  0.20 - Correction: It seems that multibyte entries are encrypted in a v6 file.
+#  0.21 - Added support for multiple pids
+#  0.22 - revised structure to hold MobiBook as a class to allow an extended interface
+#  0.23 - fixed problem with older files with no EXTH section
+#  0.24 - add support for type 1 encryption and 'TEXtREAd' books as well
+#  0.25 - Fixed support for 'BOOKMOBI' type 1 encryption
+#  0.26 - Now enables Text-To-Speech flag and sets clipping limit to 100%
+#  0.27 - Correct pid metadata token generation to match that used by skindle (Thank You Bart!)
+#  0.28 - slight additional changes to metadata token generation (None -> '')
+#  0.29 - It seems that the ideas about when multibyte trailing characters were
+#         included in the encryption were wrong. They are for DOC compressed
+#         files, but they are not for HUFF/CDIC compress files!
+#  0.30 - Modified interface slightly to work better with new calibre plugin style
+#  0.31 - The multibyte encrytion info is true for version 7 files too.
+#  0.32 - Added support for "Print Replica" Kindle ebooks
+#  0.33 - Performance improvements for large files (concatenation)
+#  0.34 - Performance improvements in decryption (libalfcrypto)
+#  0.35 - add interface to get mobi_version
+#  0.36 - fixed problem with TEXtREAd and getBookTitle interface
+#  0.37 - Fixed double announcement for stand-alone operation
+
+
+__version__ = '0.37'
+
+import sys
+
+class Unbuffered:
+    def __init__(self, stream):
+        self.stream = stream
+    def write(self, data):
+        self.stream.write(data)
+        self.stream.flush()
+    def __getattr__(self, attr):
+        return getattr(self.stream, attr)
+sys.stdout=Unbuffered(sys.stdout)
+
+import os
+import struct
+import binascii
+from alfcrypto import Pukall_Cipher
+
+class DrmException(Exception):
+    pass
+
+
+#
+# MobiBook Utility Routines
+#
+
+# Implementation of Pukall Cipher 1
+def PC1(key, src, decryption=True):
+    return Pukall_Cipher().PC1(key,src,decryption)
+#     sum1 = 0;
+#     sum2 = 0;
+#     keyXorVal = 0;
+#     if len(key)!=16:
+#         print "Bad key length!"
+#         return None
+#     wkey = []
+#     for i in xrange(8):
+#         wkey.append(ord(key[i*2])<<8 | ord(key[i*2+1]))
+#     dst = ""
+#     for i in xrange(len(src)):
+#         temp1 = 0;
+#         byteXorVal = 0;
+#         for j in xrange(8):
+#             temp1 ^= wkey[j]
+#             sum2  = (sum2+j)*20021 + sum1
+#             sum1  = (temp1*346)&0xFFFF
+#             sum2  = (sum2+sum1)&0xFFFF
+#             temp1 = (temp1*20021+1)&0xFFFF
+#             byteXorVal ^= temp1 ^ sum2
+#         curByte = ord(src[i])
+#         if not decryption:
+#             keyXorVal = curByte * 257;
+#         curByte = ((curByte ^ (byteXorVal >> 8)) ^ byteXorVal) & 0xFF
+#         if decryption:
+#             keyXorVal = curByte * 257;
+#         for j in xrange(8):
+#             wkey[j] ^= keyXorVal;
+#         dst+=chr(curByte)
+#     return dst
+
+def checksumPid(s):
+    letters = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
+    crc = (~binascii.crc32(s,-1))&0xFFFFFFFF
+    crc = crc ^ (crc >> 16)
+    res = s
+    l = len(letters)
+    for i in (0,1):
+        b = crc & 0xff
+        pos = (b // l) ^ (b % l)
+        res += letters[pos%l]
+        crc >>= 8
+    return res
+
+def getSizeOfTrailingDataEntries(ptr, size, flags):
+    def getSizeOfTrailingDataEntry(ptr, size):
+        bitpos, result = 0, 0
+        if size <= 0:
+            return result
+        while True:
+            v = ord(ptr[size-1])
+            result |= (v & 0x7F) << bitpos
+            bitpos += 7
+            size -= 1
+            if (v & 0x80) != 0 or (bitpos >= 28) or (size == 0):
+                return result
+    num = 0
+    testflags = flags >> 1
+    while testflags:
+        if testflags & 1:
+            num += getSizeOfTrailingDataEntry(ptr, size - num)
+        testflags >>= 1
+    # Check the low bit to see if there's multibyte data present.
+    # if multibyte data is included in the encryped data, we'll
+    # have already cleared this flag.
+    if flags & 1:
+        num += (ord(ptr[size - num - 1]) & 0x3) + 1
+    return num
+
+
+
+class MobiBook:
+    def loadSection(self, section):
+        if (section + 1 == self.num_sections):
+            endoff = len(self.data_file)
+        else:
+            endoff = self.sections[section + 1][0]
+        off = self.sections[section][0]
+        return self.data_file[off:endoff]
+
+    def __init__(self, infile, announce = True):
+        if announce:
+            print ('MobiDeDrm v%(__version__)s. '
+               'Copyright 2008-2012 The Dark Reverser et al.' % globals())
+
+        # initial sanity check on file
+        self.data_file = file(infile, 'rb').read()
+        self.mobi_data = ''
+        self.header = self.data_file[0:78]
+        if self.header[0x3C:0x3C+8] != 'BOOKMOBI' and self.header[0x3C:0x3C+8] != 'TEXtREAd':
+            raise DrmException("invalid file format")
+        self.magic = self.header[0x3C:0x3C+8]
+        self.crypto_type = -1
+
+        # build up section offset and flag info
+        self.num_sections, = struct.unpack('>H', self.header[76:78])
+        self.sections = []
+        for i in xrange(self.num_sections):
+            offset, a1,a2,a3,a4 = struct.unpack('>LBBBB', self.data_file[78+i*8:78+i*8+8])
+            flags, val = a1, a2<<16|a3<<8|a4
+            self.sections.append( (offset, flags, val) )
+
+        # parse information from section 0
+        self.sect = self.loadSection(0)
+        self.records, = struct.unpack('>H', self.sect[0x8:0x8+2])
+        self.compression, = struct.unpack('>H', self.sect[0x0:0x0+2])
+
+        if self.magic == 'TEXtREAd':
+            print "Book has format: ", self.magic
+            self.extra_data_flags = 0
+            self.mobi_length = 0
+            self.mobi_codepage = 1252
+            self.mobi_version = -1
+            self.meta_array = {}
+            return
+        self.mobi_length, = struct.unpack('>L',self.sect[0x14:0x18])
+        self.mobi_codepage, = struct.unpack('>L',self.sect[0x1c:0x20])
+        self.mobi_version, = struct.unpack('>L',self.sect[0x68:0x6C])
+        print "MOBI header version = %d, length = %d" %(self.mobi_version, self.mobi_length)
+        self.extra_data_flags = 0
+        if (self.mobi_length >= 0xE4) and (self.mobi_version >= 5):
+            self.extra_data_flags, = struct.unpack('>H', self.sect[0xF2:0xF4])
+            print "Extra Data Flags = %d" % self.extra_data_flags
+        if (self.compression != 17480):
+            # multibyte utf8 data is included in the encryption for PalmDoc compression
+            # so clear that byte so that we leave it to be decrypted.
+            self.extra_data_flags &= 0xFFFE
+
+        # if exth region exists parse it for metadata array
+        self.meta_array = {}
+        try:
+            exth_flag, = struct.unpack('>L', self.sect[0x80:0x84])
+            exth = 'NONE'
+            if exth_flag & 0x40:
+                exth = self.sect[16 + self.mobi_length:]
+            if (len(exth) >= 4) and (exth[:4] == 'EXTH'):
+                nitems, = struct.unpack('>I', exth[8:12])
+                pos = 12
+                for i in xrange(nitems):
+                    type, size = struct.unpack('>II', exth[pos: pos + 8])
+                    content = exth[pos + 8: pos + size]
+                    self.meta_array[type] = content
+                    # reset the text to speech flag and clipping limit, if present
+                    if type == 401 and size == 9:
+                        # set clipping limit to 100%
+                        self.patchSection(0, "\144", 16 + self.mobi_length + pos + 8)
+                    elif type == 404 and size == 9:
+                        # make sure text to speech is enabled
+                        self.patchSection(0, "\0", 16 + self.mobi_length + pos + 8)
+                    # print type, size, content, content.encode('hex')
+                    pos += size
+        except:
+            self.meta_array = {}
+            pass
+        self.print_replica = False
+
+    def getBookTitle(self):
+        codec_map = {
+            1252 : 'windows-1252',
+            65001 : 'utf-8',
+        }
+        title = ''
+        codec = 'windows-1252'
+        if self.magic == 'BOOKMOBI':
+            if 503 in self.meta_array:
+                title = self.meta_array[503]
+            else:
+                toff, tlen = struct.unpack('>II', self.sect[0x54:0x5c])
+                tend = toff + tlen
+                title = self.sect[toff:tend]
+            if self.mobi_codepage in codec_map.keys():
+                codec = codec_map[self.mobi_codepage]
+        if title == '':
+            title = self.header[:32]
+            title = title.split("\0")[0]
+        return unicode(title, codec).encode('utf-8')
+
+    def getPIDMetaInfo(self):
+        rec209 = ''
+        token = ''
+        if 209 in self.meta_array:
+            rec209 = self.meta_array[209]
+            data = rec209
+            # The 209 data comes in five byte groups. Interpret the last four bytes
+            # of each group as a big endian unsigned integer to get a key value
+            # if that key exists in the meta_array, append its contents to the token
+            for i in xrange(0,len(data),5):
+                val,  = struct.unpack('>I',data[i+1:i+5])
+                sval = self.meta_array.get(val,'')
+                token += sval
+        return rec209, token
+
+    def patch(self, off, new):
+        self.data_file = self.data_file[:off] + new + self.data_file[off+len(new):]
+
+    def patchSection(self, section, new, in_off = 0):
+        if (section + 1 == self.num_sections):
+            endoff = len(self.data_file)
+        else:
+            endoff = self.sections[section + 1][0]
+        off = self.sections[section][0]
+        assert off + in_off + len(new) <= endoff
+        self.patch(off + in_off, new)
+
+    def parseDRM(self, data, count, pidlist):
+        found_key = None
+        keyvec1 = "\x72\x38\x33\xB0\xB4\xF2\xE3\xCA\xDF\x09\x01\xD6\xE2\xE0\x3F\x96"
+        for pid in pidlist:
+            bigpid = pid.ljust(16,'\0')
+            temp_key = PC1(keyvec1, bigpid, False)
+            temp_key_sum = sum(map(ord,temp_key)) & 0xff
+            found_key = None
+            for i in xrange(count):
+                verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
+                if cksum == temp_key_sum:
+                    cookie = PC1(temp_key, cookie)
+                    ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
+                    if verification == ver and (flags & 0x1F) == 1:
+                        found_key = finalkey
+                        break
+            if found_key != None:
+                break
+        if not found_key:
+            # Then try the default encoding that doesn't require a PID
+            pid = "00000000"
+            temp_key = keyvec1
+            temp_key_sum = sum(map(ord,temp_key)) & 0xff
+            for i in xrange(count):
+                verification, size, type, cksum, cookie = struct.unpack('>LLLBxxx32s', data[i*0x30:i*0x30+0x30])
+                if cksum == temp_key_sum:
+                    cookie = PC1(temp_key, cookie)
+                    ver,flags,finalkey,expiry,expiry2 = struct.unpack('>LL16sLL', cookie)
+                    if verification == ver:
+                        found_key = finalkey
+                        break
+        return [found_key,pid]
+
+    def getMobiFile(self, outpath):
+        file(outpath,'wb').write(self.mobi_data)
+
+    def getMobiVersion(self):
+        return self.mobi_version
+        
+    def getPrintReplica(self):
+        return self.print_replica
+
+    def processBook(self, pidlist):
+        crypto_type, = struct.unpack('>H', self.sect[0xC:0xC+2])
+        print 'Crypto Type is: ', crypto_type
+        self.crypto_type = crypto_type
+        if crypto_type == 0:
+            print "This book is not encrypted."
+            # we must still check for Print Replica
+            self.print_replica = (self.loadSection(1)[0:4] == '%MOP')
+            self.mobi_data = self.data_file
+            return
+        if crypto_type != 2 and crypto_type != 1:
+            raise DrmException("Cannot decode unknown Mobipocket encryption type %d" % crypto_type)
+        if 406 in self.meta_array:
+            data406 = self.meta_array[406]
+            val406, = struct.unpack('>Q',data406)
+            if val406 != 0:
+                raise DrmException("Cannot decode library or rented ebooks.")
+
+        goodpids = []
+        for pid in pidlist:
+            if len(pid)==10:
+                if checksumPid(pid[0:-2]) != pid:
+                    print "Warning: PID " + pid + " has incorrect checksum, should have been "+checksumPid(pid[0:-2])
+                goodpids.append(pid[0:-2])
+            elif len(pid)==8:
+                goodpids.append(pid)
+
+        if self.crypto_type == 1:
+            t1_keyvec = "QDCVEPMU675RUBSZ"
+            if self.magic == 'TEXtREAd':
+                bookkey_data = self.sect[0x0E:0x0E+16]
+            elif self.mobi_version < 0:
+                bookkey_data = self.sect[0x90:0x90+16]
+            else:
+                bookkey_data = self.sect[self.mobi_length+16:self.mobi_length+32]
+            pid = "00000000"
+            found_key = PC1(t1_keyvec, bookkey_data)
+        else :
+            # calculate the keys
+            drm_ptr, drm_count, drm_size, drm_flags = struct.unpack('>LLLL', self.sect[0xA8:0xA8+16])
+            if drm_count == 0:
+                raise DrmException("Not yet initialised with PID. Must be opened with Mobipocket Reader first.")
+            found_key, pid = self.parseDRM(self.sect[drm_ptr:drm_ptr+drm_size], drm_count, goodpids)
+            if not found_key:
+                raise DrmException("No key found in " + str(len(goodpids)) + " keys tried. Please report this failure for help.")
+            # kill the drm keys
+            self.patchSection(0, "\0" * drm_size, drm_ptr)
+            # kill the drm pointers
+            self.patchSection(0, "\xff" * 4 + "\0" * 12, 0xA8)
+
+        if pid=="00000000":
+            print "File has default encryption, no specific PID."
+        else:
+            print "File is encoded with PID "+checksumPid(pid)+"."
+
+        # clear the crypto type
+        self.patchSection(0, "\0" * 2, 0xC)
+
+        # decrypt sections
+        print "Decrypting. Please wait . . .",
+        mobidataList = []
+        mobidataList.append(self.data_file[:self.sections[1][0]])
+        for i in xrange(1, self.records+1):
+            data = self.loadSection(i)
+            extra_size = getSizeOfTrailingDataEntries(data, len(data), self.extra_data_flags)
+            if i%100 == 0:
+                print ".",
+            # print "record %d, extra_size %d" %(i,extra_size)
+            decoded_data = PC1(found_key, data[0:len(data) - extra_size])
+            if i==1:
+                self.print_replica = (decoded_data[0:4] == '%MOP')
+            mobidataList.append(decoded_data)
+            if extra_size > 0:
+                mobidataList.append(data[-extra_size:])
+        if self.num_sections > self.records+1:
+            mobidataList.append(self.data_file[self.sections[self.records+1][0]:])
+        self.mobi_data = "".join(mobidataList)
+        print "done"
+        return
+
+def getUnencryptedBook(infile,pid,announce=True):
+    if not os.path.isfile(infile):
+        raise DrmException('Input File Not Found')
+    book = MobiBook(infile,announce)
+    book.processBook([pid])
+    return book.mobi_data
+
+def getUnencryptedBookWithList(infile,pidlist,announce=True):
+    if not os.path.isfile(infile):
+        raise DrmException('Input File Not Found')
+    book = MobiBook(infile, announce)
+    book.processBook(pidlist)
+    return book.mobi_data
+
+
+def main(argv=sys.argv):
+    print ('MobiDeDrm v%(__version__)s. '
+        'Copyright 2008-2012 The Dark Reverser et al.' % globals())
+    if len(argv)<3 or len(argv)>4:
+        print "Removes protection from Kindle/Mobipocket, Kindle/KF8 and Kindle/Print Replica ebooks"
+        print "Usage:"
+        print "    %s <infile> <outfile> [<Comma separated list of PIDs to try>]" % sys.argv[0]
+        return 1
+    else:
+        infile = argv[1]
+        outfile = argv[2]
+        if len(argv) is 4:
+            pidlist = argv[3].split(',')
+        else:
+            pidlist = {}
+        try:
+            stripped_file = getUnencryptedBookWithList(infile, pidlist, False)
+            file(outfile, 'wb').write(stripped_file)
+        except DrmException, e:
+            print "Error: %s" % e
+            return 1
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
index 2347f6aeb128bd5cdfc22f8d52dc5d8c05ba63b2..906c6e9463757a4371a4a14fbb2100205e34ca58 100644 (file)
-#! /usr/bin/python
-# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
-# For use with Topaz Scripts Version 2.6
-
-import csv
-import sys
-import os
-import getopt
-import re
-from struct import pack
-from struct import unpack
-
-
-class DocParser(object):
-    def __init__(self, flatxml, fontsize, ph, pw):
-        self.flatdoc = flatxml.split('\n')
-        self.fontsize = int(fontsize)
-        self.ph = int(ph) * 1.0
-        self.pw = int(pw) * 1.0
-
-    stags = {
-        'paragraph' : 'p',
-        'graphic'   : '.graphic'
-    }
-
-    attr_val_map = {
-        'hang'            : 'text-indent: ',
-        'indent'          : 'text-indent: ',
-        'line-space'      : 'line-height: ',
-        'margin-bottom'   : 'margin-bottom: ',
-        'margin-left'     : 'margin-left: ',
-        'margin-right'    : 'margin-right: ',
-        'margin-top'      : 'margin-top: ',
-        'space-after'     : 'padding-bottom: ',
-    }
-
-    attr_str_map = {
-        'align-center' : 'text-align: center; margin-left: auto; margin-right: auto;',
-        'align-left'   : 'text-align: left;',
-        'align-right'  : 'text-align: right;',
-        'align-justify' : 'text-align: justify;',
-        'display-inline' : 'display: inline;',
-        'pos-left' : 'text-align: left;',
-        'pos-right' : 'text-align: right;',
-        'pos-center' : 'text-align: center; margin-left: auto; margin-right: auto;',
-    }
-
-
-    # find tag if within pos to end inclusive
-    def findinDoc(self, tagpath, pos, end) :
-        result = None
-        docList = self.flatdoc
-        cnt = len(docList)
-        if end == -1 :
-            end = cnt
-        else:
-            end = min(cnt,end)
-        foundat = -1
-        for j in xrange(pos, end):
-            item = docList[j]
-            if item.find('=') >= 0:
-                (name, argres) = item.split('=',1)
-            else :
-                name = item
-                argres = ''
-            if name.endswith(tagpath) :
-                result = argres
-                foundat = j
-                break
-        return foundat, result
-
-
-    # return list of start positions for the tagpath
-    def posinDoc(self, tagpath):
-        startpos = []
-        pos = 0
-        res = ""
-        while res != None :
-            (foundpos, res) = self.findinDoc(tagpath, pos, -1)
-            if res != None :
-                startpos.append(foundpos)
-            pos = foundpos + 1
-        return startpos
-
-    # returns a vector of integers for the tagpath
-    def getData(self, tagpath, pos, end, clean=False):
-        if clean:
-            digits_only = re.compile(r'''([0-9]+)''')
-        argres=[]
-        (foundat, argt) = self.findinDoc(tagpath, pos, end)
-        if (argt != None) and (len(argt) > 0) :
-            argList = argt.split('|')
-            for strval in argList:
-                if clean:
-                    m = re.search(digits_only, strval)
-                    if m != None:
-                        strval = m.group()
-                argres.append(int(strval))
-        return argres
-
-    def process(self):
-
-        classlst = ''
-        csspage = '.cl-center { text-align: center; margin-left: auto; margin-right: auto; }\n'
-        csspage += '.cl-right { text-align: right; }\n'
-        csspage += '.cl-left { text-align: left; }\n'
-        csspage += '.cl-justify { text-align: justify; }\n'
-
-        # generate a list of each <style> starting point in the stylesheet
-        styleList= self.posinDoc('book.stylesheet.style')
-        stylecnt = len(styleList)
-        styleList.append(-1)
-
-        # process each style converting what you can
-
-        for j in xrange(stylecnt):
-            start = styleList[j]
-            end = styleList[j+1]
-
-            (pos, tag) = self.findinDoc('style._tag',start,end)
-            if tag == None :
-                (pos, tag) = self.findinDoc('style.type',start,end)
-
-            # Is this something we know how to convert to css
-            if tag in self.stags :
-
-                # get the style class
-                (pos, sclass) = self.findinDoc('style.class',start,end)
-                if sclass != None:
-                    sclass = sclass.replace(' ','-')
-                    sclass = '.cl-' + sclass.lower()
-                else :
-                    sclass = ''
-
-                # check for any "after class" specifiers
-                (pos, aftclass) = self.findinDoc('style._after_class',start,end)
-                if aftclass != None:
-                    aftclass = aftclass.replace(' ','-')
-                    aftclass = '.cl-' + aftclass.lower()
-                else :
-                    aftclass = ''
-
-                cssargs = {}
-
-                while True :
-
-                    (pos1, attr) = self.findinDoc('style.rule.attr', start, end)
-                    (pos2, val) = self.findinDoc('style.rule.value', start, end)
-
-                    if attr == None : break
-
-                    if (attr == 'display') or (attr == 'pos') or (attr == 'align'):
-                        # handle text based attributess
-                        attr = attr + '-' + val
-                        if attr in self.attr_str_map :
-                            cssargs[attr] = (self.attr_str_map[attr], '')
-                    else :
-                        # handle value based attributes
-                        if attr in self.attr_val_map :
-                            name = self.attr_val_map[attr]
-                            if attr in ('margin-bottom', 'margin-top', 'space-after') :
-                                scale = self.ph
-                            elif attr in ('margin-right', 'indent', 'margin-left', 'hang') :
-                                scale = self.pw
-                            elif attr == 'line-space':
-                                scale = self.fontsize * 2.0
-                            
-                            if val == "":
-                                val = 0
-
-                            if not ((attr == 'hang') and (int(val) == 0)) :
-                                pv = float(val)/scale
-                                cssargs[attr] = (self.attr_val_map[attr], pv)
-                                keep = True
-
-                    start = max(pos1, pos2) + 1
-
-                # disable all of the after class tags until I figure out how to handle them
-                if aftclass != "" : keep = False
-
-                if keep :
-                    # make sure line-space does not go below 100% or above 300% since
-                    # it can be wacky in some styles
-                    if 'line-space' in cssargs:
-                        seg = cssargs['line-space'][0]
-                        val = cssargs['line-space'][1]
-                        if val < 1.0: val = 1.0
-                        if val > 3.0: val = 3.0
-                        del cssargs['line-space']
-                        cssargs['line-space'] = (self.attr_val_map['line-space'], val)
-
-
-                    # handle modifications for css style hanging indents
-                    if 'hang' in cssargs:
-                        hseg = cssargs['hang'][0]
-                        hval = cssargs['hang'][1]
-                        del cssargs['hang']
-                        cssargs['hang'] = (self.attr_val_map['hang'], -hval)
-                        mval = 0
-                        mseg = 'margin-left: '
-                        mval = hval
-                        if 'margin-left' in cssargs:
-                            mseg = cssargs['margin-left'][0]
-                            mval = cssargs['margin-left'][1]
-                            if mval < 0: mval = 0
-                            mval = hval + mval
-                        cssargs['margin-left'] = (mseg, mval)
-                        if 'indent' in cssargs:
-                            del cssargs['indent']
-
-                    cssline = sclass + ' { '
-                    for key in iter(cssargs):
-                        mseg = cssargs[key][0]
-                        mval = cssargs[key][1]
-                        if mval == '':
-                            cssline += mseg + ' '
-                        else :
-                            aseg = mseg + '%.1f%%;' % (mval * 100.0)
-                            cssline += aseg + ' '
-
-                    cssline += '}'
-
-                    if sclass != '' :
-                        classlst += sclass + '\n'
-
-                    # handle special case of paragraph class used inside chapter heading
-                    # and non-chapter headings
-                    if sclass != '' :
-                        ctype = sclass[4:7]
-                        if ctype == 'ch1' :
-                            csspage += 'h1' + cssline + '\n'
-                        if ctype == 'ch2' :
-                            csspage += 'h2' + cssline + '\n'
-                        if ctype == 'ch3' :
-                            csspage += 'h3' + cssline + '\n'
-                        if ctype == 'h1-' :
-                            csspage += 'h4' + cssline + '\n'
-                        if ctype == 'h2-' :
-                            csspage += 'h5' + cssline + '\n'
-                        if ctype == 'h3_' :
-                            csspage += 'h6' + cssline + '\n'
-
-                    if cssline != ' { }':
-                        csspage += self.stags[tag] + cssline + '\n'
-
-
-        return csspage, classlst
-
-
-
-def convert2CSS(flatxml, fontsize, ph, pw):
-
-    print '          ', 'Using font size:',fontsize
-    print '          ', 'Using page height:', ph
-    print '          ', 'Using page width:', pw
-
-    # create a document parser
-    dp = DocParser(flatxml, fontsize, ph, pw)
-    csspage = dp.process()
-    return csspage
-
-
-def getpageIDMap(flatxml):
-    dp = DocParser(flatxml, 0, 0, 0)
-    pageidnumbers = dp.getData('info.original.pid', 0, -1, True)
-    return pageidnumbers
+# -*- coding: utf-8 -*-
+#
+# Adapted and simplified from the kitchen project
+#
+# Kitchen Project Copyright (c) 2012 Red Hat, Inc.
+#
+# kitchen 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.
+#
+# kitchen 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 kitchen; if not, see <http://www.gnu.org/licenses/>
+#
+# Authors:
+#   Toshio Kuratomi <toshio@fedoraproject.org>
+#   Seth Vidal
+#
+# Portions of code taken from yum/i18n.py and
+# python-fedora: fedora/textutils.py
+
+import codecs
+
+# returns a char string unchanged
+# returns a unicode string converted to a char string of the passed encoding
+# return the empty string for anything else
+def getwriter(encoding):
+    class _StreamWriter(codecs.StreamWriter):
+        def __init__(self, stream):
+            codecs.StreamWriter.__init__(self, stream, 'replace')
+
+        def encode(self, msg, errors='replace'):
+            if isinstance(msg, basestring):
+                if isinstance(msg, str):
+                    return (msg, len(msg))
+                return (msg.encode(self.encoding, 'replace'), len(msg))
+            return ('',0)
+
+    _StreamWriter.encoding = encoding
+    return _StreamWriter
index de084d303fcc72ce25e9213ab87f8f1d91bfdb03..30fc9181827213d8a0c36901fb5d0274dd786384 100644 (file)
Binary files a/Calibre_Plugins/K4MobiDeDRM_plugin/subasyncio.py and b/Calibre_Plugins/K4MobiDeDRM_plugin/subasyncio.py differ
index f1d8574d1bee52c60b39c5215703217336251e2b..65220a950808997c8598098d76a14cfe610e229f 100644 (file)
-#!/usr/bin/env python
+# A simple implementation of pbkdf2 using stock python modules. See RFC2898
+# for details. Basically, it derives a key from a password and salt.
 
-class Unbuffered:
-    def __init__(self, stream):
-        self.stream = stream
-    def write(self, data):
-        self.stream.write(data)
-        self.stream.flush()
-    def __getattr__(self, attr):
-        return getattr(self.stream, attr)
+# Copyright 2004 Matt Johnston <matt @ ucc asn au>
+# Copyright 2009 Daniel Holth <dholth@fastmail.fm>
+# This code may be freely used and modified for any purpose.
 
-import sys
-
-if 'calibre' in sys.modules:
-    inCalibre = True
-else:
-    inCalibre = False
+# Revision history
+# v0.1  October 2004    - Initial release
+# v0.2  8 March 2007    - Make usable with hashlib in Python 2.5 and use
+# v0.3  ""                 the correct digest_size rather than always 20
+# v0.4  Oct 2009        - Rescue from chandler svn, test and optimize.
 
-buildXML = False
-
-import os, csv, getopt
-import zlib, zipfile, tempfile, shutil
+import sys
+import hmac
 from struct import pack
-from struct import unpack
-from alfcrypto import Topaz_Cipher
-
-class TpzDRMError(Exception):
-    pass
-
-
-# local support routines
-if inCalibre:
-    from calibre_plugins.k4mobidedrm import kgenpids
-else:
-    import kgenpids
-
-# recursive zip creation support routine
-def zipUpDir(myzip, tdir, localname):
-    currentdir = tdir
-    if localname != "":
-        currentdir = os.path.join(currentdir,localname)
-    list = os.listdir(currentdir)
-    for file in list:
-        afilename = file
-        localfilePath = os.path.join(localname, afilename)
-        realfilePath = os.path.join(currentdir,file)
-        if os.path.isfile(realfilePath):
-            myzip.write(realfilePath, localfilePath)
-        elif os.path.isdir(realfilePath):
-            zipUpDir(myzip, tdir, localfilePath)
-
-#
-# Utility routines
-#
-
-# Get a 7 bit encoded number from file
-def bookReadEncodedNumber(fo):
-    flag = False
-    data = ord(fo.read(1))
-    if data == 0xFF:
-        flag = True
-        data = ord(fo.read(1))
-    if data >= 0x80:
-        datax = (data & 0x7F)
-        while data >= 0x80 :
-            data = ord(fo.read(1))
-            datax = (datax <<7) + (data & 0x7F)
-        data = datax
-    if flag:
-        data = -data
-    return data
-
-# Get a length prefixed string from file
-def bookReadString(fo):
-    stringLength = bookReadEncodedNumber(fo)
-    return unpack(str(stringLength)+"s",fo.read(stringLength))[0]
-
-#
-# crypto routines
-#
-
-# Context initialisation for the Topaz Crypto
-def topazCryptoInit(key):
-    return Topaz_Cipher().ctx_init(key)
-
-#     ctx1 = 0x0CAFFE19E
-#     for keyChar in key:
-#         keyByte = ord(keyChar)
-#         ctx2 = ctx1
-#         ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF )
-#     return [ctx1,ctx2]
-
-# decrypt data with the context prepared by topazCryptoInit()
-def topazCryptoDecrypt(data, ctx):
-    return Topaz_Cipher().decrypt(data, ctx)
-#     ctx1 = ctx[0]
-#     ctx2 = ctx[1]
-#     plainText = ""
-#     for dataChar in data:
-#         dataByte = ord(dataChar)
-#         m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF
-#         ctx2 = ctx1
-#         ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF)
-#         plainText += chr(m)
-#     return plainText
-
-# Decrypt data with the PID
-def decryptRecord(data,PID):
-    ctx = topazCryptoInit(PID)
-    return topazCryptoDecrypt(data, ctx)
-
-# Try to decrypt a dkey record (contains the bookPID)
-def decryptDkeyRecord(data,PID):
-    record = decryptRecord(data,PID)
-    fields = unpack("3sB8sB8s3s",record)
-    if fields[0] != "PID" or fields[5] != "pid" :
-        raise TpzDRMError("Didn't find PID magic numbers in record")
-    elif fields[1] != 8 or fields[3] != 8 :
-        raise TpzDRMError("Record didn't contain correct length fields")
-    elif fields[2] != PID :
-        raise TpzDRMError("Record didn't contain PID")
-    return fields[4]
-
-# Decrypt all dkey records (contain the book PID)
-def decryptDkeyRecords(data,PID):
-    nbKeyRecords = ord(data[0])
-    records = []
-    data = data[1:]
-    for i in range (0,nbKeyRecords):
-        length = ord(data[0])
-        try:
-            key = decryptDkeyRecord(data[1:length+1],PID)
-            records.append(key)
-        except TpzDRMError:
-            pass
-        data = data[1+length:]
-    if len(records) == 0:
-        raise TpzDRMError("BookKey Not Found")
-    return records
-
-
-class TopazBook:
-    def __init__(self, filename):
-        self.fo = file(filename, 'rb')
-        self.outdir = tempfile.mkdtemp()
-        # self.outdir = 'rawdat'
-        self.bookPayloadOffset = 0
-        self.bookHeaderRecords = {}
-        self.bookMetadata = {}
-        self.bookKey = None
-        magic = unpack("4s",self.fo.read(4))[0]
-        if magic != 'TPZ0':
-            raise TpzDRMError("Parse Error : Invalid Header, not a Topaz file")
-        self.parseTopazHeaders()
-        self.parseMetadata()
-
-    def parseTopazHeaders(self):
-        def bookReadHeaderRecordData():
-            # Read and return the data of one header record at the current book file position
-            # [[offset,decompressedLength,compressedLength],...]
-            nbValues = bookReadEncodedNumber(self.fo)
-            values = []
-            for i in range (0,nbValues):
-                values.append([bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo),bookReadEncodedNumber(self.fo)])
-            return values
-        def parseTopazHeaderRecord():
-            # Read and parse one header record at the current book file position and return the associated data
-            # [[offset,decompressedLength,compressedLength],...]
-            if ord(self.fo.read(1)) != 0x63:
-                raise TpzDRMError("Parse Error : Invalid Header")
-            tag = bookReadString(self.fo)
-            record = bookReadHeaderRecordData()
-            return [tag,record]
-        nbRecords = bookReadEncodedNumber(self.fo)
-        for i in range (0,nbRecords):
-            result = parseTopazHeaderRecord()
-            # print result[0], result[1]
-            self.bookHeaderRecords[result[0]] = result[1]
-        if ord(self.fo.read(1))  != 0x64 :
-            raise TpzDRMError("Parse Error : Invalid Header")
-        self.bookPayloadOffset = self.fo.tell()
-
-    def parseMetadata(self):
-        # Parse the metadata record from the book payload and return a list of [key,values]
-        self.fo.seek(self.bookPayloadOffset + self.bookHeaderRecords["metadata"][0][0])
-        tag = bookReadString(self.fo)
-        if tag != "metadata" :
-            raise TpzDRMError("Parse Error : Record Names Don't Match")
-        flags = ord(self.fo.read(1))
-        nbRecords = ord(self.fo.read(1))
-        # print nbRecords
-        for i in range (0,nbRecords) :
-            keyval = bookReadString(self.fo)
-            content = bookReadString(self.fo)
-            # print keyval
-            # print content
-            self.bookMetadata[keyval] = content
-        return self.bookMetadata
-
-    def getPIDMetaInfo(self):
-        keysRecord = self.bookMetadata.get('keys','')
-        keysRecordRecord = ''
-        if keysRecord != '':
-            keylst = keysRecord.split(',')
-            for keyval in keylst:
-                keysRecordRecord += self.bookMetadata.get(keyval,'')
-        return keysRecord, keysRecordRecord
-
-    def getBookTitle(self):
-        title = ''
-        if 'Title' in self.bookMetadata:
-            title = self.bookMetadata['Title']
-        return title
-
-    def setBookKey(self, key):
-        self.bookKey = key
-
-    def getBookPayloadRecord(self, name, index):
-        # Get a record in the book payload, given its name and index.
-        # decrypted and decompressed if necessary
-        encrypted = False
-        compressed = False
-        try:
-            recordOffset = self.bookHeaderRecords[name][index][0]
-        except:
-            raise TpzDRMError("Parse Error : Invalid Record, record not found")
-
-        self.fo.seek(self.bookPayloadOffset + recordOffset)
-
-        tag = bookReadString(self.fo)
-        if tag != name :
-            raise TpzDRMError("Parse Error : Invalid Record, record name doesn't match")
-
-        recordIndex = bookReadEncodedNumber(self.fo)
-        if recordIndex < 0 :
-            encrypted = True
-            recordIndex = -recordIndex -1
-
-        if recordIndex != index :
-            raise TpzDRMError("Parse Error : Invalid Record, index doesn't match")
-
-        if (self.bookHeaderRecords[name][index][2] > 0):
-            compressed = True
-            record = self.fo.read(self.bookHeaderRecords[name][index][2])
-        else:
-            record = self.fo.read(self.bookHeaderRecords[name][index][1])
-
-        if encrypted:
-            if self.bookKey:
-                ctx = topazCryptoInit(self.bookKey)
-                record = topazCryptoDecrypt(record,ctx)
-            else :
-                raise TpzDRMError("Error: Attempt to decrypt without bookKey")
-
-        if compressed:
-            record = zlib.decompress(record)
-
-        return record
-
-    def processBook(self, pidlst):
-        raw = 0
-        fixedimage=True
-        try:
-            keydata = self.getBookPayloadRecord('dkey', 0)
-        except TpzDRMError, e:
-            print "no dkey record found, book may not be encrypted"
-            print "attempting to extrct files without a book key"
-            self.createBookDirectory()
-            self.extractFiles()
-            print "Successfully Extracted Topaz contents"
-            if inCalibre:
-                from calibre_plugins.k4mobidedrm import genbook
-            else:
-                import genbook
-            
-            rv = genbook.generateBook(self.outdir, raw, fixedimage)
-            if rv == 0:
-                print "\nBook Successfully generated"
-            return rv
-
-        # try each pid to decode the file
-        bookKey = None
-        for pid in pidlst:
-            # use 8 digit pids here
-            pid = pid[0:8]
-            print "\nTrying: ", pid
-            bookKeys = []
-            data = keydata
-            try:
-                bookKeys+=decryptDkeyRecords(data,pid)
-            except TpzDRMError, e:
-                pass
-            else:
-                bookKey = bookKeys[0]
-                print "Book Key Found!"
-                break
-
-        if not bookKey:
-            raise TpzDRMError('Decryption Unsucessful; No valid pid found')
-
-        self.setBookKey(bookKey)
-        self.createBookDirectory()
-        self.extractFiles()
-        print "Successfully Extracted Topaz contents"
-        if inCalibre:
-            from calibre_plugins.k4mobidedrm import genbook
-        else:
-            import genbook
-        
-        rv = genbook.generateBook(self.outdir, raw, fixedimage)
-        if rv == 0:
-            print "\nBook Successfully generated"
-        return rv
-
-    def createBookDirectory(self):
-        outdir = self.outdir
-        # create output directory structure
-        if not os.path.exists(outdir):
-            os.makedirs(outdir)
-        destdir =  os.path.join(outdir,'img')
-        if not os.path.exists(destdir):
-            os.makedirs(destdir)
-        destdir =  os.path.join(outdir,'color_img')
-        if not os.path.exists(destdir):
-            os.makedirs(destdir)
-        destdir =  os.path.join(outdir,'page')
-        if not os.path.exists(destdir):
-            os.makedirs(destdir)
-        destdir =  os.path.join(outdir,'glyphs')
-        if not os.path.exists(destdir):
-            os.makedirs(destdir)
-
-    def extractFiles(self):
-        outdir = self.outdir
-        for headerRecord in self.bookHeaderRecords:
-            name = headerRecord
-            if name != "dkey" :
-                ext = '.dat'
-                if name == 'img' : ext = '.jpg'
-                if name == 'color' : ext = '.jpg'
-                print "\nProcessing Section: %s " % name
-                for index in range (0,len(self.bookHeaderRecords[name])) :
-                    fnum = "%04d" % index
-                    fname = name + fnum + ext
-                    destdir = outdir
-                    if name == 'img':
-                        destdir =  os.path.join(outdir,'img')
-                    if name == 'color':
-                        destdir =  os.path.join(outdir,'color_img')
-                    if name == 'page':
-                        destdir =  os.path.join(outdir,'page')
-                    if name == 'glyphs':
-                        destdir =  os.path.join(outdir,'glyphs')
-                    outputFile = os.path.join(destdir,fname)
-                    print ".",
-                    record = self.getBookPayloadRecord(name,index)
-                    if record != '':
-                        file(outputFile, 'wb').write(record)
-        print " "
-
-    def getHTMLZip(self, zipname):
-        htmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
-        htmlzip.write(os.path.join(self.outdir,'book.html'),'book.html')
-        htmlzip.write(os.path.join(self.outdir,'book.opf'),'book.opf')
-        if os.path.isfile(os.path.join(self.outdir,'cover.jpg')):
-            htmlzip.write(os.path.join(self.outdir,'cover.jpg'),'cover.jpg')
-        htmlzip.write(os.path.join(self.outdir,'style.css'),'style.css')
-        zipUpDir(htmlzip, self.outdir, 'img')
-        htmlzip.close()
-
-    def getSVGZip(self, zipname):
-        svgzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
-        svgzip.write(os.path.join(self.outdir,'index_svg.xhtml'),'index_svg.xhtml')
-        zipUpDir(svgzip, self.outdir, 'svg')
-        zipUpDir(svgzip, self.outdir, 'img')
-        svgzip.close()
-    
-    def getXMLZip(self, zipname):
-        xmlzip = zipfile.ZipFile(zipname,'w',zipfile.ZIP_DEFLATED, False)
-        targetdir = os.path.join(self.outdir,'xml')
-        zipUpDir(xmlzip, targetdir, '')
-        zipUpDir(xmlzip, self.outdir, 'img')
-        xmlzip.close()
-
-    def cleanup(self):
-        if os.path.isdir(self.outdir):
-            shutil.rmtree(self.outdir, True)
-
-def usage(progname):
-    print "Removes DRM protection from Topaz ebooks and extract the contents"
-    print "Usage:"
-    print "    %s [-k <kindle.info>] [-p <pidnums>] [-s <kindleSerialNumbers>] <infile> <outdir>  " % progname
-
-
-# Main
-def main(argv=sys.argv):
-    global buildXML
-    progname = os.path.basename(argv[0])
-    k4 = False
-    pids = []
-    serials = []
-    kInfoFiles = []
-
+try:
+    # only in python 2.5
+    import hashlib
+    sha = hashlib.sha1
+    md5 = hashlib.md5
+    sha256 = hashlib.sha256
+except ImportError: # pragma: NO COVERAGE
+    # fallback
+    import sha
+    import md5
+
+# this is what you want to call.
+def pbkdf2( password, salt, itercount, keylen, hashfn = sha ):
     try:
-        opts, args = getopt.getopt(sys.argv[1:], "k:p:s:")
-    except getopt.GetoptError, err:
-        print str(err)
-        usage(progname)
-        return 1
-    if len(args)<2:
-        usage(progname)
-        return 1
-
-    for o, a in opts:
-        if o == "-k":
-            if a == None :
-                print "Invalid parameter for -k"
-                return 1
-            kInfoFiles.append(a)
-        if o == "-p":
-            if a == None :
-                print "Invalid parameter for -p"
-                return 1
-            pids = a.split(',')
-        if o == "-s":
-            if a == None :
-                print "Invalid parameter for -s"
-                return 1
-            serials = a.split(',')
-    k4 = True
-
-    infile = args[0]
-    outdir = args[1]
-
-    if not os.path.isfile(infile):
-        print "Input File Does Not Exist"
-        return 1
-
-    bookname = os.path.splitext(os.path.basename(infile))[0]
-
-    tb = TopazBook(infile)
-    title = tb.getBookTitle()
-    print "Processing Book: ", title
-    keysRecord, keysRecordRecord = tb.getPIDMetaInfo()
-    pidlst = kgenpids.getPidList(keysRecord, keysRecordRecord, k4, pids, serials, kInfoFiles)
-
-    try:
-        print "Decrypting Book"
-        tb.processBook(pidlst)
-
-        print "   Creating HTML ZIP Archive"
-        zipname = os.path.join(outdir, bookname + '_nodrm' + '.htmlz')
-        tb.getHTMLZip(zipname)
-
-        print "   Creating SVG ZIP Archive"
-        zipname = os.path.join(outdir, bookname + '_SVG' + '.zip')
-        tb.getSVGZip(zipname)
-
-        if buildXML:
-            print "   Creating XML ZIP Archive"
-            zipname = os.path.join(outdir, bookname + '_XML' + '.zip')
-            tb.getXMLZip(zipname)
-
-        # removing internal temporary directory of pieces
-        tb.cleanup()
-
-    except TpzDRMError, e:
-        print str(e)
-        # tb.cleanup()
-        return 1
-
-    except Exception, e:
-        print str(e)
-        # tb.cleanup
-        return 1
-
-    return 0
-
-
-if __name__ == '__main__':
-    sys.stdout=Unbuffered(sys.stdout)
-    sys.exit(main())
+        # depending whether the hashfn is from hashlib or sha/md5
+        digest_size = hashfn().digest_size
+    except TypeError: # pragma: NO COVERAGE
+        digest_size = hashfn.digest_size
+    # l - number of output blocks to produce
+    l = keylen / digest_size
+    if keylen % digest_size != 0:
+        l += 1
+
+    h = hmac.new( password, None, hashfn )
+
+    T = ""
+    for i in range(1, l+1):
+        T += pbkdf2_F( h, salt, itercount, i )
+
+    return T[0: keylen]
+
+def xorstr( a, b ):
+    if len(a) != len(b):
+        raise ValueError("xorstr(): lengths differ")
+    return ''.join((chr(ord(x)^ord(y)) for x, y in zip(a, b)))
+
+def prf( h, data ):
+    hm = h.copy()
+    hm.update( data )
+    return hm.digest()
+
+# Helper as per the spec. h is a hmac which has been created seeded with the
+# password, it will be copy()ed and not modified.
+def pbkdf2_F( h, salt, itercount, blocknum ):
+    U = prf( h, salt + pack('>i',blocknum ) )
+    T = U
+
+    for i in range(2, itercount+1):
+        U = prf( h, U )
+        T = xorstr( T, U )
+
+    return T
index 38356dd507d8d28fdd30652f606582ca5fe4ea46..a4d3e81aa5a4cc1a0ea4be7897364dddca8ce989 100644 (file)
@@ -1,4 +1,4 @@
-eReader PDB2PML - eReaderPDB2PML_v06_plugin.zip
+eReader PDB2PML - eReaderPDB2PML_v07_plugin.zip
 
 All credit given to The Dark Reverser for the original standalone script. I had the much easier job of converting it to a Calibre plugin.
 
@@ -7,7 +7,7 @@ This plugin is meant to convert secure Ereader files (PDB) to unsecured PMLZ fil
 
 Installation:
 
-Go to Calibre's Preferences page. Do **NOT** select "Get Plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file  (eReaderPDB2PML_vXX_plugin.zip) and click the 'Add' button. You're done.
+Go to Calibre's Preferences page. Do **NOT** select "Get Plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file  (eReaderPDB2PML_v07_plugin.zip) and click the 'Add' button. You're done.
 
 
 Please note:  Calibre does not provide any immediate feedback to indicate that adding the plugin was a success. You can always click on the File-Type plugins to see if the plugin was added.
@@ -17,15 +17,25 @@ Configuration:
 
 Highlight the plugin (eReader PDB 2 PML under the "File type plugins" category) and click the "Customize Plugin" button on Calibre's Preferences->Plugins page. Enter your name and last 8 digits of the credit card number separated by a comma: Your Name,12341234
 
-If you've purchased books with more than one credit card, separate the info with a colon: Your Name,12341234:Other Name,23452345 (NOTE: Do NOT put quotes around your name like you do with the original script!!)
+If you've purchased books with more than one credit card, separate the info with a colon: Your Name,12341234:Other Name,23452345
 
 
 Troubleshooting:
 
-If you find that it's not working for you (imported pdb's are not converted to pmlz format), you can save a lot of time and trouble by trying to add the pdb to Calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might as well get used to it. ;)
+If you find that it's not working for you (imported ebooks still have DRM), you can save a lot of time and trouble by first deleting the DRMed ebook from calibre and then trying to add the ebook to calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might as well get used to it. ;)
 
-Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook.pdb". Don't type the quotes and obviously change the 'your_ebook.pdb' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make.
+On Macintosh only you must first run calibre, open Preferences, open Miscellaneous, and click on the “Install command line tools” button. (On Windows and Linux the command line tools are installed automatically.)
 
-** Note: the Mac version of Calibre doesn't install the command line tools by default. If you go to the 'Preferences' page and click on the miscellaneous button, you'll see the option to install the command line tools.
+On Windows, open a terminal/command window. (Start/Run… and then type ‘cmd’ (without the ‘s) as the program to run).
+On Macintosh, open the Terminal application (in your Utilities folder).
+On Linux open a command window. Hopefully all Linux users know how to do this, as I do not.
 
+You should now have a text-based command-line window open. Also have open the folder containing the ebook to be imported. Make sure that book isn’t already in calibre, and that calibre isn’t running.
 
+Now type in "calibredb add " (without the " but don’t miss that final space) and now drag the book to be imported onto the window. The full path to the book should be inserted into the command line. Now press the return/enter key. The import routines will run and produce some logging information.
+
+Now copy the output from the terminal window.
+On Windows, you must use the window menu (little icon at left of window bar) to select all the text and then to copy it.
+On Macintosh and Linux, just use the normal text select and copy commands.
+
+Paste the information into a comment at my blog, describing your problem.
\ No newline at end of file
index 49e400b6b3136fdb22ce16b327a4dedc7722f68c..0282220edfbb8c93f3ae1cf557b1d3ff93054025 100644 (file)
Binary files a/Calibre_Plugins/eReaderPDB2PML_plugin.zip and b/Calibre_Plugins/eReaderPDB2PML_plugin.zip differ
index 954c52262efae231875ba1c53e153c45fc636dc5..b42cc1f28e412de241c00bfae650d2a5474025f3 100644 (file)
@@ -1,4 +1,5 @@
 #!/usr/bin/env python
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
 
 # eReaderPDB2PML_plugin.py
 # Released under the terms of the GNU General Public Licence, version 3 or
 #   0.0.3 - removed added psyco code as it is not supported under Calibre's Python 2.7
 #   0.0.4 - minor typos fixed
 #   0.0.5 - updated to the new calibre plugin interface
+#   0.0.6 - unknown changes
+#   0.0.7 - improved config dialog processing and fix possible output/unicode problem
 
 import sys, os
 
 from calibre.customize import FileTypePlugin
 from calibre.ptempfile import PersistentTemporaryDirectory
 from calibre.constants import iswindows, isosx
-from calibre_plugins.erdrpdb2pml import erdr2pml
 
 class eRdrDeDRM(FileTypePlugin):
     name                = 'eReader PDB 2 PML' # Name of the plugin
@@ -47,12 +49,22 @@ class eRdrDeDRM(FileTypePlugin):
                             Credit given to The Dark Reverser for the original standalone script.'
     supported_platforms = ['linux', 'osx', 'windows'] # Platforms this plugin will run on
     author              = 'DiapDealer' # The author of this plugin
-    version             = (0, 0, 6)   # The version number of this plugin
+    version             = (0, 0, 7)   # The version number of this plugin
     file_types          = set(['pdb']) # The file types that this plugin will be applied to
     on_import           = True # Run this plugin during the import
     minimum_calibre_version = (0, 7, 55)
 
     def run(self, path_to_ebook):
+        from calibre_plugins.erdrpdb2pml import erdr2pml, outputfix
+         
+        if sys.stdout.encoding == None:
+            sys.stdout = outputfix.getwriter('utf-8')(sys.stdout)
+        else:
+            sys.stdout = outputfix.getwriter(sys.stdout.encoding)(sys.stdout)
+        if sys.stderr.encoding == None:
+            sys.stderr = outputfix.getwriter('utf-8')(sys.stderr)
+        else:
+            sys.stderr = outputfix.getwriter(sys.stderr.encoding)(sys.stderr)
         
         global bookname, erdr2pml
         
@@ -67,10 +79,13 @@ class eRdrDeDRM(FileTypePlugin):
             for i in ar:
                 try:
                     name, cc = i.split(',')
+                    #remove spaces at start or end of name, and anywhere in CC
+                    name = name.strip()
+                    cc = cc.replace(" ","")
                 except ValueError:
                     print '   Error parsing user supplied data.'
                     return path_to_ebook
-
+                
                 try:
                     print "Processing..."
                     import time
diff --git a/Calibre_Plugins/eReaderPDB2PML_plugin/outputfix.py b/Calibre_Plugins/eReaderPDB2PML_plugin/outputfix.py
new file mode 100644 (file)
index 0000000..906c6e9
--- /dev/null
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+#
+# Adapted and simplified from the kitchen project
+#
+# Kitchen Project Copyright (c) 2012 Red Hat, Inc.
+#
+# kitchen 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.
+#
+# kitchen 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 kitchen; if not, see <http://www.gnu.org/licenses/>
+#
+# Authors:
+#   Toshio Kuratomi <toshio@fedoraproject.org>
+#   Seth Vidal
+#
+# Portions of code taken from yum/i18n.py and
+# python-fedora: fedora/textutils.py
+
+import codecs
+
+# returns a char string unchanged
+# returns a unicode string converted to a char string of the passed encoding
+# return the empty string for anything else
+def getwriter(encoding):
+    class _StreamWriter(codecs.StreamWriter):
+        def __init__(self, stream):
+            codecs.StreamWriter.__init__(self, stream, 'replace')
+
+        def encode(self, msg, errors='replace'):
+            if isinstance(msg, basestring):
+                if isinstance(msg, str):
+                    return (msg, len(msg))
+                return (msg.encode(self.encoding, 'replace'), len(msg))
+            return ('',0)
+
+    _StreamWriter.encoding = encoding
+    return _StreamWriter
index c03ca38b68346be132b2741dae7fdafe7cd9fb9c..9970e710003439686dcf552fa733911c83d2a791 100644 (file)
Binary files a/Calibre_Plugins/ignobleepub_plugin.zip and b/Calibre_Plugins/ignobleepub_plugin.zip differ
diff --git a/Calibre_Plugins/ignobleepub_plugin/Ignoble Epub DeDRM_Help.htm b/Calibre_Plugins/ignobleepub_plugin/Ignoble Epub DeDRM_Help.htm
new file mode 100644 (file)
index 0000000..7963d18
--- /dev/null
@@ -0,0 +1,89 @@
+<html>
+
+<head>
+<title>Ignoble Epub DeDRM Plugin Configuration</title>
+</head>
+
+<body>
+
+<h1>Ignoble Epub DeDRM Plugin</h1>
+<h3>(version 0.2.3)</h3>
+<h3> For additional help read the <a href="http://apprenticealf.wordpress.com/2011/01/17/frequently-asked-questions-about-the-drm-removal-tools/" target="_blank">FAQ</a> on <a href="http://apprenticealf.wordpress.com" target="_blank">Apprentice Alf's Blog</a> and ask questions in the comments section of the <a href="http://apprenticealf.wordpress.com/2012/09/10/drm-removal-tools-for-ebooks/" target="_blank">first post</a>.</h3>
+
+<p>All credit given to I &lt;3 Cabbages for the original standalone scripts (I had the much easier job of converting them to a calibre plugin).</p>
+
+<p>This plugin is meant to decrypt Barnes & Noble ePubs that are protected with Adobe's Adept encryption. It is meant to function without having to install any dependencies... other than having calibre installed, of course. It will still work if you have Python and PyCrypto already installed, but they aren't necessary.</p>
+
+<p>This help file is always available from within the plugin's customization dialog in calibre (when installed, of course). The "Plugin Help" link can be found in the upper-right portion of the customization dialog.</p> 
+
+<h3>Installation:</h3>
+
+<p>Go to calibre's Preferences page.  Do **NOT** select "Get plugins to enhance calibre" as this is reserved for "official" calibre plugins, instead select "Change calibre behavior". Under "Advanced" click on the Plugins button. Use the "Load plugin from file" button to select the plugin's zip file  (ignobleepub_v02.3_plugin.zip) and click the 'Add' button. Click 'Yes' in the the "Are you sure?" dialog. Click OK in the "Success" dialog. <b><u>Now restart calibre</u></b>.</p>
+
+
+<h3>Configuration:</h3>
+
+<p>Upon first installing the plugin (or upgrading from a version earlier than 0.2.0), the plugin will be unconfigured. Until you create at least one B&amp;N key&mdash;or migrate your existing key(s)/data from an earlier version of the plugin&mdash;the plugin will not function. When unconfigured (no saved keys)... an error message will occur whenever ePubs are imported to calibre. To eliminate the error message, open the plugin's customization dialog and create/import/migrate a key (or disable/uninstall the plugin). You can get to the plugin's customization dialog by opening calibre's Preferences dialog, and clicking Plugins (under the Advanced section). Once in the Plugin Preferences, expand the "File type plugins" section and look for the "Ignoble Epub DeDRM" plugin. Highlight that plugin and click the "Customize plugin" button.</p>
+
+<p>If you are upgrading from an earlier version of this plugin and have provided your name(s) and credit card number(s) as part of the old plugin's customization string, you will be prompted to migrate this data to the plugin's new, more secure, key storage method when you open the customization dialog for the first time. If you choose NOT to migrate that data, you will be prompted to save that data as a text file in a location of your choosing. Either way, this plugin will no longer be storing names and credit card numbers in plain sight (or anywhere for that matter) on your computer or in calibre. If you don't choose to migrate OR save the data, that data will be lost. You have been warned!!</p>
+
+<p>Upon configuring for the first time, you may also be asked if you wish to import your existing *.b64 keyfiles (if you use them) to the plugin's new key storage method. The new plugin no longer looks for keyfiles in calibre's configuration directory, so it's highly recommended that you import any existing keyfiles when prompted ... but you <i>always</i> have the ability to import existing keyfiles anytime you might need/want to.</p>
+
+<p>If you have upgraded from an earlier version of the plugin, the above instructions may be all you need to do to get the new plugin up and running. Continue reading for new-key generation and existing-key management instructions.</p>
+
+<h4 style="margin-left: 1.0em;"><u>Creating New Keys:</u></h4>
+
+<p style="margin-left: 1.0em">On the right-hand side of the plugin's customization dialog, you will see a button with an icon that looks like a green plus sign (+). Clicking this button will open a new dialog for entering the necessary data to generate a new key.</p>
+<ul style="margin-left: 2.0em;">
+<li><b>Unique Key Name:</b> this is a unique name you choose to help you identify the key after it's created. This name will show in the list of configured keys. Choose something that will help you remember the data (name, cc#) it was created with.</i>
+<li style="margin-top: 0.5em;"><b>Your Name:</b> Your name as set in your Barnes & Noble account, My Account page, directly under PERSONAL INFORMATION. It is usually just your first name and last name separated by a space. This name will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key that's stored in the preferences.</i>
+<li style="margin-top: 0.5em;"><b>Credit Card#:</b> this is the default credit card number that was on file with Barnes & Noble at the time of download of the ebook to be de-DRMed. Nothing fancy here; no dashes or spaces ... just the 16 (15 for American Express) digits. Again... this number will not be stored anywhere on your computer or in calibre. It will only be used in the creation of the one-way hash/key that's stored in the preferences.</i> 
+</ul> 
+
+<p style="margin-left: 1.0em;">Click the 'OK" button to create and store the generated key. Or Cancel if you didn't want to create a key.</p>
+
+<h4 style="margin-left: 1.0em;"><u>Deleting Keys:</u></h4>
+
+<p style="margin-left: 1.0em;">On the right-hand side of the plugin's customization dialog, you will see a button with an icon that looks like a red "X". Clicking this button will delete the highlighted key in the list. You will be prompted once to be sure that's what you truly mean to do. Once gone, it's permanently gone.</p>
+
+<h4 style="margin-left: 1.0em;"><u>Exporting Keys:</u></h4>
+
+<p style="margin-left: 1.0em;">On the right-hand side of the plugin's customization dialog, you will see a button with an icon that looks like a computer's hard-drive. Use this button to export the highlighted key to a file (*.b64). Used for backup purposes or to migrate key data to other computers/calibre installations. The dialog will prompt you for a place to save the file.</p>
+
+<h4 style="margin-left: 1.0em;"><u>Importing Existing Keyfiles:</u></h4>
+
+<p style="margin-left: 1.0em;">At the bottom-left of the plugin's customization dialog, you will see a button labeled "Import Existing Keyfiles". Use this button to import existing *.b64 keyfiles. Used for migrating keyfiles from older versions of the plugin (or keys generated with the original I &lt;3 Cabbages script), or moving keyfiles from computer to computer, or restoring a backup. Some very basic validation is done to try to avoid overwriting already configured keys with incoming, imported keyfiles with the same base file name, but I'm sure that could be broken if someone tried hard. Just take care when importing.</p>
+
+<p>Once done creating/importing/exporting/deleting decryption keys; click "OK" to exit the customization dialogue (the cancel button will actually work the same way here ... at this point all data/changes are committed already, so take your pick).</p>
+
+<h3>Troubleshooting:</h3>
+
+<p style="margin-top: 0.5em;">If you find that it's not working for you (imported Barnes & Noble epubs still have DRM), you can save a lot of time and trouble by trying to add the epub to Calibre with the command line tools. This will print out a lot of helpful debugging info that can be copied into any online help requests. I'm going to ask you to do it first, anyway, so you might as well get used to it. ;)</p>
+
+<p>Open a command prompt (terminal) and change to the directory where the ebook you're trying to import resides. Then type the command "calibredb add your_ebook.epub" **. Don't type the quotes and obviously change the 'your_ebook.epub' to whatever the filename of your book is. Copy the resulting output and paste it into any online help request you make.</p>
+
+<p>Another way to debug (perhaps easier if you're not all that comfortable with command-line stuff) is to launch calibre in debug mode. Open a command prompt (terminal) and type "calibre-debug -g" (again without the quotes). Calibre will launch, and you can can add the problem book(s) using the normal gui method. The debug info will be output to the original command prompt (terminal window). Copy the resulting output and paste it into any online help request you make.</p>
+<p>&nbsp;</p>
+<p>** Note: the Mac version of Calibre doesn't install the command line tools by default. If you go to the 'Preferences' page and click on the miscellaneous button, you'll see the option to install the command line tools.</p>
+
+<p>&nbsp;</p>
+<h4>Revision history:</h4>
+<pre>
+   0.1.0 - Initial release
+   0.1.1 - Allow Windows users to make use of openssl if they have it installed.
+          - Incorporated SomeUpdates zipfix routine.
+   0.1.2 - bug fix for non-ascii file names in encryption.xml
+   0.1.3 - Try PyCrypto on Windows first
+   0.1.4 - update zipfix to deal with mimetype not in correct place
+   0.1.5 - update zipfix to deal with completely missing mimetype files
+   0.1.6 - update to the new calibre plugin interface
+   0.1.7 - Fix for potential problem with PyCrypto
+   0.1.8 - an updated/modified zipfix.py and included zipfilerugged.py
+   0.2.0 - Completely overhauled plugin configuration dialog and key management/storage
+   0.2.1 - an updated/modified zipfix.py and included zipfilerugged.py
+   0.2.2 - added in potential fixes from 0.1.7 that had been missed.
+   0.2.3 - fixed possible output/unicode problem
+</pre>
+</body>
+
+</html>
similarity index 50%
rename from Calibre_Plugins/ignobleepub_plugin/ignobleepub_plugin.py
rename to Calibre_Plugins/ignobleepub_plugin/__init__.py
index b63c9499f6f42f9134bb774d4e2b61bb566c2948..bd1bc3bdcab23163027c1273961ed0ede2cce536 100644 (file)
@@ -1,6 +1,11 @@
 #!/usr/bin/env python
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+
+from __future__ import with_statement
+__license__   = 'GPL v3'
+__docformat__ = 'restructuredtext en'
+
 
-# ignobleepub_plugin.py
 # Released under the terms of the GNU General Public Licence, version 3 or
 # later.  <http://www.gnu.org/licenses/>
 #
 # I had the much easier job of converting them to Calibre a plugin.
 #
 # This plugin is meant to decrypt Barnes & Noble Epubs that are protected
-# with Adobe's Adept encryption. It is meant to function without having to install
-# any dependencies... other than having Calibre installed, of course. It will still
+# with a version of Adobe's Adept encryption. It is meant to function without having to
+# install any dependencies... other than having Calibre installed, of course. It will still
 # work if you have Python and PyCrypto already installed, but they aren't necessary.
 #
 # Configuration:
-# 1) The easiest way to configure the plugin is to enter your name (Barnes & Noble account
-# name) and credit card number (the one used to purchase the books) into the plugin's
-# customization window. Highlight the plugin (Ignoble Epub DeDRM) and click the
-# "Customize Plugin" button on Calibre's Preferences->Plugins page.
-# Enter the name and credit card number separated by a comma: Your Name,1234123412341234
-#
-# If you've purchased books with more than one credit card, separate the info with
-# a colon: Your Name,1234123412341234:Other Name,2345234523452345
-#
-# ** Method 1 is your only option if you don't have/can't run the original
-# I <3 Cabbages scripts on your particular machine. **
-#
-# 2) If you already have keyfiles generated with I <3 Cabbages' ignoblekeygen.pyw
-# script, you can put those keyfiles in Calibre's configuration directory. The easiest
-# way to find the correct directory is to go to Calibre's Preferences page... click
-# on the 'Miscellaneous' button (looks like a gear),  and then click the 'Open Calibre
-# configuration directory' button. Paste your keyfiles in there. Just make sure that
-# they have different names and are saved with the '.b64' extension (like the ignoblekeygen
-# script produces). This directory isn't touched when upgrading Calibre, so it's quite safe
-# to leave then there.
-#
-# All keyfiles from option 2 and all data entered from option 1 will be used to attempt
-# to decrypt a book. You can use option 1 or option 2, or a combination of both.
-#
+# Check out the plugin's configuration settings by clicking the "Customize plugin"
+# button when you have the "BnN ePub DeDRM" plugin highlighted (under Preferences->
+# Plugins->File type plugins). Once you have the configuration dialog open, you'll
+# see a Help link on the top right-hand side.
 #
 # Revision history:
 #   0.1.0 - Initial release
 #   0.1.3 - Try PyCrypto on Windows first
 #   0.1.4 - update zipfix to deal with mimetype not in correct place
 #   0.1.5 - update zipfix to deal with completely missing mimetype files
-#   0.1.6 - update ot the new calibre plugin interface 
+#   0.1.6 - update for the new calibre plugin interface
+#   0.1.7 - Fix for potential problem with PyCrypto
+#   0.1.8 - an updated/modified zipfix.py and included zipfilerugged.py
+#   0.2.0 - Completely overhauled plugin configuration dialog and key management/storage
+#   0.2.1 - an updated/modified zipfix.py and included zipfilerugged.py
+#   0.2.2 - added in potential fixes from 0.1.7 that had been missed.
+#   0.2.3 - fixed possible output/unicode problem
 
 """
 Decrypt Barnes & Noble ADEPT encrypted EPUB books.
 """
 
-from __future__ import with_statement
-
-__license__ = 'GPL v3'
+PLUGIN_NAME = 'Ignoble Epub DeDRM'
+PLUGIN_VERSION_TUPLE = (0, 2, 3)
+PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
+# Include an html helpfile in the plugin's zipfile with the following name.
+RESOURCE_NAME = PLUGIN_NAME + '_Help.htm'
 
-import sys
-import os
-import hashlib
-import zlib
-import zipfile
-import re
+import sys, os, zlib, re
 from zipfile import ZipFile, ZIP_STORED, ZIP_DEFLATED
-import xml.etree.ElementTree as etree
+from zipfile import ZipInfo as _ZipInfo
+#from lxml import etree
+try:
+    import xml.etree.cElementTree as etree
+except ImportError:
+    import xml.etree.ElementTree as etree
 from contextlib import closing
 
 global AES
-global AES2
 
 META_NAMES = ('mimetype', 'META-INF/rights.xml', 'META-INF/encryption.xml')
 NSMAP = {'adept': 'http://ns.adobe.com/adept',
@@ -77,7 +69,7 @@ NSMAP = {'adept': 'http://ns.adobe.com/adept',
 
 class IGNOBLEError(Exception):
     pass
-
+    
 def _load_crypto_libcrypto():
     from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
         Structure, c_ulong, create_string_buffer, cast
@@ -88,7 +80,7 @@ def _load_crypto_libcrypto():
     else:
         libcrypto = find_library('crypto')
     if libcrypto is None:
-        raise IGNOBLEError('libcrypto not found')
+        raise IGNOBLEError('%s Plugin v%s: libcrypto not found' % (PLUGIN_NAME, PLUGIN_VERSION))
     libcrypto = CDLL(libcrypto)
 
     AES_MAXNR = 14
@@ -107,8 +99,6 @@ def _load_crypto_libcrypto():
         func.argtypes = argtypes
         return func
     
-    AES_set_encrypt_key = F(c_int, 'AES_set_encrypt_key',
-                            [c_char_p, c_int, AES_KEY_p])
     AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
                             [c_char_p, c_int, AES_KEY_p])
     AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
@@ -119,87 +109,51 @@ def _load_crypto_libcrypto():
         def __init__(self, userkey):
             self._blocksize = len(userkey)
             if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
-                raise IGNOBLEError('AES improper key used')
+                raise IGNOBLEError('%s Plugin v%s: AES improper key used' % (PLUGIN_NAME, PLUGIN_VERSION))
                 return
             key = self._key = AES_KEY()
             rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
             if rv < 0:
-                raise IGNOBLEError('Failed to initialize AES key')
+                raise IGNOBLEError('%s Plugin v%s: Failed to initialize AES key' % (PLUGIN_NAME, PLUGIN_VERSION))
     
         def decrypt(self, data):
             out = create_string_buffer(len(data))
             iv = ("\x00" * self._blocksize)
             rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
             if rv == 0:
-                raise IGNOBLEError('AES decryption failed')
+                raise IGNOBLEError('%s Plugin v%s: AES decryption failed' % (PLUGIN_NAME, PLUGIN_VERSION))
             return out.raw
         
-    class AES2(object):
-         def __init__(self, userkey, iv):
-            self._blocksize = len(userkey)
-            self._iv = iv
-            key = self._key = AES_KEY()
-            rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key)
-            if rv < 0:
-                raise IGNOBLEError('Failed to initialize AES Encrypt key')
-    
-         def encrypt(self, data):
-            out = create_string_buffer(len(data))
-            rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1)
-            if rv == 0:
-                raise IGNOBLEError('AES encryption failed')
-            return out.raw
-    print 'IgnobleEpub: Using libcrypto.'
-    return (AES, AES2)
+    print '%s Plugin v%s: Using libcrypto.' %(PLUGIN_NAME, PLUGIN_VERSION)
+    return AES
 
 def _load_crypto_pycrypto():
     from Crypto.Cipher import AES as _AES
 
     class AES(object):
         def __init__(self, key):
-            self._aes = _AES.new(key, _AES.MODE_CBC)
+            self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
 
         def decrypt(self, data):
             return self._aes.decrypt(data)
             
-    class AES2(object):
-        def __init__(self, key, iv):
-            self._aes = _AES.new(key, _AES.MODE_CBC, iv)
-
-        def encrypt(self, data):
-            return self._aes.encrypt(data)
-    print 'IgnobleEpub: Using PyCrypto.'
-    return (AES, AES2)
+    print '%s Plugin v%s: Using PyCrypto.' %(PLUGIN_NAME, PLUGIN_VERSION)
+    return AES
     
 def _load_crypto():
-    _aes = _aes2 = None
+    _aes = None
     cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
     if sys.platform.startswith('win'):
         cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
     for loader in cryptolist:
         try:
-            _aes, _aes2 = loader()
+            _aes = loader()
             break
         except (ImportError, IGNOBLEError):
             pass
-    return (_aes, _aes2)
-
-def normalize_name(name): # Strip spaces and convert to lowercase.
-    return ''.join(x for x in name.lower() if x != ' ')
-
-def generate_keyfile(name, ccn):
-    name = normalize_name(name) + '\x00'
-    ccn = ccn + '\x00'
-    name_sha = hashlib.sha1(name).digest()[:16]
-    ccn_sha = hashlib.sha1(ccn).digest()[:16]
-    both_sha = hashlib.sha1(name + ccn).digest()
-    aes = AES2(ccn_sha, name_sha)
-    crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c))
-    userkey = hashlib.sha1(crypt).digest()
+    return _aes
 
-    return userkey.encode('base64')
-
-class ZipInfo(zipfile.ZipInfo):
+class ZipInfo(_ZipInfo):
     def __init__(self, *args, **kwargs):
         if 'compress_type' in kwargs:
             compress_type = kwargs.pop('compress_type')
@@ -241,8 +195,8 @@ def plugin_main(userkey, inpath, outpath):
     
     with closing(ZipFile(open(inpath, 'rb'))) as inf:
         namelist = set(inf.namelist())
-        if 'META-INF/rights.xml' not in namelist or \
-           'META-INF/encryption.xml' not in namelist:
+        if 'META-INF/rights.xml' not in namelist or 'META-INF/encryption.xml' not in namelist:
+            print '%s Plugin: Not Encrypted.' % PLUGIN_NAME
             return 1
         for name in META_NAMES:
             namelist.remove(name)
@@ -267,116 +221,116 @@ def plugin_main(userkey, inpath, outpath):
     return 0
 
 from calibre.customize import FileTypePlugin
-from calibre.constants import iswindows, isosx
+from calibre.gui2 import is_ok_to_use_qt
 
 class IgnobleDeDRM(FileTypePlugin):
-    name                    = 'Ignoble Epub DeDRM'
-    description             = 'Removes DRM from secure Barnes & Noble epub files. \
-                                Credit given to I <3 Cabbages for the original stand-alone scripts.'
+    name                    = PLUGIN_NAME
+    description             = 'Removes DRM from secure Barnes & Noble epub files. Credit given to I <3 Cabbages for the original stand-alone scripts.'
     supported_platforms     = ['linux', 'osx', 'windows']
     author                  = 'DiapDealer'
-    version                 = (0, 1, 6)
+    version                 = PLUGIN_VERSION_TUPLE
     minimum_calibre_version = (0, 7, 55)  # Compiled python libraries cannot be imported in earlier versions.
     file_types              = set(['epub'])
     on_import               = True
-
+    
     def run(self, path_to_ebook):
+        from calibre_plugins.ignoble_epub import outputfix
+         
+        if sys.stdout.encoding == None:
+            sys.stdout = outputfix.getwriter('utf-8')(sys.stdout)
+        else:
+            sys.stdout = outputfix.getwriter(sys.stdout.encoding)(sys.stdout)
+        if sys.stderr.encoding == None:
+            sys.stderr = outputfix.getwriter('utf-8')(sys.stderr)
+        else:
+            sys.stderr = outputfix.getwriter(sys.stderr.encoding)(sys.stderr)
+            
         global AES
-        global AES2
-        
-        AES, AES2 = _load_crypto()
-        
-        if AES == None or AES2 == None:
+
+        print '\n\nRunning {0} v{1} on "{2}"'.format(PLUGIN_NAME, PLUGIN_VERSION, os.path.basename(path_to_ebook))
+        AES = _load_crypto()
+        if AES == None:
             # Failed to load libcrypto or PyCrypto... Adobe Epubs can't be decrypted.'
-            raise IGNOBLEError('IgnobleEpub - Failed to load crypto libs.')
-            return
+            raise Exception('%s Plugin v%s: Failed to load crypto libs.' % (PLUGIN_NAME, PLUGIN_VERSION))
 
-        # Load any keyfiles (*.b64) included Calibre's config directory.
-        userkeys = []
+        # First time use or first time after upgrade to new key-handling/storage method
+        # or no keys configured. Give a visual prompt to configure.
+        import calibre_plugins.ignoble_epub.config as cfg
+        if not cfg.prefs['configured']:
+            titlemsg = '%s v%s' % (PLUGIN_NAME, PLUGIN_VERSION)
+            errmsg = 'Plugin not configured! Decryption unsuccessful.\n' + \
+                    '\nThis may be the first time you\'ve used this plugin\n' + \
+                    '(or the first time since upgrading this plugin).\n' + \
+                    '\nYou\'ll need to open the customization dialog (Preferences->Plugins->File type plugins).'
+            if is_ok_to_use_qt():
+                from PyQt4.Qt import QMessageBox
+                d = QMessageBox(QMessageBox.Warning, titlemsg, errmsg )
+                d.show()
+                d.raise_()
+                d.exec_()
+            raise Exception('%s Plugin v%s: Plugin not configured.' % (PLUGIN_NAME, PLUGIN_VERSION))
+
+        # Check original epub archive for zip errors.
+        from calibre_plugins.ignoble_epub import zipfix
+        inf = self.temporary_file('.epub')
         try:
-            # Find Calibre's configuration directory.
-            confpath = os.path.split(os.path.split(self.plugin_path)[0])[0]
-            print 'IgnobleEpub: Calibre configuration directory = %s' % confpath
-            files = os.listdir(confpath)
-            filefilter = re.compile("\.b64$", re.IGNORECASE)
-            files = filter(filefilter.search, files)
-
-            if files:
-                for filename in files:
-                    fpath = os.path.join(confpath, filename)
-                    with open(fpath, 'rb') as f:
-                        userkeys.append(f.read())
-                    print 'IgnobleEpub: Keyfile %s found in config folder.' % filename
-            else:
-                print 'IgnobleEpub: No keyfiles found. Checking plugin customization string.'
-        except IOError:
-            print 'IgnobleEpub: Error reading keyfiles from config directory.'
-            pass
-        
-        # Get name and credit card number from Plugin Customization
-        if not userkeys and not self.site_customization:
-            # Plugin hasn't been configured... do nothing.
-            raise IGNOBLEError('IgnobleEpub - No keys found. Plugin not configured.')
-            return
-        
-        if self.site_customization:
-            keystuff = self.site_customization
-            ar = keystuff.split(':')
-            keycount = 0
-            for i in ar:
-                try:
-                    name, ccn = i.split(',')
-                    keycount += 1
-                except ValueError:
-                    raise IGNOBLEError('IgnobleEpub - Error parsing user supplied data.')
-                    return
-        
-                # Generate Barnes & Noble EPUB user key from name and credit card number.
-                userkeys.append( generate_keyfile(name, ccn) )
-            print 'IgnobleEpub: %d userkey(s) generated from customization data.' % keycount
+            print '%s Plugin: Verifying zip archive integrity.' % PLUGIN_NAME
+            fr = zipfix.fixZip(path_to_ebook, inf.name)
+            fr.fix()
+        except Exception, e:
+            print '%s Plugin: unforeseen zip archive issue.' % PLUGIN_NAME
+            raise Exception(e)
+        # Create a TemporaryPersistent file to work with.
+        of = self.temporary_file('.epub')
         
         # Attempt to decrypt epub with each encryption key (generated or provided).
-        for userkey in userkeys:
-            # Create a TemporaryPersistent file to work with.
-            # Check original epub archive for zip errors.
-            from calibre_plugins.ignobleepub import zipfix
-            inf = self.temporary_file('.epub')
-            try:
-                fr = zipfix.fixZip(path_to_ebook, inf.name)
-                fr.fix()
-            except Exception, e:
-                raise Exception(e)
-                return
-            of = self.temporary_file('.epub')
-        
+        key_counter = 1
+        for keyname, userkey in cfg.prefs['keys'].items():
+            keyname_masked = keyname[:4] + ''.join('x' for x in keyname[4:]) 
             # Give the user key, ebook and TemporaryPersistent file to the Stripper function.
             result = plugin_main(userkey, inf.name, of.name)
-        
+
             # Ebook is not a B&N Adept epub... do nothing and pass it on.
             # This allows a non-encrypted epub to be imported without error messages.
             if  result == 1:
-                print 'IgnobleEpub: Not a B&N Adept Epub... punting.'
+                print '%s Plugin: Not a B&N Epub - doing nothing.\n' % PLUGIN_NAME
                 of.close()
                 return path_to_ebook
                 break
-        
+
             # Decryption was successful return the modified PersistentTemporary
             # file to Calibre's import process.
             if  result == 0:
-                print 'IgnobleEpub: Encryption successfully removed.'
+                print '{0} Plugin: Encryption key {1} ("{2}") correct!'.format(PLUGIN_NAME, key_counter, keyname_masked)
                 of.close()
                 return of.name
                 break
-            
-            print 'IgnobleEpub: Encryption key invalid... trying others.'
-            of.close()
-        
+
+            print '{0} Plugin: Encryption key {1} ("{2}") incorrect!'.format(PLUGIN_NAME, key_counter, keyname_masked)
+            key_counter += 1
+
         # Something went wrong with decryption.
         # Import the original unmolested epub.
         of.close
-        raise IGNOBLEError('IgnobleEpub - Ultimately failed to decrypt.')
-        return
-        
-        
-    def customization_help(self, gui=False):
-        return 'Enter B&N Account name and CC# (separate name and CC# with a comma)'
+        raise Exception('%s Plugin v%s: Ultimately failed to decrypt.\n' % (PLUGIN_NAME, PLUGIN_VERSION))
+
+    def is_customizable(self):
+        # return true to allow customization via the Plugin->Preferences.
+        return True
+
+    def config_widget(self):
+        from calibre_plugins.ignoble_epub.config import ConfigWidget
+        # Extract the helpfile contents from in the plugin's zipfile.
+        # The helpfile must be named <plugin name variable> + '_Help.htm'
+        return ConfigWidget(self.load_resources(RESOURCE_NAME)[RESOURCE_NAME])
+
+    def load_resources(self, names):
+        ans = {}
+        with ZipFile(self.plugin_path, 'r') as zf:
+            for candidate in zf.namelist():
+                if candidate in names:
+                    ans[candidate] = zf.read(candidate)
+        return ans
+
+    def save_settings(self, config_widget):
+        config_widget.save_settings()
diff --git a/Calibre_Plugins/ignobleepub_plugin/config.py b/Calibre_Plugins/ignobleepub_plugin/config.py
new file mode 100644 (file)
index 0000000..467ea6b
--- /dev/null
@@ -0,0 +1,274 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+__license__ = 'GPL v3'
+
+# Standard Python modules.
+import os, sys, re, hashlib
+
+# PyQT4 modules (part of calibre).
+from PyQt4.Qt import (Qt, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
+                      QGroupBox, QPushButton, QListWidget, QListWidgetItem,
+                      QAbstractItemView, QIcon, QDialog, QUrl, QString)
+from PyQt4 import QtGui
+
+# calibre modules and constants.
+from calibre.gui2 import (error_dialog, question_dialog, info_dialog, open_url,
+                            choose_dir, choose_files)
+from calibre.utils.config import dynamic, config_dir, JSONConfig
+
+# modules from this plugin's zipfile.
+from calibre_plugins.ignoble_epub.__init__ import PLUGIN_NAME, PLUGIN_VERSION 
+from calibre_plugins.ignoble_epub.__init__ import RESOURCE_NAME as help_file_name
+from calibre_plugins.ignoble_epub.utilities import (_load_crypto, normalize_name,
+                                generate_keyfile, caselessStrCmp, AddKeyDialog,
+                                DETAILED_MESSAGE, parseCustString)
+
+JSON_NAME = PLUGIN_NAME.strip().lower().replace(' ', '_')
+JSON_PATH = 'plugins/' + JSON_NAME + '.json'
+
+# This is where all preferences for this plugin will be stored
+# You should always prefix your config file name with plugins/,
+# so as to ensure you dont accidentally clobber a calibre config file
+prefs = JSONConfig(JSON_PATH)
+
+# Set defaults
+prefs.defaults['keys'] = {}
+prefs.defaults['configured'] = False
+
+class ConfigWidget(QWidget):
+    def __init__(self, help_file_data):
+        QWidget.__init__(self)
+        
+        self.help_file_data = help_file_data
+        self.plugin_keys = prefs['keys']
+
+        # Handle the old plugin's customization string by either converting the
+        # old string to stored keys or by saving the string to a text file of the
+        # user's choice. Either way... get that personal data out of plain sight.
+        from calibre.customize.ui import config
+        sc = config['plugin_customization']
+        val = sc.get(PLUGIN_NAME, None)
+        if val is not None:
+            title = 'Convert existing customization data?'
+            msg = '<p>Convert your existing insecure customization data? (Please '+ \
+                        'read the detailed message)'
+            det_msg = DETAILED_MESSAGE
+
+            # Offer to convert the old string to the new format
+            if question_dialog(self, _(title), _(msg), det_msg, True, True):
+                userkeys = parseCustString(str(val))
+                if userkeys:
+                    counter = 0
+                    # Yay! We found valid customization data... add it to the new plugin
+                    for k in userkeys:
+                        counter += 1
+                        self.plugin_keys['Converted Old Plugin Key - ' + str(counter)] = k
+                    msg = '<p><b>' + str(counter) + '</b> User key(s) configured from old plugin customization string'
+                    inf = info_dialog(None, _(PLUGIN_NAME + 'info_dlg'), _(msg), show=True)
+                    val = sc.pop(PLUGIN_NAME, None)
+                    if val is not None:
+                        config['plugin_customization'] = sc
+                else:
+                    # The existing customization string was invalid and wouldn't have
+                    # worked anyway. Offer to save it as a text file and get rid of it.
+                    errmsg = '<p>Unknown Error converting user supplied-customization string'
+                    r = error_dialog(None, PLUGIN_NAME,
+                                    _(errmsg), show=True, show_copy_button=False)
+                    self.saveOldCustomizationData(str(val))
+                    val = sc.pop(PLUGIN_NAME, None)
+                    if val is not None:
+                        config['plugin_customization'] = sc
+            # If they don't want to convert the old string to keys then
+            # offer to save the old string to a text file and delete the
+            # the old customization string.
+            else:
+                self.saveOldCustomizationData(str(val))
+                val = sc.pop(PLUGIN_NAME, None)
+                if val is not None:
+                    config['plugin_customization'] = sc
+                    
+        # First time run since upgrading to new key storage method, or 0 keys configured.
+        # Prompt to import pre-existing key files.
+        if not prefs['configured']:
+            title = 'Import existing key files?'
+            msg = '<p>This plugin no longer uses *.b64 keyfiles stored in calibre\'s configuration '+ \
+                        'directory. Do you have any exsiting key files there (or anywhere) that you\'d '+ \
+                        'like to migrate into the new plugin preferences method?'
+            if question_dialog(self, _(title), _(msg)):
+                self.migrate_files()
+
+        # Start Qt Gui dialog layout
+        layout = QVBoxLayout(self)
+        self.setLayout(layout)
+        
+        help_layout = QHBoxLayout()
+        layout.addLayout(help_layout)
+        # Add hyperlink to a help file at the right. We will replace the correct name when it is clicked.
+        help_label = QLabel('<a href="http://www.foo.com/">Plugin Help</a>', self)
+        help_label.setTextInteractionFlags(Qt.LinksAccessibleByMouse | Qt.LinksAccessibleByKeyboard)
+        help_label.setAlignment(Qt.AlignRight)
+        help_label.linkActivated.connect(self.help_link_activated)
+        help_layout.addWidget(help_label)
+        
+        keys_group_box = QGroupBox(_('Configured Ignoble Keys:'), self)
+        layout.addWidget(keys_group_box)
+        keys_group_box_layout = QHBoxLayout()
+        keys_group_box.setLayout(keys_group_box_layout)
+        
+        self.listy = QListWidget(self)
+        self.listy.setToolTip(_('<p>Stored Ignoble keys that will be used for decryption'))
+        self.listy.setSelectionMode(QAbstractItemView.SingleSelection)
+        self.populate_list()
+        keys_group_box_layout.addWidget(self.listy)
+
+        button_layout = QVBoxLayout()
+        keys_group_box_layout.addLayout(button_layout)
+        self._add_key_button = QtGui.QToolButton(self)
+        self._add_key_button.setToolTip(_('Create new key'))
+        self._add_key_button.setIcon(QIcon(I('plus.png')))
+        self._add_key_button.clicked.connect(self.add_key)
+        button_layout.addWidget(self._add_key_button)
+        
+        self._delete_key_button = QtGui.QToolButton(self)
+        self._delete_key_button.setToolTip(_('Delete highlighted key'))
+        self._delete_key_button.setIcon(QIcon(I('list_remove.png')))
+        self._delete_key_button.clicked.connect(self.delete_key)
+        button_layout.addWidget(self._delete_key_button)
+        
+        self.export_key_button = QtGui.QToolButton(self)
+        self.export_key_button.setToolTip(_('Export highlighted key'))
+        self.export_key_button.setIcon(QIcon(I('save.png')))
+        self.export_key_button.clicked.connect(self.export_key)
+        button_layout.addWidget(self.export_key_button)
+        spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding)
+        button_layout.addItem(spacerItem)
+        
+        layout.addSpacing(20)
+        migrate_layout = QHBoxLayout()
+        layout.addLayout(migrate_layout)
+        self.migrate_btn = QPushButton(_('Import Existing Keyfiles'), self)
+        self.migrate_btn.setToolTip(_('<p>Import *.b64 keyfiles (used by older versions of the plugin).'))
+        self.migrate_btn.clicked.connect(self.migrate_wrapper)
+        migrate_layout.setAlignment(Qt.AlignLeft)
+        migrate_layout.addWidget(self.migrate_btn)
+        
+        self.resize(self.sizeHint())
+
+    def populate_list(self):
+        for key in self.plugin_keys.keys():
+            self.listy.addItem(QListWidgetItem(key))
+
+    def add_key(self):
+        d = AddKeyDialog(self)
+        d.exec_()
+
+        if d.result() != d.Accepted:
+            # New key generation cancelled.
+            return
+        self.plugin_keys[d.key_name] = generate_keyfile(d.user_name, d.cc_number)
+
+        self.listy.clear()
+        self.populate_list()
+
+    def delete_key(self):
+        if not self.listy.currentItem():
+            return
+        keyname = unicode(self.listy.currentItem().text())
+        if not question_dialog(self, _('Are you sure?'), _('<p>'+
+                    'Do you really want to delete the Ignoble key named <strong>%s</strong>?') % keyname,
+                    show_copy_button=False, default_yes=False):
+            return
+        del self.plugin_keys[keyname]
+        
+        self.listy.clear()
+        self.populate_list()
+  
+    def help_link_activated(self, url):
+        def get_help_file_resource():
+            # Copy the HTML helpfile to the plugin directory each time the
+            # link is clicked in case the helpfile is updated in newer plugins.
+            file_path = os.path.join(config_dir, 'plugins', help_file_name)
+            with open(file_path,'w') as f:
+                f.write(self.help_file_data)
+            return file_path
+        url = 'file:///' + get_help_file_resource()
+        open_url(QUrl(url))
+        
+    def save_settings(self):
+        prefs['keys'] = self.plugin_keys
+        if prefs['keys']:
+            prefs['configured'] = True
+        else:
+            prefs['configured'] = False
+
+    def migrate_files(self):
+        dynamic[PLUGIN_NAME + 'config_dir'] = config_dir
+        files = choose_files(self, PLUGIN_NAME + 'config_dir',
+                _('Select Ignoble keyfiles to import'), [('Ignoble Keyfiles', ['b64'])], False)
+        if files:
+            counter = 0
+            skipped = 0
+            for filename in files:
+                fpath = os.path.join(config_dir, filename)
+                new_key_name = os.path.splitext(os.path.basename(filename))[0]
+                match = False
+                for key in self.plugin_keys.keys():
+                    if caselessStrCmp(new_key_name, key) == 0:
+                        match = True
+                        break
+                if not match:
+                    with open(fpath, 'rb') as f:
+                        counter += 1
+                        self.plugin_keys[unicode(new_key_name)] = f.read()
+                else:
+                    skipped += 1
+                    msg = '<p>A key with the name <strong>' + new_key_name + '</strong> already exists! </p>' + \
+                           '<p>Skipping key file named <strong>' + filename + '</strong>.</p>' + \
+                           '<p>Either delete the existing key and re-migrate, or ' + \
+                           'create that key manually with a different name.'
+                    inf = info_dialog(None, _(PLUGIN_NAME + 'info_dlg'),
+                                _(msg), show=True)
+
+            msg = '<p>Done migrating <strong>' + str(counter) + '</strong> ' + \
+                                'key files...</p><p>Skipped <strong>' + str(skipped) + '</strong> key files.'
+            inf = info_dialog(None, _(PLUGIN_NAME + 'info_dlg'),
+                                    _(msg), show=True)
+            return 1
+        return 0
+
+    def migrate_wrapper(self):
+        if self.migrate_files():
+            self.listy.clear()
+            self.populate_list()
+
+    def export_key(self):
+        if not self.listy.currentItem():
+            errmsg = '<p>No keyfile selected to export. Highlight a keyfile first.'
+            r = error_dialog(None, PLUGIN_NAME,
+                                    _(errmsg), show=True, show_copy_button=False)
+            return
+        filter = QString('Ignoble Key Files (*.b64)')
+        keyname = unicode(self.listy.currentItem().text())
+        if dynamic.get(PLUGIN_NAME + 'save_dir'):
+            defaultname = os.path.join(dynamic.get(PLUGIN_NAME + 'save_dir'), keyname + '.b64')
+        else:
+            defaultname = os.path.join(os.path.expanduser('~'), keyname + '.b64')
+        filename = str(QtGui.QFileDialog.getSaveFileName(self, "Save Ignoble Key File as...", defaultname,
+                                            "Ignoble Key Files (*.b64)", filter))
+        if filename:
+            dynamic[PLUGIN_NAME + 'save_dir'] = os.path.split(filename)[0]
+            fname = open(filename, 'w')
+            fname.write(self.plugin_keys[keyname])
+            fname.close()
+
+    def saveOldCustomizationData(self, strdata):
+        filter = QString('Text files (*.txt)')
+        default_basefilename = PLUGIN_NAME + ' old customization data.txt'
+        defaultname = os.path.join(os.path.expanduser('~'), default_basefilename)
+        filename = str(QtGui.QFileDialog.getSaveFileName(self, "Save old plugin style customization data as...", defaultname,
+                                    "Text Files (*.txt)", filter))
+        if filename:
+            fname = open(filename, 'w')
+            fname.write(strdata)
+            fname.close()
\ No newline at end of file
diff --git a/Calibre_Plugins/ignobleepub_plugin/outputfix.py b/Calibre_Plugins/ignobleepub_plugin/outputfix.py
new file mode 100644 (file)
index 0000000..906c6e9
--- /dev/null
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+#
+# Adapted and simplified from the kitchen project
+#
+# Kitchen Project Copyright (c) 2012 Red Hat, Inc.
+#
+# kitchen 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.
+#
+# kitchen 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 kitchen; if not, see <http://www.gnu.org/licenses/>
+#
+# Authors:
+#   Toshio Kuratomi <toshio@fedoraproject.org>
+#   Seth Vidal
+#
+# Portions of code taken from yum/i18n.py and
+# python-fedora: fedora/textutils.py
+
+import codecs
+
+# returns a char string unchanged
+# returns a unicode string converted to a char string of the passed encoding
+# return the empty string for anything else
+def getwriter(encoding):
+    class _StreamWriter(codecs.StreamWriter):
+        def __init__(self, stream):
+            codecs.StreamWriter.__init__(self, stream, 'replace')
+
+        def encode(self, msg, errors='replace'):
+            if isinstance(msg, basestring):
+                if isinstance(msg, str):
+                    return (msg, len(msg))
+                return (msg.encode(self.encoding, 'replace'), len(msg))
+            return ('',0)
+
+    _StreamWriter.encoding = encoding
+    return _StreamWriter
diff --git a/Calibre_Plugins/ignobleepub_plugin/plugin-import-name-ignoble_epub.txt b/Calibre_Plugins/ignobleepub_plugin/plugin-import-name-ignoble_epub.txt
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/Calibre_Plugins/ignobleepub_plugin/utilities.py b/Calibre_Plugins/ignobleepub_plugin/utilities.py
new file mode 100644 (file)
index 0000000..9194987
--- /dev/null
@@ -0,0 +1,260 @@
+#!/usr/bin/env python
+
+from __future__ import with_statement
+__license__ = 'GPL v3'
+
+import hashlib
+
+from ctypes import CDLL, POINTER, c_void_p, c_char_p, c_int, c_long, \
+                                               Structure, c_ulong, create_string_buffer, cast
+from ctypes.util import find_library
+
+from PyQt4.Qt import (Qt, QHBoxLayout, QVBoxLayout, QLabel, QLineEdit,
+                      QGroupBox, QDialog, QDialogButtonBox)
+
+from calibre.gui2 import error_dialog
+from calibre.constants import iswindows
+
+from calibre_plugins.ignoble_epub.__init__ import PLUGIN_NAME, PLUGIN_VERSION
+
+DETAILED_MESSAGE = \
+'You have personal information stored in this plugin\'s customization '+ \
+'string from a previous version of this plugin.\n\n'+ \
+'This new version of the plugin can convert that info '+ \
+'into key data that the new plugin can then use (which doesn\'t '+ \
+'require personal information to be stored/displayed in an insecure '+ \
+'manner like the old plugin did).\n\nIf you choose NOT to migrate this data at this time '+ \
+'you will be prompted to save that personal data to a file elsewhere; and you\'ll have '+ \
+'to manually re-configure this plugin with your information.\n\nEither way... ' + \
+'this new version of the plugin will not be responsible for storing that personal '+ \
+'info in plain sight any longer.'
+
+class IGNOBLEError(Exception):
+    pass
+
+def normalize_name(name): # Strip spaces and convert to lowercase.
+    return ''.join(x for x in name.lower() if x != ' ')
+
+# These are the key ENCRYPTING aes crypto functions
+def generate_keyfile(name, ccn):
+       # Load the necessary crypto libs.
+       AES = _load_crypto()
+       name = normalize_name(name) + '\x00'
+       ccn = ccn + '\x00'
+       name_sha = hashlib.sha1(name).digest()[:16]
+       ccn_sha = hashlib.sha1(ccn).digest()[:16]
+       both_sha = hashlib.sha1(name + ccn).digest()
+       aes = AES(ccn_sha, name_sha)
+       crypt = aes.encrypt(both_sha + ('\x0c' * 0x0c))
+       userkey = hashlib.sha1(crypt).digest()
+
+       return userkey.encode('base64')
+
+def _load_crypto_libcrypto():
+    if iswindows:
+        libcrypto = find_library('libeay32')
+    else:
+        libcrypto = find_library('crypto')
+    if libcrypto is None:
+        raise IGNOBLEError('libcrypto not found')
+    libcrypto = CDLL(libcrypto)
+
+    AES_MAXNR = 14
+    
+    c_char_pp = POINTER(c_char_p)
+    c_int_p = POINTER(c_int)
+
+    class AES_KEY(Structure):
+        _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
+                    ('rounds', c_int)]
+    AES_KEY_p = POINTER(AES_KEY)
+    
+    def F(restype, name, argtypes):
+        func = getattr(libcrypto, name)
+        func.restype = restype
+        func.argtypes = argtypes
+        return func
+    
+    AES_set_encrypt_key = F(c_int, 'AES_set_encrypt_key',
+                            [c_char_p, c_int, AES_KEY_p])
+    AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
+                        [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
+                         c_int])
+    
+    class AES(object):
+        def __init__(self, userkey, iv):
+            self._blocksize = len(userkey)
+            self._iv = iv
+            key = self._key = AES_KEY()
+            rv = AES_set_encrypt_key(userkey, len(userkey) * 8, key)
+            if rv < 0:
+                raise IGNOBLEError('Failed to initialize AES Encrypt key')
+    
+        def encrypt(self, data):
+            out = create_string_buffer(len(data))
+            rv = AES_cbc_encrypt(data, out, len(data), self._key, self._iv, 1)
+            if rv == 0:
+                raise IGNOBLEError('AES encryption failed')
+            return out.raw
+    return AES
+
+def _load_crypto_pycrypto():
+    from Crypto.Cipher import AES as _AES
+
+    class AES(object):
+        def __init__(self, key, iv):
+            self._aes = _AES.new(key, _AES.MODE_CBC, iv)
+
+        def encrypt(self, data):
+            return self._aes.encrypt(data)
+    return AES
+    
+def _load_crypto():
+    _aes = None
+    cryptolist = (_load_crypto_libcrypto, _load_crypto_pycrypto)
+    if iswindows:
+        cryptolist = (_load_crypto_pycrypto, _load_crypto_libcrypto)
+    for loader in cryptolist:
+        try:
+            _aes = loader()
+            break
+        except (ImportError, IGNOBLEError):
+            pass
+    return _aes
+
+def caselessStrCmp(s1, s2):
+    """
+    A function to case-insensitively compare strings. Python's .lower() function
+    isn't always very accurate when it comes to unicode. Using the standard C lib's
+    strcasecmp instead. Maybe a tad slower, but we're not scouring scads of string lists here.
+    """
+    str1 = unicode(s1)
+    str2 = unicode(s2)
+    
+    c_char_pp = POINTER(c_char_p)
+    c_int_p = POINTER(c_int)
+    
+    if iswindows:
+        libc = find_library('msvcrt')
+    else:
+        libc = find_library('c')
+    if libc is None:
+        raise IgnobleError('libc not found')
+    libc = CDLL(libc)
+    
+    def F(restype, name, argtypes):
+        func = getattr(libc, name)
+        func.restype = restype
+        func.argtypes = argtypes
+        return func
+    
+    if iswindows:
+        _stricmp = F(c_int, '_stricmp', [c_char_p, c_char_p])
+        return _stricmp(str1, str2)
+    strcasecmp = F(c_int, 'strcasecmp', [c_char_p, c_char_p])
+    return strcasecmp(str1, str2)
+
+class AddKeyDialog(QDialog):
+       def __init__(self, parent=None,):
+               QDialog.__init__(self, parent)
+               self.parent = parent
+               self.setWindowTitle('Create New Ignoble Key')
+               layout = QVBoxLayout(self)
+               self.setLayout(layout)
+
+               data_group_box = QGroupBox('', self)
+               layout.addWidget(data_group_box)
+               data_group_box_layout = QVBoxLayout()
+               data_group_box.setLayout(data_group_box_layout)
+               
+               key_group = QHBoxLayout()
+               data_group_box_layout.addLayout(key_group)
+               key_group.addWidget(QLabel('Unique Key Name:', self))
+               self.key_ledit = QLineEdit('', self)
+               self.key_ledit.setToolTip(_('<p>Enter an identifying name for this new Ignoble key.</p>' +
+                                                               '<p>It should be something that will help you remember ' +
+                                                               'what personal information was used to create it.'))
+               key_group.addWidget(self.key_ledit)
+               key_label = QLabel(_(''), self)
+               key_label.setAlignment(Qt.AlignHCenter)
+               data_group_box_layout.addWidget(key_label)
+
+               name_group = QHBoxLayout()
+               data_group_box_layout.addLayout(name_group)
+               name_group.addWidget(QLabel('Your Name:', self))
+               self.name_ledit = QLineEdit('', self)
+               self.name_ledit.setToolTip(_('<p>Enter your name as it appears in your B&N ' +
+                                                               'account and/or on your credit card.</p>' +
+                                                               '<p>It will only be used to generate this ' +
+                                                               'one-time key and won\'t be stored anywhere ' +
+                                                               'in calibre or on your computer.</p>' +
+                                                               '<p>(ex: Jonathan Smith)'))
+               name_group.addWidget(self.name_ledit)
+               name_disclaimer_label = QLabel(_('Will not be stored/saved in configuration data:'), self)
+               name_disclaimer_label.setAlignment(Qt.AlignHCenter)
+               data_group_box_layout.addWidget(name_disclaimer_label)
+       
+               ccn_group = QHBoxLayout()
+               data_group_box_layout.addLayout(ccn_group)
+               ccn_group.addWidget(QLabel('Credit Card#:', self))
+               self.cc_ledit = QLineEdit('', self)
+               self.cc_ledit.setToolTip(_('<p>Enter the full credit card number on record ' +
+                                                               'in your B&N account.</p>' +
+                                                               '<p>No spaces or dashes... just the numbers. ' +
+                                                               'This CC# will only be used to generate this ' +
+                                                               'one-time key and won\'t be stored anywhere in ' +
+                                                               'calibre or on your computer.'))
+               ccn_group.addWidget(self.cc_ledit)
+               ccn_disclaimer_label = QLabel(_('Will not be stored/saved in configuration data:'), self)
+               ccn_disclaimer_label.setAlignment(Qt.AlignHCenter)
+               data_group_box_layout.addWidget(ccn_disclaimer_label)
+               layout.addSpacing(20)
+
+               self.button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
+               self.button_box.accepted.connect(self.accept)
+               self.button_box.rejected.connect(self.reject)
+               layout.addWidget(self.button_box)
+
+               self.resize(self.parent.sizeHint())
+
+       def accept(self):
+               match = False
+               if (self.key_ledit.text().isEmpty() or self.name_ledit.text().isEmpty()
+                                                               or self.cc_ledit.text().isEmpty()):
+                       errmsg = '<p>All fields are required!'
+                       return error_dialog(None, PLUGIN_NAME + 'error_dialog',
+                                                                       _(errmsg), show=True, show_copy_button=False)
+               for k in self.parent.plugin_keys.keys():
+                       if caselessStrCmp(self.key_ledit.text(), k) == 0:
+                               match = True
+                               break
+               if match:
+                       errmsg = '<p>The key name <strong>%s</strong> is already being used.' % self.key_ledit.text()
+                       return error_dialog(None, PLUGIN_NAME + 'error_dialog',
+                                                                       _(errmsg), show=True, show_copy_button=False)
+               else:
+                       QDialog.accept(self)
+
+       @property
+       def user_name(self):
+               return unicode(self.name_ledit.text()).strip().lower().replace(' ', '')
+               
+       @property
+       def cc_number(self):
+               return unicode(self.cc_ledit.text()).strip().replace(' ', '').replace('-','')
+               
+       @property
+       def key_name(self):
+               return unicode(self.key_ledit.text())
+
+def parseCustString(keystuff):
+       userkeys = []
+       ar = keystuff.split(':')
+       for i in ar:
+               try:
+                       name, ccn = i.split(',')
+               except:
+                       return False
+               # Generate Barnes & Noble EPUB user key from name and credit card number.
+               userkeys.append(generate_keyfile(name, ccn))
+       return userkeys
\ No newline at end of file
diff --git a/Calibre_Plugins/ignobleepub_plugin/zipfilerugged.py b/Calibre_Plugins/ignobleepub_plugin/zipfilerugged.py
new file mode 100644 (file)
index 0000000..adf3c53
--- /dev/null
@@ -0,0 +1,1400 @@
+"""
+Read and write ZIP files.
+"""
+import struct, os, time, sys, shutil
+import binascii, cStringIO, stat
+import io
+import re
+
+try:
+    import zlib # We may need its compression method
+    crc32 = zlib.crc32
+except ImportError:
+    zlib = None
+    crc32 = binascii.crc32
+
+__all__ = ["BadZipfile", "error", "ZIP_STORED", "ZIP_DEFLATED", "is_zipfile",
+           "ZipInfo", "ZipFile", "PyZipFile", "LargeZipFile" ]
+
+class BadZipfile(Exception):
+    pass
+
+
+class LargeZipFile(Exception):
+    """
+    Raised when writing a zipfile, the zipfile requires ZIP64 extensions
+    and those extensions are disabled.
+    """
+
+error = BadZipfile      # The exception raised by this module
+
+ZIP64_LIMIT = (1 << 31) - 1
+ZIP_FILECOUNT_LIMIT = 1 << 16
+ZIP_MAX_COMMENT = (1 << 16) - 1
+
+# constants for Zip file compression methods
+ZIP_STORED = 0
+ZIP_DEFLATED = 8
+# Other ZIP compression methods not supported
+
+# Below are some formats and associated data for reading/writing headers using
+# the struct module.  The names and structures of headers/records are those used
+# in the PKWARE description of the ZIP file format:
+#     http://www.pkware.com/documents/casestudies/APPNOTE.TXT
+# (URL valid as of January 2008)
+
+# The "end of central directory" structure, magic number, size, and indices
+# (section V.I in the format document)
+structEndArchive = "<4s4H2LH"
+stringEndArchive = "PK\005\006"
+sizeEndCentDir = struct.calcsize(structEndArchive)
+
+_ECD_SIGNATURE = 0
+_ECD_DISK_NUMBER = 1
+_ECD_DISK_START = 2
+_ECD_ENTRIES_THIS_DISK = 3
+_ECD_ENTRIES_TOTAL = 4
+_ECD_SIZE = 5
+_ECD_OFFSET = 6
+_ECD_COMMENT_SIZE = 7
+# These last two indices are not part of the structure as defined in the
+# spec, but they are used internally by this module as a convenience
+_ECD_COMMENT = 8
+_ECD_LOCATION = 9
+
+# The "central directory" structure, magic number, size, and indices
+# of entries in the structure (section V.F in the format document)
+structCentralDir = "<4s4B4HL2L5H2L"
+stringCentralDir = "PK\001\002"
+sizeCentralDir = struct.calcsize(structCentralDir)
+
+# indexes of entries in the central directory structure
+_CD_SIGNATURE = 0
+_CD_CREATE_VERSION = 1
+_CD_CREATE_SYSTEM = 2
+_CD_EXTRACT_VERSION = 3
+_CD_EXTRACT_SYSTEM = 4
+_CD_FLAG_BITS = 5
+_CD_COMPRESS_TYPE = 6
+_CD_TIME = 7
+_CD_DATE = 8
+_CD_CRC = 9
+_CD_COMPRESSED_SIZE = 10
+_CD_UNCOMPRESSED_SIZE = 11
+_CD_FILENAME_LENGTH = 12
+_CD_EXTRA_FIELD_LENGTH = 13
+_CD_COMMENT_LENGTH = 14
+_CD_DISK_NUMBER_START = 15
+_CD_INTERNAL_FILE_ATTRIBUTES = 16
+_CD_EXTERNAL_FILE_ATTRIBUTES = 17
+_CD_LOCAL_HEADER_OFFSET = 18
+
+# The "local file header" structure, magic number, size, and indices
+# (section V.A in the format document)
+structFileHeader = "<4s2B4HL2L2H"
+stringFileHeader = "PK\003\004"
+sizeFileHeader = struct.calcsize(structFileHeader)
+
+_FH_SIGNATURE = 0
+_FH_EXTRACT_VERSION = 1
+_FH_EXTRACT_SYSTEM = 2
+_FH_GENERAL_PURPOSE_FLAG_BITS = 3
+_FH_COMPRESSION_METHOD = 4
+_FH_LAST_MOD_TIME = 5
+_FH_LAST_MOD_DATE = 6
+_FH_CRC = 7
+_FH_COMPRESSED_SIZE = 8
+_FH_UNCOMPRESSED_SIZE = 9
+_FH_FILENAME_LENGTH = 10
+_FH_EXTRA_FIELD_LENGTH = 11
+
+# The "Zip64 end of central directory locator" structure, magic number, and size
+structEndArchive64Locator = "<4sLQL"
+stringEndArchive64Locator = "PK\x06\x07"
+sizeEndCentDir64Locator = struct.calcsize(structEndArchive64Locator)
+
+# The "Zip64 end of central directory" record, magic number, size, and indices
+# (section V.G in the format document)
+structEndArchive64 = "<4sQ2H2L4Q"
+stringEndArchive64 = "PK\x06\x06"
+sizeEndCentDir64 = struct.calcsize(structEndArchive64)
+
+_CD64_SIGNATURE = 0
+_CD64_DIRECTORY_RECSIZE = 1
+_CD64_CREATE_VERSION = 2
+_CD64_EXTRACT_VERSION = 3
+_CD64_DISK_NUMBER = 4
+_CD64_DISK_NUMBER_START = 5
+_CD64_NUMBER_ENTRIES_THIS_DISK = 6
+_CD64_NUMBER_ENTRIES_TOTAL = 7
+_CD64_DIRECTORY_SIZE = 8
+_CD64_OFFSET_START_CENTDIR = 9
+
+def _check_zipfile(fp):
+    try:
+        if _EndRecData(fp):
+            return True         # file has correct magic number
+    except IOError:
+        pass
+    return False
+
+def is_zipfile(filename):
+    """Quickly see if a file is a ZIP file by checking the magic number.
+
+    The filename argument may be a file or file-like object too.
+    """
+    result = False
+    try:
+        if hasattr(filename, "read"):
+            result = _check_zipfile(fp=filename)
+        else:
+            with open(filename, "rb") as fp:
+                result = _check_zipfile(fp)
+    except IOError:
+        pass
+    return result
+
+def _EndRecData64(fpin, offset, endrec):
+    """
+    Read the ZIP64 end-of-archive records and use that to update endrec
+    """
+    fpin.seek(offset - sizeEndCentDir64Locator, 2)
+    data = fpin.read(sizeEndCentDir64Locator)
+    sig, diskno, reloff, disks = struct.unpack(structEndArchive64Locator, data)
+    if sig != stringEndArchive64Locator:
+        return endrec
+
+    if diskno != 0 or disks != 1:
+        raise BadZipfile("zipfiles that span multiple disks are not supported")
+
+    # Assume no 'zip64 extensible data'
+    fpin.seek(offset - sizeEndCentDir64Locator - sizeEndCentDir64, 2)
+    data = fpin.read(sizeEndCentDir64)
+    sig, sz, create_version, read_version, disk_num, disk_dir, \
+            dircount, dircount2, dirsize, diroffset = \
+            struct.unpack(structEndArchive64, data)
+    if sig != stringEndArchive64:
+        return endrec
+
+    # Update the original endrec using data from the ZIP64 record
+    endrec[_ECD_SIGNATURE] = sig
+    endrec[_ECD_DISK_NUMBER] = disk_num
+    endrec[_ECD_DISK_START] = disk_dir
+    endrec[_ECD_ENTRIES_THIS_DISK] = dircount
+    endrec[_ECD_ENTRIES_TOTAL] = dircount2
+    endrec[_ECD_SIZE] = dirsize
+    endrec[_ECD_OFFSET] = diroffset
+    return endrec
+
+
+def _EndRecData(fpin):
+    """Return data from the "End of Central Directory" record, or None.
+
+    The data is a list of the nine items in the ZIP "End of central dir"
+    record followed by a tenth item, the file seek offset of this record."""
+
+    # Determine file size
+    fpin.seek(0, 2)
+    filesize = fpin.tell()
+
+    # Check to see if this is ZIP file with no archive comment (the
+    # "end of central directory" structure should be the last item in the
+    # file if this is the case).
+    try:
+        fpin.seek(-sizeEndCentDir, 2)
+    except IOError:
+        return None
+    data = fpin.read()
+    if data[0:4] == stringEndArchive and data[-2:] == "\000\000":
+        # the signature is correct and there's no comment, unpack structure
+        endrec = struct.unpack(structEndArchive, data)
+        endrec=list(endrec)
+
+        # Append a blank comment and record start offset
+        endrec.append("")
+        endrec.append(filesize - sizeEndCentDir)
+
+        # Try to read the "Zip64 end of central directory" structure
+        return _EndRecData64(fpin, -sizeEndCentDir, endrec)
+
+    # Either this is not a ZIP file, or it is a ZIP file with an archive
+    # comment.  Search the end of the file for the "end of central directory"
+    # record signature. The comment is the last item in the ZIP file and may be
+    # up to 64K long.  It is assumed that the "end of central directory" magic
+    # number does not appear in the comment.
+    maxCommentStart = max(filesize - (1 << 16) - sizeEndCentDir, 0)
+    fpin.seek(maxCommentStart, 0)
+    data = fpin.read()
+    start = data.rfind(stringEndArchive)
+    if start >= 0:
+        # found the magic number; attempt to unpack and interpret
+        recData = data[start:start+sizeEndCentDir]
+        endrec = list(struct.unpack(structEndArchive, recData))
+        comment = data[start+sizeEndCentDir:]
+        # check that comment length is correct
+        if endrec[_ECD_COMMENT_SIZE] == len(comment):
+            # Append the archive comment and start offset
+            endrec.append(comment)
+            endrec.append(maxCommentStart + start)
+
+            # Try to read the "Zip64 end of central directory" structure
+            return _EndRecData64(fpin, maxCommentStart + start - filesize,
+                                 endrec)
+
+    # Unable to find a valid end of central directory structure
+    return
+
+
+class ZipInfo (object):
+    """Class with attributes describing each file in the ZIP archive."""
+
+    __slots__ = (
+            'orig_filename',
+            'filename',
+            'date_time',
+            'compress_type',
+            'comment',
+            'extra',
+            'create_system',
+            'create_version',
+            'extract_version',
+            'reserved',
+            'flag_bits',
+            'volume',
+            'internal_attr',
+            'external_attr',
+            'header_offset',
+            'CRC',
+            'compress_size',
+            'file_size',
+            '_raw_time',
+        )
+
+    def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)):
+        self.orig_filename = filename   # Original file name in archive
+
+        # Terminate the file name at the first null byte.  Null bytes in file
+        # names are used as tricks by viruses in archives.
+        null_byte = filename.find(chr(0))
+        if null_byte >= 0:
+            filename = filename[0:null_byte]
+        # This is used to ensure paths in generated ZIP files always use
+        # forward slashes as the directory separator, as required by the
+        # ZIP format specification.
+        if os.sep != "/" and os.sep in filename:
+            filename = filename.replace(os.sep, "/")
+
+        self.filename = filename        # Normalized file name
+        self.date_time = date_time      # year, month, day, hour, min, sec
+        # Standard values:
+        self.compress_type = ZIP_STORED # Type of compression for the file
+        self.comment = ""               # Comment for each file
+        self.extra = ""                 # ZIP extra data
+        if sys.platform == 'win32':
+            self.create_system = 0          # System which created ZIP archive
+        else:
+            # Assume everything else is unix-y
+            self.create_system = 3          # System which created ZIP archive
+        self.create_version = 20        # Version which created ZIP archive
+        self.extract_version = 20       # Version needed to extract archive
+        self.reserved = 0               # Must be zero
+        self.flag_bits = 0              # ZIP flag bits
+        self.volume = 0                 # Volume number of file header
+        self.internal_attr = 0          # Internal attributes
+        self.external_attr = 0          # External file attributes
+        # Other attributes are set by class ZipFile:
+        # header_offset         Byte offset to the file header
+        # CRC                   CRC-32 of the uncompressed file
+        # compress_size         Size of the compressed file
+        # file_size             Size of the uncompressed file
+
+    def FileHeader(self):
+        """Return the per-file header as a string."""
+        dt = self.date_time
+        dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2]
+        dostime = dt[3] << 11 | dt[4] << 5 | (dt[5] // 2)
+        if self.flag_bits & 0x08:
+            # Set these to zero because we write them after the file data
+            CRC = compress_size = file_size = 0
+        else:
+            CRC = self.CRC
+            compress_size = self.compress_size
+            file_size = self.file_size
+
+        extra = self.extra
+
+        if file_size > ZIP64_LIMIT or compress_size > ZIP64_LIMIT:
+            # File is larger than what fits into a 4 byte integer,
+            # fall back to the ZIP64 extension
+            fmt = '<HHQQ'
+            extra = extra + struct.pack(fmt,
+                    1, struct.calcsize(fmt)-4, file_size, compress_size)
+            file_size = 0xffffffff
+            compress_size = 0xffffffff
+            self.extract_version = max(45, self.extract_version)
+            self.create_version = max(45, self.extract_version)
+
+        filename, flag_bits = self._encodeFilenameFlags()
+        header = struct.pack(structFileHeader, stringFileHeader,
+                 self.extract_version, self.reserved, flag_bits,
+                 self.compress_type, dostime, dosdate, CRC,
+                 compress_size, file_size,
+                 len(filename), len(extra))
+        return header + filename + extra
+
+    def _encodeFilenameFlags(self):
+        if isinstance(self.filename, unicode):
+            try:
+                return self.filename.encode('ascii'), self.flag_bits
+            except UnicodeEncodeError:
+                return self.filename.encode('utf-8'), self.flag_bits | 0x800
+        else:
+            return self.filename, self.flag_bits
+
+    def _decodeFilename(self):
+        if self.flag_bits & 0x800:
+            try:
+                print "decoding filename",self.filename
+                return self.filename.decode('utf-8')
+            except:
+                return self.filename
+        else:
+            return self.filename
+
+    def _decodeExtra(self):
+        # Try to decode the extra field.
+        extra = self.extra
+        unpack = struct.unpack
+        while extra:
+            tp, ln = unpack('<HH', extra[:4])
+            if tp == 1:
+                if ln >= 24:
+                    counts = unpack('<QQQ', extra[4:28])
+                elif ln == 16:
+                    counts = unpack('<QQ', extra[4:20])
+                elif ln == 8:
+                    counts = unpack('<Q', extra[4:12])
+                elif ln == 0:
+                    counts = ()
+                else:
+                    raise RuntimeError, "Corrupt extra field %s"%(ln,)
+
+                idx = 0
+
+                # ZIP64 extension (large files and/or large archives)
+                if self.file_size in (0xffffffffffffffffL, 0xffffffffL):
+                    self.file_size = counts[idx]
+                    idx += 1
+
+                if self.compress_size == 0xFFFFFFFFL:
+                    self.compress_size = counts[idx]
+                    idx += 1
+
+                if self.header_offset == 0xffffffffL:
+                    old = self.header_offset
+                    self.header_offset = counts[idx]
+                    idx+=1
+
+            extra = extra[ln+4:]
+
+
+class _ZipDecrypter:
+    """Class to handle decryption of files stored within a ZIP archive.
+
+    ZIP supports a password-based form of encryption. Even though known
+    plaintext attacks have been found against it, it is still useful
+    to be able to get data out of such a file.
+
+    Usage:
+        zd = _ZipDecrypter(mypwd)
+        plain_char = zd(cypher_char)
+        plain_text = map(zd, cypher_text)
+    """
+
+    def _GenerateCRCTable():
+        """Generate a CRC-32 table.
+
+        ZIP encryption uses the CRC32 one-byte primitive for scrambling some
+        internal keys. We noticed that a direct implementation is faster than
+        relying on binascii.crc32().
+        """
+        poly = 0xedb88320
+        table = [0] * 256
+        for i in range(256):
+            crc = i
+            for j in range(8):
+                if crc & 1:
+                    crc = ((crc >> 1) & 0x7FFFFFFF) ^ poly
+                else:
+                    crc = ((crc >> 1) & 0x7FFFFFFF)
+            table[i] = crc
+        return table
+    crctable = _GenerateCRCTable()
+
+    def _crc32(self, ch, crc):
+        """Compute the CRC32 primitive on one byte."""
+        return ((crc >> 8) & 0xffffff) ^ self.crctable[(crc ^ ord(ch)) & 0xff]
+
+    def __init__(self, pwd):
+        self.key0 = 305419896
+        self.key1 = 591751049
+        self.key2 = 878082192
+        for p in pwd:
+            self._UpdateKeys(p)
+
+    def _UpdateKeys(self, c):
+        self.key0 = self._crc32(c, self.key0)
+        self.key1 = (self.key1 + (self.key0 & 255)) & 4294967295
+        self.key1 = (self.key1 * 134775813 + 1) & 4294967295
+        self.key2 = self._crc32(chr((self.key1 >> 24) & 255), self.key2)
+
+    def __call__(self, c):
+        """Decrypt a single character."""
+        c = ord(c)
+        k = self.key2 | 2
+        c = c ^ (((k * (k^1)) >> 8) & 255)
+        c = chr(c)
+        self._UpdateKeys(c)
+        return c
+
+class ZipExtFile(io.BufferedIOBase):
+    """File-like object for reading an archive member.
+       Is returned by ZipFile.open().
+    """
+
+    # Max size supported by decompressor.
+    MAX_N = 1 << 31 - 1
+
+    # Read from compressed files in 4k blocks.
+    MIN_READ_SIZE = 4096
+
+    # Search for universal newlines or line chunks.
+    PATTERN = re.compile(r'^(?P<chunk>[^\r\n]+)|(?P<newline>\n|\r\n?)')
+
+    def __init__(self, fileobj, mode, zipinfo, decrypter=None):
+        self._fileobj = fileobj
+        self._decrypter = decrypter
+
+        self._compress_type = zipinfo.compress_type
+        self._compress_size = zipinfo.compress_size
+        self._compress_left = zipinfo.compress_size
+
+        if self._compress_type == ZIP_DEFLATED:
+            self._decompressor = zlib.decompressobj(-15)
+        self._unconsumed = ''
+
+        self._readbuffer = ''
+        self._offset = 0
+
+        self._universal = 'U' in mode
+        self.newlines = None
+
+        # Adjust read size for encrypted files since the first 12 bytes
+        # are for the encryption/password information.
+        if self._decrypter is not None:
+            self._compress_left -= 12
+
+        self.mode = mode
+        self.name = zipinfo.filename
+
+    def readline(self, limit=-1):
+        """Read and return a line from the stream.
+
+        If limit is specified, at most limit bytes will be read.
+        """
+
+        if not self._universal and limit < 0:
+            # Shortcut common case - newline found in buffer.
+            i = self._readbuffer.find('\n', self._offset) + 1
+            if i > 0:
+                line = self._readbuffer[self._offset: i]
+                self._offset = i
+                return line
+
+        if not self._universal:
+            return io.BufferedIOBase.readline(self, limit)
+
+        line = ''
+        while limit < 0 or len(line) < limit:
+            readahead = self.peek(2)
+            if readahead == '':
+                return line
+
+            #
+            # Search for universal newlines or line chunks.
+            #
+            # The pattern returns either a line chunk or a newline, but not
+            # both. Combined with peek(2), we are assured that the sequence
+            # '\r\n' is always retrieved completely and never split into
+            # separate newlines - '\r', '\n' due to coincidental readaheads.
+            #
+            match = self.PATTERN.search(readahead)
+            newline = match.group('newline')
+            if newline is not None:
+                if self.newlines is None:
+                    self.newlines = []
+                if newline not in self.newlines:
+                    self.newlines.append(newline)
+                self._offset += len(newline)
+                return line + '\n'
+
+            chunk = match.group('chunk')
+            if limit >= 0:
+                chunk = chunk[: limit - len(line)]
+
+            self._offset += len(chunk)
+            line += chunk
+
+        return line
+
+    def peek(self, n=1):
+        """Returns buffered bytes without advancing the position."""
+        if n > len(self._readbuffer) - self._offset:
+            chunk = self.read(n)
+            self._offset -= len(chunk)
+
+        # Return up to 512 bytes to reduce allocation overhead for tight loops.
+        return self._readbuffer[self._offset: self._offset + 512]
+
+    def readable(self):
+        return True
+
+    def read(self, n=-1):
+        """Read and return up to n bytes.
+        If the argument is omitted, None, or negative, data is read and returned until EOF is reached..
+        """
+
+        buf = ''
+        while n < 0 or n is None or n > len(buf):
+            data = self.read1(n)
+            if len(data) == 0:
+                return buf
+
+            buf += data
+
+        return buf
+
+    def read1(self, n):
+        """Read up to n bytes with at most one read() system call."""
+
+        # Simplify algorithm (branching) by transforming negative n to large n.
+        if n < 0 or n is None:
+            n = self.MAX_N
+
+        # Bytes available in read buffer.
+        len_readbuffer = len(self._readbuffer) - self._offset
+
+        # Read from file.
+        if self._compress_left > 0 and n > len_readbuffer + len(self._unconsumed):
+            nbytes = n - len_readbuffer - len(self._unconsumed)
+            nbytes = max(nbytes, self.MIN_READ_SIZE)
+            nbytes = min(nbytes, self._compress_left)
+
+            data = self._fileobj.read(nbytes)
+            self._compress_left -= len(data)
+
+            if data and self._decrypter is not None:
+                data = ''.join(map(self._decrypter, data))
+
+            if self._compress_type == ZIP_STORED:
+                self._readbuffer = self._readbuffer[self._offset:] + data
+                self._offset = 0
+            else:
+                # Prepare deflated bytes for decompression.
+                self._unconsumed += data
+
+        # Handle unconsumed data.
+        if (len(self._unconsumed) > 0 and n > len_readbuffer and
+            self._compress_type == ZIP_DEFLATED):
+            data = self._decompressor.decompress(
+                self._unconsumed,
+                max(n - len_readbuffer, self.MIN_READ_SIZE)
+            )
+
+            self._unconsumed = self._decompressor.unconsumed_tail
+            if len(self._unconsumed) == 0 and self._compress_left == 0:
+                data += self._decompressor.flush()
+
+            self._readbuffer = self._readbuffer[self._offset:] + data
+            self._offset = 0
+
+        # Read from buffer.
+        data = self._readbuffer[self._offset: self._offset + n]
+        self._offset += len(data)
+        return data
+
+
+
+class ZipFile:
+    """ Class with methods to open, read, write, close, list zip files.
+
+    z = ZipFile(file, mode="r", compression=ZIP_STORED, allowZip64=False)
+
+    file: Either the path to the file, or a file-like object.
+          If it is a path, the file will be opened and closed by ZipFile.
+    mode: The mode can be either read "r", write "w" or append "a".
+    compression: ZIP_STORED (no compression) or ZIP_DEFLATED (requires zlib).
+    allowZip64: if True ZipFile will create files with ZIP64 extensions when
+                needed, otherwise it will raise an exception when this would
+                be necessary.
+
+    """
+
+    fp = None                   # Set here since __del__ checks it
+
+    def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=False):
+        """Open the ZIP file with mode read "r", write "w" or append "a"."""
+        if mode not in ("r", "w", "a"):
+            raise RuntimeError('ZipFile() requires mode "r", "w", or "a"')
+
+        if compression == ZIP_STORED:
+            pass
+        elif compression == ZIP_DEFLATED:
+            if not zlib:
+                raise RuntimeError,\
+                      "Compression requires the (missing) zlib module"
+        else:
+            raise RuntimeError, "That compression method is not supported"
+
+        self._allowZip64 = allowZip64
+        self._didModify = False
+        self.debug = 0  # Level of printing: 0 through 3
+        self.NameToInfo = {}    # Find file info given name
+        self.filelist = []      # List of ZipInfo instances for archive
+        self.compression = compression  # Method of compression
+        self.mode = key = mode.replace('b', '')[0]
+        self.pwd = None
+        self.comment = ''
+
+        # Check if we were passed a file-like object
+        if isinstance(file, basestring):
+            self._filePassed = 0
+            self.filename = file
+            modeDict = {'r' : 'rb', 'w': 'wb', 'a' : 'r+b'}
+            try:
+                self.fp = open(file, modeDict[mode])
+            except IOError:
+                if mode == 'a':
+                    mode = key = 'w'
+                    self.fp = open(file, modeDict[mode])
+                else:
+                    raise
+        else:
+            self._filePassed = 1
+            self.fp = file
+            self.filename = getattr(file, 'name', None)
+
+        if key == 'r':
+            self._GetContents()
+        elif key == 'w':
+            pass
+        elif key == 'a':
+            try:                        # See if file is a zip file
+                self._RealGetContents()
+                # seek to start of directory and overwrite
+                self.fp.seek(self.start_dir, 0)
+            except BadZipfile:          # file is not a zip file, just append
+                self.fp.seek(0, 2)
+        else:
+            if not self._filePassed:
+                self.fp.close()
+                self.fp = None
+            raise RuntimeError, 'Mode must be "r", "w" or "a"'
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, traceback):
+        self.close()
+
+    def _GetContents(self):
+        """Read the directory, making sure we close the file if the format
+        is bad."""
+        try:
+            self._RealGetContents()
+        except BadZipfile:
+            if not self._filePassed:
+                self.fp.close()
+                self.fp = None
+            raise
+
+    def _RealGetContents(self):
+        """Read in the table of contents for the ZIP file."""
+        fp = self.fp
+        endrec = _EndRecData(fp)
+        if not endrec:
+            raise BadZipfile, "File is not a zip file"
+        if self.debug > 1:
+            print endrec
+        size_cd = endrec[_ECD_SIZE]             # bytes in central directory
+        offset_cd = endrec[_ECD_OFFSET]         # offset of central directory
+        self.comment = endrec[_ECD_COMMENT]     # archive comment
+
+        # "concat" is zero, unless zip was concatenated to another file
+        concat = endrec[_ECD_LOCATION] - size_cd - offset_cd
+        if endrec[_ECD_SIGNATURE] == stringEndArchive64:
+            # If Zip64 extension structures are present, account for them
+            concat -= (sizeEndCentDir64 + sizeEndCentDir64Locator)
+
+        if self.debug > 2:
+            inferred = concat + offset_cd
+            print "given, inferred, offset", offset_cd, inferred, concat
+        # self.start_dir:  Position of start of central directory
+        self.start_dir = offset_cd + concat
+        fp.seek(self.start_dir, 0)
+        data = fp.read(size_cd)
+        fp = cStringIO.StringIO(data)
+        total = 0
+        while total < size_cd:
+            centdir = fp.read(sizeCentralDir)
+            if centdir[0:4] != stringCentralDir:
+                raise BadZipfile, "Bad magic number for central directory"
+            centdir = struct.unpack(structCentralDir, centdir)
+            if self.debug > 2:
+                print centdir
+            filename = fp.read(centdir[_CD_FILENAME_LENGTH])
+            # Create ZipInfo instance to store file information
+            x = ZipInfo(filename)
+            x.extra = fp.read(centdir[_CD_EXTRA_FIELD_LENGTH])
+            x.comment = fp.read(centdir[_CD_COMMENT_LENGTH])
+            x.header_offset = centdir[_CD_LOCAL_HEADER_OFFSET]
+            (x.create_version, x.create_system, x.extract_version, x.reserved,
+                x.flag_bits, x.compress_type, t, d,
+                x.CRC, x.compress_size, x.file_size) = centdir[1:12]
+            x.volume, x.internal_attr, x.external_attr = centdir[15:18]
+            # Convert date/time code to (year, month, day, hour, min, sec)
+            x._raw_time = t
+            x.date_time = ( (d>>9)+1980, (d>>5)&0xF, d&0x1F,
+                                     t>>11, (t>>5)&0x3F, (t&0x1F) * 2 )
+
+            x._decodeExtra()
+            x.header_offset = x.header_offset + concat
+            x.filename = x._decodeFilename()
+            self.filelist.append(x)
+            self.NameToInfo[x.filename] = x
+
+            # update total bytes read from central directory
+            total = (total + sizeCentralDir + centdir[_CD_FILENAME_LENGTH]
+                     + centdir[_CD_EXTRA_FIELD_LENGTH]
+                     + centdir[_CD_COMMENT_LENGTH])
+
+            if self.debug > 2:
+                print "total", total
+
+
+    def namelist(self):
+        """Return a list of file names in the archive."""
+        l = []
+        for data in self.filelist:
+            l.append(data.filename)
+        return l
+
+    def infolist(self):
+        """Return a list of class ZipInfo instances for files in the
+        archive."""
+        return self.filelist
+
+    def printdir(self):
+        """Print a table of contents for the zip file."""
+        print "%-46s %19s %12s" % ("File Name", "Modified    ", "Size")
+        for zinfo in self.filelist:
+            date = "%d-%02d-%02d %02d:%02d:%02d" % zinfo.date_time[:6]
+            print "%-46s %s %12d" % (zinfo.filename, date, zinfo.file_size)
+
+    def testzip(self):
+        """Read all the files and check the CRC."""
+        chunk_size = 2 ** 20
+        for zinfo in self.filelist:
+            try:
+                # Read by chunks, to avoid an OverflowError or a
+                # MemoryError with very large embedded files.
+                f = self.open(zinfo.filename, "r")
+                while f.read(chunk_size):     # Check CRC-32
+                    pass
+            except BadZipfile:
+                return zinfo.filename
+
+    def getinfo(self, name):
+        """Return the instance of ZipInfo given 'name'."""
+        info = self.NameToInfo.get(name)
+        if info is None:
+            raise KeyError(
+                'There is no item named %r in the archive' % name)
+
+        return info
+
+    def setpassword(self, pwd):
+        """Set default password for encrypted files."""
+        self.pwd = pwd
+
+    def read(self, name, pwd=None):
+        """Return file bytes (as a string) for name."""
+        return self.open(name, "r", pwd).read()
+
+    def open(self, name, mode="r", pwd=None):
+        """Return file-like object for 'name'."""
+        if mode not in ("r", "U", "rU"):
+            raise RuntimeError, 'open() requires mode "r", "U", or "rU"'
+        if not self.fp:
+            raise RuntimeError, \
+                  "Attempt to read ZIP archive that was already closed"
+
+        # Only open a new file for instances where we were not
+        # given a file object in the constructor
+        if self._filePassed:
+            zef_file = self.fp
+        else:
+            zef_file = open(self.filename, 'rb')
+
+        # Make sure we have an info object
+        if isinstance(name, ZipInfo):
+            # 'name' is already an info object
+            zinfo = name
+        else:
+            # Get info object for name
+            zinfo = self.getinfo(name)
+
+        zef_file.seek(zinfo.header_offset, 0)
+
+        # Skip the file header:
+        fheader = zef_file.read(sizeFileHeader)
+        if fheader[0:4] != stringFileHeader:
+            raise BadZipfile, "Bad magic number for file header"
+
+        fheader = struct.unpack(structFileHeader, fheader)
+        fname = zef_file.read(fheader[_FH_FILENAME_LENGTH])
+        if fheader[_FH_EXTRA_FIELD_LENGTH]:
+            zef_file.read(fheader[_FH_EXTRA_FIELD_LENGTH])
+
+        if fname != zinfo.orig_filename:
+            raise BadZipfile, \
+                      'File name in directory "%s" and header "%s" differ.' % (
+                          zinfo.orig_filename, fname)
+
+        # check for encrypted flag & handle password
+        is_encrypted = zinfo.flag_bits & 0x1
+        zd = None
+        if is_encrypted:
+            if not pwd:
+                pwd = self.pwd
+            if not pwd:
+                raise RuntimeError, "File %s is encrypted, " \
+                      "password required for extraction" % name
+
+            zd = _ZipDecrypter(pwd)
+            # The first 12 bytes in the cypher stream is an encryption header
+            #  used to strengthen the algorithm. The first 11 bytes are
+            #  completely random, while the 12th contains the MSB of the CRC,
+            #  or the MSB of the file time depending on the header type
+            #  and is used to check the correctness of the password.
+            bytes = zef_file.read(12)
+            h = map(zd, bytes[0:12])
+            if zinfo.flag_bits & 0x8:
+                # compare against the file type from extended local headers
+                check_byte = (zinfo._raw_time >> 8) & 0xff
+            else:
+                # compare against the CRC otherwise
+                check_byte = (zinfo.CRC >> 24) & 0xff
+            if ord(h[11]) != check_byte:
+                raise RuntimeError("Bad password for file", name)
+
+        return  ZipExtFile(zef_file, mode, zinfo, zd)
+
+    def extract(self, member, path=None, pwd=None):
+        """Extract a member from the archive to the current working directory,
+           using its full name. Its file information is extracted as accurately
+           as possible. `member' may be a filename or a ZipInfo object. You can
+           specify a different directory using `path'.
+        """
+        if not isinstance(member, ZipInfo):
+            member = self.getinfo(member)
+
+        if path is None:
+            path = os.getcwd()
+
+        return self._extract_member(member, path, pwd)
+
+    def extractall(self, path=None, members=None, pwd=None):
+        """Extract all members from the archive to the current working
+           directory. `path' specifies a different directory to extract to.
+           `members' is optional and must be a subset of the list returned
+           by namelist().
+        """
+        if members is None:
+            members = self.namelist()
+
+        for zipinfo in members:
+            self.extract(zipinfo, path, pwd)
+
+    def _extract_member(self, member, targetpath, pwd):
+        """Extract the ZipInfo object 'member' to a physical
+           file on the path targetpath.
+        """
+        # build the destination pathname, replacing
+        # forward slashes to platform specific separators.
+        # Strip trailing path separator, unless it represents the root.
+        if (targetpath[-1:] in (os.path.sep, os.path.altsep)
+            and len(os.path.splitdrive(targetpath)[1]) > 1):
+            targetpath = targetpath[:-1]
+
+        # don't include leading "/" from file name if present
+        if member.filename[0] == '/':
+            targetpath = os.path.join(targetpath, member.filename[1:])
+        else:
+            targetpath = os.path.join(targetpath, member.filename)
+
+        targetpath = os.path.normpath(targetpath)
+
+        # Create all upper directories if necessary.
+        upperdirs = os.path.dirname(targetpath)
+        if upperdirs and not os.path.exists(upperdirs):
+            os.makedirs(upperdirs)
+
+        if member.filename[-1] == '/':
+            if not os.path.isdir(targetpath):
+                os.mkdir(targetpath)
+            return targetpath
+
+        source = self.open(member, pwd=pwd)
+        target = file(targetpath, "wb")
+        shutil.copyfileobj(source, target)
+        source.close()
+        target.close()
+
+        return targetpath
+
+    def _writecheck(self, zinfo):
+        """Check for errors before writing a file to the archive."""
+        if zinfo.filename in self.NameToInfo:
+            if self.debug:      # Warning for duplicate names
+                print "Duplicate name:", zinfo.filename
+        if self.mode not in ("w", "a"):
+            raise RuntimeError, 'write() requires mode "w" or "a"'
+        if not self.fp:
+            raise RuntimeError, \
+                  "Attempt to write ZIP archive that was already closed"
+        if zinfo.compress_type == ZIP_DEFLATED and not zlib:
+            raise RuntimeError, \
+                  "Compression requires the (missing) zlib module"
+        if zinfo.compress_type not in (ZIP_STORED, ZIP_DEFLATED):
+            raise RuntimeError, \
+                  "That compression method is not supported"
+        if zinfo.file_size > ZIP64_LIMIT:
+            if not self._allowZip64:
+                raise LargeZipFile("Filesize would require ZIP64 extensions")
+        if zinfo.header_offset > ZIP64_LIMIT:
+            if not self._allowZip64:
+                raise LargeZipFile("Zipfile size would require ZIP64 extensions")
+
+    def write(self, filename, arcname=None, compress_type=None):
+        """Put the bytes from filename into the archive under the name
+        arcname."""
+        if not self.fp:
+            raise RuntimeError(
+                  "Attempt to write to ZIP archive that was already closed")
+
+        st = os.stat(filename)
+        isdir = stat.S_ISDIR(st.st_mode)
+        mtime = time.localtime(st.st_mtime)
+        date_time = mtime[0:6]
+        # Create ZipInfo instance to store file information
+        if arcname is None:
+            arcname = filename
+        arcname = os.path.normpath(os.path.splitdrive(arcname)[1])
+        while arcname[0] in (os.sep, os.altsep):
+            arcname = arcname[1:]
+        if isdir:
+            arcname += '/'
+        zinfo = ZipInfo(arcname, date_time)
+        zinfo.external_attr = (st[0] & 0xFFFF) << 16L      # Unix attributes
+        if compress_type is None:
+            zinfo.compress_type = self.compression
+        else:
+            zinfo.compress_type = compress_type
+
+        zinfo.file_size = st.st_size
+        zinfo.flag_bits = 0x00
+        zinfo.header_offset = self.fp.tell()    # Start of header bytes
+
+        self._writecheck(zinfo)
+        self._didModify = True
+
+        if isdir:
+            zinfo.file_size = 0
+            zinfo.compress_size = 0
+            zinfo.CRC = 0
+            self.filelist.append(zinfo)
+            self.NameToInfo[zinfo.filename] = zinfo
+            self.fp.write(zinfo.FileHeader())
+            return
+
+        with open(filename, "rb") as fp:
+            # Must overwrite CRC and sizes with correct data later
+            zinfo.CRC = CRC = 0
+            zinfo.compress_size = compress_size = 0
+            zinfo.file_size = file_size = 0
+            self.fp.write(zinfo.FileHeader())
+            if zinfo.compress_type == ZIP_DEFLATED:
+                cmpr = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION,
+                     zlib.DEFLATED, -15)
+            else:
+                cmpr = None
+            while 1:
+                buf = fp.read(1024 * 8)
+                if not buf:
+                    break
+                file_size = file_size + len(buf)
+                CRC = crc32(buf, CRC) & 0xffffffff
+                if cmpr:
+                    buf = cmpr.compress(buf)
+                    compress_size = compress_size + len(buf)
+                self.fp.write(buf)
+        if cmpr:
+            buf = cmpr.flush()
+            compress_size = compress_size + len(buf)
+            self.fp.write(buf)
+            zinfo.compress_size = compress_size
+        else:
+            zinfo.compress_size = file_size
+        zinfo.CRC = CRC
+        zinfo.file_size = file_size
+        # Seek backwards and write CRC and file sizes
+        position = self.fp.tell()       # Preserve current position in file
+        self.fp.seek(zinfo.header_offset + 14, 0)
+        self.fp.write(struct.pack("<LLL", zinfo.CRC, zinfo.compress_size,
+              zinfo.file_size))
+        self.fp.seek(position, 0)
+        self.filelist.append(zinfo)
+        self.NameToInfo[zinfo.filename] = zinfo
+
+    def writestr(self, zinfo_or_arcname, bytes, compress_type=None):
+        """Write a file into the archive.  The contents is the string
+        'bytes'.  'zinfo_or_arcname' is either a ZipInfo instance or
+        the name of the file in the archive."""
+        if not isinstance(zinfo_or_arcname, ZipInfo):
+            zinfo = ZipInfo(filename=zinfo_or_arcname,
+                            date_time=time.localtime(time.time())[:6])
+
+            zinfo.compress_type = self.compression
+            zinfo.external_attr = 0600 << 16
+        else:
+            zinfo = zinfo_or_arcname
+
+        if not self.fp:
+            raise RuntimeError(
+                  "Attempt to write to ZIP archive that was already closed")
+
+        if compress_type is not None:
+            zinfo.compress_type = compress_type
+
+        zinfo.file_size = len(bytes)            # Uncompressed size
+        zinfo.header_offset = self.fp.tell()    # Start of header bytes
+        self._writecheck(zinfo)
+        self._didModify = True
+        zinfo.CRC = crc32(bytes) & 0xffffffff       # CRC-32 checksum
+        if zinfo.compress_type == ZIP_DEFLATED:
+            co = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION,
+                 zlib.DEFLATED, -15)
+            bytes = co.compress(bytes) + co.flush()
+            zinfo.compress_size = len(bytes)    # Compressed size
+        else:
+            zinfo.compress_size = zinfo.file_size
+        zinfo.header_offset = self.fp.tell()    # Start of header bytes
+        self.fp.write(zinfo.FileHeader())
+        self.fp.write(bytes)
+        self.fp.flush()
+        if zinfo.flag_bits & 0x08:
+            # Write CRC and file sizes after the file data
+            self.fp.write(struct.pack("<LLL", zinfo.CRC, zinfo.compress_size,
+                  zinfo.file_size))
+        self.filelist.append(zinfo)
+        self.NameToInfo[zinfo.filename] = zinfo
+
+    def __del__(self):
+        """Call the "close()" method in case the user forgot."""
+        self.close()
+
+    def close(self):
+        """Close the file, and for mode "w" and "a" write the ending
+        records."""
+        if self.fp is None:
+            return
+
+        if self.mode in ("w", "a") and self._didModify: # write ending records
+            count = 0
+            pos1 = self.fp.tell()
+            for zinfo in self.filelist:         # write central directory
+                count = count + 1
+                dt = zinfo.date_time
+                dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2]
+                dostime = dt[3] << 11 | dt[4] << 5 | (dt[5] // 2)
+                extra = []
+                if zinfo.file_size > ZIP64_LIMIT \
+                        or zinfo.compress_size > ZIP64_LIMIT:
+                    extra.append(zinfo.file_size)
+                    extra.append(zinfo.compress_size)
+                    file_size = 0xffffffff
+                    compress_size = 0xffffffff
+                else:
+                    file_size = zinfo.file_size
+                    compress_size = zinfo.compress_size
+
+                if zinfo.header_offset > ZIP64_LIMIT:
+                    extra.append(zinfo.header_offset)
+                    header_offset = 0xffffffffL
+                else:
+                    header_offset = zinfo.header_offset
+
+                extra_data = zinfo.extra
+                if extra:
+                    # Append a ZIP64 field to the extra's
+                    extra_data = struct.pack(
+                            '<HH' + 'Q'*len(extra),
+                            1, 8*len(extra), *extra) + extra_data
+
+                    extract_version = max(45, zinfo.extract_version)
+                    create_version = max(45, zinfo.create_version)
+                else:
+                    extract_version = zinfo.extract_version
+                    create_version = zinfo.create_version
+
+                try:
+                    filename, flag_bits = zinfo._encodeFilenameFlags()
+                    centdir = struct.pack(structCentralDir,
+                     stringCentralDir, create_version,
+                     zinfo.create_system, extract_version, zinfo.reserved,
+                     flag_bits, zinfo.compress_type, dostime, dosdate,
+                     zinfo.CRC, compress_size, file_size,
+                     len(filename), len(extra_data), len(zinfo.comment),
+                     0, zinfo.internal_attr, zinfo.external_attr,
+                     header_offset)
+                except DeprecationWarning:
+                    print >>sys.stderr, (structCentralDir,
+                     stringCentralDir, create_version,
+                     zinfo.create_system, extract_version, zinfo.reserved,
+                     zinfo.flag_bits, zinfo.compress_type, dostime, dosdate,
+                     zinfo.CRC, compress_size, file_size,
+                     len(zinfo.filename), len(extra_data), len(zinfo.comment),
+                     0, zinfo.internal_attr, zinfo.external_attr,
+                     header_offset)
+                    raise
+                self.fp.write(centdir)
+                self.fp.write(filename)
+                self.fp.write(extra_data)
+                self.fp.write(zinfo.comment)
+
+            pos2 = self.fp.tell()
+            # Write end-of-zip-archive record
+            centDirCount = count
+            centDirSize = pos2 - pos1
+            centDirOffset = pos1
+            if (centDirCount >= ZIP_FILECOUNT_LIMIT or
+                centDirOffset > ZIP64_LIMIT or
+                centDirSize > ZIP64_LIMIT):
+                # Need to write the ZIP64 end-of-archive records
+                zip64endrec = struct.pack(
+                        structEndArchive64, stringEndArchive64,
+                        44, 45, 45, 0, 0, centDirCount, centDirCount,
+                        centDirSize, centDirOffset)
+                self.fp.write(zip64endrec)
+
+                zip64locrec = struct.pack(
+                        structEndArchive64Locator,
+                        stringEndArchive64Locator, 0, pos2, 1)
+                self.fp.write(zip64locrec)
+                centDirCount = min(centDirCount, 0xFFFF)
+                centDirSize = min(centDirSize, 0xFFFFFFFF)
+                centDirOffset = min(centDirOffset, 0xFFFFFFFF)
+
+            # check for valid comment length
+            if len(self.comment) >= ZIP_MAX_COMMENT:
+                if self.debug > 0:
+                    msg = 'Archive comment is too long; truncating to %d bytes' \
+                          % ZIP_MAX_COMMENT
+                self.comment = self.comment[:ZIP_MAX_COMMENT]
+
+            endrec = struct.pack(structEndArchive, stringEndArchive,
+                                 0, 0, centDirCount, centDirCount,
+                                 centDirSize, centDirOffset, len(self.comment))
+            self.fp.write(endrec)
+            self.fp.write(self.comment)
+            self.fp.flush()
+
+        if not self._filePassed:
+            self.fp.close()
+        self.fp = None
+
+
+class PyZipFile(ZipFile):
+    """Class to create ZIP archives with Python library files and packages."""
+
+    def writepy(self, pathname, basename = ""):
+        """Add all files from "pathname" to the ZIP archive.
+
+        If pathname is a package directory, search the directory and
+        all package subdirectories recursively for all *.py and enter
+        the modules into the archive.  If pathname is a plain
+        directory, listdir *.py and enter all modules.  Else, pathname
+        must be a Python *.py file and the module will be put into the
+        archive.  Added modules are always module.pyo or module.pyc.
+        This method will compile the module.py into module.pyc if
+        necessary.
+        """
+        dir, name = os.path.split(pathname)
+        if os.path.isdir(pathname):
+            initname = os.path.join(pathname, "__init__.py")
+            if os.path.isfile(initname):
+                # This is a package directory, add it
+                if basename:
+                    basename = "%s/%s" % (basename, name)
+                else:
+                    basename = name
+                if self.debug:
+                    print "Adding package in", pathname, "as", basename
+                fname, arcname = self._get_codename(initname[0:-3], basename)
+                if self.debug:
+                    print "Adding", arcname
+                self.write(fname, arcname)
+                dirlist = os.listdir(pathname)
+                dirlist.remove("__init__.py")
+                # Add all *.py files and package subdirectories
+                for filename in dirlist:
+                    path = os.path.join(pathname, filename)
+                    root, ext = os.path.splitext(filename)
+                    if os.path.isdir(path):
+                        if os.path.isfile(os.path.join(path, "__init__.py")):
+                            # This is a package directory, add it
+                            self.writepy(path, basename)  # Recursive call
+                    elif ext == ".py":
+                        fname, arcname = self._get_codename(path[0:-3],
+                                         basename)
+                        if self.debug:
+                            print "Adding", arcname
+                        self.write(fname, arcname)
+            else:
+                # This is NOT a package directory, add its files at top level
+                if self.debug:
+                    print "Adding files from directory", pathname
+                for filename in os.listdir(pathname):
+                    path = os.path.join(pathname, filename)
+                    root, ext = os.path.splitext(filename)
+                    if ext == ".py":
+                        fname, arcname = self._get_codename(path[0:-3],
+                                         basename)
+                        if self.debug:
+                            print "Adding", arcname
+                        self.write(fname, arcname)
+        else:
+            if pathname[-3:] != ".py":
+                raise RuntimeError, \
+                      'Files added with writepy() must end with ".py"'
+            fname, arcname = self._get_codename(pathname[0:-3], basename)
+            if self.debug:
+                print "Adding file", arcname
+            self.write(fname, arcname)
+
+    def _get_codename(self, pathname, basename):
+        """Return (filename, archivename) for the path.
+
+        Given a module name path, return the correct file path and
+        archive name, compiling if necessary.  For example, given
+        /python/lib/string, return (/python/lib/string.pyc, string).
+        """
+        file_py  = pathname + ".py"
+        file_pyc = pathname + ".pyc"
+        file_pyo = pathname + ".pyo"
+        if os.path.isfile(file_pyo) and \
+                            os.stat(file_pyo).st_mtime >= os.stat(file_py).st_mtime:
+            fname = file_pyo    # Use .pyo file
+        elif not os.path.isfile(file_pyc) or \
+             os.stat(file_pyc).st_mtime < os.stat(file_py).st_mtime:
+            import py_compile
+            if self.debug:
+                print "Compiling", file_py
+            try:
+                py_compile.compile(file_py, file_pyc, None, True)
+            except py_compile.PyCompileError,err:
+                print err.msg
+            fname = file_pyc
+        else:
+            fname = file_pyc
+        archivename = os.path.split(fname)[1]
+        if basename:
+            archivename = "%s/%s" % (basename, archivename)
+        return (fname, archivename)
+
+
+def main(args = None):
+    import textwrap
+    USAGE=textwrap.dedent("""\
+        Usage:
+            zipfile.py -l zipfile.zip        # Show listing of a zipfile
+            zipfile.py -t zipfile.zip        # Test if a zipfile is valid
+            zipfile.py -e zipfile.zip target # Extract zipfile into target dir
+            zipfile.py -c zipfile.zip src ... # Create zipfile from sources
+        """)
+    if args is None:
+        args = sys.argv[1:]
+
+    if not args or args[0] not in ('-l', '-c', '-e', '-t'):
+        print USAGE
+        sys.exit(1)
+
+    if args[0] == '-l':
+        if len(args) != 2:
+            print USAGE
+            sys.exit(1)
+        zf = ZipFile(args[1], 'r')
+        zf.printdir()
+        zf.close()
+
+    elif args[0] == '-t':
+        if len(args) != 2:
+            print USAGE
+            sys.exit(1)
+        zf = ZipFile(args[1], 'r')
+        zf.testzip()
+        print "Done testing"
+
+    elif args[0] == '-e':
+        if len(args) != 3:
+            print USAGE
+            sys.exit(1)
+
+        zf = ZipFile(args[1], 'r')
+        out = args[2]
+        for path in zf.namelist():
+            if path.startswith('./'):
+                tgt = os.path.join(out, path[2:])
+            else:
+                tgt = os.path.join(out, path)
+
+            tgtdir = os.path.dirname(tgt)
+            if not os.path.exists(tgtdir):
+                os.makedirs(tgtdir)
+            with open(tgt, 'wb') as fp:
+                fp.write(zf.read(path))
+        zf.close()
+
+    elif args[0] == '-c':
+        if len(args) < 3:
+            print USAGE
+            sys.exit(1)
+
+        def addToZip(zf, path, zippath):
+            if os.path.isfile(path):
+                zf.write(path, zippath, ZIP_DEFLATED)
+            elif os.path.isdir(path):
+                for nm in os.listdir(path):
+                    addToZip(zf,
+                            os.path.join(path, nm), os.path.join(zippath, nm))
+            # else: ignore
+
+        zf = ZipFile(args[1], 'w', allowZip64=True)
+        for src in args[2:]:
+            addToZip(zf, src, os.path.basename(src))
+
+        zf.close()
+
+if __name__ == "__main__":
+    main()
index a1aafde23e57ee26d332d06ee6bd71dfc53d6ed8..c401b36352bd7ee5d9780dddac7b193428a8076e 100644 (file)
@@ -2,7 +2,7 @@
 
 import sys
 import zlib
-import zipfile
+import zipfilerugged
 import os
 import os.path
 import getopt
@@ -15,7 +15,7 @@ _FILENAME_OFFSET = 30
 _MAX_SIZE = 64 * 1024
 _MIMETYPE = 'application/epub+zip'
 
-class ZipInfo(zipfile.ZipInfo):
+class ZipInfo(zipfilerugged.ZipInfo):
     def __init__(self, *args, **kwargs):
         if 'compress_type' in kwargs:
             compress_type = kwargs.pop('compress_type')
@@ -27,11 +27,11 @@ class fixZip:
         self.ztype = 'zip'
         if zinput.lower().find('.epub') >= 0 :
             self.ztype = 'epub'
-        self.inzip = zipfile.ZipFile(zinput,'r')
-        self.outzip = zipfile.ZipFile(zoutput,'w')
+        self.inzip = zipfilerugged.ZipFile(zinput,'r')
+        self.outzip = zipfilerugged.ZipFile(zoutput,'w')
         # open the input zip for reading only as a raw file
-       self.bzf = file(zinput,'rb')
-        
+        self.bzf = file(zinput,'rb')
+
     def getlocalname(self, zi):
         local_header_offset = zi.header_offset
         self.bzf.seek(local_header_offset + _FILENAME_LEN_OFFSET)
@@ -76,17 +76,17 @@ class fixZip:
         data = None
 
         # if not compressed we are good to go
-        if zi.compress_type == zipfile.ZIP_STORED:
+        if zi.compress_type == zipfilerugged.ZIP_STORED:
             data = self.bzf.read(zi.file_size)
 
         # if compressed we must decompress it using zlib
-        if zi.compress_type == zipfile.ZIP_DEFLATED:
+        if zi.compress_type == zipfilerugged.ZIP_DEFLATED:
             cmpdata = self.bzf.read(zi.compress_size)
             data = self.uncompress(cmpdata)
 
         return data
 
-        
+
 
     def fix(self):
         # get the zipinfo for each member of the input archive
@@ -95,7 +95,7 @@ class fixZip:
 
         # if epub write mimetype file first, with no compression
         if self.ztype == 'epub':
-            nzinfo = ZipInfo('mimetype', compress_type=zipfile.ZIP_STORED)
+            nzinfo = ZipInfo('mimetype', compress_type=zipfilerugged.ZIP_STORED)
             self.outzip.writestr(nzinfo, _MIMETYPE)
 
         # write the rest of the files
@@ -103,9 +103,9 @@ class fixZip:
             if zinfo.filename != "mimetype" or self.ztype == '.zip':
                 data = None
                 nzinfo = zinfo
-                try: 
+                try:
                     data = self.inzip.read(zinfo.filename)
-                except zipfile.BadZipfile or zipfile.error:
+                except zipfilerugged.BadZipfile or zipfilerugged.error:
                     local_name = self.getlocalname(zinfo)
                     data = self.getfiledata(zinfo)
                     nzinfo.filename = local_name
@@ -126,7 +126,7 @@ def usage():
      inputzip is the source zipfile to fix
      outputzip is the fixed zip archive
     """
-    
+
 
 def repairBook(infile, outfile):
     if not os.path.exists(infile):
@@ -152,5 +152,3 @@ def main(argv=sys.argv):
 
 if __name__ == '__main__' :
     sys.exit(main())
-
-
index b62a6255a83f164ed74d647ecb39f96ce1c410aa..216505bf556327360897ca7b5bf822f07bfd6824 100644 (file)
Binary files a/Calibre_Plugins/ineptepub_plugin.zip and b/Calibre_Plugins/ineptepub_plugin.zip differ
index 264b9eed83f320fa178489d4f380e9c102c70b71..5573027d6927212fd84a62e962f55b022f753c7c 100644 (file)
@@ -1,4 +1,7 @@
-#! /usr/bin/python
+#!/usr/bin/env python
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+
+from __future__ import with_statement
 
 # ineptepub_plugin.py
 # Released under the terms of the GNU General Public Licence, version 3 or
 #   0.1.5 - update zipfix to handle out of position mimetypes
 #   0.1.6 - update zipfix to handle completely missing mimetype files
 #   0.1.7 - update to new calibre plugin interface
+#   0.1.8 - Fix for potential problem with PyCrypto
+#   0.1.9 - Fix for potential problem with ADE keys and fix possible output/unicode problem
 
 """
 Decrypt Adobe ADEPT-encrypted EPUB books.
 """
 
-from __future__ import with_statement
+PLUGIN_NAME = 'Inept Epub DeDRM'
+PLUGIN_VERSION_TUPLE = (0, 1, 9)
+PLUGIN_VERSION = '.'.join([str(x) for x in PLUGIN_VERSION_TUPLE])
 
 __license__ = 'GPL v3'
 
@@ -89,7 +96,7 @@ def _load_crypto_libcrypto():
     else:
         libcrypto = find_library('crypto')
     if libcrypto is None:
-        raise ADEPTError('libcrypto not found')
+        raise ADEPTError('%s Plugin v%s: libcrypto not found' % (PLUGIN_NAME, PLUGIN_VERSION))
     libcrypto = CDLL(libcrypto)
 
     RSA_NO_PADDING = 3
@@ -262,7 +269,7 @@ def _load_crypto_pycrypto():
 
     class AES(object):
         def __init__(self, key):
-            self._aes = _AES.new(key, _AES.MODE_CBC)
+            self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
 
         def decrypt(self, data):
             return self._aes.decrypt(data)
@@ -369,18 +376,29 @@ from calibre.customize import FileTypePlugin
 from calibre.constants import iswindows, isosx
 
 class IneptDeDRM(FileTypePlugin):
-    name                    = 'Inept Epub DeDRM'
+    name                    = PLUGIN_NAME
     description             = 'Removes DRM from secure Adobe epub files. \
                                 Credit given to I <3 Cabbages for the original stand-alone scripts.'
     supported_platforms     = ['linux', 'osx', 'windows']
     author                  = 'DiapDealer'
-    version                 = (0, 1, 7)
+    version                 = PLUGIN_VERSION_TUPLE
     minimum_calibre_version = (0, 7, 55)  # Compiled python libraries cannot be imported in earlier versions.
     file_types              = set(['epub'])
     on_import               = True
     priority                = 100
     
     def run(self, path_to_ebook):
+        from calibre_plugins.ineptepub import outputfix
+         
+        if sys.stdout.encoding == None:
+            sys.stdout = outputfix.getwriter('utf-8')(sys.stdout)
+        else:
+            sys.stdout = outputfix.getwriter(sys.stdout.encoding)(sys.stdout)
+        if sys.stderr.encoding == None:
+            sys.stderr = outputfix.getwriter('utf-8')(sys.stderr)
+        else:
+            sys.stderr = outputfix.getwriter(sys.stderr.encoding)(sys.stderr)
+
         global AES
         global RSA
         
@@ -400,10 +418,13 @@ class IneptDeDRM(FileTypePlugin):
         files = os.listdir(confpath)
         filefilter = re.compile("\.der$", re.IGNORECASE)
         files = filter(filefilter.search, files)
+        foundDefault = False
 
         if files:
             try:
                 for filename in files:
+                    if filename[:16] == 'calibre-adeptkey':
+                        foundDefault = True
                     fpath = os.path.join(confpath, filename)
                     with open(fpath, 'rb') as f:
                         userkeys.append(f.read())
@@ -411,22 +432,23 @@ class IneptDeDRM(FileTypePlugin):
             except IOError:
                 print 'IneptEpub: Error reading keyfiles from config directory.'
                 pass
-        else:
+        
+        if not foundDefault:
             # Try to find key from ADE install and save the key in
             # Calibre's configuration directory for future use.
             if iswindows or isosx:
                 # ADE key retrieval script included in respective OS folder.
-                from calibre_plugins.ineptepub.ade_key import retrieve_key
+                from calibre_plugins.ineptepub.ineptkey import retrieve_keys
                 try:
-                    keydata = retrieve_key()
-                    userkeys.append(keydata)
-                    keypath = os.path.join(confpath, 'calibre-adeptkey.der')
-                    with open(keypath, 'wb') as f:
-                        f.write(keydata)
-                    print 'IneptEpub: Created keyfile from ADE install.'    
+                    keys = retrieve_keys()
+                    for i,key in enumerate(keys):
+                        userkeys.append(key)
+                        keypath = os.path.join(confpath, 'calibre-adeptkey{0:d}.der'.format(i))
+                        open(keypath, 'wb').write(key)
+                        print 'IneptEpub: Created keyfile %s from ADE install.' % keypath
                 except:
-                    print 'IneptEpub: Couldn\'t Retrieve key from ADE install.'
-                    pass
+                   print 'IneptEpub: Couldn\'t Retrieve key from ADE install.'
+                   pass
 
         if not userkeys:
             # No user keys found... bail out.
@@ -440,9 +462,11 @@ class IneptDeDRM(FileTypePlugin):
             from calibre_plugins.ineptepub import zipfix
             inf = self.temporary_file('.epub')
             try:
+                print '%s Plugin: Verifying zip archive integrity.' % PLUGIN_NAME
                 fr = zipfix.fixZip(path_to_ebook, inf.name)
                 fr.fix()
             except Exception, e:
+                print '%s Plugin: unforeseen zip archive issue.' % PLUGIN_NAME
                 raise Exception(e)
                 return
             of = self.temporary_file('.epub')
similarity index 72%
rename from Calibre_Plugins/ineptepub_plugin/ade_key.py
rename to Calibre_Plugins/ineptepub_plugin/ineptkey.py
index 4b743f74330c7f5a9f18b6acdafbf5a6981a7428..723b7c64eeab9ead313fc8283196cb5baa32407c 100644 (file)
@@ -1,17 +1,59 @@
-#!/usr/bin/env python
+#! /usr/bin/python
+# -*- coding: utf-8 -*-
+
+from __future__ import with_statement
+
+# ineptkey.pyw, version 5.6
+# Copyright © 2009-2010 i♥cabbages
+
+# Released under the terms of the GNU General Public Licence, version 3 or
+# later.  <http://www.gnu.org/licenses/>
+
+# Windows users: Before running this program, you must first install Python 2.6
+#   from <http://www.python.org/download/> and PyCrypto from
+#   <http://www.voidspace.org.uk/python/modules.shtml#pycrypto> (make certain
+#   to install the version for Python 2.6).  Then save this script file as
+#   ineptkey.pyw and double-click on it to run it.  It will create a file named
+#   adeptkey.der in the same directory.  This is your ADEPT user key.
+#
+# Mac OS X users: Save this script file as ineptkey.pyw.  You can run this
+#   program from the command line (pythonw ineptkey.pyw) or by double-clicking
+#   it when it has been associated with PythonLauncher.  It will create a file
+#   named adeptkey.der in the same directory.  This is your ADEPT user key.
+
+# Revision history:
+#   1 - Initial release, for Adobe Digital Editions 1.7
+#   2 - Better algorithm for finding pLK; improved error handling
+#   3 - Rename to INEPT
+#   4 - Series of changes by joblack (and others?) --
+#   4.1 - quick beta fix for ADE 1.7.2 (anon)
+#   4.2 - added old 1.7.1 processing
+#   4.3 - better key search
+#   4.4 - Make it working on 64-bit Python
+#   5 - Clean up and improve 4.x changes;
+#       Clean up and merge OS X support by unknown
+#   5.1 - add support for using OpenSSL on Windows in place of PyCrypto
+#   5.2 - added support for output of key to a particular file
+#   5.3 - On Windows try PyCrypto first, OpenSSL next
+#   5.4 - Modify interface to allow use of import
+#   5.5 - Fix for potential problem with PyCrypto
+#   5.6 - Revise to allow use in Plugins to eliminate need for duplicate code
 
 """
 Retrieve Adobe ADEPT user key.
 """
 
-from __future__ import with_statement
-
 __license__ = 'GPL v3'
 
 import sys
 import os
 import struct
-from calibre.constants import iswindows, isosx
+
+try:
+    from calibre.constants import iswindows, isosx
+except:
+    iswindows = sys.platform.startswith('win')
+    isosx = sys.platform.startswith('darwin')
 
 class ADEPTError(Exception):
     pass
@@ -72,7 +114,7 @@ if iswindows:
         from Crypto.Cipher import AES as _AES
         class AES(object):
             def __init__(self, key):
-                self._aes = _AES.new(key, _AES.MODE_CBC)
+                self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
             def decrypt(self, data):
                 return self._aes.decrypt(data)
         return AES
@@ -254,13 +296,9 @@ if iswindows:
         return CryptUnprotectData
     CryptUnprotectData = CryptUnprotectData()
 
-    def retrieve_key():
+    def retrieve_keys():
         if AES is None:
-            tkMessageBox.showerror(
-                "ADEPT Key",
-                "This script requires PyCrypto or OpenSSL which must be installed "
-                "separately.  Read the top-of-script comment for details.")
-            return False
+            raise ADEPTError("PyCrypto or OpenSSL must be installed")
         root = GetSystemDirectory().split('\\')[0] + '\\'
         serial = GetVolumeSerialNumber(root)
         vendor = cpuid0()
@@ -275,6 +313,7 @@ if iswindows:
         device = winreg.QueryValueEx(regkey, 'key')[0]
         keykey = CryptUnprotectData(device, entropy)
         userkey = None
+        keys = []
         try:
             plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH)
         except WindowsError:
@@ -296,19 +335,17 @@ if iswindows:
                 if ktype != 'privateLicenseKey':
                     continue
                 userkey = winreg.QueryValueEx(plkkey, 'value')[0]
-                break
-            if userkey is not None:
-                break
-        if userkey is None:
+                userkey = userkey.decode('base64')
+                aes = AES(keykey)
+                userkey = aes.decrypt(userkey)
+                userkey = userkey[26:-ord(userkey[-1])]
+                keys.append(userkey)
+        if len(keys) == 0:
             raise ADEPTError('Could not locate privateLicenseKey')
-        userkey = userkey.decode('base64')
-        aes = AES(keykey)
-        userkey = aes.decrypt(userkey)
-        userkey = userkey[26:-ord(userkey[-1])]
-        return userkey
-
-else:
+        return keys
+        
 
+elif isosx:
     import xml.etree.ElementTree as etree
     import subprocess
 
@@ -332,8 +369,8 @@ else:
         if os.path.exists(ActDatPath):
             return ActDatPath
         return None
-    
-    def retrieve_key():
+
+    def retrieve_keys():
         actpath = findActivationDat()
         if actpath is None:
             raise ADEPTError("Could not locate ADE activation")
@@ -343,4 +380,78 @@ else:
         userkey = tree.findtext(expr)
         userkey = userkey.decode('base64')
         userkey = userkey[26:]
-        return userkey
+        return [userkey]
+
+else:
+    def retrieve_keys(keypath):
+        raise ADEPTError("This script only supports Windows and Mac OS X.")
+        return []
+        
+def retrieve_key(keypath):
+    keys = retrieve_keys()
+    with open(keypath, 'wb') as f:
+        f.write(keys[0])
+    return True
+
+def extractKeyfile(keypath):
+    try:
+        success = retrieve_key(keypath)
+    except ADEPTError, e:
+        print "Key generation Error: " + str(e)
+        return 1
+    except Exception, e:
+        print "General Error: " + str(e)
+        return 1
+    if not success:
+        return 1
+    return 0
+
+
+def cli_main(argv=sys.argv):
+    keypath = argv[1]
+    return extractKeyfile(keypath)
+
+
+def main(argv=sys.argv):
+    import Tkinter
+    import Tkconstants
+    import tkMessageBox
+    import traceback
+
+    class ExceptionDialog(Tkinter.Frame):
+        def __init__(self, root, text):
+            Tkinter.Frame.__init__(self, root, border=5)
+            label = Tkinter.Label(self, text="Unexpected error:",
+                                  anchor=Tkconstants.W, justify=Tkconstants.LEFT)
+            label.pack(fill=Tkconstants.X, expand=0)
+            self.text = Tkinter.Text(self)
+            self.text.pack(fill=Tkconstants.BOTH, expand=1)
+    
+            self.text.insert(Tkconstants.END, text)
+
+
+    root = Tkinter.Tk()
+    root.withdraw()
+    progname = os.path.basename(argv[0])
+    keypath = os.path.abspath("adeptkey.der")
+    success = False
+    try:
+        success = retrieve_key(keypath)
+    except ADEPTError, e:
+        tkMessageBox.showerror("ADEPT Key", "Error: " + str(e))
+    except Exception:
+        root.wm_state('normal')
+        root.title('ADEPT Key')
+        text = traceback.format_exc()
+        ExceptionDialog(root, text).pack(fill=Tkconstants.BOTH, expand=1)
+        root.mainloop()
+    if not success:
+        return 1
+    tkMessageBox.showinfo(
+        "ADEPT Key", "Key successfully retrieved to %s" % (keypath))
+    return 0
+
+if __name__ == '__main__':
+    if len(sys.argv) > 1:
+        sys.exit(cli_main())
+    sys.exit(main())
diff --git a/Calibre_Plugins/ineptepub_plugin/outputfix.py b/Calibre_Plugins/ineptepub_plugin/outputfix.py
new file mode 100644 (file)
index 0000000..906c6e9
--- /dev/null
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+#
+# Adapted and simplified from the kitchen project
+#
+# Kitchen Project Copyright (c) 2012 Red Hat, Inc.
+#
+# kitchen 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.
+#
+# kitchen 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 kitchen; if not, see <http://www.gnu.org/licenses/>
+#
+# Authors:
+#   Toshio Kuratomi <toshio@fedoraproject.org>
+#   Seth Vidal
+#
+# Portions of code taken from yum/i18n.py and
+# python-fedora: fedora/textutils.py
+
+import codecs
+
+# returns a char string unchanged
+# returns a unicode string converted to a char string of the passed encoding
+# return the empty string for anything else
+def getwriter(encoding):
+    class _StreamWriter(codecs.StreamWriter):
+        def __init__(self, stream):
+            codecs.StreamWriter.__init__(self, stream, 'replace')
+
+        def encode(self, msg, errors='replace'):
+            if isinstance(msg, basestring):
+                if isinstance(msg, str):
+                    return (msg, len(msg))
+                return (msg.encode(self.encoding, 'replace'), len(msg))
+            return ('',0)
+
+    _StreamWriter.encoding = encoding
+    return _StreamWriter
diff --git a/Calibre_Plugins/ineptepub_plugin/zipfilerugged.py b/Calibre_Plugins/ineptepub_plugin/zipfilerugged.py
new file mode 100644 (file)
index 0000000..adf3c53
--- /dev/null
@@ -0,0 +1,1400 @@
+"""
+Read and write ZIP files.
+"""
+import struct, os, time, sys, shutil
+import binascii, cStringIO, stat
+import io
+import re
+
+try:
+    import zlib # We may need its compression method
+    crc32 = zlib.crc32
+except ImportError:
+    zlib = None
+    crc32 = binascii.crc32
+
+__all__ = ["BadZipfile", "error", "ZIP_STORED", "ZIP_DEFLATED", "is_zipfile",
+           "ZipInfo", "ZipFile", "PyZipFile", "LargeZipFile" ]
+
+class BadZipfile(Exception):
+    pass
+
+
+class LargeZipFile(Exception):
+    """
+    Raised when writing a zipfile, the zipfile requires ZIP64 extensions
+    and those extensions are disabled.
+    """
+
+error = BadZipfile      # The exception raised by this module
+
+ZIP64_LIMIT = (1 << 31) - 1
+ZIP_FILECOUNT_LIMIT = 1 << 16
+ZIP_MAX_COMMENT = (1 << 16) - 1
+
+# constants for Zip file compression methods
+ZIP_STORED = 0
+ZIP_DEFLATED = 8
+# Other ZIP compression methods not supported
+
+# Below are some formats and associated data for reading/writing headers using
+# the struct module.  The names and structures of headers/records are those used
+# in the PKWARE description of the ZIP file format:
+#     http://www.pkware.com/documents/casestudies/APPNOTE.TXT
+# (URL valid as of January 2008)
+
+# The "end of central directory" structure, magic number, size, and indices
+# (section V.I in the format document)
+structEndArchive = "<4s4H2LH"
+stringEndArchive = "PK\005\006"
+sizeEndCentDir = struct.calcsize(structEndArchive)
+
+_ECD_SIGNATURE = 0
+_ECD_DISK_NUMBER = 1
+_ECD_DISK_START = 2
+_ECD_ENTRIES_THIS_DISK = 3
+_ECD_ENTRIES_TOTAL = 4
+_ECD_SIZE = 5
+_ECD_OFFSET = 6
+_ECD_COMMENT_SIZE = 7
+# These last two indices are not part of the structure as defined in the
+# spec, but they are used internally by this module as a convenience
+_ECD_COMMENT = 8
+_ECD_LOCATION = 9
+
+# The "central directory" structure, magic number, size, and indices
+# of entries in the structure (section V.F in the format document)
+structCentralDir = "<4s4B4HL2L5H2L"
+stringCentralDir = "PK\001\002"
+sizeCentralDir = struct.calcsize(structCentralDir)
+
+# indexes of entries in the central directory structure
+_CD_SIGNATURE = 0
+_CD_CREATE_VERSION = 1
+_CD_CREATE_SYSTEM = 2
+_CD_EXTRACT_VERSION = 3
+_CD_EXTRACT_SYSTEM = 4
+_CD_FLAG_BITS = 5
+_CD_COMPRESS_TYPE = 6
+_CD_TIME = 7
+_CD_DATE = 8
+_CD_CRC = 9
+_CD_COMPRESSED_SIZE = 10
+_CD_UNCOMPRESSED_SIZE = 11
+_CD_FILENAME_LENGTH = 12
+_CD_EXTRA_FIELD_LENGTH = 13
+_CD_COMMENT_LENGTH = 14
+_CD_DISK_NUMBER_START = 15
+_CD_INTERNAL_FILE_ATTRIBUTES = 16
+_CD_EXTERNAL_FILE_ATTRIBUTES = 17
+_CD_LOCAL_HEADER_OFFSET = 18
+
+# The "local file header" structure, magic number, size, and indices
+# (section V.A in the format document)
+structFileHeader = "<4s2B4HL2L2H"
+stringFileHeader = "PK\003\004"
+sizeFileHeader = struct.calcsize(structFileHeader)
+
+_FH_SIGNATURE = 0
+_FH_EXTRACT_VERSION = 1
+_FH_EXTRACT_SYSTEM = 2
+_FH_GENERAL_PURPOSE_FLAG_BITS = 3
+_FH_COMPRESSION_METHOD = 4
+_FH_LAST_MOD_TIME = 5
+_FH_LAST_MOD_DATE = 6
+_FH_CRC = 7
+_FH_COMPRESSED_SIZE = 8
+_FH_UNCOMPRESSED_SIZE = 9
+_FH_FILENAME_LENGTH = 10
+_FH_EXTRA_FIELD_LENGTH = 11
+
+# The "Zip64 end of central directory locator" structure, magic number, and size
+structEndArchive64Locator = "<4sLQL"
+stringEndArchive64Locator = "PK\x06\x07"
+sizeEndCentDir64Locator = struct.calcsize(structEndArchive64Locator)
+
+# The "Zip64 end of central directory" record, magic number, size, and indices
+# (section V.G in the format document)
+structEndArchive64 = "<4sQ2H2L4Q"
+stringEndArchive64 = "PK\x06\x06"
+sizeEndCentDir64 = struct.calcsize(structEndArchive64)
+
+_CD64_SIGNATURE = 0
+_CD64_DIRECTORY_RECSIZE = 1
+_CD64_CREATE_VERSION = 2
+_CD64_EXTRACT_VERSION = 3
+_CD64_DISK_NUMBER = 4
+_CD64_DISK_NUMBER_START = 5
+_CD64_NUMBER_ENTRIES_THIS_DISK = 6
+_CD64_NUMBER_ENTRIES_TOTAL = 7
+_CD64_DIRECTORY_SIZE = 8
+_CD64_OFFSET_START_CENTDIR = 9
+
+def _check_zipfile(fp):
+    try:
+        if _EndRecData(fp):
+            return True         # file has correct magic number
+    except IOError:
+        pass
+    return False
+
+def is_zipfile(filename):
+    """Quickly see if a file is a ZIP file by checking the magic number.
+
+    The filename argument may be a file or file-like object too.
+    """
+    result = False
+    try:
+        if hasattr(filename, "read"):
+            result = _check_zipfile(fp=filename)
+        else:
+            with open(filename, "rb") as fp:
+                result = _check_zipfile(fp)
+    except IOError:
+        pass
+    return result
+
+def _EndRecData64(fpin, offset, endrec):
+    """
+    Read the ZIP64 end-of-archive records and use that to update endrec
+    """
+    fpin.seek(offset - sizeEndCentDir64Locator, 2)
+    data = fpin.read(sizeEndCentDir64Locator)
+    sig, diskno, reloff, disks = struct.unpack(structEndArchive64Locator, data)
+    if sig != stringEndArchive64Locator:
+        return endrec
+
+    if diskno != 0 or disks != 1:
+        raise BadZipfile("zipfiles that span multiple disks are not supported")
+
+    # Assume no 'zip64 extensible data'
+    fpin.seek(offset - sizeEndCentDir64Locator - sizeEndCentDir64, 2)
+    data = fpin.read(sizeEndCentDir64)
+    sig, sz, create_version, read_version, disk_num, disk_dir, \
+            dircount, dircount2, dirsize, diroffset = \
+            struct.unpack(structEndArchive64, data)
+    if sig != stringEndArchive64:
+        return endrec
+
+    # Update the original endrec using data from the ZIP64 record
+    endrec[_ECD_SIGNATURE] = sig
+    endrec[_ECD_DISK_NUMBER] = disk_num
+    endrec[_ECD_DISK_START] = disk_dir
+    endrec[_ECD_ENTRIES_THIS_DISK] = dircount
+    endrec[_ECD_ENTRIES_TOTAL] = dircount2
+    endrec[_ECD_SIZE] = dirsize
+    endrec[_ECD_OFFSET] = diroffset
+    return endrec
+
+
+def _EndRecData(fpin):
+    """Return data from the "End of Central Directory" record, or None.
+
+    The data is a list of the nine items in the ZIP "End of central dir"
+    record followed by a tenth item, the file seek offset of this record."""
+
+    # Determine file size
+    fpin.seek(0, 2)
+    filesize = fpin.tell()
+
+    # Check to see if this is ZIP file with no archive comment (the
+    # "end of central directory" structure should be the last item in the
+    # file if this is the case).
+    try:
+        fpin.seek(-sizeEndCentDir, 2)
+    except IOError:
+        return None
+    data = fpin.read()
+    if data[0:4] == stringEndArchive and data[-2:] == "\000\000":
+        # the signature is correct and there's no comment, unpack structure
+        endrec = struct.unpack(structEndArchive, data)
+        endrec=list(endrec)
+
+        # Append a blank comment and record start offset
+        endrec.append("")
+        endrec.append(filesize - sizeEndCentDir)
+
+        # Try to read the "Zip64 end of central directory" structure
+        return _EndRecData64(fpin, -sizeEndCentDir, endrec)
+
+    # Either this is not a ZIP file, or it is a ZIP file with an archive
+    # comment.  Search the end of the file for the "end of central directory"
+    # record signature. The comment is the last item in the ZIP file and may be
+    # up to 64K long.  It is assumed that the "end of central directory" magic
+    # number does not appear in the comment.
+    maxCommentStart = max(filesize - (1 << 16) - sizeEndCentDir, 0)
+    fpin.seek(maxCommentStart, 0)
+    data = fpin.read()
+    start = data.rfind(stringEndArchive)
+    if start >= 0:
+        # found the magic number; attempt to unpack and interpret
+        recData = data[start:start+sizeEndCentDir]
+        endrec = list(struct.unpack(structEndArchive, recData))
+        comment = data[start+sizeEndCentDir:]
+        # check that comment length is correct
+        if endrec[_ECD_COMMENT_SIZE] == len(comment):
+            # Append the archive comment and start offset
+            endrec.append(comment)
+            endrec.append(maxCommentStart + start)
+
+            # Try to read the "Zip64 end of central directory" structure
+            return _EndRecData64(fpin, maxCommentStart + start - filesize,
+                                 endrec)
+
+    # Unable to find a valid end of central directory structure
+    return
+
+
+class ZipInfo (object):
+    """Class with attributes describing each file in the ZIP archive."""
+
+    __slots__ = (
+            'orig_filename',
+            'filename',
+            'date_time',
+            'compress_type',
+            'comment',
+            'extra',
+            'create_system',
+            'create_version',
+            'extract_version',
+            'reserved',
+            'flag_bits',
+            'volume',
+            'internal_attr',
+            'external_attr',
+            'header_offset',
+            'CRC',
+            'compress_size',
+            'file_size',
+            '_raw_time',
+        )
+
+    def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)):
+        self.orig_filename = filename   # Original file name in archive
+
+        # Terminate the file name at the first null byte.  Null bytes in file
+        # names are used as tricks by viruses in archives.
+        null_byte = filename.find(chr(0))
+        if null_byte >= 0:
+            filename = filename[0:null_byte]
+        # This is used to ensure paths in generated ZIP files always use
+        # forward slashes as the directory separator, as required by the
+        # ZIP format specification.
+        if os.sep != "/" and os.sep in filename:
+            filename = filename.replace(os.sep, "/")
+
+        self.filename = filename        # Normalized file name
+        self.date_time = date_time      # year, month, day, hour, min, sec
+        # Standard values:
+        self.compress_type = ZIP_STORED # Type of compression for the file
+        self.comment = ""               # Comment for each file
+        self.extra = ""                 # ZIP extra data
+        if sys.platform == 'win32':
+            self.create_system = 0          # System which created ZIP archive
+        else:
+            # Assume everything else is unix-y
+            self.create_system = 3          # System which created ZIP archive
+        self.create_version = 20        # Version which created ZIP archive
+        self.extract_version = 20       # Version needed to extract archive
+        self.reserved = 0               # Must be zero
+        self.flag_bits = 0              # ZIP flag bits
+        self.volume = 0                 # Volume number of file header
+        self.internal_attr = 0          # Internal attributes
+        self.external_attr = 0          # External file attributes
+        # Other attributes are set by class ZipFile:
+        # header_offset         Byte offset to the file header
+        # CRC                   CRC-32 of the uncompressed file
+        # compress_size         Size of the compressed file
+        # file_size             Size of the uncompressed file
+
+    def FileHeader(self):
+        """Return the per-file header as a string."""
+        dt = self.date_time
+        dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2]
+        dostime = dt[3] << 11 | dt[4] << 5 | (dt[5] // 2)
+        if self.flag_bits & 0x08:
+            # Set these to zero because we write them after the file data
+            CRC = compress_size = file_size = 0
+        else:
+            CRC = self.CRC
+            compress_size = self.compress_size
+            file_size = self.file_size
+
+        extra = self.extra
+
+        if file_size > ZIP64_LIMIT or compress_size > ZIP64_LIMIT:
+            # File is larger than what fits into a 4 byte integer,
+            # fall back to the ZIP64 extension
+            fmt = '<HHQQ'
+            extra = extra + struct.pack(fmt,
+                    1, struct.calcsize(fmt)-4, file_size, compress_size)
+            file_size = 0xffffffff
+            compress_size = 0xffffffff
+            self.extract_version = max(45, self.extract_version)
+            self.create_version = max(45, self.extract_version)
+
+        filename, flag_bits = self._encodeFilenameFlags()
+        header = struct.pack(structFileHeader, stringFileHeader,
+                 self.extract_version, self.reserved, flag_bits,
+                 self.compress_type, dostime, dosdate, CRC,
+                 compress_size, file_size,
+                 len(filename), len(extra))
+        return header + filename + extra
+
+    def _encodeFilenameFlags(self):
+        if isinstance(self.filename, unicode):
+            try:
+                return self.filename.encode('ascii'), self.flag_bits
+            except UnicodeEncodeError:
+                return self.filename.encode('utf-8'), self.flag_bits | 0x800
+        else:
+            return self.filename, self.flag_bits
+
+    def _decodeFilename(self):
+        if self.flag_bits & 0x800:
+            try:
+                print "decoding filename",self.filename
+                return self.filename.decode('utf-8')
+            except:
+                return self.filename
+        else:
+            return self.filename
+
+    def _decodeExtra(self):
+        # Try to decode the extra field.
+        extra = self.extra
+        unpack = struct.unpack
+        while extra:
+            tp, ln = unpack('<HH', extra[:4])
+            if tp == 1:
+                if ln >= 24:
+                    counts = unpack('<QQQ', extra[4:28])
+                elif ln == 16:
+                    counts = unpack('<QQ', extra[4:20])
+                elif ln == 8:
+                    counts = unpack('<Q', extra[4:12])
+                elif ln == 0:
+                    counts = ()
+                else:
+                    raise RuntimeError, "Corrupt extra field %s"%(ln,)
+
+                idx = 0
+
+                # ZIP64 extension (large files and/or large archives)
+                if self.file_size in (0xffffffffffffffffL, 0xffffffffL):
+                    self.file_size = counts[idx]
+                    idx += 1
+
+                if self.compress_size == 0xFFFFFFFFL:
+                    self.compress_size = counts[idx]
+                    idx += 1
+
+                if self.header_offset == 0xffffffffL:
+                    old = self.header_offset
+                    self.header_offset = counts[idx]
+                    idx+=1
+
+            extra = extra[ln+4:]
+
+
+class _ZipDecrypter:
+    """Class to handle decryption of files stored within a ZIP archive.
+
+    ZIP supports a password-based form of encryption. Even though known
+    plaintext attacks have been found against it, it is still useful
+    to be able to get data out of such a file.
+
+    Usage:
+        zd = _ZipDecrypter(mypwd)
+        plain_char = zd(cypher_char)
+        plain_text = map(zd, cypher_text)
+    """
+
+    def _GenerateCRCTable():
+        """Generate a CRC-32 table.
+
+        ZIP encryption uses the CRC32 one-byte primitive for scrambling some
+        internal keys. We noticed that a direct implementation is faster than
+        relying on binascii.crc32().
+        """
+        poly = 0xedb88320
+        table = [0] * 256
+        for i in range(256):
+            crc = i
+            for j in range(8):
+                if crc & 1:
+                    crc = ((crc >> 1) & 0x7FFFFFFF) ^ poly
+                else:
+                    crc = ((crc >> 1) & 0x7FFFFFFF)
+            table[i] = crc
+        return table
+    crctable = _GenerateCRCTable()
+
+    def _crc32(self, ch, crc):
+        """Compute the CRC32 primitive on one byte."""
+        return ((crc >> 8) & 0xffffff) ^ self.crctable[(crc ^ ord(ch)) & 0xff]
+
+    def __init__(self, pwd):
+        self.key0 = 305419896
+        self.key1 = 591751049
+        self.key2 = 878082192
+        for p in pwd:
+            self._UpdateKeys(p)
+
+    def _UpdateKeys(self, c):
+        self.key0 = self._crc32(c, self.key0)
+        self.key1 = (self.key1 + (self.key0 & 255)) & 4294967295
+        self.key1 = (self.key1 * 134775813 + 1) & 4294967295
+        self.key2 = self._crc32(chr((self.key1 >> 24) & 255), self.key2)
+
+    def __call__(self, c):
+        """Decrypt a single character."""
+        c = ord(c)
+        k = self.key2 | 2
+        c = c ^ (((k * (k^1)) >> 8) & 255)
+        c = chr(c)
+        self._UpdateKeys(c)
+        return c
+
+class ZipExtFile(io.BufferedIOBase):
+    """File-like object for reading an archive member.
+       Is returned by ZipFile.open().
+    """
+
+    # Max size supported by decompressor.
+    MAX_N = 1 << 31 - 1
+
+    # Read from compressed files in 4k blocks.
+    MIN_READ_SIZE = 4096
+
+    # Search for universal newlines or line chunks.
+    PATTERN = re.compile(r'^(?P<chunk>[^\r\n]+)|(?P<newline>\n|\r\n?)')
+
+    def __init__(self, fileobj, mode, zipinfo, decrypter=None):
+        self._fileobj = fileobj
+        self._decrypter = decrypter
+
+        self._compress_type = zipinfo.compress_type
+        self._compress_size = zipinfo.compress_size
+        self._compress_left = zipinfo.compress_size
+
+        if self._compress_type == ZIP_DEFLATED:
+            self._decompressor = zlib.decompressobj(-15)
+        self._unconsumed = ''
+
+        self._readbuffer = ''
+        self._offset = 0
+
+        self._universal = 'U' in mode
+        self.newlines = None
+
+        # Adjust read size for encrypted files since the first 12 bytes
+        # are for the encryption/password information.
+        if self._decrypter is not None:
+            self._compress_left -= 12
+
+        self.mode = mode
+        self.name = zipinfo.filename
+
+    def readline(self, limit=-1):
+        """Read and return a line from the stream.
+
+        If limit is specified, at most limit bytes will be read.
+        """
+
+        if not self._universal and limit < 0:
+            # Shortcut common case - newline found in buffer.
+            i = self._readbuffer.find('\n', self._offset) + 1
+            if i > 0:
+                line = self._readbuffer[self._offset: i]
+                self._offset = i
+                return line
+
+        if not self._universal:
+            return io.BufferedIOBase.readline(self, limit)
+
+        line = ''
+        while limit < 0 or len(line) < limit:
+            readahead = self.peek(2)
+            if readahead == '':
+                return line
+
+            #
+            # Search for universal newlines or line chunks.
+            #
+            # The pattern returns either a line chunk or a newline, but not
+            # both. Combined with peek(2), we are assured that the sequence
+            # '\r\n' is always retrieved completely and never split into
+            # separate newlines - '\r', '\n' due to coincidental readaheads.
+            #
+            match = self.PATTERN.search(readahead)
+            newline = match.group('newline')
+            if newline is not None:
+                if self.newlines is None:
+                    self.newlines = []
+                if newline not in self.newlines:
+                    self.newlines.append(newline)
+                self._offset += len(newline)
+                return line + '\n'
+
+            chunk = match.group('chunk')
+            if limit >= 0:
+                chunk = chunk[: limit - len(line)]
+
+            self._offset += len(chunk)
+            line += chunk
+
+        return line
+
+    def peek(self, n=1):
+        """Returns buffered bytes without advancing the position."""
+        if n > len(self._readbuffer) - self._offset:
+            chunk = self.read(n)
+            self._offset -= len(chunk)
+
+        # Return up to 512 bytes to reduce allocation overhead for tight loops.
+        return self._readbuffer[self._offset: self._offset + 512]
+
+    def readable(self):
+        return True
+
+    def read(self, n=-1):
+        """Read and return up to n bytes.
+        If the argument is omitted, None, or negative, data is read and returned until EOF is reached..
+        """
+
+        buf = ''
+        while n < 0 or n is None or n > len(buf):
+            data = self.read1(n)
+            if len(data) == 0:
+                return buf
+
+            buf += data
+
+        return buf
+
+    def read1(self, n):
+        """Read up to n bytes with at most one read() system call."""
+
+        # Simplify algorithm (branching) by transforming negative n to large n.
+        if n < 0 or n is None:
+            n = self.MAX_N
+
+        # Bytes available in read buffer.
+        len_readbuffer = len(self._readbuffer) - self._offset
+
+        # Read from file.
+        if self._compress_left > 0 and n > len_readbuffer + len(self._unconsumed):
+            nbytes = n - len_readbuffer - len(self._unconsumed)
+            nbytes = max(nbytes, self.MIN_READ_SIZE)
+            nbytes = min(nbytes, self._compress_left)
+
+            data = self._fileobj.read(nbytes)
+            self._compress_left -= len(data)
+
+            if data and self._decrypter is not None:
+                data = ''.join(map(self._decrypter, data))
+
+            if self._compress_type == ZIP_STORED:
+                self._readbuffer = self._readbuffer[self._offset:] + data
+                self._offset = 0
+            else:
+                # Prepare deflated bytes for decompression.
+                self._unconsumed += data
+
+        # Handle unconsumed data.
+        if (len(self._unconsumed) > 0 and n > len_readbuffer and
+            self._compress_type == ZIP_DEFLATED):
+            data = self._decompressor.decompress(
+                self._unconsumed,
+                max(n - len_readbuffer, self.MIN_READ_SIZE)
+            )
+
+            self._unconsumed = self._decompressor.unconsumed_tail
+            if len(self._unconsumed) == 0 and self._compress_left == 0:
+                data += self._decompressor.flush()
+
+            self._readbuffer = self._readbuffer[self._offset:] + data
+            self._offset = 0
+
+        # Read from buffer.
+        data = self._readbuffer[self._offset: self._offset + n]
+        self._offset += len(data)
+        return data
+
+
+
+class ZipFile:
+    """ Class with methods to open, read, write, close, list zip files.
+
+    z = ZipFile(file, mode="r", compression=ZIP_STORED, allowZip64=False)
+
+    file: Either the path to the file, or a file-like object.
+          If it is a path, the file will be opened and closed by ZipFile.
+    mode: The mode can be either read "r", write "w" or append "a".
+    compression: ZIP_STORED (no compression) or ZIP_DEFLATED (requires zlib).
+    allowZip64: if True ZipFile will create files with ZIP64 extensions when
+                needed, otherwise it will raise an exception when this would
+                be necessary.
+
+    """
+
+    fp = None                   # Set here since __del__ checks it
+
+    def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=False):
+        """Open the ZIP file with mode read "r", write "w" or append "a"."""
+        if mode not in ("r", "w", "a"):
+            raise RuntimeError('ZipFile() requires mode "r", "w", or "a"')
+
+        if compression == ZIP_STORED:
+            pass
+        elif compression == ZIP_DEFLATED:
+            if not zlib:
+                raise RuntimeError,\
+                      "Compression requires the (missing) zlib module"
+        else:
+            raise RuntimeError, "That compression method is not supported"
+
+        self._allowZip64 = allowZip64
+        self._didModify = False
+        self.debug = 0  # Level of printing: 0 through 3
+        self.NameToInfo = {}    # Find file info given name
+        self.filelist = []      # List of ZipInfo instances for archive
+        self.compression = compression  # Method of compression
+        self.mode = key = mode.replace('b', '')[0]
+        self.pwd = None
+        self.comment = ''
+
+        # Check if we were passed a file-like object
+        if isinstance(file, basestring):
+            self._filePassed = 0
+            self.filename = file
+            modeDict = {'r' : 'rb', 'w': 'wb', 'a' : 'r+b'}
+            try:
+                self.fp = open(file, modeDict[mode])
+            except IOError:
+                if mode == 'a':
+                    mode = key = 'w'
+                    self.fp = open(file, modeDict[mode])
+                else:
+                    raise
+        else:
+            self._filePassed = 1
+            self.fp = file
+            self.filename = getattr(file, 'name', None)
+
+        if key == 'r':
+            self._GetContents()
+        elif key == 'w':
+            pass
+        elif key == 'a':
+            try:                        # See if file is a zip file
+                self._RealGetContents()
+                # seek to start of directory and overwrite
+                self.fp.seek(self.start_dir, 0)
+            except BadZipfile:          # file is not a zip file, just append
+                self.fp.seek(0, 2)
+        else:
+            if not self._filePassed:
+                self.fp.close()
+                self.fp = None
+            raise RuntimeError, 'Mode must be "r", "w" or "a"'
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, traceback):
+        self.close()
+
+    def _GetContents(self):
+        """Read the directory, making sure we close the file if the format
+        is bad."""
+        try:
+            self._RealGetContents()
+        except BadZipfile:
+            if not self._filePassed:
+                self.fp.close()
+                self.fp = None
+            raise
+
+    def _RealGetContents(self):
+        """Read in the table of contents for the ZIP file."""
+        fp = self.fp
+        endrec = _EndRecData(fp)
+        if not endrec:
+            raise BadZipfile, "File is not a zip file"
+        if self.debug > 1:
+            print endrec
+        size_cd = endrec[_ECD_SIZE]             # bytes in central directory
+        offset_cd = endrec[_ECD_OFFSET]         # offset of central directory
+        self.comment = endrec[_ECD_COMMENT]     # archive comment
+
+        # "concat" is zero, unless zip was concatenated to another file
+        concat = endrec[_ECD_LOCATION] - size_cd - offset_cd
+        if endrec[_ECD_SIGNATURE] == stringEndArchive64:
+            # If Zip64 extension structures are present, account for them
+            concat -= (sizeEndCentDir64 + sizeEndCentDir64Locator)
+
+        if self.debug > 2:
+            inferred = concat + offset_cd
+            print "given, inferred, offset", offset_cd, inferred, concat
+        # self.start_dir:  Position of start of central directory
+        self.start_dir = offset_cd + concat
+        fp.seek(self.start_dir, 0)
+        data = fp.read(size_cd)
+        fp = cStringIO.StringIO(data)
+        total = 0
+        while total < size_cd:
+            centdir = fp.read(sizeCentralDir)
+            if centdir[0:4] != stringCentralDir:
+                raise BadZipfile, "Bad magic number for central directory"
+            centdir = struct.unpack(structCentralDir, centdir)
+            if self.debug > 2:
+                print centdir
+            filename = fp.read(centdir[_CD_FILENAME_LENGTH])
+            # Create ZipInfo instance to store file information
+            x = ZipInfo(filename)
+            x.extra = fp.read(centdir[_CD_EXTRA_FIELD_LENGTH])
+            x.comment = fp.read(centdir[_CD_COMMENT_LENGTH])
+            x.header_offset = centdir[_CD_LOCAL_HEADER_OFFSET]
+            (x.create_version, x.create_system, x.extract_version, x.reserved,
+                x.flag_bits, x.compress_type, t, d,
+                x.CRC, x.compress_size, x.file_size) = centdir[1:12]
+            x.volume, x.internal_attr, x.external_attr = centdir[15:18]
+            # Convert date/time code to (year, month, day, hour, min, sec)
+            x._raw_time = t
+            x.date_time = ( (d>>9)+1980, (d>>5)&0xF, d&0x1F,
+                                     t>>11, (t>>5)&0x3F, (t&0x1F) * 2 )
+
+            x._decodeExtra()
+            x.header_offset = x.header_offset + concat
+            x.filename = x._decodeFilename()
+            self.filelist.append(x)
+            self.NameToInfo[x.filename] = x
+
+            # update total bytes read from central directory
+            total = (total + sizeCentralDir + centdir[_CD_FILENAME_LENGTH]
+                     + centdir[_CD_EXTRA_FIELD_LENGTH]
+                     + centdir[_CD_COMMENT_LENGTH])
+
+            if self.debug > 2:
+                print "total", total
+
+
+    def namelist(self):
+        """Return a list of file names in the archive."""
+        l = []
+        for data in self.filelist:
+            l.append(data.filename)
+        return l
+
+    def infolist(self):
+        """Return a list of class ZipInfo instances for files in the
+        archive."""
+        return self.filelist
+
+    def printdir(self):
+        """Print a table of contents for the zip file."""
+        print "%-46s %19s %12s" % ("File Name", "Modified    ", "Size")
+        for zinfo in self.filelist:
+            date = "%d-%02d-%02d %02d:%02d:%02d" % zinfo.date_time[:6]
+            print "%-46s %s %12d" % (zinfo.filename, date, zinfo.file_size)
+
+    def testzip(self):
+        """Read all the files and check the CRC."""
+        chunk_size = 2 ** 20
+        for zinfo in self.filelist:
+            try:
+                # Read by chunks, to avoid an OverflowError or a
+                # MemoryError with very large embedded files.
+                f = self.open(zinfo.filename, "r")
+                while f.read(chunk_size):     # Check CRC-32
+                    pass
+            except BadZipfile:
+                return zinfo.filename
+
+    def getinfo(self, name):
+        """Return the instance of ZipInfo given 'name'."""
+        info = self.NameToInfo.get(name)
+        if info is None:
+            raise KeyError(
+                'There is no item named %r in the archive' % name)
+
+        return info
+
+    def setpassword(self, pwd):
+        """Set default password for encrypted files."""
+        self.pwd = pwd
+
+    def read(self, name, pwd=None):
+        """Return file bytes (as a string) for name."""
+        return self.open(name, "r", pwd).read()
+
+    def open(self, name, mode="r", pwd=None):
+        """Return file-like object for 'name'."""
+        if mode not in ("r", "U", "rU"):
+            raise RuntimeError, 'open() requires mode "r", "U", or "rU"'
+        if not self.fp:
+            raise RuntimeError, \
+                  "Attempt to read ZIP archive that was already closed"
+
+        # Only open a new file for instances where we were not
+        # given a file object in the constructor
+        if self._filePassed:
+            zef_file = self.fp
+        else:
+            zef_file = open(self.filename, 'rb')
+
+        # Make sure we have an info object
+        if isinstance(name, ZipInfo):
+            # 'name' is already an info object
+            zinfo = name
+        else:
+            # Get info object for name
+            zinfo = self.getinfo(name)
+
+        zef_file.seek(zinfo.header_offset, 0)
+
+        # Skip the file header:
+        fheader = zef_file.read(sizeFileHeader)
+        if fheader[0:4] != stringFileHeader:
+            raise BadZipfile, "Bad magic number for file header"
+
+        fheader = struct.unpack(structFileHeader, fheader)
+        fname = zef_file.read(fheader[_FH_FILENAME_LENGTH])
+        if fheader[_FH_EXTRA_FIELD_LENGTH]:
+            zef_file.read(fheader[_FH_EXTRA_FIELD_LENGTH])
+
+        if fname != zinfo.orig_filename:
+            raise BadZipfile, \
+                      'File name in directory "%s" and header "%s" differ.' % (
+                          zinfo.orig_filename, fname)
+
+        # check for encrypted flag & handle password
+        is_encrypted = zinfo.flag_bits & 0x1
+        zd = None
+        if is_encrypted:
+            if not pwd:
+                pwd = self.pwd
+            if not pwd:
+                raise RuntimeError, "File %s is encrypted, " \
+                      "password required for extraction" % name
+
+            zd = _ZipDecrypter(pwd)
+            # The first 12 bytes in the cypher stream is an encryption header
+            #  used to strengthen the algorithm. The first 11 bytes are
+            #  completely random, while the 12th contains the MSB of the CRC,
+            #  or the MSB of the file time depending on the header type
+            #  and is used to check the correctness of the password.
+            bytes = zef_file.read(12)
+            h = map(zd, bytes[0:12])
+            if zinfo.flag_bits & 0x8:
+                # compare against the file type from extended local headers
+                check_byte = (zinfo._raw_time >> 8) & 0xff
+            else:
+                # compare against the CRC otherwise
+                check_byte = (zinfo.CRC >> 24) & 0xff
+            if ord(h[11]) != check_byte:
+                raise RuntimeError("Bad password for file", name)
+
+        return  ZipExtFile(zef_file, mode, zinfo, zd)
+
+    def extract(self, member, path=None, pwd=None):
+        """Extract a member from the archive to the current working directory,
+           using its full name. Its file information is extracted as accurately
+           as possible. `member' may be a filename or a ZipInfo object. You can
+           specify a different directory using `path'.
+        """
+        if not isinstance(member, ZipInfo):
+            member = self.getinfo(member)
+
+        if path is None:
+            path = os.getcwd()
+
+        return self._extract_member(member, path, pwd)
+
+    def extractall(self, path=None, members=None, pwd=None):
+        """Extract all members from the archive to the current working
+           directory. `path' specifies a different directory to extract to.
+           `members' is optional and must be a subset of the list returned
+           by namelist().
+        """
+        if members is None:
+            members = self.namelist()
+
+        for zipinfo in members:
+            self.extract(zipinfo, path, pwd)
+
+    def _extract_member(self, member, targetpath, pwd):
+        """Extract the ZipInfo object 'member' to a physical
+           file on the path targetpath.
+        """
+        # build the destination pathname, replacing
+        # forward slashes to platform specific separators.
+        # Strip trailing path separator, unless it represents the root.
+        if (targetpath[-1:] in (os.path.sep, os.path.altsep)
+            and len(os.path.splitdrive(targetpath)[1]) > 1):
+            targetpath = targetpath[:-1]
+
+        # don't include leading "/" from file name if present
+        if member.filename[0] == '/':
+            targetpath = os.path.join(targetpath, member.filename[1:])
+        else:
+            targetpath = os.path.join(targetpath, member.filename)
+
+        targetpath = os.path.normpath(targetpath)
+
+        # Create all upper directories if necessary.
+        upperdirs = os.path.dirname(targetpath)
+        if upperdirs and not os.path.exists(upperdirs):
+            os.makedirs(upperdirs)
+
+        if member.filename[-1] == '/':
+            if not os.path.isdir(targetpath):
+                os.mkdir(targetpath)
+            return targetpath
+
+        source = self.open(member, pwd=pwd)
+        target = file(targetpath, "wb")
+        shutil.copyfileobj(source, target)
+        source.close()
+        target.close()
+
+        return targetpath
+
+    def _writecheck(self, zinfo):
+        """Check for errors before writing a file to the archive."""
+        if zinfo.filename in self.NameToInfo:
+            if self.debug:      # Warning for duplicate names
+                print "Duplicate name:", zinfo.filename
+        if self.mode not in ("w", "a"):
+            raise RuntimeError, 'write() requires mode "w" or "a"'
+        if not self.fp:
+            raise RuntimeError, \
+                  "Attempt to write ZIP archive that was already closed"
+        if zinfo.compress_type == ZIP_DEFLATED and not zlib:
+            raise RuntimeError, \
+                  "Compression requires the (missing) zlib module"
+        if zinfo.compress_type not in (ZIP_STORED, ZIP_DEFLATED):
+            raise RuntimeError, \
+                  "That compression method is not supported"
+        if zinfo.file_size > ZIP64_LIMIT:
+            if not self._allowZip64:
+                raise LargeZipFile("Filesize would require ZIP64 extensions")
+        if zinfo.header_offset > ZIP64_LIMIT:
+            if not self._allowZip64:
+                raise LargeZipFile("Zipfile size would require ZIP64 extensions")
+
+    def write(self, filename, arcname=None, compress_type=None):
+        """Put the bytes from filename into the archive under the name
+        arcname."""
+        if not self.fp:
+            raise RuntimeError(
+                  "Attempt to write to ZIP archive that was already closed")
+
+        st = os.stat(filename)
+        isdir = stat.S_ISDIR(st.st_mode)
+        mtime = time.localtime(st.st_mtime)
+        date_time = mtime[0:6]
+        # Create ZipInfo instance to store file information
+        if arcname is None:
+            arcname = filename
+        arcname = os.path.normpath(os.path.splitdrive(arcname)[1])
+        while arcname[0] in (os.sep, os.altsep):
+            arcname = arcname[1:]
+        if isdir:
+            arcname += '/'
+        zinfo = ZipInfo(arcname, date_time)
+        zinfo.external_attr = (st[0] & 0xFFFF) << 16L      # Unix attributes
+        if compress_type is None:
+            zinfo.compress_type = self.compression
+        else:
+            zinfo.compress_type = compress_type
+
+        zinfo.file_size = st.st_size
+        zinfo.flag_bits = 0x00
+        zinfo.header_offset = self.fp.tell()    # Start of header bytes
+
+        self._writecheck(zinfo)
+        self._didModify = True
+
+        if isdir:
+            zinfo.file_size = 0
+            zinfo.compress_size = 0
+            zinfo.CRC = 0
+            self.filelist.append(zinfo)
+            self.NameToInfo[zinfo.filename] = zinfo
+            self.fp.write(zinfo.FileHeader())
+            return
+
+        with open(filename, "rb") as fp:
+            # Must overwrite CRC and sizes with correct data later
+            zinfo.CRC = CRC = 0
+            zinfo.compress_size = compress_size = 0
+            zinfo.file_size = file_size = 0
+            self.fp.write(zinfo.FileHeader())
+            if zinfo.compress_type == ZIP_DEFLATED:
+                cmpr = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION,
+                     zlib.DEFLATED, -15)
+            else:
+                cmpr = None
+            while 1:
+                buf = fp.read(1024 * 8)
+                if not buf:
+                    break
+                file_size = file_size + len(buf)
+                CRC = crc32(buf, CRC) & 0xffffffff
+                if cmpr:
+                    buf = cmpr.compress(buf)
+                    compress_size = compress_size + len(buf)
+                self.fp.write(buf)
+        if cmpr:
+            buf = cmpr.flush()
+            compress_size = compress_size + len(buf)
+            self.fp.write(buf)
+            zinfo.compress_size = compress_size
+        else:
+            zinfo.compress_size = file_size
+        zinfo.CRC = CRC
+        zinfo.file_size = file_size
+        # Seek backwards and write CRC and file sizes
+        position = self.fp.tell()       # Preserve current position in file
+        self.fp.seek(zinfo.header_offset + 14, 0)
+        self.fp.write(struct.pack("<LLL", zinfo.CRC, zinfo.compress_size,
+              zinfo.file_size))
+        self.fp.seek(position, 0)
+        self.filelist.append(zinfo)
+        self.NameToInfo[zinfo.filename] = zinfo
+
+    def writestr(self, zinfo_or_arcname, bytes, compress_type=None):
+        """Write a file into the archive.  The contents is the string
+        'bytes'.  'zinfo_or_arcname' is either a ZipInfo instance or
+        the name of the file in the archive."""
+        if not isinstance(zinfo_or_arcname, ZipInfo):
+            zinfo = ZipInfo(filename=zinfo_or_arcname,
+                            date_time=time.localtime(time.time())[:6])
+
+            zinfo.compress_type = self.compression
+            zinfo.external_attr = 0600 << 16
+        else:
+            zinfo = zinfo_or_arcname
+
+        if not self.fp:
+            raise RuntimeError(
+                  "Attempt to write to ZIP archive that was already closed")
+
+        if compress_type is not None:
+            zinfo.compress_type = compress_type
+
+        zinfo.file_size = len(bytes)            # Uncompressed size
+        zinfo.header_offset = self.fp.tell()    # Start of header bytes
+        self._writecheck(zinfo)
+        self._didModify = True
+        zinfo.CRC = crc32(bytes) & 0xffffffff       # CRC-32 checksum
+        if zinfo.compress_type == ZIP_DEFLATED:
+            co = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION,
+                 zlib.DEFLATED, -15)
+            bytes = co.compress(bytes) + co.flush()
+            zinfo.compress_size = len(bytes)    # Compressed size
+        else:
+            zinfo.compress_size = zinfo.file_size
+        zinfo.header_offset = self.fp.tell()    # Start of header bytes
+        self.fp.write(zinfo.FileHeader())
+        self.fp.write(bytes)
+        self.fp.flush()
+        if zinfo.flag_bits & 0x08:
+            # Write CRC and file sizes after the file data
+            self.fp.write(struct.pack("<LLL", zinfo.CRC, zinfo.compress_size,
+                  zinfo.file_size))
+        self.filelist.append(zinfo)
+        self.NameToInfo[zinfo.filename] = zinfo
+
+    def __del__(self):
+        """Call the "close()" method in case the user forgot."""
+        self.close()
+
+    def close(self):
+        """Close the file, and for mode "w" and "a" write the ending
+        records."""
+        if self.fp is None:
+            return
+
+        if self.mode in ("w", "a") and self._didModify: # write ending records
+            count = 0
+            pos1 = self.fp.tell()
+            for zinfo in self.filelist:         # write central directory
+                count = count + 1
+                dt = zinfo.date_time
+                dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2]
+                dostime = dt[3] << 11 | dt[4] << 5 | (dt[5] // 2)
+                extra = []
+                if zinfo.file_size > ZIP64_LIMIT \
+                        or zinfo.compress_size > ZIP64_LIMIT:
+                    extra.append(zinfo.file_size)
+                    extra.append(zinfo.compress_size)
+                    file_size = 0xffffffff
+                    compress_size = 0xffffffff
+                else:
+                    file_size = zinfo.file_size
+                    compress_size = zinfo.compress_size
+
+                if zinfo.header_offset > ZIP64_LIMIT:
+                    extra.append(zinfo.header_offset)
+                    header_offset = 0xffffffffL
+                else:
+                    header_offset = zinfo.header_offset
+
+                extra_data = zinfo.extra
+                if extra:
+                    # Append a ZIP64 field to the extra's
+                    extra_data = struct.pack(
+                            '<HH' + 'Q'*len(extra),
+                            1, 8*len(extra), *extra) + extra_data
+
+                    extract_version = max(45, zinfo.extract_version)
+                    create_version = max(45, zinfo.create_version)
+                else:
+                    extract_version = zinfo.extract_version
+                    create_version = zinfo.create_version
+
+                try:
+                    filename, flag_bits = zinfo._encodeFilenameFlags()
+                    centdir = struct.pack(structCentralDir,
+                     stringCentralDir, create_version,
+                     zinfo.create_system, extract_version, zinfo.reserved,
+                     flag_bits, zinfo.compress_type, dostime, dosdate,
+                     zinfo.CRC, compress_size, file_size,
+                     len(filename), len(extra_data), len(zinfo.comment),
+                     0, zinfo.internal_attr, zinfo.external_attr,
+                     header_offset)
+                except DeprecationWarning:
+                    print >>sys.stderr, (structCentralDir,
+                     stringCentralDir, create_version,
+                     zinfo.create_system, extract_version, zinfo.reserved,
+                     zinfo.flag_bits, zinfo.compress_type, dostime, dosdate,
+                     zinfo.CRC, compress_size, file_size,
+                     len(zinfo.filename), len(extra_data), len(zinfo.comment),
+                     0, zinfo.internal_attr, zinfo.external_attr,
+                     header_offset)
+                    raise
+                self.fp.write(centdir)
+                self.fp.write(filename)
+                self.fp.write(extra_data)
+                self.fp.write(zinfo.comment)
+
+            pos2 = self.fp.tell()
+            # Write end-of-zip-archive record
+            centDirCount = count
+            centDirSize = pos2 - pos1
+            centDirOffset = pos1
+            if (centDirCount >= ZIP_FILECOUNT_LIMIT or
+                centDirOffset > ZIP64_LIMIT or
+                centDirSize > ZIP64_LIMIT):
+                # Need to write the ZIP64 end-of-archive records
+                zip64endrec = struct.pack(
+                        structEndArchive64, stringEndArchive64,
+                        44, 45, 45, 0, 0, centDirCount, centDirCount,
+                        centDirSize, centDirOffset)
+                self.fp.write(zip64endrec)
+
+                zip64locrec = struct.pack(
+                        structEndArchive64Locator,
+                        stringEndArchive64Locator, 0, pos2, 1)
+                self.fp.write(zip64locrec)
+                centDirCount = min(centDirCount, 0xFFFF)
+                centDirSize = min(centDirSize, 0xFFFFFFFF)
+                centDirOffset = min(centDirOffset, 0xFFFFFFFF)
+
+            # check for valid comment length
+            if len(self.comment) >= ZIP_MAX_COMMENT:
+                if self.debug > 0:
+                    msg = 'Archive comment is too long; truncating to %d bytes' \
+                          % ZIP_MAX_COMMENT
+                self.comment = self.comment[:ZIP_MAX_COMMENT]
+
+            endrec = struct.pack(structEndArchive, stringEndArchive,
+                                 0, 0, centDirCount, centDirCount,
+                                 centDirSize, centDirOffset, len(self.comment))
+            self.fp.write(endrec)
+            self.fp.write(self.comment)
+            self.fp.flush()
+
+        if not self._filePassed:
+            self.fp.close()
+        self.fp = None
+
+
+class PyZipFile(ZipFile):
+    """Class to create ZIP archives with Python library files and packages."""
+
+    def writepy(self, pathname, basename = ""):
+        """Add all files from "pathname" to the ZIP archive.
+
+        If pathname is a package directory, search the directory and
+        all package subdirectories recursively for all *.py and enter
+        the modules into the archive.  If pathname is a plain
+        directory, listdir *.py and enter all modules.  Else, pathname
+        must be a Python *.py file and the module will be put into the
+        archive.  Added modules are always module.pyo or module.pyc.
+        This method will compile the module.py into module.pyc if
+        necessary.
+        """
+        dir, name = os.path.split(pathname)
+        if os.path.isdir(pathname):
+            initname = os.path.join(pathname, "__init__.py")
+            if os.path.isfile(initname):
+                # This is a package directory, add it
+                if basename:
+                    basename = "%s/%s" % (basename, name)
+                else:
+                    basename = name
+                if self.debug:
+                    print "Adding package in", pathname, "as", basename
+                fname, arcname = self._get_codename(initname[0:-3], basename)
+                if self.debug:
+                    print "Adding", arcname
+                self.write(fname, arcname)
+                dirlist = os.listdir(pathname)
+                dirlist.remove("__init__.py")
+                # Add all *.py files and package subdirectories
+                for filename in dirlist:
+                    path = os.path.join(pathname, filename)
+                    root, ext = os.path.splitext(filename)
+                    if os.path.isdir(path):
+                        if os.path.isfile(os.path.join(path, "__init__.py")):
+                            # This is a package directory, add it
+                            self.writepy(path, basename)  # Recursive call
+                    elif ext == ".py":
+                        fname, arcname = self._get_codename(path[0:-3],
+                                         basename)
+                        if self.debug:
+                            print "Adding", arcname
+                        self.write(fname, arcname)
+            else:
+                # This is NOT a package directory, add its files at top level
+                if self.debug:
+                    print "Adding files from directory", pathname
+                for filename in os.listdir(pathname):
+                    path = os.path.join(pathname, filename)
+                    root, ext = os.path.splitext(filename)
+                    if ext == ".py":
+                        fname, arcname = self._get_codename(path[0:-3],
+                                         basename)
+                        if self.debug:
+                            print "Adding", arcname
+                        self.write(fname, arcname)
+        else:
+            if pathname[-3:] != ".py":
+                raise RuntimeError, \
+                      'Files added with writepy() must end with ".py"'
+            fname, arcname = self._get_codename(pathname[0:-3], basename)
+            if self.debug:
+                print "Adding file", arcname
+            self.write(fname, arcname)
+
+    def _get_codename(self, pathname, basename):
+        """Return (filename, archivename) for the path.
+
+        Given a module name path, return the correct file path and
+        archive name, compiling if necessary.  For example, given
+        /python/lib/string, return (/python/lib/string.pyc, string).
+        """
+        file_py  = pathname + ".py"
+        file_pyc = pathname + ".pyc"
+        file_pyo = pathname + ".pyo"
+        if os.path.isfile(file_pyo) and \
+                            os.stat(file_pyo).st_mtime >= os.stat(file_py).st_mtime:
+            fname = file_pyo    # Use .pyo file
+        elif not os.path.isfile(file_pyc) or \
+             os.stat(file_pyc).st_mtime < os.stat(file_py).st_mtime:
+            import py_compile
+            if self.debug:
+                print "Compiling", file_py
+            try:
+                py_compile.compile(file_py, file_pyc, None, True)
+            except py_compile.PyCompileError,err:
+                print err.msg
+            fname = file_pyc
+        else:
+            fname = file_pyc
+        archivename = os.path.split(fname)[1]
+        if basename:
+            archivename = "%s/%s" % (basename, archivename)
+        return (fname, archivename)
+
+
+def main(args = None):
+    import textwrap
+    USAGE=textwrap.dedent("""\
+        Usage:
+            zipfile.py -l zipfile.zip        # Show listing of a zipfile
+            zipfile.py -t zipfile.zip        # Test if a zipfile is valid
+            zipfile.py -e zipfile.zip target # Extract zipfile into target dir
+            zipfile.py -c zipfile.zip src ... # Create zipfile from sources
+        """)
+    if args is None:
+        args = sys.argv[1:]
+
+    if not args or args[0] not in ('-l', '-c', '-e', '-t'):
+        print USAGE
+        sys.exit(1)
+
+    if args[0] == '-l':
+        if len(args) != 2:
+            print USAGE
+            sys.exit(1)
+        zf = ZipFile(args[1], 'r')
+        zf.printdir()
+        zf.close()
+
+    elif args[0] == '-t':
+        if len(args) != 2:
+            print USAGE
+            sys.exit(1)
+        zf = ZipFile(args[1], 'r')
+        zf.testzip()
+        print "Done testing"
+
+    elif args[0] == '-e':
+        if len(args) != 3:
+            print USAGE
+            sys.exit(1)
+
+        zf = ZipFile(args[1], 'r')
+        out = args[2]
+        for path in zf.namelist():
+            if path.startswith('./'):
+                tgt = os.path.join(out, path[2:])
+            else:
+                tgt = os.path.join(out, path)
+
+            tgtdir = os.path.dirname(tgt)
+            if not os.path.exists(tgtdir):
+                os.makedirs(tgtdir)
+            with open(tgt, 'wb') as fp:
+                fp.write(zf.read(path))
+        zf.close()
+
+    elif args[0] == '-c':
+        if len(args) < 3:
+            print USAGE
+            sys.exit(1)
+
+        def addToZip(zf, path, zippath):
+            if os.path.isfile(path):
+                zf.write(path, zippath, ZIP_DEFLATED)
+            elif os.path.isdir(path):
+                for nm in os.listdir(path):
+                    addToZip(zf,
+                            os.path.join(path, nm), os.path.join(zippath, nm))
+            # else: ignore
+
+        zf = ZipFile(args[1], 'w', allowZip64=True)
+        for src in args[2:]:
+            addToZip(zf, src, os.path.basename(src))
+
+        zf.close()
+
+if __name__ == "__main__":
+    main()
index a1aafde23e57ee26d332d06ee6bd71dfc53d6ed8..c401b36352bd7ee5d9780dddac7b193428a8076e 100644 (file)
@@ -2,7 +2,7 @@
 
 import sys
 import zlib
-import zipfile
+import zipfilerugged
 import os
 import os.path
 import getopt
@@ -15,7 +15,7 @@ _FILENAME_OFFSET = 30
 _MAX_SIZE = 64 * 1024
 _MIMETYPE = 'application/epub+zip'
 
-class ZipInfo(zipfile.ZipInfo):
+class ZipInfo(zipfilerugged.ZipInfo):
     def __init__(self, *args, **kwargs):
         if 'compress_type' in kwargs:
             compress_type = kwargs.pop('compress_type')
@@ -27,11 +27,11 @@ class fixZip:
         self.ztype = 'zip'
         if zinput.lower().find('.epub') >= 0 :
             self.ztype = 'epub'
-        self.inzip = zipfile.ZipFile(zinput,'r')
-        self.outzip = zipfile.ZipFile(zoutput,'w')
+        self.inzip = zipfilerugged.ZipFile(zinput,'r')
+        self.outzip = zipfilerugged.ZipFile(zoutput,'w')
         # open the input zip for reading only as a raw file
-       self.bzf = file(zinput,'rb')
-        
+        self.bzf = file(zinput,'rb')
+
     def getlocalname(self, zi):
         local_header_offset = zi.header_offset
         self.bzf.seek(local_header_offset + _FILENAME_LEN_OFFSET)
@@ -76,17 +76,17 @@ class fixZip:
         data = None
 
         # if not compressed we are good to go
-        if zi.compress_type == zipfile.ZIP_STORED:
+        if zi.compress_type == zipfilerugged.ZIP_STORED:
             data = self.bzf.read(zi.file_size)
 
         # if compressed we must decompress it using zlib
-        if zi.compress_type == zipfile.ZIP_DEFLATED:
+        if zi.compress_type == zipfilerugged.ZIP_DEFLATED:
             cmpdata = self.bzf.read(zi.compress_size)
             data = self.uncompress(cmpdata)
 
         return data
 
-        
+
 
     def fix(self):
         # get the zipinfo for each member of the input archive
@@ -95,7 +95,7 @@ class fixZip:
 
         # if epub write mimetype file first, with no compression
         if self.ztype == 'epub':
-            nzinfo = ZipInfo('mimetype', compress_type=zipfile.ZIP_STORED)
+            nzinfo = ZipInfo('mimetype', compress_type=zipfilerugged.ZIP_STORED)
             self.outzip.writestr(nzinfo, _MIMETYPE)
 
         # write the rest of the files
@@ -103,9 +103,9 @@ class fixZip:
             if zinfo.filename != "mimetype" or self.ztype == '.zip':
                 data = None
                 nzinfo = zinfo
-                try: 
+                try:
                     data = self.inzip.read(zinfo.filename)
-                except zipfile.BadZipfile or zipfile.error:
+                except zipfilerugged.BadZipfile or zipfilerugged.error:
                     local_name = self.getlocalname(zinfo)
                     data = self.getfiledata(zinfo)
                     nzinfo.filename = local_name
@@ -126,7 +126,7 @@ def usage():
      inputzip is the source zipfile to fix
      outputzip is the fixed zip archive
     """
-    
+
 
 def repairBook(infile, outfile):
     if not os.path.exists(infile):
@@ -152,5 +152,3 @@ def main(argv=sys.argv):
 
 if __name__ == '__main__' :
     sys.exit(main())
-
-
index 81389b913e4a6fda12125a4d66fd7a9f5f788de3..a22bddcc9ef7832c88a11692484af050338aca1a 100644 (file)
Binary files a/Calibre_Plugins/ineptpdf_plugin.zip and b/Calibre_Plugins/ineptpdf_plugin.zip differ
index c76e332565bdef9880567a6f42d8fdc8073e45e8..09db1c6ee727eea72a9b34adacaa8e52c3d98b81 100644 (file)
@@ -55,6 +55,9 @@ from __future__ import with_statement
 #   0.1.3 - add in fix for improper rejection of session bookkeys with len(bookkey) = length + 1 
 #   0.1.4 - update to the new calibre plugin interface
 #   0.1.5 - synced to ineptpdf 7.11
+#   0.1.6 - Fix for potential problem with PyCrypto
+#   0.1.7 - Fix for potential problem with ADE keys and fix possible output/unicode problem
+
 """
 Decrypts Adobe ADEPT-encrypted PDF files.
 """
@@ -2137,15 +2140,25 @@ class IneptPDFDeDRM(FileTypePlugin):
                                 Credit given to I <3 Cabbages for the original stand-alone scripts.'
     supported_platforms     = ['linux', 'osx', 'windows']
     author                  = 'DiapDealer'
-    version                 = (0, 1, 5)
+    version                 = (0, 1, 7)
     minimum_calibre_version = (0, 7, 55)  # for the new plugin interface
     file_types              = set(['pdf'])
     on_import               = True
     
     def run(self, path_to_ebook):
+        from calibre_plugins.ineptpdf import outputfix
+         
+        if sys.stdout.encoding == None:
+            sys.stdout = outputfix.getwriter('utf-8')(sys.stdout)
+        else:
+            sys.stdout = outputfix.getwriter(sys.stdout.encoding)(sys.stdout)
+        if sys.stderr.encoding == None:
+            sys.stderr = outputfix.getwriter('utf-8')(sys.stderr)
+        else:
+            sys.stderr = outputfix.getwriter(sys.stderr.encoding)(sys.stderr)
+
         global ARC4, RSA, AES
         
-        
         ARC4, RSA, AES = _load_crypto()
         
         if AES == None or RSA == None or ARC4 == None:
@@ -2162,10 +2175,13 @@ class IneptPDFDeDRM(FileTypePlugin):
         files = os.listdir(confpath)
         filefilter = re.compile("\.der$", re.IGNORECASE)
         files = filter(filefilter.search, files)
+        foundDefault = False
 
         if files:
             try:
                 for filename in files:
+                    if filename[:16] == 'calibre-adeptkey':
+                        foundDefault = True
                     fpath = os.path.join(confpath, filename)
                     with open(fpath, 'rb') as f:
                         userkeys.append(f.read())
@@ -2173,22 +2189,23 @@ class IneptPDFDeDRM(FileTypePlugin):
             except IOError:
                 print 'IneptPDF: Error reading keyfiles from config directory.'
                 pass
-        else:
+        
+        if not foundDefault:
             # Try to find key from ADE install and save the key in
             # Calibre's configuration directory for future use.
             if iswindows or isosx:
-                # ADE key retrieval script.
-                from calibre_plugins.ineptpdf.ade_key import retrieve_key
+                # ADE key retrieval script included in respective OS folder.
+                from calibre_plugins.ineptepub.ineptkey import retrieve_keys
                 try:
-                    keydata = retrieve_key()
-                    userkeys.append(keydata)
-                    keypath = os.path.join(confpath, 'calibre-adeptkey.der')
-                    with open(keypath, 'wb') as f:
-                        f.write(keydata)
-                    print 'IneptPDF: Created keyfile from ADE install.'    
+                    keys = retrieve_keys()
+                    for i,key in enumerate(keys):
+                        userkeys.append(key)
+                        keypath = os.path.join(confpath, 'calibre-adeptkey{0:d}.der'.format(i))
+                        open(keypath, 'wb').write(key)
+                        print 'IneptPDF: Created keyfile %s from ADE install.' % keypath
                 except:
-                    print 'IneptPDF: Couldn\'t Retrieve key from ADE install.'
-                    pass
+                   print 'IneptPDF: Couldn\'t Retrieve key from ADE install.'
+                   pass
 
         if not userkeys:
             # No user keys found... bail out.
index 4b743f74330c7f5a9f18b6acdafbf5a6981a7428..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644 (file)
@@ -1,346 +0,0 @@
-#!/usr/bin/env python
-
-"""
-Retrieve Adobe ADEPT user key.
-"""
-
-from __future__ import with_statement
-
-__license__ = 'GPL v3'
-
-import sys
-import os
-import struct
-from calibre.constants import iswindows, isosx
-
-class ADEPTError(Exception):
-    pass
-
-if iswindows:
-    from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
-        create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
-        string_at, Structure, c_void_p, cast, c_size_t, memmove, CDLL, c_int, \
-        c_long, c_ulong
-
-    from ctypes.wintypes import LPVOID, DWORD, BOOL
-    import _winreg as winreg
-
-    def _load_crypto_libcrypto():
-        from ctypes.util import find_library
-        libcrypto = find_library('libeay32')
-        if libcrypto is None:
-            raise ADEPTError('libcrypto not found')
-        libcrypto = CDLL(libcrypto)
-        AES_MAXNR = 14
-        c_char_pp = POINTER(c_char_p)
-        c_int_p = POINTER(c_int)
-        class AES_KEY(Structure):
-            _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
-                        ('rounds', c_int)]
-        AES_KEY_p = POINTER(AES_KEY)
-    
-        def F(restype, name, argtypes):
-            func = getattr(libcrypto, name)
-            func.restype = restype
-            func.argtypes = argtypes
-            return func
-    
-        AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
-                                [c_char_p, c_int, AES_KEY_p])
-        AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
-                            [c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,
-                             c_int])
-        class AES(object):
-            def __init__(self, userkey):
-                self._blocksize = len(userkey)
-                if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
-                    raise ADEPTError('AES improper key used')
-                key = self._key = AES_KEY()
-                rv = AES_set_decrypt_key(userkey, len(userkey) * 8, key)
-                if rv < 0:
-                    raise ADEPTError('Failed to initialize AES key')
-            def decrypt(self, data):
-                out = create_string_buffer(len(data))
-                iv = ("\x00" * self._blocksize)
-                rv = AES_cbc_encrypt(data, out, len(data), self._key, iv, 0)
-                if rv == 0:
-                    raise ADEPTError('AES decryption failed')
-                return out.raw
-        return AES
-
-    def _load_crypto_pycrypto():
-        from Crypto.Cipher import AES as _AES
-        class AES(object):
-            def __init__(self, key):
-                self._aes = _AES.new(key, _AES.MODE_CBC)
-            def decrypt(self, data):
-                return self._aes.decrypt(data)
-        return AES
-
-    def _load_crypto():
-        AES = None
-        for loader in (_load_crypto_pycrypto, _load_crypto_libcrypto):
-            try:
-                AES = loader()
-                break
-            except (ImportError, ADEPTError):
-                pass
-        return AES
-
-    AES = _load_crypto()
-
-
-    DEVICE_KEY_PATH = r'Software\Adobe\Adept\Device'
-    PRIVATE_LICENCE_KEY_PATH = r'Software\Adobe\Adept\Activation'
-
-    MAX_PATH = 255
-
-    kernel32 = windll.kernel32
-    advapi32 = windll.advapi32
-    crypt32 = windll.crypt32
-
-    def GetSystemDirectory():
-        GetSystemDirectoryW = kernel32.GetSystemDirectoryW
-        GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint]
-        GetSystemDirectoryW.restype = c_uint
-        def GetSystemDirectory():
-            buffer = create_unicode_buffer(MAX_PATH + 1)
-            GetSystemDirectoryW(buffer, len(buffer))
-            return buffer.value
-        return GetSystemDirectory
-    GetSystemDirectory = GetSystemDirectory()
-
-    def GetVolumeSerialNumber():
-        GetVolumeInformationW = kernel32.GetVolumeInformationW
-        GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint,
-                                          POINTER(c_uint), POINTER(c_uint),
-                                          POINTER(c_uint), c_wchar_p, c_uint]
-        GetVolumeInformationW.restype = c_uint
-        def GetVolumeSerialNumber(path):
-            vsn = c_uint(0)
-            GetVolumeInformationW(
-                path, None, 0, byref(vsn), None, None, None, 0)
-            return vsn.value
-        return GetVolumeSerialNumber
-    GetVolumeSerialNumber = GetVolumeSerialNumber()
-
-    def GetUserName():
-        GetUserNameW = advapi32.GetUserNameW
-        GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
-        GetUserNameW.restype = c_uint
-        def GetUserName():
-            buffer = create_unicode_buffer(32)
-            size = c_uint(len(buffer))
-            while not GetUserNameW(buffer, byref(size)):
-                buffer = create_unicode_buffer(len(buffer) * 2)
-                size.value = len(buffer)
-            return buffer.value.encode('utf-16-le')[::2]
-        return GetUserName
-    GetUserName = GetUserName()
-
-    PAGE_EXECUTE_READWRITE = 0x40
-    MEM_COMMIT  = 0x1000
-    MEM_RESERVE = 0x2000
-
-    def VirtualAlloc():
-        _VirtualAlloc = kernel32.VirtualAlloc
-        _VirtualAlloc.argtypes = [LPVOID, c_size_t, DWORD, DWORD]
-        _VirtualAlloc.restype = LPVOID
-        def VirtualAlloc(addr, size, alloctype=(MEM_COMMIT | MEM_RESERVE),
-                         protect=PAGE_EXECUTE_READWRITE):
-            return _VirtualAlloc(addr, size, alloctype, protect)
-        return VirtualAlloc
-    VirtualAlloc = VirtualAlloc()
-
-    MEM_RELEASE = 0x8000
-
-    def VirtualFree():
-        _VirtualFree = kernel32.VirtualFree
-        _VirtualFree.argtypes = [LPVOID, c_size_t, DWORD]
-        _VirtualFree.restype = BOOL
-        def VirtualFree(addr, size=0, freetype=MEM_RELEASE):
-            return _VirtualFree(addr, size, freetype)
-        return VirtualFree
-    VirtualFree = VirtualFree()
-
-    class NativeFunction(object):
-        def __init__(self, restype, argtypes, insns):
-            self._buf = buf = VirtualAlloc(None, len(insns))
-            memmove(buf, insns, len(insns))
-            ftype = CFUNCTYPE(restype, *argtypes)
-            self._native = ftype(buf)
-
-        def __call__(self, *args):
-            return self._native(*args)
-
-        def __del__(self):
-            if self._buf is not None:
-                VirtualFree(self._buf)
-                self._buf = None
-
-    if struct.calcsize("P") == 4:
-        CPUID0_INSNS = (
-            "\x53"             # push   %ebx
-            "\x31\xc0"         # xor    %eax,%eax
-            "\x0f\xa2"         # cpuid
-            "\x8b\x44\x24\x08" # mov    0x8(%esp),%eax
-            "\x89\x18"         # mov    %ebx,0x0(%eax)
-            "\x89\x50\x04"     # mov    %edx,0x4(%eax)
-            "\x89\x48\x08"     # mov    %ecx,0x8(%eax)
-            "\x5b"             # pop    %ebx
-            "\xc3"             # ret
-        )
-        CPUID1_INSNS = (
-            "\x53"             # push   %ebx
-            "\x31\xc0"         # xor    %eax,%eax
-            "\x40"             # inc    %eax
-            "\x0f\xa2"         # cpuid
-            "\x5b"             # pop    %ebx
-            "\xc3"             # ret
-        )
-    else:
-        CPUID0_INSNS = (
-            "\x49\x89\xd8"     # mov    %rbx,%r8
-            "\x49\x89\xc9"     # mov    %rcx,%r9
-            "\x48\x31\xc0"     # xor    %rax,%rax
-            "\x0f\xa2"         # cpuid
-            "\x4c\x89\xc8"     # mov    %r9,%rax
-            "\x89\x18"         # mov    %ebx,0x0(%rax)
-            "\x89\x50\x04"     # mov    %edx,0x4(%rax)
-            "\x89\x48\x08"     # mov    %ecx,0x8(%rax)
-            "\x4c\x89\xc3"     # mov    %r8,%rbx
-            "\xc3"             # retq
-        )
-        CPUID1_INSNS = (
-            "\x53"             # push   %rbx
-            "\x48\x31\xc0"     # xor    %rax,%rax
-            "\x48\xff\xc0"     # inc    %rax
-            "\x0f\xa2"         # cpuid
-            "\x5b"             # pop    %rbx
-            "\xc3"             # retq
-        )
-
-    def cpuid0():
-        _cpuid0 = NativeFunction(None, [c_char_p], CPUID0_INSNS)
-        buf = create_string_buffer(12)
-        def cpuid0():
-            _cpuid0(buf)
-            return buf.raw
-        return cpuid0
-    cpuid0 = cpuid0()
-
-    cpuid1 = NativeFunction(c_uint, [], CPUID1_INSNS)
-
-    class DataBlob(Structure):
-        _fields_ = [('cbData', c_uint),
-                    ('pbData', c_void_p)]
-    DataBlob_p = POINTER(DataBlob)
-
-    def CryptUnprotectData():
-        _CryptUnprotectData = crypt32.CryptUnprotectData
-        _CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
-                                       c_void_p, c_void_p, c_uint, DataBlob_p]
-        _CryptUnprotectData.restype = c_uint
-        def CryptUnprotectData(indata, entropy):
-            indatab = create_string_buffer(indata)
-            indata = DataBlob(len(indata), cast(indatab, c_void_p))
-            entropyb = create_string_buffer(entropy)
-            entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
-            outdata = DataBlob()
-            if not _CryptUnprotectData(byref(indata), None, byref(entropy),
-                                       None, None, 0, byref(outdata)):
-                raise ADEPTError("Failed to decrypt user key key (sic)")
-            return string_at(outdata.pbData, outdata.cbData)
-        return CryptUnprotectData
-    CryptUnprotectData = CryptUnprotectData()
-
-    def retrieve_key():
-        if AES is None:
-            tkMessageBox.showerror(
-                "ADEPT Key",
-                "This script requires PyCrypto or OpenSSL which must be installed "
-                "separately.  Read the top-of-script comment for details.")
-            return False
-        root = GetSystemDirectory().split('\\')[0] + '\\'
-        serial = GetVolumeSerialNumber(root)
-        vendor = cpuid0()
-        signature = struct.pack('>I', cpuid1())[1:]
-        user = GetUserName()
-        entropy = struct.pack('>I12s3s13s', serial, vendor, signature, user)
-        cuser = winreg.HKEY_CURRENT_USER
-        try:
-            regkey = winreg.OpenKey(cuser, DEVICE_KEY_PATH)
-        except WindowsError:
-            raise ADEPTError("Adobe Digital Editions not activated")
-        device = winreg.QueryValueEx(regkey, 'key')[0]
-        keykey = CryptUnprotectData(device, entropy)
-        userkey = None
-        try:
-            plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH)
-        except WindowsError:
-            raise ADEPTError("Could not locate ADE activation")
-        for i in xrange(0, 16):
-            try:
-                plkparent = winreg.OpenKey(plkroot, "%04d" % (i,))
-            except WindowsError:
-                break
-            ktype = winreg.QueryValueEx(plkparent, None)[0]
-            if ktype != 'credentials':
-                continue
-            for j in xrange(0, 16):
-                try:
-                    plkkey = winreg.OpenKey(plkparent, "%04d" % (j,))
-                except WindowsError:
-                    break
-                ktype = winreg.QueryValueEx(plkkey, None)[0]
-                if ktype != 'privateLicenseKey':
-                    continue
-                userkey = winreg.QueryValueEx(plkkey, 'value')[0]
-                break
-            if userkey is not None:
-                break
-        if userkey is None:
-            raise ADEPTError('Could not locate privateLicenseKey')
-        userkey = userkey.decode('base64')
-        aes = AES(keykey)
-        userkey = aes.decrypt(userkey)
-        userkey = userkey[26:-ord(userkey[-1])]
-        return userkey
-
-else:
-
-    import xml.etree.ElementTree as etree
-    import subprocess
-
-    NSMAP = {'adept': 'http://ns.adobe.com/adept',
-             'enc': 'http://www.w3.org/2001/04/xmlenc#'}
-
-    def findActivationDat():
-        home = os.getenv('HOME')
-        cmdline = 'find "' + home + '/Library/Application Support/Adobe/Digital Editions" -name "activation.dat"'
-        cmdline = cmdline.encode(sys.getfilesystemencoding())
-        p2 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
-        out1, out2 = p2.communicate()
-        reslst = out1.split('\n')
-        cnt = len(reslst)
-        for j in xrange(cnt):
-            resline = reslst[j]
-            pp = resline.find('activation.dat')
-            if pp >= 0:
-                ActDatPath = resline
-                break
-        if os.path.exists(ActDatPath):
-            return ActDatPath
-        return None
-    
-    def retrieve_key():
-        actpath = findActivationDat()
-        if actpath is None:
-            raise ADEPTError("Could not locate ADE activation")
-        tree = etree.parse(actpath)
-        adept = lambda tag: '{%s}%s' % (NSMAP['adept'], tag)
-        expr = '//%s/%s' % (adept('credentials'), adept('privateLicenseKey'))
-        userkey = tree.findtext(expr)
-        userkey = userkey.decode('base64')
-        userkey = userkey[26:]
-        return userkey
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..419e0ecdcbcb8641bfa843e09878033f738cc6c0 100644 (file)
Binary files a/Calibre_Plugins/ineptpdf_plugin/plugin-import-name-ineptpdf.txt and b/Calibre_Plugins/ineptpdf_plugin/plugin-import-name-ineptpdf.txt differ
index 91249f0e6dbe6bdbc489678319f2652037505eb3..b2a7fb1c169dda7672dca85fdaf69c39fe4f4a4c 100644 (file)
Binary files a/Calibre_Plugins/k4mobidedrm_plugin.zip and b/Calibre_Plugins/k4mobidedrm_plugin.zip differ
index c4a6322643c97c1c773b25593d38eb88c1348a84..c4716bd7f820d22eaf8b6577d898afc566721006 100644 (file)
-# standlone set of Mac OSX specific routines needed for KindleBooks
-
-from __future__ import with_statement
-
-import sys
-import os
-import os.path
-import re
-import copy
-import subprocess
-from struct import pack, unpack, unpack_from
-
-class DrmException(Exception):
-    pass
-
-
-# interface to needed routines in openssl's libcrypto
-def _load_crypto_libcrypto():
-    from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \
-        Structure, c_ulong, create_string_buffer, addressof, string_at, cast
-    from ctypes.util import find_library
-
-    libcrypto = find_library('crypto')
-    if libcrypto is None:
-        raise DrmException('libcrypto not found')
-    libcrypto = CDLL(libcrypto)
-
-    # From OpenSSL's crypto aes header
-    #
-    # AES_ENCRYPT     1
-    # AES_DECRYPT     0
-    # AES_MAXNR 14 (in bytes)
-    # AES_BLOCK_SIZE 16 (in bytes)
-    # 
-    # struct aes_key_st {
-    #    unsigned long rd_key[4 *(AES_MAXNR + 1)];
-    #    int rounds;
-    # };
-    # typedef struct aes_key_st AES_KEY;
-    #
-    # int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key);
-    #
-    # note:  the ivec string, and output buffer are both mutable
-    # void AES_cbc_encrypt(const unsigned char *in, unsigned char *out,
-    #     const unsigned long length, const AES_KEY *key, unsigned char *ivec, const int enc);
-
-    AES_MAXNR = 14
-    c_char_pp = POINTER(c_char_p)
-    c_int_p = POINTER(c_int)
-
-    class AES_KEY(Structure):
-        _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)]
-    AES_KEY_p = POINTER(AES_KEY)
-
-    def F(restype, name, argtypes):
-        func = getattr(libcrypto, name)
-        func.restype = restype
-        func.argtypes = argtypes
-        return func
-
-    AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,c_int])
-
-    AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p])
-
-    # From OpenSSL's Crypto evp/p5_crpt2.c
-    #
-    # int PKCS5_PBKDF2_HMAC_SHA1(const char *pass, int passlen,
-    #                        const unsigned char *salt, int saltlen, int iter,
-    #                        int keylen, unsigned char *out);
-
-    PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1',
-                                [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
-
-    class LibCrypto(object):
-        def __init__(self):
-            self._blocksize = 0
-            self._keyctx = None
-            self._iv = 0
-
-        def set_decrypt_key(self, userkey, iv):
-            self._blocksize = len(userkey)
-            if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
-                raise DrmException('AES improper key used')
-                return
-            keyctx = self._keyctx = AES_KEY()
-            self._iv = iv
-            self._userkey = userkey
-            rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
-            if rv < 0:
-                raise DrmException('Failed to initialize AES key')
-
-        def decrypt(self, data):
-            out = create_string_buffer(len(data))
-            mutable_iv = create_string_buffer(self._iv, len(self._iv))
-            keyctx = self._keyctx
-            rv = AES_cbc_encrypt(data, out, len(data), keyctx, mutable_iv, 0)
-            if rv == 0:
-                raise DrmException('AES decryption failed')
-            return out.raw
-
-        def keyivgen(self, passwd, salt, iter, keylen):
-            saltlen = len(salt)
-            passlen = len(passwd)
-            out = create_string_buffer(keylen)
-            rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out)
-            return out.raw
-    return LibCrypto
-
-def _load_crypto():
-    LibCrypto = None
-    try:
-        LibCrypto = _load_crypto_libcrypto()
-    except (ImportError, DrmException):
-        pass
-    return LibCrypto
-
-LibCrypto = _load_crypto()
-
+#!/usr/bin/python
 #
-# Utility Routines
+# This is a python script. You need a Python interpreter to run it.
+# For example, ActiveState Python, which exists for windows.
 #
+# Changelog
+#  1.00 - Initial version
 
-# crypto digestroutines
-import hashlib
-
-def MD5(message):
-    ctx = hashlib.md5()
-    ctx.update(message)
-    return ctx.digest()
-
-def SHA1(message):
-    ctx = hashlib.sha1()
-    ctx.update(message)
-    return ctx.digest()
-
-def SHA256(message):
-    ctx = hashlib.sha256()
-    ctx.update(message)
-    return ctx.digest()
-
-# Various character maps used to decrypt books. Probably supposed to act as obfuscation
-charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
-charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
-
-# For kinf approach of K4Mac 1.6.X or later
-# On K4PC charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
-# For Mac they seem to re-use charMap2 here
-charMap5 = charMap2
-
-# new in K4M 1.9.X
-testMap8 = "YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD"
-
-
-def encode(data, map):
-    result = ""
-    for char in data:
-        value = ord(char)
-        Q = (value ^ 0x80) // len(map)
-        R = value % len(map)
-        result += map[Q]
-        result += map[R]
-    return result
-
-# Hash the bytes in data and then encode the digest with the characters in map
-def encodeHash(data,map):
-    return encode(MD5(data),map)
-
-# Decode the string in data with the characters in map. Returns the decoded bytes
-def decode(data,map):
-    result = ""
-    for i in range (0,len(data)-1,2):
-        high = map.find(data[i])
-        low = map.find(data[i+1])
-        if (high == -1) or (low == -1) :
-            break
-        value = (((high * len(map)) ^ 0x80) & 0xFF) + low
-        result += pack("B",value)
-    return result
-
-# For K4M 1.6.X and later
-# generate table of prime number less than or equal to int n
-def primes(n):
-    if n==2: return [2]
-    elif n<2: return []
-    s=range(3,n+1,2)
-    mroot = n ** 0.5
-    half=(n+1)/2-1
-    i=0
-    m=3
-    while m <= mroot:
-        if s[i]:
-            j=(m*m-3)/2
-            s[j]=0
-            while j<half:
-                s[j]=0
-                j+=m
-        i=i+1
-        m=2*i+3
-    return [2]+[x for x in s if x]
-
-
-# uses a sub process to get the Hard Drive Serial Number using ioreg
-# returns with the serial number of drive whose BSD Name is "disk0"
-def GetVolumeSerialNumber():
-    sernum = os.getenv('MYSERIALNUMBER')
-    if sernum != None:
-        return sernum
-    cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver'
-    cmdline = cmdline.encode(sys.getfilesystemencoding())
-    p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
-    out1, out2 = p.communicate()
-    reslst = out1.split('\n')
-    cnt = len(reslst)
-    bsdname = None
-    sernum = None
-    foundIt = False
-    for j in xrange(cnt):
-        resline = reslst[j]
-        pp = resline.find('"Serial Number" = "')
-        if pp >= 0:
-            sernum = resline[pp+19:-1]
-            sernum = sernum.strip()
-        bb = resline.find('"BSD Name" = "')
-        if bb >= 0:
-            bsdname = resline[bb+14:-1]
-            bsdname = bsdname.strip()
-            if (bsdname == 'disk0') and (sernum != None):
-                foundIt = True
-                break
-    if not foundIt:
-        sernum = ''
-    return sernum
-
-def GetUserHomeAppSupKindleDirParitionName():
-    home = os.getenv('HOME')
-    dpath =  home + '/Library'
-    cmdline = '/sbin/mount'
-    cmdline = cmdline.encode(sys.getfilesystemencoding())
-    p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
-    out1, out2 = p.communicate()
-    reslst = out1.split('\n')
-    cnt = len(reslst)
-    disk = ''
-    foundIt = False
-    for j in xrange(cnt):
-        resline = reslst[j]
-        if resline.startswith('/dev'):
-            (devpart, mpath) = resline.split(' on ')
-            dpart = devpart[5:]
-            pp = mpath.find('(')
-            if pp >= 0:
-                mpath = mpath[:pp-1]
-            if dpath.startswith(mpath):
-                disk = dpart
-    return disk
-
-# uses a sub process to get the UUID of the specified disk partition using ioreg
-def GetDiskPartitionUUID(diskpart):
-    uuidnum = os.getenv('MYUUIDNUMBER')
-    if uuidnum != None:
-        return uuidnum
-    cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver'
-    cmdline = cmdline.encode(sys.getfilesystemencoding())
-    p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
-    out1, out2 = p.communicate()
-    reslst = out1.split('\n')
-    cnt = len(reslst)
-    bsdname = None
-    uuidnum = None
-    foundIt = False
-    nest = 0
-    uuidnest = -1
-    partnest = -2
-    for j in xrange(cnt):
-        resline = reslst[j]
-        if resline.find('{') >= 0:
-            nest += 1
-        if resline.find('}') >= 0:
-            nest -= 1
-        pp = resline.find('"UUID" = "')
-        if pp >= 0:
-            uuidnum = resline[pp+10:-1]
-            uuidnum = uuidnum.strip()
-            uuidnest = nest
-            if partnest == uuidnest and uuidnest > 0:
-                foundIt = True
-                break
-        bb = resline.find('"BSD Name" = "')
-        if bb >= 0:
-            bsdname = resline[bb+14:-1]
-            bsdname = bsdname.strip()
-            if (bsdname == diskpart):
-                partnest = nest
-            else :
-                partnest = -2
-            if partnest == uuidnest and partnest > 0:
-                foundIt = True
-                break
-        if nest == 0:
-            partnest = -2
-            uuidnest = -1
-            uuidnum = None
-            bsdname = None
-    if not foundIt:
-        uuidnum = ''
-    return uuidnum
-
-def GetMACAddressMunged():
-    macnum = os.getenv('MYMACNUM')
-    if macnum != None:
-        return macnum
-    cmdline = '/sbin/ifconfig en0'
-    cmdline = cmdline.encode(sys.getfilesystemencoding())
-    p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
-    out1, out2 = p.communicate()
-    reslst = out1.split('\n')
-    cnt = len(reslst)
-    macnum = None
-    foundIt = False
-    for j in xrange(cnt):
-        resline = reslst[j]
-        pp = resline.find('ether ')
-        if pp >= 0:
-            macnum = resline[pp+6:-1]
-            macnum = macnum.strip()
-            # print "original mac", macnum
-            # now munge it up the way Kindle app does
-            # by xoring it with 0xa5 and swapping elements 3 and 4
-            maclst = macnum.split(':')
-            n = len(maclst)
-            if n != 6:
-                fountIt = False
-                break
-            for i in range(6):
-                maclst[i] = int('0x' + maclst[i], 0)
-            mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
-            mlst[5] = maclst[5] ^ 0xa5
-            mlst[4] = maclst[3] ^ 0xa5
-            mlst[3] = maclst[4] ^ 0xa5
-            mlst[2] = maclst[2] ^ 0xa5
-            mlst[1] = maclst[1] ^ 0xa5
-            mlst[0] = maclst[0] ^ 0xa5
-            macnum = "%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x" % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5])
-            foundIt = True
-            break
-    if not foundIt:
-        macnum = ''
-    return macnum
-
-
-# uses unix env to get username instead of using sysctlbyname
-def GetUserName():
-    username = os.getenv('USER')
-    return username
-
-def isNewInstall():
-    home = os.getenv('HOME')
-    # soccer game fan anyone
-    dpath = home + '/Library/Application Support/Kindle/storage/.pes2011'
-    # print dpath, os.path.exists(dpath)
-    if os.path.exists(dpath):
-        return True
-    dpath = home + '/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/storage/.pes2011'
-    # print dpath, os.path.exists(dpath)
-    if os.path.exists(dpath):
-        return True
-    return False
-
-
-def GetIDString():
-    # K4Mac now has an extensive set of ids strings it uses
-    # in encoding pids and in creating unique passwords
-    # for use in its own version of CryptUnprotectDataV2
-
-    # BUT Amazon has now become nasty enough to detect when its app
-    # is being run under a debugger and actually changes code paths
-    # including which one of these strings is chosen, all to try
-    # to prevent reverse engineering
-
-    # Sad really ... they will only hurt their own sales ...
-    # true book lovers really want to keep their books forever
-    # and move them to their devices and DRM prevents that so they
-    # will just buy from someplace else that they can remove
-    # the DRM from
-
-    # Amazon should know by now that true book lover's are not like
-    # penniless kids that pirate music, we do not pirate books
+__version__ = '1.00'
 
-    if isNewInstall():
-        mungedmac = GetMACAddressMunged()
-        if len(mungedmac) > 7:
-            print('Using Munged MAC Address for ID: '+mungedmac)
-            return mungedmac
-    sernum = GetVolumeSerialNumber()
-    if len(sernum) > 7:
-        print('Using Volume Serial Number for ID: '+sernum)
-        return sernum
-    diskpart = GetUserHomeAppSupKindleDirParitionName()
-    uuidnum = GetDiskPartitionUUID(diskpart)
-    if len(uuidnum) > 7:
-        print('Using Disk Partition UUID for ID: '+uuidnum)
-        return uuidnum
-    mungedmac = GetMACAddressMunged()
-    if len(mungedmac) > 7:
-        print('Using Munged MAC Address for ID: '+mungedmac)
-        return mungedmac
-    print('Using Fixed constant 9999999999 for ID.')
-    return '9999999999'
-
-
-# implements an Pseudo Mac Version of Windows built-in Crypto routine
-# used by Kindle for Mac versions < 1.6.0
-class CryptUnprotectData(object):
-    def __init__(self):
-        sernum = GetVolumeSerialNumber()
-        if sernum == '':
-            sernum = '9999999999'
-        sp = sernum + '!@#' + GetUserName()
-        passwdData = encode(SHA256(sp),charMap1)
-        salt = '16743'
-        self.crp = LibCrypto()
-        iter = 0x3e8
-        keylen = 0x80
-        key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen)
-        self.key = key_iv[0:32]
-        self.iv = key_iv[32:48]
-        self.crp.set_decrypt_key(self.key, self.iv)
-
-    def decrypt(self, encryptedData):
-        cleartext = self.crp.decrypt(encryptedData)
-        cleartext = decode(cleartext,charMap1)
-        return cleartext
-
-
-# implements an Pseudo Mac Version of Windows built-in Crypto routine
-# used for Kindle for Mac Versions >= 1.6.0
-class CryptUnprotectDataV2(object):
-    def __init__(self):
-        sp = GetUserName() + ':&%:' + GetIDString()
-        passwdData = encode(SHA256(sp),charMap5)
-        # salt generation as per the code
-        salt = 0x0512981d * 2 * 1 * 1
-        salt = str(salt) + GetUserName()
-        salt = encode(salt,charMap5)
-        self.crp = LibCrypto()
-        iter = 0x800
-        keylen = 0x400
-        key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen)
-        self.key = key_iv[0:32]
-        self.iv = key_iv[32:48]
-        self.crp.set_decrypt_key(self.key, self.iv)
-
-    def decrypt(self, encryptedData):
-        cleartext = self.crp.decrypt(encryptedData)
-        cleartext = decode(cleartext, charMap5)
-        return cleartext
-
-
-# unprotect the new header blob in .kinf2011
-# used in Kindle for Mac Version >= 1.9.0
-def UnprotectHeaderData(encryptedData):
-    passwdData = 'header_key_data'
-    salt = 'HEADER.2011'
-    iter = 0x80
-    keylen = 0x100
-    crp = LibCrypto()
-    key_iv = crp.keyivgen(passwdData, salt, iter, keylen)
-    key = key_iv[0:32]
-    iv = key_iv[32:48]
-    crp.set_decrypt_key(key,iv)
-    cleartext = crp.decrypt(encryptedData)
-    return cleartext
-
-
-# implements an Pseudo Mac Version of Windows built-in Crypto routine
-# used for Kindle for Mac Versions >= 1.9.0
-class CryptUnprotectDataV3(object):
-    def __init__(self, entropy):
-        sp = GetUserName() + '+@#$%+' + GetIDString()
-        passwdData = encode(SHA256(sp),charMap2)
-        salt = entropy
-        self.crp = LibCrypto()
-        iter = 0x800
-        keylen = 0x400
-        key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen)
-        self.key = key_iv[0:32]
-        self.iv = key_iv[32:48]
-        self.crp.set_decrypt_key(self.key, self.iv)
-
-    def decrypt(self, encryptedData):
-        cleartext = self.crp.decrypt(encryptedData)
-        cleartext = decode(cleartext, charMap2)
-        return cleartext
-
-
-# Locate the .kindle-info files
-def getKindleInfoFiles(kInfoFiles):
-    home = os.getenv('HOME')
-    # search for any .kinf2011 files in new location (Sep 2012)
-    cmdline = 'find "' + home + '/Library/Containers/com.amazon.Kindle/Data/Library/Application Support" -name ".kinf2011"'
-    cmdline = cmdline.encode(sys.getfilesystemencoding())
-    p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
-    out1, out2 = p1.communicate()
-    reslst = out1.split('\n')
-    for resline in reslst:
-        if os.path.isfile(resline):
-            kInfoFiles.append(resline)
-            print('Found k4Mac kinf2011 file: ' + resline)
-            found = True
-   # search for any .kinf2011 files
-    cmdline = 'find "' + home + '/Library/Application Support" -name ".kinf2011"'
-    cmdline = cmdline.encode(sys.getfilesystemencoding())
-    p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
-    out1, out2 = p1.communicate()
-    reslst = out1.split('\n')
-    for resline in reslst:
-        if os.path.isfile(resline):
-            kInfoFiles.append(resline)
-            print('Found k4Mac kinf2011 file: ' + resline)
-            found = True
-    # search for any .kindle-info files
-    cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"'
-    cmdline = cmdline.encode(sys.getfilesystemencoding())
-    p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
-    out1, out2 = p1.communicate()
-    reslst = out1.split('\n')
-    kinfopath = 'NONE'
-    found = False
-    for resline in reslst:
-        if os.path.isfile(resline):
-            kInfoFiles.append(resline)
-            print('Found K4Mac kindle-info file: ' + resline)
-            found = True
-    # search for any .rainier*-kinf files
-    cmdline = 'find "' + home + '/Library/Application Support" -name ".rainier*-kinf"'
-    cmdline = cmdline.encode(sys.getfilesystemencoding())
-    p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
-    out1, out2 = p1.communicate()
-    reslst = out1.split('\n')
-    for resline in reslst:
-        if os.path.isfile(resline):
-            kInfoFiles.append(resline)
-            print('Found k4Mac kinf file: ' + resline)
-            found = True
-    if not found:
-        print('No k4Mac kindle-info/kinf/kinf2011 files have been found.')
-    return kInfoFiles
-
-# determine type of kindle info provided and return a
-# database of keynames and values
-def getDBfromFile(kInfoFile):
-    names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber", "max_date", "SIGVERIF"]
-    DB = {}
-    cnt = 0
-    infoReader = open(kInfoFile, 'r')
-    hdr = infoReader.read(1)
-    data = infoReader.read()
-
-    if data.find('[') != -1 :
-
-        # older style kindle-info file
-        cud = CryptUnprotectData()
-        items = data.split('[')
-        for item in items:
-            if item != '':
-                keyhash, rawdata = item.split(':')
-                keyname = "unknown"
-                for name in names:
-                    if encodeHash(name,charMap2) == keyhash:
-                        keyname = name
-                        break
-                if keyname == "unknown":
-                    keyname = keyhash
-                encryptedValue = decode(rawdata,charMap2)
-                cleartext = cud.decrypt(encryptedValue)
-                DB[keyname] = cleartext
-                cnt = cnt + 1
-        if cnt == 0:
-            DB = None
-        return DB
-
-    if hdr == '/':
-
-        # else newer style .kinf file used by K4Mac >= 1.6.0
-        # the .kinf file uses "/" to separate it into records
-        # so remove the trailing "/" to make it easy to use split
-        data = data[:-1]
-        items = data.split('/')
-        cud = CryptUnprotectDataV2()
-
-        # loop through the item records until all are processed
-        while len(items) > 0:
-
-            # get the first item record
-            item = items.pop(0)
-
-            # the first 32 chars of the first record of a group
-            # is the MD5 hash of the key name encoded by charMap5
-            keyhash = item[0:32]
-            keyname = "unknown"
-
-            # the raw keyhash string is also used to create entropy for the actual
-            # CryptProtectData Blob that represents that keys contents
-            # "entropy" not used for K4Mac only K4PC
-            # entropy = SHA1(keyhash)
-
-            # the remainder of the first record when decoded with charMap5
-            # has the ':' split char followed by the string representation
-            # of the number of records that follow
-            # and make up the contents
-            srcnt = decode(item[34:],charMap5)
-            rcnt = int(srcnt)
-
-            # read and store in rcnt records of data
-            # that make up the contents value
-            edlst = []
-            for i in xrange(rcnt):
-                item = items.pop(0)
-                edlst.append(item)
-
-            keyname = "unknown"
-            for name in names:
-                if encodeHash(name,charMap5) == keyhash:
-                    keyname = name
-                    break
-            if keyname == "unknown":
-                keyname = keyhash
-
-            # the charMap5 encoded contents data has had a length
-            # of chars (always odd) cut off of the front and moved
-            # to the end to prevent decoding using charMap5 from
-            # working properly, and thereby preventing the ensuing
-            # CryptUnprotectData call from succeeding.
-
-            # The offset into the charMap5 encoded contents seems to be:
-            # len(contents) - largest prime number less than or equal to int(len(content)/3)
-            # (in other words split "about" 2/3rds of the way through)
-
-            # move first offsets chars to end to align for decode by charMap5
-            encdata = "".join(edlst)
-            contlen = len(encdata)
-
-            # now properly split and recombine
-            # by moving noffset chars from the start of the
-            # string to the end of the string
-            noffset = contlen - primes(int(contlen/3))[-1]
-            pfx = encdata[0:noffset]
-            encdata = encdata[noffset:]
-            encdata = encdata + pfx
-
-            # decode using charMap5 to get the CryptProtect Data
-            encryptedValue = decode(encdata,charMap5)
-            cleartext = cud.decrypt(encryptedValue)
-            DB[keyname] = cleartext
-            cnt = cnt + 1
-
-        if cnt == 0:
-            DB = None
-        return DB
-
-    # the latest .kinf2011 version for K4M 1.9.1
-    # put back the hdr char, it is needed
-    data = hdr + data
-    data = data[:-1]
-    items = data.split('/')
-
-    # the headerblob is the encrypted information needed to build the entropy string
-    headerblob = items.pop(0)
-    encryptedValue = decode(headerblob, charMap1)
-    cleartext = UnprotectHeaderData(encryptedValue)
-
-    # now extract the pieces in the same way
-    # this version is different from K4PC it scales the build number by multipying by 735
-    pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
-    for m in re.finditer(pattern, cleartext):
-        entropy = str(int(m.group(2)) * 0x2df) + m.group(4)
-
-    cud = CryptUnprotectDataV3(entropy)
-
-    # loop through the item records until all are processed
-    while len(items) > 0:
-
-        # get the first item record
-        item = items.pop(0)
-
-        # the first 32 chars of the first record of a group
-        # is the MD5 hash of the key name encoded by charMap5
-        keyhash = item[0:32]
-        keyname = "unknown"
-
-        # unlike K4PC the keyhash is not used in generating entropy
-        # entropy = SHA1(keyhash) + added_entropy
-        # entropy = added_entropy
-
-        # the remainder of the first record when decoded with charMap5
-        # has the ':' split char followed by the string representation
-        # of the number of records that follow
-        # and make up the contents
-        srcnt = decode(item[34:],charMap5)
-        rcnt = int(srcnt)
-
-        # read and store in rcnt records of data
-        # that make up the contents value
-        edlst = []
-        for i in xrange(rcnt):
-            item = items.pop(0)
-            edlst.append(item)
-
-        keyname = "unknown"
-        for name in names:
-            if encodeHash(name,testMap8) == keyhash:
-                keyname = name
-                break
-        if keyname == "unknown":
-            keyname = keyhash
-
-        # the testMap8 encoded contents data has had a length
-        # of chars (always odd) cut off of the front and moved
-        # to the end to prevent decoding using testMap8 from
-        # working properly, and thereby preventing the ensuing
-        # CryptUnprotectData call from succeeding.
-
-        # The offset into the testMap8 encoded contents seems to be:
-        # len(contents) - largest prime number less than or equal to int(len(content)/3)
-        # (in other words split "about" 2/3rds of the way through)
+import sys
 
-        # move first offsets chars to end to align for decode by testMap8
-        encdata = "".join(edlst)
-        contlen = len(encdata)
+class Unbuffered:
+    def __init__(self, stream):
+        self.stream = stream
+    def write(self, data):
+        self.stream.write(data)
+        self.stream.flush()
+    def __getattr__(self, attr):
+        return getattr(self.stream, attr)
+sys.stdout=Unbuffered(sys.stdout)
 
-        # now properly split and recombine
-        # by moving noffset chars from the start of the
-        # string to the end of the string
-        noffset = contlen - primes(int(contlen/3))[-1]
-        pfx = encdata[0:noffset]
-        encdata = encdata[noffset:]
-        encdata = encdata + pfx
+import os
+import struct
+import binascii
+import kgenpids
+import topazextract
+import mobidedrm
+from alfcrypto import Pukall_Cipher
 
-        # decode using testMap8 to get the CryptProtect Data
-        encryptedValue = decode(encdata,testMap8)
-        cleartext = cud.decrypt(encryptedValue)
-        # print keyname
-        # print cleartext
-        DB[keyname] = cleartext
-        cnt = cnt + 1
+class DrmException(Exception):
+    pass
 
-    if cnt == 0:
-        DB = None
-    return DB
+def getK4PCpids(path_to_ebook):
+    # Return Kindle4PC PIDs. Assumes that the caller checked that we are not on Linux, which will raise an exception
+
+    mobi = True
+    magic3 = file(path_to_ebook,'rb').read(3)
+    if magic3 == 'TPZ':
+        mobi = False
+
+    if mobi:
+        mb = mobidedrm.MobiBook(path_to_ebook,False)
+    else:
+        mb = topazextract.TopazBook(path_to_ebook)
+    
+    md1, md2 = mb.getPIDMetaInfo()
+
+    return kgenpids.getPidList(md1, md2, True, [], [], []) 
+
+
+def main(argv=sys.argv):
+    print ('getk4pcpids.py v%(__version__)s. '
+        'Copyright 2012 Apprentic Alf' % globals())
+
+    if len(argv)<2 or len(argv)>3:
+        print "Gets the possible book-specific PIDs from K4PC for a particular book"
+        print "Usage:"
+        print "    %s <bookfile> [<outfile>]" % sys.argv[0]
+        return 1
+    else:
+        infile = argv[1]
+        try:
+            pidlist = getK4PCpids(infile)
+        except DrmException, e:
+            print "Error: %s" % e
+            return 1
+        pidstring = ','.join(pidlist)
+        print "Possible PIDs are: ", pidstring
+        if len(argv) is 3:
+            outfile = argv[2]
+            file(outfile, 'w').write(pidstring)
+        
+    return 0
+
+if __name__ == "__main__":
+    sys.exit(main())
index 1bd256234dfd4159fa15113db0efa2993ee80e5d..03858390b434858493ae0ab797b54d5f1e5b6954 100644 (file)
-#!/usr/bin/env python
-# K4PC Windows specific routines
+# standlone set of Mac OSX specific routines needed for KindleBooks
 
 from __future__ import with_statement
 
-import sys, os, re
+import sys
+import os
+import os.path
+import re
+import copy
+import subprocess
 from struct import pack, unpack, unpack_from
 
-from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
-    create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
-    string_at, Structure, c_void_p, cast
+class DrmException(Exception):
+    pass
 
-import _winreg as winreg
-MAX_PATH = 255
-kernel32 = windll.kernel32
-advapi32 = windll.advapi32
-crypt32 = windll.crypt32
 
-import traceback
+# interface to needed routines in openssl's libcrypto
+def _load_crypto_libcrypto():
+    from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \
+        Structure, c_ulong, create_string_buffer, addressof, string_at, cast
+    from ctypes.util import find_library
+
+    libcrypto = find_library('crypto')
+    if libcrypto is None:
+        raise DrmException('libcrypto not found')
+    libcrypto = CDLL(libcrypto)
+
+    # From OpenSSL's crypto aes header
+    #
+    # AES_ENCRYPT     1
+    # AES_DECRYPT     0
+    # AES_MAXNR 14 (in bytes)
+    # AES_BLOCK_SIZE 16 (in bytes)
+    # 
+    # struct aes_key_st {
+    #    unsigned long rd_key[4 *(AES_MAXNR + 1)];
+    #    int rounds;
+    # };
+    # typedef struct aes_key_st AES_KEY;
+    #
+    # int AES_set_decrypt_key(const unsigned char *userKey, const int bits, AES_KEY *key);
+    #
+    # note:  the ivec string, and output buffer are both mutable
+    # void AES_cbc_encrypt(const unsigned char *in, unsigned char *out,
+    #     const unsigned long length, const AES_KEY *key, unsigned char *ivec, const int enc);
+
+    AES_MAXNR = 14
+    c_char_pp = POINTER(c_char_p)
+    c_int_p = POINTER(c_int)
+
+    class AES_KEY(Structure):
+        _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)]
+    AES_KEY_p = POINTER(AES_KEY)
+
+    def F(restype, name, argtypes):
+        func = getattr(libcrypto, name)
+        func.restype = restype
+        func.argtypes = argtypes
+        return func
+
+    AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,c_int])
+
+    AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p])
+
+    # From OpenSSL's Crypto evp/p5_crpt2.c
+    #
+    # int PKCS5_PBKDF2_HMAC_SHA1(const char *pass, int passlen,
+    #                        const unsigned char *salt, int saltlen, int iter,
+    #                        int keylen, unsigned char *out);
+
+    PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1',
+                                [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
+
+    class LibCrypto(object):
+        def __init__(self):
+            self._blocksize = 0
+            self._keyctx = None
+            self._iv = 0
+
+        def set_decrypt_key(self, userkey, iv):
+            self._blocksize = len(userkey)
+            if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
+                raise DrmException('AES improper key used')
+                return
+            keyctx = self._keyctx = AES_KEY()
+            self._iv = iv
+            self._userkey = userkey
+            rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
+            if rv < 0:
+                raise DrmException('Failed to initialize AES key')
+
+        def decrypt(self, data):
+            out = create_string_buffer(len(data))
+            mutable_iv = create_string_buffer(self._iv, len(self._iv))
+            keyctx = self._keyctx
+            rv = AES_cbc_encrypt(data, out, len(data), keyctx, mutable_iv, 0)
+            if rv == 0:
+                raise DrmException('AES decryption failed')
+            return out.raw
+
+        def keyivgen(self, passwd, salt, iter, keylen):
+            saltlen = len(salt)
+            passlen = len(passwd)
+            out = create_string_buffer(keylen)
+            rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out)
+            return out.raw
+    return LibCrypto
+
+def _load_crypto():
+    LibCrypto = None
+    try:
+        LibCrypto = _load_crypto_libcrypto()
+    except (ImportError, DrmException):
+        pass
+    return LibCrypto
+
+LibCrypto = _load_crypto()
+
+#
+# Utility Routines
+#
 
 # crypto digestroutines
 import hashlib
@@ -36,62 +138,19 @@ def SHA256(message):
     ctx.update(message)
     return ctx.digest()
 
-# For K4PC 1.9.X
-# use routines in alfcrypto:
-#    AES_cbc_encrypt
-#    AES_set_decrypt_key
-#    PKCS5_PBKDF2_HMAC_SHA1
-
-from alfcrypto import AES_CBC, KeyIVGen
-
-def UnprotectHeaderData(encryptedData):
-    passwdData = 'header_key_data'
-    salt = 'HEADER.2011'
-    iter = 0x80
-    keylen = 0x100
-    key_iv = KeyIVGen().pbkdf2(passwdData, salt, iter, keylen)
-    key = key_iv[0:32]
-    iv = key_iv[32:48]
-    aes=AES_CBC()
-    aes.set_decrypt_key(key, iv)
-    cleartext = aes.decrypt(encryptedData)
-    return cleartext
+# Various character maps used to decrypt books. Probably supposed to act as obfuscation
+charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
+charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
 
+# For kinf approach of K4Mac 1.6.X or later
+# On K4PC charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
+# For Mac they seem to re-use charMap2 here
+charMap5 = charMap2
 
-# simple primes table (<= n) calculator
-def primes(n):
-    if n==2: return [2]
-    elif n<2: return []
-    s=range(3,n+1,2)
-    mroot = n ** 0.5
-    half=(n+1)/2-1
-    i=0
-    m=3
-    while m <= mroot:
-        if s[i]:
-            j=(m*m-3)/2
-            s[j]=0
-            while j<half:
-                s[j]=0
-                j+=m
-        i=i+1
-        m=2*i+3
-    return [2]+[x for x in s if x]
-
-
-# Various character maps used to decrypt kindle info values.
-# Probably supposed to act as obfuscation
-charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
-charMap5 = "AzB0bYyCeVvaZ3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_c1XxDdW2wE"
-# New maps in K4PC 1.9.0
-testMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
-testMap6 = "9YzAb0Cd1Ef2n5Pr6St7Uvh3Jk4M8WxG"
+# new in K4M 1.9.X
 testMap8 = "YvaZ3FfUm9Nn_c1XuG4yCAzB0beVg-TtHh5SsIiR6rJjQdW2wEq7KkPpL8lOoMxD"
 
-class DrmException(Exception):
-    pass
 
-# Encode the bytes in data with the characters in map
 def encode(data, map):
     result = ""
     for char in data:
@@ -118,137 +177,375 @@ def decode(data,map):
         result += pack("B",value)
     return result
 
+# For K4M 1.6.X and later
+# generate table of prime number less than or equal to int n
+def primes(n):
+    if n==2: return [2]
+    elif n<2: return []
+    s=range(3,n+1,2)
+    mroot = n ** 0.5
+    half=(n+1)/2-1
+    i=0
+    m=3
+    while m <= mroot:
+        if s[i]:
+            j=(m*m-3)/2
+            s[j]=0
+            while j<half:
+                s[j]=0
+                j+=m
+        i=i+1
+        m=2*i+3
+    return [2]+[x for x in s if x]
 
-# interface with Windows OS Routines
-class DataBlob(Structure):
-    _fields_ = [('cbData', c_uint),
-                ('pbData', c_void_p)]
-DataBlob_p = POINTER(DataBlob)
-
-
-def GetSystemDirectory():
-    GetSystemDirectoryW = kernel32.GetSystemDirectoryW
-    GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint]
-    GetSystemDirectoryW.restype = c_uint
-    def GetSystemDirectory():
-        buffer = create_unicode_buffer(MAX_PATH + 1)
-        GetSystemDirectoryW(buffer, len(buffer))
-        return buffer.value
-    return GetSystemDirectory
-GetSystemDirectory = GetSystemDirectory()
 
+# uses a sub process to get the Hard Drive Serial Number using ioreg
+# returns with the serial number of drive whose BSD Name is "disk0"
 def GetVolumeSerialNumber():
-    GetVolumeInformationW = kernel32.GetVolumeInformationW
-    GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint,
-                                      POINTER(c_uint), POINTER(c_uint),
-                                      POINTER(c_uint), c_wchar_p, c_uint]
-    GetVolumeInformationW.restype = c_uint
-    def GetVolumeSerialNumber(path = GetSystemDirectory().split('\\')[0] + '\\'):
-        vsn = c_uint(0)
-        GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0)
-        return str(vsn.value)
-    return GetVolumeSerialNumber
-GetVolumeSerialNumber = GetVolumeSerialNumber()
+    sernum = os.getenv('MYSERIALNUMBER')
+    if sernum != None:
+        return sernum
+    cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver'
+    cmdline = cmdline.encode(sys.getfilesystemencoding())
+    p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
+    out1, out2 = p.communicate()
+    reslst = out1.split('\n')
+    cnt = len(reslst)
+    bsdname = None
+    sernum = None
+    foundIt = False
+    for j in xrange(cnt):
+        resline = reslst[j]
+        pp = resline.find('"Serial Number" = "')
+        if pp >= 0:
+            sernum = resline[pp+19:-1]
+            sernum = sernum.strip()
+        bb = resline.find('"BSD Name" = "')
+        if bb >= 0:
+            bsdname = resline[bb+14:-1]
+            bsdname = bsdname.strip()
+            if (bsdname == 'disk0') and (sernum != None):
+                foundIt = True
+                break
+    if not foundIt:
+        sernum = ''
+    return sernum
+
+def GetUserHomeAppSupKindleDirParitionName():
+    home = os.getenv('HOME')
+    dpath =  home + '/Library'
+    cmdline = '/sbin/mount'
+    cmdline = cmdline.encode(sys.getfilesystemencoding())
+    p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
+    out1, out2 = p.communicate()
+    reslst = out1.split('\n')
+    cnt = len(reslst)
+    disk = ''
+    foundIt = False
+    for j in xrange(cnt):
+        resline = reslst[j]
+        if resline.startswith('/dev'):
+            (devpart, mpath) = resline.split(' on ')
+            dpart = devpart[5:]
+            pp = mpath.find('(')
+            if pp >= 0:
+                mpath = mpath[:pp-1]
+            if dpath.startswith(mpath):
+                disk = dpart
+    return disk
+
+# uses a sub process to get the UUID of the specified disk partition using ioreg
+def GetDiskPartitionUUID(diskpart):
+    uuidnum = os.getenv('MYUUIDNUMBER')
+    if uuidnum != None:
+        return uuidnum
+    cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver'
+    cmdline = cmdline.encode(sys.getfilesystemencoding())
+    p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
+    out1, out2 = p.communicate()
+    reslst = out1.split('\n')
+    cnt = len(reslst)
+    bsdname = None
+    uuidnum = None
+    foundIt = False
+    nest = 0
+    uuidnest = -1
+    partnest = -2
+    for j in xrange(cnt):
+        resline = reslst[j]
+        if resline.find('{') >= 0:
+            nest += 1
+        if resline.find('}') >= 0:
+            nest -= 1
+        pp = resline.find('"UUID" = "')
+        if pp >= 0:
+            uuidnum = resline[pp+10:-1]
+            uuidnum = uuidnum.strip()
+            uuidnest = nest
+            if partnest == uuidnest and uuidnest > 0:
+                foundIt = True
+                break
+        bb = resline.find('"BSD Name" = "')
+        if bb >= 0:
+            bsdname = resline[bb+14:-1]
+            bsdname = bsdname.strip()
+            if (bsdname == diskpart):
+                partnest = nest
+            else :
+                partnest = -2
+            if partnest == uuidnest and partnest > 0:
+                foundIt = True
+                break
+        if nest == 0:
+            partnest = -2
+            uuidnest = -1
+            uuidnum = None
+            bsdname = None
+    if not foundIt:
+        uuidnum = ''
+    return uuidnum
+
+def GetMACAddressMunged():
+    macnum = os.getenv('MYMACNUM')
+    if macnum != None:
+        return macnum
+    cmdline = '/sbin/ifconfig en0'
+    cmdline = cmdline.encode(sys.getfilesystemencoding())
+    p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
+    out1, out2 = p.communicate()
+    reslst = out1.split('\n')
+    cnt = len(reslst)
+    macnum = None
+    foundIt = False
+    for j in xrange(cnt):
+        resline = reslst[j]
+        pp = resline.find('ether ')
+        if pp >= 0:
+            macnum = resline[pp+6:-1]
+            macnum = macnum.strip()
+            # print "original mac", macnum
+            # now munge it up the way Kindle app does
+            # by xoring it with 0xa5 and swapping elements 3 and 4
+            maclst = macnum.split(':')
+            n = len(maclst)
+            if n != 6:
+                fountIt = False
+                break
+            for i in range(6):
+                maclst[i] = int('0x' + maclst[i], 0)
+            mlst = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
+            mlst[5] = maclst[5] ^ 0xa5
+            mlst[4] = maclst[3] ^ 0xa5
+            mlst[3] = maclst[4] ^ 0xa5
+            mlst[2] = maclst[2] ^ 0xa5
+            mlst[1] = maclst[1] ^ 0xa5
+            mlst[0] = maclst[0] ^ 0xa5
+            macnum = "%0.2x%0.2x%0.2x%0.2x%0.2x%0.2x" % (mlst[0], mlst[1], mlst[2], mlst[3], mlst[4], mlst[5])
+            foundIt = True
+            break
+    if not foundIt:
+        macnum = ''
+    return macnum
 
-def GetIDString():
-    vsn = GetVolumeSerialNumber()
-    print('Using Volume Serial Number for ID: '+vsn)
-    return vsn
-
-def getLastError():
-    GetLastError = kernel32.GetLastError
-    GetLastError.argtypes = None
-    GetLastError.restype = c_uint
-    def getLastError():
-        return GetLastError()
-    return getLastError
-getLastError = getLastError()
 
+# uses unix env to get username instead of using sysctlbyname
 def GetUserName():
-    GetUserNameW = advapi32.GetUserNameW
-    GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
-    GetUserNameW.restype = c_uint
-    def GetUserName():
-        buffer = create_unicode_buffer(2)
-        size = c_uint(len(buffer))
-        while not GetUserNameW(buffer, byref(size)):
-            errcd = getLastError()
-            if errcd == 234:
-                # bad wine implementation up through wine 1.3.21
-                return "AlternateUserName"
-            buffer = create_unicode_buffer(len(buffer) * 2)
-            size.value = len(buffer)
-        return buffer.value.encode('utf-16-le')[::2]
-    return GetUserName
-GetUserName = GetUserName()
-
-def CryptUnprotectData():
-    _CryptUnprotectData = crypt32.CryptUnprotectData
-    _CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
-                                   c_void_p, c_void_p, c_uint, DataBlob_p]
-    _CryptUnprotectData.restype = c_uint
-    def CryptUnprotectData(indata, entropy, flags):
-        indatab = create_string_buffer(indata)
-        indata = DataBlob(len(indata), cast(indatab, c_void_p))
-        entropyb = create_string_buffer(entropy)
-        entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
-        outdata = DataBlob()
-        if not _CryptUnprotectData(byref(indata), None, byref(entropy),
-                                   None, None, flags, byref(outdata)):
-            # raise DrmException("Failed to Unprotect Data")
-            return 'failed'
-        return string_at(outdata.pbData, outdata.cbData)
-    return CryptUnprotectData
-CryptUnprotectData = CryptUnprotectData()
-
-
-# Locate all of the kindle-info style files and return as list
-def getKindleInfoFiles(kInfoFiles):
-    regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
-    path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
+    username = os.getenv('USER')
+    return username
+
+def isNewInstall():
+    home = os.getenv('HOME')
+    # soccer game fan anyone
+    dpath = home + '/Library/Application Support/Kindle/storage/.pes2011'
+    # print dpath, os.path.exists(dpath)
+    if os.path.exists(dpath):
+        return True
+    dpath = home + '/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/storage/.pes2011'
+    # print dpath, os.path.exists(dpath)
+    if os.path.exists(dpath):
+        return True
+    return False
 
-    # some 64 bit machines do not have the proper registry key for some reason
-    # or the pythonn interface to the 32 vs 64 bit registry is broken
-    if 'LOCALAPPDATA' in os.environ.keys():
-        path = os.environ['LOCALAPPDATA']
 
-    print('searching for kinfoFiles in ' + path)
-    found = False
+def GetIDString():
+    # K4Mac now has an extensive set of ids strings it uses
+    # in encoding pids and in creating unique passwords
+    # for use in its own version of CryptUnprotectDataV2
+
+    # BUT Amazon has now become nasty enough to detect when its app
+    # is being run under a debugger and actually changes code paths
+    # including which one of these strings is chosen, all to try
+    # to prevent reverse engineering
+
+    # Sad really ... they will only hurt their own sales ...
+    # true book lovers really want to keep their books forever
+    # and move them to their devices and DRM prevents that so they
+    # will just buy from someplace else that they can remove
+    # the DRM from
+
+    # Amazon should know by now that true book lover's are not like
+    # penniless kids that pirate music, we do not pirate books
+
+    if isNewInstall():
+        mungedmac = GetMACAddressMunged()
+        if len(mungedmac) > 7:
+            print('Using Munged MAC Address for ID: '+mungedmac)
+            return mungedmac
+    sernum = GetVolumeSerialNumber()
+    if len(sernum) > 7:
+        print('Using Volume Serial Number for ID: '+sernum)
+        return sernum
+    diskpart = GetUserHomeAppSupKindleDirParitionName()
+    uuidnum = GetDiskPartitionUUID(diskpart)
+    if len(uuidnum) > 7:
+        print('Using Disk Partition UUID for ID: '+uuidnum)
+        return uuidnum
+    mungedmac = GetMACAddressMunged()
+    if len(mungedmac) > 7:
+        print('Using Munged MAC Address for ID: '+mungedmac)
+        return mungedmac
+    print('Using Fixed constant 9999999999 for ID.')
+    return '9999999999'
+
+
+# implements an Pseudo Mac Version of Windows built-in Crypto routine
+# used by Kindle for Mac versions < 1.6.0
+class CryptUnprotectData(object):
+    def __init__(self):
+        sernum = GetVolumeSerialNumber()
+        if sernum == '':
+            sernum = '9999999999'
+        sp = sernum + '!@#' + GetUserName()
+        passwdData = encode(SHA256(sp),charMap1)
+        salt = '16743'
+        self.crp = LibCrypto()
+        iter = 0x3e8
+        keylen = 0x80
+        key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen)
+        self.key = key_iv[0:32]
+        self.iv = key_iv[32:48]
+        self.crp.set_decrypt_key(self.key, self.iv)
+
+    def decrypt(self, encryptedData):
+        cleartext = self.crp.decrypt(encryptedData)
+        cleartext = decode(cleartext,charMap1)
+        return cleartext
+
+
+# implements an Pseudo Mac Version of Windows built-in Crypto routine
+# used for Kindle for Mac Versions >= 1.6.0
+class CryptUnprotectDataV2(object):
+    def __init__(self):
+        sp = GetUserName() + ':&%:' + GetIDString()
+        passwdData = encode(SHA256(sp),charMap5)
+        # salt generation as per the code
+        salt = 0x0512981d * 2 * 1 * 1
+        salt = str(salt) + GetUserName()
+        salt = encode(salt,charMap5)
+        self.crp = LibCrypto()
+        iter = 0x800
+        keylen = 0x400
+        key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen)
+        self.key = key_iv[0:32]
+        self.iv = key_iv[32:48]
+        self.crp.set_decrypt_key(self.key, self.iv)
+
+    def decrypt(self, encryptedData):
+        cleartext = self.crp.decrypt(encryptedData)
+        cleartext = decode(cleartext, charMap5)
+        return cleartext
+
+
+# unprotect the new header blob in .kinf2011
+# used in Kindle for Mac Version >= 1.9.0
+def UnprotectHeaderData(encryptedData):
+    passwdData = 'header_key_data'
+    salt = 'HEADER.2011'
+    iter = 0x80
+    keylen = 0x100
+    crp = LibCrypto()
+    key_iv = crp.keyivgen(passwdData, salt, iter, keylen)
+    key = key_iv[0:32]
+    iv = key_iv[32:48]
+    crp.set_decrypt_key(key,iv)
+    cleartext = crp.decrypt(encryptedData)
+    return cleartext
 
-    # first look for older kindle-info files
-    kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
-    if os.path.isfile(kinfopath):
-        found = True
-        print('Found K4PC kindle.info file: ' + kinfopath)
-        kInfoFiles.append(kinfopath)
-
-    # now look for newer (K4PC 1.5.0 and later rainier.2.1.1.kinf file
-
-    kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf'
-    if os.path.isfile(kinfopath):
-        found = True
-        print('Found K4PC 1.5.X kinf file: ' + kinfopath)
-        kInfoFiles.append(kinfopath)
-
-    # now look for even newer (K4PC 1.6.0 and later) rainier.2.1.1.kinf file
-    kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf'
-    if os.path.isfile(kinfopath):
-        found = True
-        print('Found K4PC 1.6.X kinf file: ' + kinfopath)
-        kInfoFiles.append(kinfopath)
-
-    # now look for even newer (K4PC 1.9.0 and later) .kinf2011 file
-    kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2011'
-    if os.path.isfile(kinfopath):
-        found = True
-        print('Found K4PC kinf2011 file: ' + kinfopath)
-        kInfoFiles.append(kinfopath)
 
+# implements an Pseudo Mac Version of Windows built-in Crypto routine
+# used for Kindle for Mac Versions >= 1.9.0
+class CryptUnprotectDataV3(object):
+    def __init__(self, entropy):
+        sp = GetUserName() + '+@#$%+' + GetIDString()
+        passwdData = encode(SHA256(sp),charMap2)
+        salt = entropy
+        self.crp = LibCrypto()
+        iter = 0x800
+        keylen = 0x400
+        key_iv = self.crp.keyivgen(passwdData, salt, iter, keylen)
+        self.key = key_iv[0:32]
+        self.iv = key_iv[32:48]
+        self.crp.set_decrypt_key(self.key, self.iv)
+
+    def decrypt(self, encryptedData):
+        cleartext = self.crp.decrypt(encryptedData)
+        cleartext = decode(cleartext, charMap2)
+        return cleartext
+
+
+# Locate the .kindle-info files
+def getKindleInfoFiles(kInfoFiles):
+    found = False
+    home = os.getenv('HOME')
+    # search for any .kinf2011 files in new location (Sep 2012)
+    cmdline = 'find "' + home + '/Library/Containers/com.amazon.Kindle/Data/Library/Application Support" -name ".kinf2011"'
+    cmdline = cmdline.encode(sys.getfilesystemencoding())
+    p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
+    out1, out2 = p1.communicate()
+    reslst = out1.split('\n')
+    for resline in reslst:
+        if os.path.isfile(resline):
+            kInfoFiles.append(resline)
+            print('Found k4Mac kinf2011 file: ' + resline)
+            found = True
+   # search for any .kinf2011 files
+    cmdline = 'find "' + home + '/Library/Application Support" -name ".kinf2011"'
+    cmdline = cmdline.encode(sys.getfilesystemencoding())
+    p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
+    out1, out2 = p1.communicate()
+    reslst = out1.split('\n')
+    for resline in reslst:
+        if os.path.isfile(resline):
+            kInfoFiles.append(resline)
+            print('Found k4Mac kinf2011 file: ' + resline)
+            found = True
+    # search for any .kindle-info files
+    cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"'
+    cmdline = cmdline.encode(sys.getfilesystemencoding())
+    p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
+    out1, out2 = p1.communicate()
+    reslst = out1.split('\n')
+    kinfopath = 'NONE'
+    for resline in reslst:
+        if os.path.isfile(resline):
+            kInfoFiles.append(resline)
+            print('Found K4Mac kindle-info file: ' + resline)
+            found = True
+    # search for any .rainier*-kinf files
+    cmdline = 'find "' + home + '/Library/Application Support" -name ".rainier*-kinf"'
+    cmdline = cmdline.encode(sys.getfilesystemencoding())
+    p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
+    out1, out2 = p1.communicate()
+    reslst = out1.split('\n')
+    for resline in reslst:
+        if os.path.isfile(resline):
+            kInfoFiles.append(resline)
+            print('Found k4Mac kinf file: ' + resline)
+            found = True
     if not found:
-        print('No K4PC kindle.info/kinf/kinf2011 files have been found.')
+        print('No k4Mac kindle-info/kinf/kinf2011 files have been found.')
     return kInfoFiles
 
-
 # determine type of kindle info provided and return a
 # database of keynames and values
 def getDBfromFile(kInfoFile):
@@ -259,10 +556,11 @@ def getDBfromFile(kInfoFile):
     hdr = infoReader.read(1)
     data = infoReader.read()
 
-    if data.find('{') != -1 :
+    if data.find('[') != -1 :
 
         # older style kindle-info file
-        items = data.split('{')
+        cud = CryptUnprotectData()
+        items = data.split('[')
         for item in items:
             if item != '':
                 keyhash, rawdata = item.split(':')
@@ -274,18 +572,21 @@ def getDBfromFile(kInfoFile):
                 if keyname == "unknown":
                     keyname = keyhash
                 encryptedValue = decode(rawdata,charMap2)
-                DB[keyname] = CryptUnprotectData(encryptedValue, "", 0)
+                cleartext = cud.decrypt(encryptedValue)
+                DB[keyname] = cleartext
                 cnt = cnt + 1
         if cnt == 0:
             DB = None
         return DB
 
     if hdr == '/':
-        # else rainier-2-1-1 .kinf file
+
+        # else newer style .kinf file used by K4Mac >= 1.6.0
         # the .kinf file uses "/" to separate it into records
         # so remove the trailing "/" to make it easy to use split
         data = data[:-1]
         items = data.split('/')
+        cud = CryptUnprotectDataV2()
 
         # loop through the item records until all are processed
         while len(items) > 0:
@@ -296,10 +597,12 @@ def getDBfromFile(kInfoFile):
             # the first 32 chars of the first record of a group
             # is the MD5 hash of the key name encoded by charMap5
             keyhash = item[0:32]
+            keyname = "unknown"
 
-            # the raw keyhash string is used to create entropy for the actual
+            # the raw keyhash string is also used to create entropy for the actual
             # CryptProtectData Blob that represents that keys contents
-            entropy = SHA1(keyhash)
+            # "entropy" not used for K4Mac only K4PC
+            # entropy = SHA1(keyhash)
 
             # the remainder of the first record when decoded with charMap5
             # has the ':' split char followed by the string representation
@@ -322,6 +625,7 @@ def getDBfromFile(kInfoFile):
                     break
             if keyname == "unknown":
                 keyname = keyhash
+
             # the charMap5 encoded contents data has had a length
             # of chars (always odd) cut off of the front and moved
             # to the end to prevent decoding using charMap5 from
@@ -329,47 +633,49 @@ def getDBfromFile(kInfoFile):
             # CryptUnprotectData call from succeeding.
 
             # The offset into the charMap5 encoded contents seems to be:
-            # len(contents)-largest prime number <=  int(len(content)/3)
+            # len(contents) - largest prime number less than or equal to int(len(content)/3)
             # (in other words split "about" 2/3rds of the way through)
 
             # move first offsets chars to end to align for decode by charMap5
             encdata = "".join(edlst)
             contlen = len(encdata)
-            noffset = contlen - primes(int(contlen/3))[-1]
 
             # now properly split and recombine
             # by moving noffset chars from the start of the
             # string to the end of the string
+            noffset = contlen - primes(int(contlen/3))[-1]
             pfx = encdata[0:noffset]
             encdata = encdata[noffset:]
             encdata = encdata + pfx
 
-            # decode using Map5 to get the CryptProtect Data
+            # decode using charMap5 to get the CryptProtect Data
             encryptedValue = decode(encdata,charMap5)
-            DB[keyname] = CryptUnprotectData(encryptedValue, entropy, 1)
+            cleartext = cud.decrypt(encryptedValue)
+            DB[keyname] = cleartext
             cnt = cnt + 1
 
         if cnt == 0:
             DB = None
         return DB
 
-    # else newest .kinf2011 style .kinf file
-    # the .kinf file uses "/" to separate it into records
-    # so remove the trailing "/" to make it easy to use split
-    # need to put back the first char read because it it part
-    # of the added entropy blob
-    data = hdr + data[:-1]
+    # the latest .kinf2011 version for K4M 1.9.1
+    # put back the hdr char, it is needed
+    data = hdr + data
+    data = data[:-1]
     items = data.split('/')
 
-    # starts with and encoded and encrypted header blob
+    # the headerblob is the encrypted information needed to build the entropy string
     headerblob = items.pop(0)
-    encryptedValue = decode(headerblob, testMap1)
+    encryptedValue = decode(headerblob, charMap1)
     cleartext = UnprotectHeaderData(encryptedValue)
-    # now extract the pieces that form the added entropy
+
+    # now extract the pieces in the same way
+    # this version is different from K4PC it scales the build number by multipying by 735
     pattern = re.compile(r'''\[Version:(\d+)\]\[Build:(\d+)\]\[Cksum:([^\]]+)\]\[Guid:([\{\}a-z0-9\-]+)\]''', re.IGNORECASE)
     for m in re.finditer(pattern, cleartext):
-        added_entropy = m.group(2) + m.group(4)
+        entropy = str(int(m.group(2)) * 0x2df) + m.group(4)
 
+    cud = CryptUnprotectDataV3(entropy)
 
     # loop through the item records until all are processed
     while len(items) > 0:
@@ -380,10 +686,11 @@ def getDBfromFile(kInfoFile):
         # the first 32 chars of the first record of a group
         # is the MD5 hash of the key name encoded by charMap5
         keyhash = item[0:32]
+        keyname = "unknown"
 
-        # the sha1 of raw keyhash string is used to create entropy along
-        # with the added entropy provided above from the headerblob
-        entropy = SHA1(keyhash) + added_entropy
+        # unlike K4PC the keyhash is not used in generating entropy
+        # entropy = SHA1(keyhash) + added_entropy
+        # entropy = added_entropy
 
         # the remainder of the first record when decoded with charMap5
         # has the ':' split char followed by the string representation
@@ -399,12 +706,13 @@ def getDBfromFile(kInfoFile):
             item = items.pop(0)
             edlst.append(item)
 
-        # key names now use the new testMap8 encoding
         keyname = "unknown"
         for name in names:
             if encodeHash(name,testMap8) == keyhash:
                 keyname = name
                 break
+        if keyname == "unknown":
+            keyname = keyhash
 
         # the testMap8 encoded contents data has had a length
         # of chars (always odd) cut off of the front and moved
@@ -413,22 +721,26 @@ def getDBfromFile(kInfoFile):
         # CryptUnprotectData call from succeeding.
 
         # The offset into the testMap8 encoded contents seems to be:
-        # len(contents)-largest prime number <=  int(len(content)/3)
+        # len(contents) - largest prime number less than or equal to int(len(content)/3)
         # (in other words split "about" 2/3rds of the way through)
 
         # move first offsets chars to end to align for decode by testMap8
-        # by moving noffset chars from the start of the
-        # string to the end of the string
         encdata = "".join(edlst)
         contlen = len(encdata)
+
+        # now properly split and recombine
+        # by moving noffset chars from the start of the
+        # string to the end of the string
         noffset = contlen - primes(int(contlen/3))[-1]
         pfx = encdata[0:noffset]
         encdata = encdata[noffset:]
         encdata = encdata + pfx
 
-        # decode using new testMap8 to get the original CryptProtect Data
+        # decode using testMap8 to get the CryptProtect Data
         encryptedValue = decode(encdata,testMap8)
-        cleartext = CryptUnprotectData(encryptedValue, entropy, 1)
+        cleartext = cud.decrypt(encryptedValue)
+        # print keyname
+        # print cleartext
         DB[keyname] = cleartext
         cnt = cnt + 1
 
index 1ad2bacca76e28c4a5197e245bc203dcec390f5b..01c348cc8a638e243754aea2cd2681ad30e629a4 100644 (file)
Binary files a/Calibre_Plugins/k4mobidedrm_plugin/mobidedrm.py and b/Calibre_Plugins/k4mobidedrm_plugin/mobidedrm.py differ
index ce1a71fe68556f4a8b1f4bd3b999b58e845e80b1..87250734280a97418f36c06362b11d7959287779 100644 (file)
@@ -9,12 +9,13 @@
 \pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\ql\qnatural\pardirnatural
 \cf0 \
 \pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\qj\pardirnatural
-\cf0 DeDRM is an application that packs all of the python drm-removal software into one easy to use program that remembers preferences and settings.\
+\cf0 DeDRM is an application that packs all of the python dm-removal software into one easy to use program that remembers preferences and settings.\
 It works without manual configuration with Kindle for Mac ebooks and Adobe Adept ePub and PDF ebooks.\
 \
-To remove the DRM of Kindle ebooks from eInk Kindles, eReader pdb ebooks, Barnes and Noble ePubs, or Mobipocket ebooks, you must first run DeDRM application (by double-clicking it) and set some additional Preferences including:\
+To remove the DRM of Kindle ebooks from eInk Kindles, eReader pdb ebooks, Barnes&Noble ePubs, or Mobipocket ebooks, you must first run DeDRM application (by double-clicking it) and set some additional Preferences including:\
 \
-Kindle (not Kindle Fire): 16 digit Serial Number\
+eInk Kindle (not Kindle Fire): 16 digit Serial Number\
+Kindle for iOS: 40 digit UDID, but this probably won't work anymore\
 Barnes & Noble ePub:  Name and CC number or key file  (bnepubkey.b64)\
 eReader Social DRM: Name and last 8 digits of CC number\
 Mobipocket: 10 digit PID\
@@ -23,14 +24,23 @@ A final preference is the destination folder for the DRM-free copies of your ebo
 \
 Once these preferences have been set, you can drag and drop ebooks (or folders of ebooks) onto the DeDRM droplet to remove the DRM.\
 \
-This program requires Mac OS X 10.5 or above. \
+This program requires Mac OS X 10.4 or above. It will not work on Mac OS X 10.3 or earlier.\
 \
 \
 \pard\tx566\tx1133\tx1700\tx2267\tx2834\tx3401\tx3968\tx4535\tx5102\tx5669\tx6236\tx6803\ql\qnatural\pardirnatural
 
 \b \cf0 Installation
 \b0 \
-Drag the DeDRM application from from tools_v5.3\\DeDRM_Applications\\Macintosh (the location of this ReadMe) to your Applications folder, or anywhere else you find convenient.\
+Mac OS X 10.4 
+\i only
+\i0 : You 
+\i must
+\i0  first install Python 2.7.3 from http://python.org/. At the time of writing, the direct download link is http://www.python.org/ftp/python/2.7.3/python-2.7.3-macosx10.3.dmg.\
+Mac OS X 10.5 and above: You do 
+\i not
+\i0  need to install Python.\
+\
+Drag the DeDRM application from from tools_v5.4\\DeDRM_Applications\\Macintosh (the location of this ReadMe) to your Applications folder, or anywhere else you find convenient.\
 \
 \
 
index 209dcf2cd4fecce92c83e2e514568a87cb343e50..17d8dff45257460121db1ea4d0681010b22db896 100644 (file)
Binary files a/DeDRM_Macintosh_Application/DeDRM.app.txt and b/DeDRM_Macintosh_Application/DeDRM.app.txt differ
index 9fd88c83252f7b4b5090a95f15b101a6dcd954ec..34b4488b0c4d4dd00274c494c1e4d24de6c60b36 100644 (file)
        <key>CFBundleExecutable</key>
        <string>droplet</string>
        <key>CFBundleGetInfoString</key>
-       <string>DeDRM 5.3.1, Written 2010–2012 by Apprentice Alf and others.</string>
+       <string>DeDRM 5.4. AppleScript written 2010–2012 by Apprentice Alf and others.</string>
        <key>CFBundleIconFile</key>
        <string>DeDRM</string>
        <key>CFBundleInfoDictionaryVersion</key>
        <string>6.0</string>
        <key>CFBundleName</key>
-       <string>DeDRM 5.3.1</string>
+       <string>DeDRM 5.4</string>
        <key>CFBundlePackageType</key>
        <string>APPL</string>
        <key>CFBundleShortVersionString</key>
-       <string>5.3.1</string>
+       <string>5.4</string>
        <key>CFBundleSignature</key>
        <string>dplt</string>
-       <key>LSMinimumSystemVersion</key>
-       <string>10.5.0</string>
        <key>LSRequiresCarbon</key>
        <true/>
        <key>WindowState</key>
        <dict>
+               <key>dividerCollapsed</key>
+               <true/>
+               <key>eventLogLevel</key>
+               <integer>-1</integer>
                <key>name</key>
                <string>ScriptWindowState</string>
                <key>positionOfDivider</key>
-               <real>554</real>
+               <real>0</real>
                <key>savedFrame</key>
-               <string>1691 92 922 818 1440 0 1920 1080 </string>
+               <string>287 405 800 473 0 0 1440 878 </string>
                <key>selectedTabView</key>
                <string>event log</string>
        </dict>
index c715860463c5434cb471de58b710cbe95976c23f..543633386e77aa1606918dbb6d824493cd5b4ef3 100644 (file)
Binary files a/DeDRM_Macintosh_Application/DeDRM.app/Contents/MacOS/droplet and b/DeDRM_Macintosh_Application/DeDRM.app/Contents/MacOS/droplet differ
index 39367189aa3c841c692e4b21b7ee93ee1d285728..d0daa2a86c54e7fcd34422dc6bd731a80d5bcf9e 100644 (file)
Binary files a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM Progress Source.zip and b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM Progress Source.zip differ
index a22c653b197911a1b17f7dbed99dbf5b84ef1459..6ca10884e0cbca5df99270e625f75945e72f8a58 100644 (file)
        <key>CFBundleVersion</key>
        <string>1.0</string>
        <key>DTCompiler</key>
-       <string></string>
+       <string>4.0</string>
        <key>DTPlatformBuild</key>
        <string>10M2518</string>
        <key>DTPlatformVersion</key>
        <string>PG</string>
        <key>DTSDKBuild</key>
-       <string>9L31a</string>
+       <string>8S2167</string>
        <key>DTSDKName</key>
-       <string>macosx10.5</string>
+       <string>macosx10.4</string>
        <key>DTXcode</key>
        <string>0400</string>
        <key>DTXcodeBuild</key>
index 01afac090e89c43f4d574172b7c47d56bfab7162..ca47eb4bcb7331b2c11d169f8297a6d3fda1903a 100644 (file)
Binary files a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM Progress.app/Contents/MacOS/DeDRM Progress and b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/DeDRM Progress.app/Contents/MacOS/DeDRM Progress differ
index bdb078541438087c87c74245960f15e899f4e18e..31555eeb1a6efe0bc0708ba05e84025b66636ad6 100644 (file)
Binary files a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/Scripts/main.scpt and b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/Scripts/main.scpt differ
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/cmbtc_v2.2.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/cmbtc_v2.2.py
deleted file mode 100644 (file)
index 7be7a8a..0000000
+++ /dev/null
@@ -1,899 +0,0 @@
-#! /usr/bin/python
-
-"""
-
-Comprehensive Mazama Book DRM with Topaz Cryptography V2.2
-
------BEGIN PUBLIC KEY-----
-MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdBHJ4CNc6DNFCw4MRCw4SWAK6
-M8hYfnNEI0yQmn5Ti+W8biT7EatpauE/5jgQMPBmdNrDr1hbHyHBSP7xeC2qlRWC
-B62UCxeu/fpfnvNHDN/wPWWH4jynZ2M6cdcnE5LQ+FfeKqZn7gnG2No1U9h7oOHx
-y2/pHuYme7U1TsgSjwIDAQAB
------END PUBLIC KEY-----
-
-"""
-
-from __future__ import with_statement
-
-import csv
-import sys
-import os
-import getopt
-import zlib
-from struct import pack
-from struct import unpack
-from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
-    create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
-    string_at, Structure, c_void_p, cast
-import _winreg as winreg
-import Tkinter
-import Tkconstants
-import tkMessageBox
-import traceback
-import hashlib
-
-MAX_PATH = 255
-
-kernel32 = windll.kernel32
-advapi32 = windll.advapi32
-crypt32 = windll.crypt32
-
-global kindleDatabase
-global bookFile
-global bookPayloadOffset
-global bookHeaderRecords
-global bookMetadata
-global bookKey
-global command
-
-#
-# Various character maps used to decrypt books. Probably supposed to act as obfuscation
-#
-
-charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
-charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
-charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
-charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
-
-#
-# Exceptions for all the problems that might happen during the script
-#
-
-class CMBDTCError(Exception):
-    pass
-
-class CMBDTCFatal(Exception):
-    pass
-
-#
-# Stolen stuff
-#
-
-class DataBlob(Structure):
-    _fields_ = [('cbData', c_uint),
-                ('pbData', c_void_p)]
-DataBlob_p = POINTER(DataBlob)
-
-def GetSystemDirectory():
-    GetSystemDirectoryW = kernel32.GetSystemDirectoryW
-    GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint]
-    GetSystemDirectoryW.restype = c_uint
-    def GetSystemDirectory():
-        buffer = create_unicode_buffer(MAX_PATH + 1)
-        GetSystemDirectoryW(buffer, len(buffer))
-        return buffer.value
-    return GetSystemDirectory
-GetSystemDirectory = GetSystemDirectory()
-
-
-def GetVolumeSerialNumber():
-    GetVolumeInformationW = kernel32.GetVolumeInformationW
-    GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint,
-                                      POINTER(c_uint), POINTER(c_uint),
-                                      POINTER(c_uint), c_wchar_p, c_uint]
-    GetVolumeInformationW.restype = c_uint
-    def GetVolumeSerialNumber(path):
-        vsn = c_uint(0)
-        GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0)
-        return vsn.value
-    return GetVolumeSerialNumber
-GetVolumeSerialNumber = GetVolumeSerialNumber()
-
-
-def GetUserName():
-    GetUserNameW = advapi32.GetUserNameW
-    GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
-    GetUserNameW.restype = c_uint
-    def GetUserName():
-        buffer = create_unicode_buffer(32)
-        size = c_uint(len(buffer))
-        while not GetUserNameW(buffer, byref(size)):
-            buffer = create_unicode_buffer(len(buffer) * 2)
-            size.value = len(buffer)
-        return buffer.value.encode('utf-16-le')[::2]
-    return GetUserName
-GetUserName = GetUserName()
-
-
-def CryptUnprotectData():
-    _CryptUnprotectData = crypt32.CryptUnprotectData
-    _CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
-                                   c_void_p, c_void_p, c_uint, DataBlob_p]
-    _CryptUnprotectData.restype = c_uint
-    def CryptUnprotectData(indata, entropy):
-        indatab = create_string_buffer(indata)
-        indata = DataBlob(len(indata), cast(indatab, c_void_p))
-        entropyb = create_string_buffer(entropy)
-        entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
-        outdata = DataBlob()
-        if not _CryptUnprotectData(byref(indata), None, byref(entropy),
-                                   None, None, 0, byref(outdata)):
-            raise CMBDTCFatal("Failed to Unprotect Data")
-        return string_at(outdata.pbData, outdata.cbData)
-    return CryptUnprotectData
-CryptUnprotectData = CryptUnprotectData()
-
-#
-# Returns the MD5 digest of "message"
-#
-
-def MD5(message):
-    ctx = hashlib.md5()
-    ctx.update(message)
-    return ctx.digest()
-
-#
-# Returns the MD5 digest of "message"
-#
-
-def SHA1(message):
-    ctx = hashlib.sha1()
-    ctx.update(message)
-    return ctx.digest()
-
-#
-# Open the book file at path
-#
-
-def openBook(path):
-    try:
-        return open(path,'rb')
-    except:
-        raise CMBDTCFatal("Could not open book file: " + path)
-#
-# Encode the bytes in data with the characters in map
-#
-
-def encode(data, map):
-    result = ""
-    for char in data:
-        value = ord(char)
-        Q = (value ^ 0x80) // len(map)
-        R = value % len(map)
-        result += map[Q]
-        result += map[R]
-    return result
-
-#
-# Hash the bytes in data and then encode the digest with the characters in map
-#
-
-def encodeHash(data,map):
-    return encode(MD5(data),map)
-
-#
-# Decode the string in data with the characters in map. Returns the decoded bytes
-#
-
-def decode(data,map):
-    result = ""
-    for i in range (0,len(data),2):
-        high = map.find(data[i])
-        low = map.find(data[i+1])
-        value = (((high * 0x40) ^ 0x80) & 0xFF) + low
-        result += pack("B",value)
-    return result
-
-#
-# Locate and open the Kindle.info file (Hopefully in the way it is done in the Kindle application)
-#
-
-def openKindleInfo():
-    regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
-    path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
-    return open(path+'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info','r')
-
-#
-# Parse the Kindle.info file and return the records as a list of key-values
-#
-
-def parseKindleInfo():
-    DB = {}
-    infoReader = openKindleInfo()
-    infoReader.read(1)
-    data = infoReader.read()
-    items = data.split('{')
-
-    for item in items:
-        splito = item.split(':')
-        DB[splito[0]] =splito[1]
-    return DB
-
-#
-# Find if the original string for a hashed/encoded string is known. If so return the original string othwise return an empty string. (Totally not optimal)
-#
-
-def findNameForHash(hash):
-    names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
-    result = ""
-    for name in names:
-        if hash == encodeHash(name, charMap2):
-            result = name
-            break
-    return name
-
-#
-# Print all the records from the kindle.info file (option -i)
-#
-
-def printKindleInfo():
-    for record in kindleDatabase:
-        name = findNameForHash(record)
-        if name != "" :
-            print (name)
-            print ("--------------------------\n")
-        else :
-            print ("Unknown Record")
-        print getKindleInfoValueForHash(record)
-        print "\n"
-#
-# Get a record from the Kindle.info file for the key "hashedKey" (already hashed and encoded). Return the decoded and decrypted record
-#
-
-def getKindleInfoValueForHash(hashedKey):
-    global kindleDatabase
-    encryptedValue = decode(kindleDatabase[hashedKey],charMap2)
-    return CryptUnprotectData(encryptedValue,"")
-
-#
-#  Get a record from the Kindle.info file for the string in "key" (plaintext). Return the decoded and decrypted record
-#
-
-def getKindleInfoValueForKey(key):
-    return getKindleInfoValueForHash(encodeHash(key,charMap2))
-
-#
-# Get a 7 bit encoded number from the book file
-#
-
-def bookReadEncodedNumber():
-    flag = False
-    data = ord(bookFile.read(1))
-
-    if data == 0xFF:
-        flag = True
-        data = ord(bookFile.read(1))
-
-    if data >= 0x80:
-        datax = (data & 0x7F)
-        while data >= 0x80 :
-            data = ord(bookFile.read(1))
-            datax = (datax <<7) + (data & 0x7F)
-        data = datax
-
-    if flag:
-        data = -data
-    return data
-
-#
-# Encode a number in 7 bit format
-#
-
-def encodeNumber(number):
-    result = ""
-    negative = False
-    flag = 0
-
-    if number < 0 :
-        number = -number + 1
-        negative = True
-
-    while True:
-        byte = number & 0x7F
-        number = number >> 7
-        byte += flag
-        result += chr(byte)
-        flag = 0x80
-        if number == 0 :
-            if (byte == 0xFF and negative == False) :
-                result += chr(0x80)
-            break
-
-    if negative:
-        result += chr(0xFF)
-
-    return result[::-1]
-
-#
-# Get a length prefixed string from the file
-#
-
-def bookReadString():
-    stringLength = bookReadEncodedNumber()
-    return unpack(str(stringLength)+"s",bookFile.read(stringLength))[0]
-
-#
-# Returns a length prefixed string
-#
-
-def lengthPrefixString(data):
-    return encodeNumber(len(data))+data
-
-
-#
-# Read and return the data of one header record at the current book file position [[offset,compressedLength,decompressedLength],...]
-#
-
-def bookReadHeaderRecordData():
-    nbValues = bookReadEncodedNumber()
-    values = []
-    for i in range (0,nbValues):
-        values.append([bookReadEncodedNumber(),bookReadEncodedNumber(),bookReadEncodedNumber()])
-    return values
-
-#
-# Read and parse one header record at the current book file position and return the associated data [[offset,compressedLength,decompressedLength],...]
-#
-
-def parseTopazHeaderRecord():
-    if ord(bookFile.read(1)) != 0x63:
-        raise CMBDTCFatal("Parse Error : Invalid Header")
-
-    tag = bookReadString()
-    record = bookReadHeaderRecordData()
-    return [tag,record]
-
-#
-# Parse the header of a Topaz file, get all the header records and the offset for the payload
-#
-
-def parseTopazHeader():
-    global bookHeaderRecords
-    global bookPayloadOffset
-    magic = unpack("4s",bookFile.read(4))[0]
-
-    if magic != 'TPZ0':
-        raise CMBDTCFatal("Parse Error : Invalid Header, not a Topaz file")
-
-    nbRecords = bookReadEncodedNumber()
-    bookHeaderRecords = {}
-
-    for i in range (0,nbRecords):
-        result = parseTopazHeaderRecord()
-        bookHeaderRecords[result[0]] = result[1]
-
-    if ord(bookFile.read(1))  != 0x64 :
-        raise CMBDTCFatal("Parse Error : Invalid Header")
-
-    bookPayloadOffset = bookFile.tell()
-
-#
-# Get a record in the book payload, given its name and index. If necessary the record is decrypted. The record is not decompressed
-#
-
-def getBookPayloadRecord(name, index):
-    encrypted = False
-
-    try:
-        recordOffset = bookHeaderRecords[name][index][0]
-    except:
-        raise CMBDTCFatal("Parse Error : Invalid Record, record not found")
-
-    bookFile.seek(bookPayloadOffset + recordOffset)
-
-    tag = bookReadString()
-    if tag != name :
-        raise CMBDTCFatal("Parse Error : Invalid Record, record name doesn't match")
-
-    recordIndex = bookReadEncodedNumber()
-
-    if recordIndex < 0 :
-        encrypted = True
-        recordIndex = -recordIndex -1
-
-    if recordIndex != index :
-        raise CMBDTCFatal("Parse Error : Invalid Record, index doesn't match")
-
-    if bookHeaderRecords[name][index][2] != 0 :
-        record = bookFile.read(bookHeaderRecords[name][index][2])
-    else:
-        record = bookFile.read(bookHeaderRecords[name][index][1])
-
-    if encrypted:
-        ctx = topazCryptoInit(bookKey)
-        record = topazCryptoDecrypt(record,ctx)
-
-    return record
-
-#
-# Extract, decrypt and decompress a book record indicated by name and index and print it or save it in "filename"
-#
-
-def extractBookPayloadRecord(name, index, filename):
-    compressed = False
-
-    try:
-        compressed = bookHeaderRecords[name][index][2] != 0
-        record = getBookPayloadRecord(name,index)
-    except:
-        print("Could not find record")
-
-    if compressed:
-        try:
-            record = zlib.decompress(record)
-        except:
-            raise CMBDTCFatal("Could not decompress record")
-
-    if filename != "":
-        try:
-            file = open(filename,"wb")
-            file.write(record)
-            file.close()
-        except:
-            raise CMBDTCFatal("Could not write to destination file")
-    else:
-        print(record)
-
-#
-# return next record [key,value] from the book metadata from the current book position
-#
-
-def readMetadataRecord():
-    return [bookReadString(),bookReadString()]
-
-#
-# Parse the metadata record from the book payload and return a list of [key,values]
-#
-
-def parseMetadata():
-    global bookHeaderRecords
-    global bookPayloadAddress
-    global bookMetadata
-    bookMetadata = {}
-    bookFile.seek(bookPayloadOffset + bookHeaderRecords["metadata"][0][0])
-    tag = bookReadString()
-    if tag != "metadata" :
-        raise CMBDTCFatal("Parse Error : Record Names Don't Match")
-
-    flags = ord(bookFile.read(1))
-    nbRecords = ord(bookFile.read(1))
-
-    for i in range (0,nbRecords) :
-        record =readMetadataRecord()
-        bookMetadata[record[0]] = record[1]
-
-#
-# Returns two bit at offset from a bit field
-#
-
-def getTwoBitsFromBitField(bitField,offset):
-    byteNumber = offset // 4
-    bitPosition = 6 - 2*(offset % 4)
-
-    return ord(bitField[byteNumber]) >> bitPosition & 3
-
-#
-# Returns the six bits at offset from a bit field
-#
-
-def getSixBitsFromBitField(bitField,offset):
-    offset *= 3
-    value = (getTwoBitsFromBitField(bitField,offset) <<4) + (getTwoBitsFromBitField(bitField,offset+1) << 2) +getTwoBitsFromBitField(bitField,offset+2)
-    return value
-
-#
-# 8 bits to six bits encoding from hash to generate PID string
-#
-
-def encodePID(hash):
-    global charMap3
-    PID = ""
-    for position in range (0,8):
-        PID += charMap3[getSixBitsFromBitField(hash,position)]
-    return PID
-
-#
-# Context initialisation for the Topaz Crypto
-#
-
-def topazCryptoInit(key):
-    ctx1 = 0x0CAFFE19E
-
-    for keyChar in key:
-        keyByte = ord(keyChar)
-        ctx2 = ctx1
-        ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF )
-    return [ctx1,ctx2]
-
-#
-# decrypt data with the context prepared by topazCryptoInit()
-#
-
-def topazCryptoDecrypt(data, ctx):
-    ctx1 = ctx[0]
-    ctx2 = ctx[1]
-
-    plainText = ""
-
-    for dataChar in data:
-        dataByte = ord(dataChar)
-        m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF
-        ctx2 = ctx1
-        ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF)
-        plainText += chr(m)
-
-    return plainText
-
-#
-# Decrypt a payload record with the PID
-#
-
-def decryptRecord(data,PID):
-    ctx = topazCryptoInit(PID)
-    return topazCryptoDecrypt(data, ctx)
-
-#
-# Try to decrypt a dkey record (contains the book PID)
-#
-
-def decryptDkeyRecord(data,PID):
-    record = decryptRecord(data,PID)
-    fields = unpack("3sB8sB8s3s",record)
-
-    if fields[0] != "PID" or fields[5] != "pid" :
-        raise CMBDTCError("Didn't find PID magic numbers in record")
-    elif fields[1] != 8 or fields[3] != 8 :
-        raise CMBDTCError("Record didn't contain correct length fields")
-    elif fields[2] != PID :
-        raise CMBDTCError("Record didn't contain PID")
-
-    return fields[4]
-
-#
-# Decrypt all the book's dkey records (contain the book PID)
-#
-
-def decryptDkeyRecords(data,PID):
-    nbKeyRecords = ord(data[0])
-    records = []
-    data = data[1:]
-    for i in range (0,nbKeyRecords):
-        length = ord(data[0])
-        try:
-            key = decryptDkeyRecord(data[1:length+1],PID)
-            records.append(key)
-        except CMBDTCError:
-            pass
-        data = data[1+length:]
-
-    return records
-
-#
-# Encryption table used to generate the device PID
-#
-
-def generatePidEncryptionTable() :
-    table = []
-    for counter1 in range (0,0x100):
-        value = counter1
-        for counter2 in range (0,8):
-            if (value & 1 == 0) :
-                value = value >> 1
-            else :
-                value = value >> 1
-                value = value ^ 0xEDB88320
-        table.append(value)
-    return table
-
-#
-# Seed value used to generate the device PID
-#
-
-def generatePidSeed(table,dsn) :
-    value = 0
-    for counter in range (0,4) :
-        index = (ord(dsn[counter]) ^ value) &0xFF
-        value = (value >> 8) ^ table[index]
-    return value
-
-#
-# Generate the device PID
-#
-
-def generateDevicePID(table,dsn,nbRoll):
-    seed = generatePidSeed(table,dsn)
-    pidAscii = ""
-    pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF]
-    index = 0
-
-    for counter in range (0,nbRoll):
-        pid[index] = pid[index] ^ ord(dsn[counter])
-        index = (index+1) %8
-
-    for counter in range (0,8):
-        index = ((((pid[counter] >>5) & 3) ^ pid[counter]) & 0x1f) + (pid[counter] >> 7)
-        pidAscii += charMap4[index]
-    return pidAscii
-
-#
-# Create decrypted book payload
-#
-
-def createDecryptedPayload(payload):
-
-    # store data to be able to create the header later
-    headerData= []
-    currentOffset = 0
-
-    # Add social DRM to decrypted files
-
-    try:
-        data = getKindleInfoValueForKey("kindle.name.info")+":"+ getKindleInfoValueForKey("login")
-        if payload!= None:
-            payload.write(lengthPrefixString("sdrm"))
-            payload.write(encodeNumber(0))
-            payload.write(data)
-        else:
-            currentOffset += len(lengthPrefixString("sdrm"))
-            currentOffset += len(encodeNumber(0))
-            currentOffset += len(data)
-    except:
-        pass
-
-    for headerRecord in bookHeaderRecords:
-        name = headerRecord
-        newRecord = []
-
-        if name != "dkey" :
-
-            for index in range (0,len(bookHeaderRecords[name])) :
-                offset = currentOffset
-
-                if payload != None:
-                    # write tag
-                    payload.write(lengthPrefixString(name))
-                    # write data
-                    payload.write(encodeNumber(index))
-                    payload.write(getBookPayloadRecord(name, index))
-
-                else :
-                    currentOffset += len(lengthPrefixString(name))
-                    currentOffset += len(encodeNumber(index))
-                    currentOffset += len(getBookPayloadRecord(name, index))
-                    newRecord.append([offset,bookHeaderRecords[name][index][1],bookHeaderRecords[name][index][2]])
-
-        headerData.append([name,newRecord])
-
-
-
-    return headerData
-
-#
-# Create decrypted book
-#
-
-def createDecryptedBook(outputFile):
-    outputFile = open(outputFile,"wb")
-    # Write the payload in a temporary file
-    headerData = createDecryptedPayload(None)
-    outputFile.write("TPZ0")
-    outputFile.write(encodeNumber(len(headerData)))
-
-    for header in headerData :
-        outputFile.write(chr(0x63))
-        outputFile.write(lengthPrefixString(header[0]))
-        outputFile.write(encodeNumber(len(header[1])))
-        for numbers in header[1] :
-            outputFile.write(encodeNumber(numbers[0]))
-            outputFile.write(encodeNumber(numbers[1]))
-            outputFile.write(encodeNumber(numbers[2]))
-
-    outputFile.write(chr(0x64))
-    createDecryptedPayload(outputFile)
-    outputFile.close()
-
-#
-# Set the command to execute by the programm according to cmdLine parameters
-#
-
-def setCommand(name) :
-    global command
-    if command != "" :
-        raise CMBDTCFatal("Invalid command line parameters")
-    else :
-        command = name
-
-#
-# Program usage
-#
-
-def usage():
-    print("\nUsage:")
-    print("\nCMBDTC.py [options] bookFileName\n")
-    print("-p Adds a PID to the list of PIDs that are tried to decrypt the book key (can be used several times)")
-    print("-d Saves a decrypted copy of the book")
-    print("-r Prints or writes to disk a record indicated in the form name:index (e.g \"img:0\")")
-    print("-o Output file name to write records and decrypted books")
-    print("-v Verbose (can be used several times)")
-    print("-i Prints kindle.info database")
-
-#
-# Main
-#
-
-def main(argv=sys.argv):
-    global kindleDatabase
-    global bookMetadata
-    global bookKey
-    global bookFile
-    global command
-
-    progname = os.path.basename(argv[0])
-
-    verbose = 0
-    recordName = ""
-    recordIndex = 0
-    outputFile = ""
-    PIDs = []
-    kindleDatabase = None
-    command = ""
-
-
-    try:
-        opts, args = getopt.getopt(sys.argv[1:], "vdir:o:p:")
-    except getopt.GetoptError, err:
-        # print help information and exit:
-        print str(err) # will print something like "option -a not recognized"
-        usage()
-        sys.exit(2)
-
-    if len(opts) == 0 and len(args) == 0 :
-        usage()
-        sys.exit(2)
-
-    for o, a in opts:
-        if o == "-v":
-            verbose+=1
-        if o == "-i":
-            setCommand("printInfo")
-        if o =="-o":
-            if a == None :
-                raise CMBDTCFatal("Invalid parameter for -o")
-            outputFile = a
-        if o =="-r":
-            setCommand("printRecord")
-            try:
-                recordName,recordIndex = a.split(':')
-            except:
-                raise CMBDTCFatal("Invalid parameter for -r")
-        if o =="-p":
-            PIDs.append(a)
-        if o =="-d":
-            setCommand("doit")
-
-    if command == "" :
-        raise CMBDTCFatal("No action supplied on command line")
-
-    #
-    # Read the encrypted database
-    #
-
-    try:
-        kindleDatabase = parseKindleInfo()
-    except Exception, message:
-        if verbose>0:
-            print(message)
-
-    if kindleDatabase != None :
-        if command == "printInfo" :
-            printKindleInfo()
-
-    #
-    # Compute the DSN
-    #
-
-    # Get the Mazama Random number
-        MazamaRandomNumber = getKindleInfoValueForKey("MazamaRandomNumber")
-
-    # Get the HDD serial
-        encodedSystemVolumeSerialNumber = encodeHash(str(GetVolumeSerialNumber(GetSystemDirectory().split('\\')[0] + '\\')),charMap1)
-
-    # Get the current user name
-        encodedUsername = encodeHash(GetUserName(),charMap1)
-
-    # concat, hash and encode
-        DSN = encode(SHA1(MazamaRandomNumber+encodedSystemVolumeSerialNumber+encodedUsername),charMap1)
-
-        if verbose >1:
-            print("DSN: " + DSN)
-
-    #
-    # Compute the device PID
-    #
-
-        table =  generatePidEncryptionTable()
-        devicePID = generateDevicePID(table,DSN,4)
-        PIDs.append(devicePID)
-
-        if verbose > 0:
-            print("Device PID: " + devicePID)
-
-    #
-    # Open book and parse metadata
-    #
-
-    if len(args) == 1:
-
-        bookFile = openBook(args[0])
-        parseTopazHeader()
-        parseMetadata()
-
-    #
-    # Compute book PID
-    #
-
-    # Get the account token
-
-        if kindleDatabase != None:
-            kindleAccountToken = getKindleInfoValueForKey("kindle.account.tokens")
-
-            if verbose >1:
-                print("Account Token: " + kindleAccountToken)
-
-            keysRecord = bookMetadata["keys"]
-            keysRecordRecord = bookMetadata[keysRecord]
-
-            pidHash = SHA1(DSN+kindleAccountToken+keysRecord+keysRecordRecord)
-
-            bookPID = encodePID(pidHash)
-            PIDs.append(bookPID)
-
-            if verbose > 0:
-                print ("Book PID: " + bookPID )
-
-    #
-    #  Decrypt book key
-    #
-
-        dkey = getBookPayloadRecord('dkey', 0)
-
-        bookKeys = []
-        for PID in PIDs :
-            bookKeys+=decryptDkeyRecords(dkey,PID)
-
-        if len(bookKeys) == 0 :
-            if verbose > 0 :
-                print ("Book key could not be found. Maybe this book is not registered with this device.")
-        else :
-            bookKey = bookKeys[0]
-            if verbose > 0:
-                print("Book key: " + bookKey.encode('hex'))
-
-
-
-            if command == "printRecord" :
-                extractBookPayloadRecord(recordName,int(recordIndex),outputFile)
-                if outputFile != "" and verbose>0 :
-                    print("Wrote record to file: "+outputFile)
-            elif command == "doit" :
-                if outputFile!="" :
-                    createDecryptedBook(outputFile)
-                    if verbose >0 :
-                        print ("Decrypted book saved. Don't pirate!")
-                elif verbose > 0:
-                    print("Output file name was not supplied.")
-
-    return 0
-
-if __name__ == '__main__':
-    sys.exit(main())
index c029760955bd4d6bed120397667051ddfdde3f1d..98258788c86dbb40a3f2e773d185baad750d4cdf 100644 (file)
@@ -20,7 +20,7 @@ class ConfigWidget(QWidget):
         self.l = QVBoxLayout()
         self.setLayout(self.l)
 
-        self.serialLabel = QLabel('Kindle Serial numbers (separate with commas, no spaces)')
+        self.serialLabel = QLabel('eInk Kindle Serial numbers (First character B, 16 characters, use commas if more than one)')
         self.l.addWidget(self.serialLabel)
 
         self.serials = QLineEdit(self)
@@ -28,7 +28,7 @@ class ConfigWidget(QWidget):
         self.l.addWidget(self.serials)
         self.serialLabel.setBuddy(self.serials)
 
-        self.pidLabel = QLabel('Mobipocket PIDs (separate with commas, no spaces)')
+        self.pidLabel = QLabel('Mobipocket PIDs (8 or 10 characters, use commas if more than one)')
         self.l.addWidget(self.pidLabel)
 
         self.pids = QLineEdit(self)
@@ -50,8 +50,8 @@ class ConfigWidget(QWidget):
         self.wpLabel.setBuddy(self.wineprefix)
 
     def save_settings(self):
-        prefs['pids'] = str(self.pids.text())
-        prefs['serials'] = str(self.serials.text())
+       prefs['pids'] = str(self.pids.text()).replace(" ","")
+        prefs['serials'] = str(self.serials.text()).replace(" ","")
         winepref=str(self.wineprefix.text())
         if winepref.strip() != '':
             prefs['WINEPREFIX'] = winepref
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/droplet.icns b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/droplet.icns
deleted file mode 100644 (file)
index c2706de..0000000
Binary files a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/droplet.icns and /dev/null differ
index 5554a802b8235703854f3ae30714817d24020c80..6620282567a28e742a167300a3620859338dbf65 100644 (file)
Binary files a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/droplet.rsrc and b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/droplet.rsrc differ
index 917aa4aaa16210a7532e733a4fd667633ad8a4d9..03aa91fe50347c2dcabb08acbffa2ed0b12c5c82 100644 (file)
@@ -2,7 +2,7 @@
 
 from __future__ import with_statement
 
-# ignobleepub.pyw, version 3.4
+# ignobleepub.pyw, version 3.5
 
 # To run this program install Python 2.6 from <http://www.python.org/download/>
 # and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
@@ -17,6 +17,7 @@ from __future__ import with_statement
 #   3.2 - add support for encoding to 'utf-8' when building up list of files to cecrypt from encryption.xml
 #   3.3 - On Windows try PyCrypto first and OpenSSL next
 #   3.4 - Modify interace to allow use with import
+#   3.5 - Fix for potential problem with PyCrypto
 
 
 __license__ = 'GPL v3'
@@ -100,7 +101,7 @@ def _load_crypto_pycrypto():
 
     class AES(object):
         def __init__(self, key):
-            self._aes = _AES.new(key, _AES.MODE_CBC)
+            self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
 
         def decrypt(self, data):
             return self._aes.decrypt(data)
@@ -143,7 +144,7 @@ class ZipInfo(zipfile.ZipInfo):
 class Decryptor(object):
     def __init__(self, bookkey, encryption):
         enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
-        # self._aes = AES.new(bookkey, AES.MODE_CBC)
+        # self._aes = AES.new(bookkey, AES.MODE_CBC, '\x00'*16)
         self._aes = AES(bookkey)
         encryption = etree.fromstring(encryption)
         self._encrypted = encrypted = set()
@@ -271,7 +272,7 @@ def decryptBook(keypath, inpath, outpath):
     with open(keypath, 'rb') as f:
         keyb64 = f.read()
     key = keyb64.decode('base64')[:16]
-    # aes = AES.new(key, AES.MODE_CBC)
+    # aes = AES.new(key, AES.MODE_CBC, '\x00'*16)
     aes = AES(key)
 
     with closing(ZipFile(open(inpath, 'rb'))) as inf:
index e7a78ea19a7d60c2b2679d3b548d5162ca193c12..e2c50e2ebb1788dd784db57ba130a28a05ec2a37 100644 (file)
@@ -2,7 +2,7 @@
 
 from __future__ import with_statement
 
-# ignoblekeygen.pyw, version 2.3
+# ignoblekeygen.pyw, version 2.4
 
 # To run this program install Python 2.6 from <http://www.python.org/download/>
 # and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
@@ -15,6 +15,7 @@ from __future__ import with_statement
 #   2.1 - Allow Windows versions of libcrypto to be found
 #   2.2 - On Windows try PyCrypto first and then OpenSSL next
 #   2.3 - Modify interface to allow use of import
+#   2.4 - Improvements to UI and now works in plugins
 
 """
 Generate Barnes & Noble EPUB user key from name and credit card number.
@@ -25,10 +26,6 @@ __license__ = 'GPL v3'
 import sys
 import os
 import hashlib
-import Tkinter
-import Tkconstants
-import tkFileDialog
-import tkMessageBox
 
 
 
@@ -124,8 +121,10 @@ def normalize_name(name):
 
 
 def generate_keyfile(name, ccn, outpath):
+    # remove spaces and case from name and CC numbers.
     name = normalize_name(name) + '\x00'
-    ccn = ccn + '\x00'
+    ccn = normalize_name(ccn) + '\x00'
+    
     name_sha = hashlib.sha1(name).digest()[:16]
     ccn_sha = hashlib.sha1(ccn).digest()[:16]
     both_sha = hashlib.sha1(name + ccn).digest()
@@ -137,69 +136,6 @@ def generate_keyfile(name, ccn, outpath):
     return userkey
 
 
-class DecryptionDialog(Tkinter.Frame):
-    def __init__(self, root):
-        Tkinter.Frame.__init__(self, root, border=5)
-        self.status = Tkinter.Label(self, text='Enter parameters')
-        self.status.pack(fill=Tkconstants.X, expand=1)
-        body = Tkinter.Frame(self)
-        body.pack(fill=Tkconstants.X, expand=1)
-        sticky = Tkconstants.E + Tkconstants.W
-        body.grid_columnconfigure(1, weight=2)
-        Tkinter.Label(body, text='Name').grid(row=1)
-        self.name = Tkinter.Entry(body, width=30)
-        self.name.grid(row=1, column=1, sticky=sticky)
-        Tkinter.Label(body, text='CC#').grid(row=2)
-        self.ccn = Tkinter.Entry(body, width=30)
-        self.ccn.grid(row=2, column=1, sticky=sticky)
-        Tkinter.Label(body, text='Output file').grid(row=0)
-        self.keypath = Tkinter.Entry(body, width=30)
-        self.keypath.grid(row=0, column=1, sticky=sticky)
-        self.keypath.insert(0, 'bnepubkey.b64')
-        button = Tkinter.Button(body, text="...", command=self.get_keypath)
-        button.grid(row=0, column=2)
-        buttons = Tkinter.Frame(self)
-        buttons.pack()
-        botton = Tkinter.Button(
-            buttons, text="Generate", width=10, command=self.generate)
-        botton.pack(side=Tkconstants.LEFT)
-        Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
-        button = Tkinter.Button(
-            buttons, text="Quit", width=10, command=self.quit)
-        button.pack(side=Tkconstants.RIGHT)
-
-    def get_keypath(self):
-        keypath = tkFileDialog.asksaveasfilename(
-            parent=None, title='Select B&N EPUB key file to produce',
-            defaultextension='.b64',
-            filetypes=[('base64-encoded files', '.b64'),
-                       ('All Files', '.*')])
-        if keypath:
-            keypath = os.path.normpath(keypath)
-            self.keypath.delete(0, Tkconstants.END)
-            self.keypath.insert(0, keypath)
-        return
-
-    def generate(self):
-        name = self.name.get()
-        ccn = self.ccn.get()
-        keypath = self.keypath.get()
-        if not name:
-            self.status['text'] = 'Name not specified'
-            return
-        if not ccn:
-            self.status['text'] = 'Credit card number not specified'
-            return
-        if not keypath:
-            self.status['text'] = 'Output keyfile path not specified'
-            return
-        self.status['text'] = 'Generating...'
-        try:
-            generate_keyfile(name, ccn, keypath)
-        except Exception, e:
-            self.status['text'] = 'Error: ' + str(e)
-            return
-        self.status['text'] = 'Keyfile successfully generated'
 
 
 def cli_main(argv=sys.argv):
@@ -218,6 +154,75 @@ def cli_main(argv=sys.argv):
 
 
 def gui_main():
+    import Tkinter
+    import Tkconstants
+    import tkFileDialog
+    import tkMessageBox
+
+    class DecryptionDialog(Tkinter.Frame):
+        def __init__(self, root):
+            Tkinter.Frame.__init__(self, root, border=5)
+            self.status = Tkinter.Label(self, text='Enter parameters')
+            self.status.pack(fill=Tkconstants.X, expand=1)
+            body = Tkinter.Frame(self)
+            body.pack(fill=Tkconstants.X, expand=1)
+            sticky = Tkconstants.E + Tkconstants.W
+            body.grid_columnconfigure(1, weight=2)
+            Tkinter.Label(body, text='Account Name').grid(row=0)
+            self.name = Tkinter.Entry(body, width=40)
+            self.name.grid(row=0, column=1, sticky=sticky)
+            Tkinter.Label(body, text='CC#').grid(row=1)
+            self.ccn = Tkinter.Entry(body, width=40)
+            self.ccn.grid(row=1, column=1, sticky=sticky)
+            Tkinter.Label(body, text='Output file').grid(row=2)
+            self.keypath = Tkinter.Entry(body, width=40)
+            self.keypath.grid(row=2, column=1, sticky=sticky)
+            self.keypath.insert(2, 'bnepubkey.b64')
+            button = Tkinter.Button(body, text="...", command=self.get_keypath)
+            button.grid(row=2, column=2)
+            buttons = Tkinter.Frame(self)
+            buttons.pack()
+            botton = Tkinter.Button(
+                buttons, text="Generate", width=10, command=self.generate)
+            botton.pack(side=Tkconstants.LEFT)
+            Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
+            button = Tkinter.Button(
+                buttons, text="Quit", width=10, command=self.quit)
+            button.pack(side=Tkconstants.RIGHT)
+    
+        def get_keypath(self):
+            keypath = tkFileDialog.asksaveasfilename(
+                parent=None, title='Select B&N EPUB key file to produce',
+                defaultextension='.b64',
+                filetypes=[('base64-encoded files', '.b64'),
+                           ('All Files', '.*')])
+            if keypath:
+                keypath = os.path.normpath(keypath)
+                self.keypath.delete(0, Tkconstants.END)
+                self.keypath.insert(0, keypath)
+            return
+    
+        def generate(self):
+            name = self.name.get()
+            ccn = self.ccn.get()
+            keypath = self.keypath.get()
+            if not name:
+                self.status['text'] = 'Name not specified'
+                return
+            if not ccn:
+                self.status['text'] = 'Credit card number not specified'
+                return
+            if not keypath:
+                self.status['text'] = 'Output keyfile path not specified'
+                return
+            self.status['text'] = 'Generating...'
+            try:
+                generate_keyfile(name, ccn, keypath)
+            except Exception, e:
+                self.status['text'] = 'Error: ' + str(e)
+                return
+            self.status['text'] = 'Keyfile successfully generated'
+
     root = Tkinter.Tk()
     if AES is None:
         root.withdraw()
index 018736acd74626a3ac075f1659e565b28cfbb2aa..2bb32b10f73c2add5cdeecb0431412456598c650 100644 (file)
@@ -30,6 +30,8 @@ from __future__ import with_statement
 #   5.4 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml
 #   5.5 - On Windows try PyCrypto first, OpenSSL next
 #   5.6 - Modify interface to allow use with import
+#   5.7 - Fix for potential problem with PyCrypto
+
 """
 Decrypt Adobe ADEPT-encrypted EPUB books.
 """
@@ -235,7 +237,7 @@ def _load_crypto_pycrypto():
 
     class AES(object):
         def __init__(self, key):
-            self._aes = _AES.new(key, _AES.MODE_CBC)
+            self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
 
         def decrypt(self, data):
             return self._aes.decrypt(data)
index da61e77eb4ab05593ecd0a6f4a5a1700edf71a09..723b7c64eeab9ead313fc8283196cb5baa32407c 100644 (file)
@@ -3,7 +3,7 @@
 
 from __future__ import with_statement
 
-# ineptkey.pyw, version 5.4
+# ineptkey.pyw, version 5.6
 # Copyright © 2009-2010 i♥cabbages
 
 # Released under the terms of the GNU General Public Licence, version 3 or
@@ -36,6 +36,8 @@ from __future__ import with_statement
 #   5.2 - added support for output of key to a particular file
 #   5.3 - On Windows try PyCrypto first, OpenSSL next
 #   5.4 - Modify interface to allow use of import
+#   5.5 - Fix for potential problem with PyCrypto
+#   5.6 - Revise to allow use in Plugins to eliminate need for duplicate code
 
 """
 Retrieve Adobe ADEPT user key.
@@ -46,15 +48,17 @@ __license__ = 'GPL v3'
 import sys
 import os
 import struct
-import Tkinter
-import Tkconstants
-import tkMessageBox
-import traceback
+
+try:
+    from calibre.constants import iswindows, isosx
+except:
+    iswindows = sys.platform.startswith('win')
+    isosx = sys.platform.startswith('darwin')
 
 class ADEPTError(Exception):
     pass
 
-if sys.platform.startswith('win'):
+if iswindows:
     from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
         create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
         string_at, Structure, c_void_p, cast, c_size_t, memmove, CDLL, c_int, \
@@ -76,13 +80,13 @@ if sys.platform.startswith('win'):
             _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
                         ('rounds', c_int)]
         AES_KEY_p = POINTER(AES_KEY)
-
+    
         def F(restype, name, argtypes):
             func = getattr(libcrypto, name)
             func.restype = restype
             func.argtypes = argtypes
             return func
-
+    
         AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
                                 [c_char_p, c_int, AES_KEY_p])
         AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
@@ -110,7 +114,7 @@ if sys.platform.startswith('win'):
         from Crypto.Cipher import AES as _AES
         class AES(object):
             def __init__(self, key):
-                self._aes = _AES.new(key, _AES.MODE_CBC)
+                self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
             def decrypt(self, data):
                 return self._aes.decrypt(data)
         return AES
@@ -292,13 +296,9 @@ if sys.platform.startswith('win'):
         return CryptUnprotectData
     CryptUnprotectData = CryptUnprotectData()
 
-    def retrieve_key(keypath):
+    def retrieve_keys():
         if AES is None:
-            tkMessageBox.showerror(
-                "ADEPT Key",
-                "This script requires PyCrypto or OpenSSL which must be installed "
-                "separately.  Read the top-of-script comment for details.")
-            return False
+            raise ADEPTError("PyCrypto or OpenSSL must be installed")
         root = GetSystemDirectory().split('\\')[0] + '\\'
         serial = GetVolumeSerialNumber(root)
         vendor = cpuid0()
@@ -313,6 +313,7 @@ if sys.platform.startswith('win'):
         device = winreg.QueryValueEx(regkey, 'key')[0]
         keykey = CryptUnprotectData(device, entropy)
         userkey = None
+        keys = []
         try:
             plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH)
         except WindowsError:
@@ -334,50 +335,43 @@ if sys.platform.startswith('win'):
                 if ktype != 'privateLicenseKey':
                     continue
                 userkey = winreg.QueryValueEx(plkkey, 'value')[0]
-                break
-            if userkey is not None:
-                break
-        if userkey is None:
+                userkey = userkey.decode('base64')
+                aes = AES(keykey)
+                userkey = aes.decrypt(userkey)
+                userkey = userkey[26:-ord(userkey[-1])]
+                keys.append(userkey)
+        if len(keys) == 0:
             raise ADEPTError('Could not locate privateLicenseKey')
-        userkey = userkey.decode('base64')
-        aes = AES(keykey)
-        userkey = aes.decrypt(userkey)
-        userkey = userkey[26:-ord(userkey[-1])]
-        with open(keypath, 'wb') as f:
-            f.write(userkey)
-        return True
-
-elif sys.platform.startswith('darwin'):
+        return keys
+        
+
+elif isosx:
     import xml.etree.ElementTree as etree
-    import Carbon.File
-    import Carbon.Folder
-    import Carbon.Folders
-    import MacOS
+    import subprocess
 
-    ACTIVATION_PATH = 'Adobe/Digital Editions/activation.dat'
     NSMAP = {'adept': 'http://ns.adobe.com/adept',
              'enc': 'http://www.w3.org/2001/04/xmlenc#'}
 
-    def find_folder(domain, dtype):
-        try:
-            fsref = Carbon.Folder.FSFindFolder(domain, dtype, False)
-            return Carbon.File.pathname(fsref)
-        except MacOS.Error:
-            return None
-
-    def find_app_support_file(subpath):
-        dtype = Carbon.Folders.kApplicationSupportFolderType
-        for domain in Carbon.Folders.kUserDomain, Carbon.Folders.kLocalDomain:
-            path = find_folder(domain, dtype)
-            if path is None:
-                continue
-            path = os.path.join(path, subpath)
-            if os.path.isfile(path):
-                return path
+    def findActivationDat():
+        home = os.getenv('HOME')
+        cmdline = 'find "' + home + '/Library/Application Support/Adobe/Digital Editions" -name "activation.dat"'
+        cmdline = cmdline.encode(sys.getfilesystemencoding())
+        p2 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
+        out1, out2 = p2.communicate()
+        reslst = out1.split('\n')
+        cnt = len(reslst)
+        for j in xrange(cnt):
+            resline = reslst[j]
+            pp = resline.find('activation.dat')
+            if pp >= 0:
+                ActDatPath = resline
+                break
+        if os.path.exists(ActDatPath):
+            return ActDatPath
         return None
 
-    def retrieve_key(keypath):
-        actpath = find_app_support_file(ACTIVATION_PATH)
+    def retrieve_keys():
+        actpath = findActivationDat()
         if actpath is None:
             raise ADEPTError("Could not locate ADE activation")
         tree = etree.parse(actpath)
@@ -386,39 +380,18 @@ elif sys.platform.startswith('darwin'):
         userkey = tree.findtext(expr)
         userkey = userkey.decode('base64')
         userkey = userkey[26:]
-        with open(keypath, 'wb') as f:
-            f.write(userkey)
-        return True
-
-elif sys.platform.startswith('cygwin'):
-    def retrieve_key(keypath):
-        tkMessageBox.showerror(
-            "ADEPT Key",
-            "This script requires a Windows-native Python, and cannot be run "
-            "under Cygwin.  Please install a Windows-native Python and/or "
-            "check your file associations.")
-        return False
+        return [userkey]
 
 else:
-    def retrieve_key(keypath):
-        tkMessageBox.showerror(
-            "ADEPT Key",
-            "This script only supports Windows and Mac OS X.  For Linux "
-            "you should be able to run ADE and this script under Wine (with "
-            "an appropriate version of Windows Python installed).")
-        return False
-
-class ExceptionDialog(Tkinter.Frame):
-    def __init__(self, root, text):
-        Tkinter.Frame.__init__(self, root, border=5)
-        label = Tkinter.Label(self, text="Unexpected error:",
-                              anchor=Tkconstants.W, justify=Tkconstants.LEFT)
-        label.pack(fill=Tkconstants.X, expand=0)
-        self.text = Tkinter.Text(self)
-        self.text.pack(fill=Tkconstants.BOTH, expand=1)
-
-        self.text.insert(Tkconstants.END, text)
-
+    def retrieve_keys(keypath):
+        raise ADEPTError("This script only supports Windows and Mac OS X.")
+        return []
+        
+def retrieve_key(keypath):
+    keys = retrieve_keys()
+    with open(keypath, 'wb') as f:
+        f.write(keys[0])
+    return True
 
 def extractKeyfile(keypath):
     try:
@@ -440,10 +413,27 @@ def cli_main(argv=sys.argv):
 
 
 def main(argv=sys.argv):
+    import Tkinter
+    import Tkconstants
+    import tkMessageBox
+    import traceback
+
+    class ExceptionDialog(Tkinter.Frame):
+        def __init__(self, root, text):
+            Tkinter.Frame.__init__(self, root, border=5)
+            label = Tkinter.Label(self, text="Unexpected error:",
+                                  anchor=Tkconstants.W, justify=Tkconstants.LEFT)
+            label.pack(fill=Tkconstants.X, expand=0)
+            self.text = Tkinter.Text(self)
+            self.text.pack(fill=Tkconstants.BOTH, expand=1)
+    
+            self.text.insert(Tkconstants.END, text)
+
+
     root = Tkinter.Tk()
     root.withdraw()
     progname = os.path.basename(argv[0])
-    keypath = 'adeptkey.der'
+    keypath = os.path.abspath("adeptkey.der")
     success = False
     try:
         success = retrieve_key(keypath)
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mdumpkinfo.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/k4mdumpkinfo.py
deleted file mode 100644 (file)
index 44881eb..0000000
+++ /dev/null
@@ -1,333 +0,0 @@
-# engine to remove drm from Kindle for Mac books
-# for personal use for archiving and converting your ebooks
-#  PLEASE DO NOT PIRATE!
-# We want all authors and Publishers, and eBook stores to live long and prosperous lives
-#
-# it borrows heavily from works by CMBDTC, IHeartCabbages, skindle,
-#    unswindle, DiapDealer, some_updates and many many others
-
-from __future__ import with_statement
-
-class Unbuffered:
-    def __init__(self, stream):
-        self.stream = stream
-    def write(self, data):
-        self.stream.write(data)
-        self.stream.flush()
-    def __getattr__(self, attr):
-        return getattr(self.stream, attr)
-
-import sys
-sys.stdout=Unbuffered(sys.stdout)
-import os, csv, getopt
-from struct import pack
-from struct import unpack
-import zlib
-
-# for handling sub processes
-import subprocess
-from subprocess import Popen, PIPE, STDOUT
-import subasyncio
-from subasyncio import Process
-
-
-#Exception Handling
-class K4MDEDRMError(Exception):
-    pass
-class K4MDEDRMFatal(Exception):
-    pass
-
-#
-# crypto routines
-#
-import hashlib
-
-def MD5(message):
-    ctx = hashlib.md5()
-    ctx.update(message)
-    return ctx.digest()
-
-def SHA1(message):
-    ctx = hashlib.sha1()
-    ctx.update(message)
-    return ctx.digest()
-
-def SHA256(message):
-    ctx = hashlib.sha256()
-    ctx.update(message)
-    return ctx.digest()
-
-# interface to needed routines in openssl's libcrypto
-def _load_crypto_libcrypto():
-    from ctypes import CDLL, byref, POINTER, c_void_p, c_char_p, c_int, c_long, \
-        Structure, c_ulong, create_string_buffer, addressof, string_at, cast
-    from ctypes.util import find_library
-
-    libcrypto = find_library('crypto')
-    if libcrypto is None:
-        raise K4MDEDRMError('libcrypto not found')
-    libcrypto = CDLL(libcrypto)
-
-    AES_MAXNR = 14
-    c_char_pp = POINTER(c_char_p)
-    c_int_p = POINTER(c_int)
-
-    class AES_KEY(Structure):
-        _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))), ('rounds', c_int)]
-    AES_KEY_p = POINTER(AES_KEY)
-
-    def F(restype, name, argtypes):
-        func = getattr(libcrypto, name)
-        func.restype = restype
-        func.argtypes = argtypes
-        return func
-
-    AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',[c_char_p, c_char_p, c_ulong, AES_KEY_p, c_char_p,c_int])
-
-    AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',[c_char_p, c_int, AES_KEY_p])
-
-    PKCS5_PBKDF2_HMAC_SHA1 = F(c_int, 'PKCS5_PBKDF2_HMAC_SHA1',
-                                [c_char_p, c_ulong, c_char_p, c_ulong, c_ulong, c_ulong, c_char_p])
-
-    class LibCrypto(object):
-        def __init__(self):
-            self._blocksize = 0
-            self._keyctx = None
-            self.iv = 0
-        def set_decrypt_key(self, userkey, iv):
-            self._blocksize = len(userkey)
-            if (self._blocksize != 16) and (self._blocksize != 24) and (self._blocksize != 32) :
-                raise K4MDEDRMError('AES improper key used')
-                return
-            keyctx = self._keyctx = AES_KEY()
-            self.iv = iv
-            rv = AES_set_decrypt_key(userkey, len(userkey) * 8, keyctx)
-            if rv < 0:
-                raise K4MDEDRMError('Failed to initialize AES key')
-        def decrypt(self, data):
-            out = create_string_buffer(len(data))
-            rv = AES_cbc_encrypt(data, out, len(data), self._keyctx, self.iv, 0)
-            if rv == 0:
-                raise K4MDEDRMError('AES decryption failed')
-            return out.raw
-        def keyivgen(self, passwd):
-            salt = '16743'
-            saltlen = 5
-            passlen = len(passwd)
-            iter = 0x3e8
-            keylen = 80
-            out = create_string_buffer(keylen)
-            rv = PKCS5_PBKDF2_HMAC_SHA1(passwd, passlen, salt, saltlen, iter, keylen, out)
-            return out.raw
-    return LibCrypto
-
-def _load_crypto():
-    LibCrypto = None
-    try:
-        LibCrypto = _load_crypto_libcrypto()
-    except (ImportError, K4MDEDRMError):
-        pass
-    return LibCrypto
-
-LibCrypto = _load_crypto()
-
-#
-# Utility Routines
-#
-
-# uses a sub process to get the Hard Drive Serial Number using ioreg
-# returns with the first found serial number in that class
-def GetVolumeSerialNumber():
-    sernum = os.getenv('MYSERIALNUMBER')
-    if sernum != None:
-        return sernum
-    cmdline = '/usr/sbin/ioreg -l -S -w 0 -r -c AppleAHCIDiskDriver'
-    cmdline = cmdline.encode(sys.getfilesystemencoding())
-    p = Process(cmdline, shell=True, bufsize=1, stdin=None, stdout=PIPE, stderr=PIPE, close_fds=False)
-    poll = p.wait('wait')
-    results = p.read()
-    reslst = results.split('\n')
-    cnt = len(reslst)
-    bsdname = None
-    sernum = None
-    foundIt = False
-    for j in xrange(cnt):
-        resline = reslst[j]
-        pp = resline.find('"Serial Number" = "')
-        if pp >= 0:
-            sernum = resline[pp+19:-1]
-            sernum = sernum.strip()
-        bb = resline.find('"BSD Name" = "')
-        if bb >= 0:
-            bsdname = resline[bb+14:-1]
-            bsdname = bsdname.strip()
-            if (bsdname == 'disk0') and (sernum != None):
-                foundIt = True
-                break
-    if not foundIt:
-        sernum = '9999999999'
-    return sernum
-
-# uses unix env to get username instead of using sysctlbyname
-def GetUserName():
-    username = os.getenv('USER')
-    return username
-
-MAX_PATH = 255
-
-#
-# start of Kindle specific routines
-#
-
-global kindleDatabase
-
-# Various character maps used to decrypt books. Probably supposed to act as obfuscation
-charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
-charMap2 = "ZB0bYyc1xDdW2wEV3Ff7KkPpL8UuGA4gz-Tme9Nn_tHh5SvXCsIiR6rJjQaqlOoM"
-charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
-charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
-
-# Encode the bytes in data with the characters in map
-def encode(data, map):
-    result = ""
-    for char in data:
-        value = ord(char)
-        Q = (value ^ 0x80) // len(map)
-        R = value % len(map)
-        result += map[Q]
-        result += map[R]
-    return result
-
-# Hash the bytes in data and then encode the digest with the characters in map
-def encodeHash(data,map):
-    return encode(MD5(data),map)
-
-# Decode the string in data with the characters in map. Returns the decoded bytes
-def decode(data,map):
-    result = ""
-    for i in range (0,len(data)-1,2):
-        high = map.find(data[i])
-        low = map.find(data[i+1])
-        if (high == -1) or (low == -1) :
-            break
-        value = (((high * len(map)) ^ 0x80) & 0xFF) + low
-        result += pack("B",value)
-    return result
-
-# implements an Pseudo Mac Version of Windows built-in Crypto routine
-def CryptUnprotectData(encryptedData):
-    sp = GetVolumeSerialNumber() + '!@#' + GetUserName()
-    passwdData = encode(SHA256(sp),charMap1)
-    crp = LibCrypto()
-    key_iv = crp.keyivgen(passwdData)
-    key = key_iv[0:32]
-    iv = key_iv[32:48]
-    crp.set_decrypt_key(key,iv)
-    cleartext = crp.decrypt(encryptedData)
-    return cleartext
-
-# Locate and open the .kindle-info file
-def openKindleInfo():
-    home = os.getenv('HOME')
-    kinfopath = home +  '/Library/Application Support/Amazon/Kindle/storage/.kindle-info'
-    if not os.path.exists(kinfopath):
-        kinfopath = home +  '/Library/Application Support/Amazon/Kindle for Mac/storage/.kindle-info'
-        if not os.path.exists(kinfopath):
-            raise K4MDEDRMError('Error: .kindle-info file can not be found')
-    return open(kinfopath,'r')
-
-# Parse the Kindle.info file and return the records as a list of key-values
-def parseKindleInfo():
-    DB = {}
-    infoReader = openKindleInfo()
-    infoReader.read(1)
-    data = infoReader.read()
-    items = data.split('[')
-    for item in items:
-        splito = item.split(':')
-        DB[splito[0]] =splito[1]
-    return DB
-
-# Get a record from the Kindle.info file for the key "hashedKey" (already hashed and encoded). Return the decoded and decrypted record
-def getKindleInfoValueForHash(hashedKey):
-    global kindleDatabase
-    encryptedValue = decode(kindleDatabase[hashedKey],charMap2)
-    cleartext = CryptUnprotectData(encryptedValue)
-    return decode(cleartext, charMap1)
-
-#  Get a record from the Kindle.info file for the string in "key" (plaintext). Return the decoded and decrypted record
-def getKindleInfoValueForKey(key):
-    return getKindleInfoValueForHash(encodeHash(key,charMap2))
-
-# Find if the original string for a hashed/encoded string is known. If so return the original string othwise return an empty string.
-def findNameForHash(hash):
-    names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
-    result = ""
-    for name in names:
-        if hash == encodeHash(name, charMap2):
-            result = name
-            break
-    return result
-
-# Print all the records from the kindle.info file (option -i)
-def printKindleInfo():
-    for record in kindleDatabase:
-        name = findNameForHash(record)
-        if name != "" :
-            print (name)
-            print ("--------------------------")
-        else :
-            print ("Unknown Record")
-        print getKindleInfoValueForHash(record)
-        print "\n"
-
-#
-# PID generation routines
-#
-
-# Returns two bit at offset from a bit field
-def getTwoBitsFromBitField(bitField,offset):
-    byteNumber = offset // 4
-    bitPosition = 6 - 2*(offset % 4)
-    return ord(bitField[byteNumber]) >> bitPosition & 3
-
-# Returns the six bits at offset from a bit field
-def getSixBitsFromBitField(bitField,offset):
-    offset *= 3
-    value = (getTwoBitsFromBitField(bitField,offset) <<4) + (getTwoBitsFromBitField(bitField,offset+1) << 2) +getTwoBitsFromBitField(bitField,offset+2)
-    return value
-
-# 8 bits to six bits encoding from hash to generate PID string
-def encodePID(hash):
-    global charMap3
-    PID = ""
-    for position in range (0,8):
-        PID += charMap3[getSixBitsFromBitField(hash,position)]
-    return PID
-
-
-#
-# Main
-#
-
-def main(argv=sys.argv):
-    global kindleDatabase
-
-    kindleDatabase = None
-
-    #
-    # Read the encrypted database
-    #
-
-    try:
-        kindleDatabase = parseKindleInfo()
-    except Exception, message:
-        print(message)
-
-    if kindleDatabase != None :
-        printKindleInfo()
-
-    return 0
-
-if __name__ == '__main__':
-    sys.exit(main())
index c4a6322643c97c1c773b25593d38eb88c1348a84..03858390b434858493ae0ab797b54d5f1e5b6954 100644 (file)
@@ -495,6 +495,7 @@ class CryptUnprotectDataV3(object):
 
 # Locate the .kindle-info files
 def getKindleInfoFiles(kInfoFiles):
+    found = False
     home = os.getenv('HOME')
     # search for any .kinf2011 files in new location (Sep 2012)
     cmdline = 'find "' + home + '/Library/Containers/com.amazon.Kindle/Data/Library/Application Support" -name ".kinf2011"'
@@ -525,7 +526,6 @@ def getKindleInfoFiles(kInfoFiles):
     out1, out2 = p1.communicate()
     reslst = out1.split('\n')
     kinfopath = 'NONE'
-    found = False
     for resline in reslst:
         if os.path.isfile(resline):
             kInfoFiles.append(resline)
index 1bd256234dfd4159fa15113db0efa2993ee80e5d..d491d7d8dd7e52c8fc98b1c3ad2b197e441521b9 100644 (file)
@@ -204,45 +204,62 @@ CryptUnprotectData = CryptUnprotectData()
 
 # Locate all of the kindle-info style files and return as list
 def getKindleInfoFiles(kInfoFiles):
-    regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
-    path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
-
     # some 64 bit machines do not have the proper registry key for some reason
     # or the pythonn interface to the 32 vs 64 bit registry is broken
+    path = ""
     if 'LOCALAPPDATA' in os.environ.keys():
         path = os.environ['LOCALAPPDATA']
-
-    print('searching for kinfoFiles in ' + path)
-    found = False
-
-    # first look for older kindle-info files
-    kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
-    if os.path.isfile(kinfopath):
-        found = True
-        print('Found K4PC kindle.info file: ' + kinfopath)
-        kInfoFiles.append(kinfopath)
-
-    # now look for newer (K4PC 1.5.0 and later rainier.2.1.1.kinf file
-
-    kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf'
-    if os.path.isfile(kinfopath):
-        found = True
-        print('Found K4PC 1.5.X kinf file: ' + kinfopath)
-        kInfoFiles.append(kinfopath)
-
-    # now look for even newer (K4PC 1.6.0 and later) rainier.2.1.1.kinf file
-    kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf'
-    if os.path.isfile(kinfopath):
-        found = True
-        print('Found K4PC 1.6.X kinf file: ' + kinfopath)
-        kInfoFiles.append(kinfopath)
-
-    # now look for even newer (K4PC 1.9.0 and later) .kinf2011 file
-    kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2011'
-    if os.path.isfile(kinfopath):
-        found = True
-        print('Found K4PC kinf2011 file: ' + kinfopath)
-        kInfoFiles.append(kinfopath)
+    else:
+        # User Shell Folders show take precedent over Shell Folders if present
+        try:
+            regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\")
+            path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
+            if not os.path.isdir(path):
+                path = ""
+                try:
+                    regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
+                    path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
+                    if not os.path.isdir(path):
+                        path = ""
+                except RegError:
+                    pass
+        except RegError:
+            pass
+
+    found = False    
+    if path == "":
+        print ('Could not find the folder in which to look for kinfoFiles.')
+    else:
+        print('searching for kinfoFiles in ' + path)
+    
+        # first look for older kindle-info files
+        kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
+        if os.path.isfile(kinfopath):
+            found = True
+            print('Found K4PC kindle.info file: ' + kinfopath)
+            kInfoFiles.append(kinfopath)
+    
+        # now look for newer (K4PC 1.5.0 and later rainier.2.1.1.kinf file
+    
+        kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf'
+        if os.path.isfile(kinfopath):
+            found = True
+            print('Found K4PC 1.5.X kinf file: ' + kinfopath)
+            kInfoFiles.append(kinfopath)
+    
+        # now look for even newer (K4PC 1.6.0 and later) rainier.2.1.1.kinf file
+        kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf'
+        if os.path.isfile(kinfopath):
+            found = True
+            print('Found K4PC 1.6.X kinf file: ' + kinfopath)
+            kInfoFiles.append(kinfopath)
+    
+        # now look for even newer (K4PC 1.9.0 and later) .kinf2011 file
+        kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2011'
+        if os.path.isfile(kinfopath):
+            found = True
+            print('Found K4PC kinf2011 file: ' + kinfopath)
+            kInfoFiles.append(kinfopath)
 
     if not found:
         print('No K4PC kindle.info/kinf/kinf2011 files have been found.')
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/libalfcrypto src.zip b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/libalfcrypto src.zip
deleted file mode 100644 (file)
index d40a3bf..0000000
Binary files a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/libalfcrypto src.zip and /dev/null differ
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/pbkdf2.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/pbkdf2.py
deleted file mode 100644 (file)
index 65220a9..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-# A simple implementation of pbkdf2 using stock python modules. See RFC2898
-# for details. Basically, it derives a key from a password and salt.
-
-# Copyright 2004 Matt Johnston <matt @ ucc asn au>
-# Copyright 2009 Daniel Holth <dholth@fastmail.fm>
-# This code may be freely used and modified for any purpose.
-
-# Revision history
-# v0.1  October 2004    - Initial release
-# v0.2  8 March 2007    - Make usable with hashlib in Python 2.5 and use
-# v0.3  ""                 the correct digest_size rather than always 20
-# v0.4  Oct 2009        - Rescue from chandler svn, test and optimize.
-
-import sys
-import hmac
-from struct import pack
-try:
-    # only in python 2.5
-    import hashlib
-    sha = hashlib.sha1
-    md5 = hashlib.md5
-    sha256 = hashlib.sha256
-except ImportError: # pragma: NO COVERAGE
-    # fallback
-    import sha
-    import md5
-
-# this is what you want to call.
-def pbkdf2( password, salt, itercount, keylen, hashfn = sha ):
-    try:
-        # depending whether the hashfn is from hashlib or sha/md5
-        digest_size = hashfn().digest_size
-    except TypeError: # pragma: NO COVERAGE
-        digest_size = hashfn.digest_size
-    # l - number of output blocks to produce
-    l = keylen / digest_size
-    if keylen % digest_size != 0:
-        l += 1
-
-    h = hmac.new( password, None, hashfn )
-
-    T = ""
-    for i in range(1, l+1):
-        T += pbkdf2_F( h, salt, itercount, i )
-
-    return T[0: keylen]
-
-def xorstr( a, b ):
-    if len(a) != len(b):
-        raise ValueError("xorstr(): lengths differ")
-    return ''.join((chr(ord(x)^ord(y)) for x, y in zip(a, b)))
-
-def prf( h, data ):
-    hm = h.copy()
-    hm.update( data )
-    return hm.digest()
-
-# Helper as per the spec. h is a hmac which has been created seeded with the
-# password, it will be copy()ed and not modified.
-def pbkdf2_F( h, salt, itercount, blocknum ):
-    U = prf( h, salt + pack('>i',blocknum ) )
-    T = U
-
-    for i in range(2, itercount+1):
-        U = prf( h, U )
-        T = xorstr( T, U )
-
-    return T
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/pycrypto_des.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/pycrypto_des.py
new file mode 100644 (file)
index 0000000..80d7d65
--- /dev/null
@@ -0,0 +1,30 @@
+#!/usr/bin/env python
+# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
+
+
+def load_pycrypto():
+    try :
+        from Crypto.Cipher import DES as _DES
+    except:
+        return None
+
+    class DES(object):
+        def __init__(self, key):
+            if len(key) != 8 :
+                raise Error('DES improper key used')
+            self.key = key
+            self._des = _DES.new(key,_DES.MODE_ECB)
+        def desdecrypt(self, data):
+            return self._des.decrypt(data)
+        def decrypt(self, data):
+            if not data:
+                return ''
+            i = 0
+            result = []
+            while i < len(data):
+                block = data[i:i+8]
+                processed_block = self.desdecrypt(block)
+                result.append(processed_block)
+                i += 8
+            return ''.join(result)
+    return DES
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/python_des.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/python_des.py
new file mode 100644 (file)
index 0000000..bd02904
--- /dev/null
@@ -0,0 +1,220 @@
+#!/usr/bin/env python
+# vim:ts=4:sw=4:softtabstop=4:smarttab:expandtab
+import sys
+
+ECB =   0
+CBC =   1
+class Des(object):
+    __pc1 = [56, 48, 40, 32, 24, 16,  8,  0, 57, 49, 41, 33, 25, 17,
+          9,  1, 58, 50, 42, 34, 26, 18, 10,  2, 59, 51, 43, 35,
+         62, 54, 46, 38, 30, 22, 14,  6, 61, 53, 45, 37, 29, 21,
+         13,  5, 60, 52, 44, 36, 28, 20, 12,  4, 27, 19, 11,  3]
+    __left_rotations = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1]
+    __pc2 = [13, 16, 10, 23,  0,  4,2, 27, 14,  5, 20,  9,
+        22, 18, 11,  3, 25,  7, 15,  6, 26, 19, 12,  1,
+        40, 51, 30, 36, 46, 54, 29, 39, 50, 44, 32, 47,
+        43, 48, 38, 55, 33, 52, 45, 41, 49, 35, 28, 31]
+    __ip = [57, 49, 41, 33, 25, 17, 9,  1,      59, 51, 43, 35, 27, 19, 11, 3,
+        61, 53, 45, 37, 29, 21, 13, 5,  63, 55, 47, 39, 31, 23, 15, 7,
+        56, 48, 40, 32, 24, 16, 8,  0,  58, 50, 42, 34, 26, 18, 10, 2,
+        60, 52, 44, 36, 28, 20, 12, 4,  62, 54, 46, 38, 30, 22, 14, 6]
+    __expansion_table = [31,  0,  1,  2,  3,  4, 3,  4,  5,  6,  7,  8,
+         7,  8,  9, 10, 11, 12,11, 12, 13, 14, 15, 16,
+        15, 16, 17, 18, 19, 20,19, 20, 21, 22, 23, 24,
+        23, 24, 25, 26, 27, 28,27, 28, 29, 30, 31,  0]
+    __sbox = [[14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7,
+         0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8,
+         4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0,
+         15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13],
+        [15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10,
+         3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5,
+         0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15,
+         13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9],
+        [10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8,
+         13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1,
+         13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7,
+         1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12],
+        [7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15,
+         13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9,
+         10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4,
+         3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14],
+        [2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9,
+         14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6,
+         4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14,
+         11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3],
+        [12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11,
+         10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8,
+         9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6,
+         4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13],
+        [4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1,
+         13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6,
+         1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2,
+         6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12],
+        [13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7,
+         1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2,
+         7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8,
+         2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11],]
+    __p = [15, 6, 19, 20, 28, 11,27, 16, 0, 14, 22, 25,
+        4, 17, 30, 9, 1, 7,23,13, 31, 26, 2, 8,18, 12, 29, 5, 21, 10,3, 24]
+    __fp = [39,  7, 47, 15, 55, 23, 63, 31,38,  6, 46, 14, 54, 22, 62, 30,
+        37,  5, 45, 13, 53, 21, 61, 29,36,  4, 44, 12, 52, 20, 60, 28,
+        35,  3, 43, 11, 51, 19, 59, 27,34,  2, 42, 10, 50, 18, 58, 26,
+        33,  1, 41,  9, 49, 17, 57, 25,32,  0, 40,  8, 48, 16, 56, 24]
+    # Type of crypting being done
+    ENCRYPT =   0x00
+    DECRYPT =   0x01
+    def __init__(self, key, mode=ECB, IV=None):
+        if len(key) != 8:
+            raise ValueError("Invalid DES key size. Key must be exactly 8 bytes long.")
+        self.block_size = 8
+        self.key_size = 8
+        self.__padding = ''
+        self.setMode(mode)
+        if IV:
+            self.setIV(IV)
+        self.L = []
+        self.R = []
+        self.Kn = [ [0] * 48 ] * 16     # 16 48-bit keys (K1 - K16)
+        self.final = []
+        self.setKey(key)
+    def getKey(self):
+        return self.__key
+    def setKey(self, key):
+        self.__key = key
+        self.__create_sub_keys()
+    def getMode(self):
+        return self.__mode
+    def setMode(self, mode):
+        self.__mode = mode
+    def getIV(self):
+        return self.__iv
+    def setIV(self, IV):
+        if not IV or len(IV) != self.block_size:
+            raise ValueError("Invalid Initial Value (IV), must be a multiple of " + str(self.block_size) + " bytes")
+        self.__iv = IV
+    def getPadding(self):
+        return self.__padding
+    def __String_to_BitList(self, data):
+        l = len(data) * 8
+        result = [0] * l
+        pos = 0
+        for c in data:
+            i = 7
+            ch = ord(c)
+            while i >= 0:
+                if ch & (1 << i) != 0:
+                    result[pos] = 1
+                else:
+                    result[pos] = 0
+                pos += 1
+                i -= 1
+        return result
+    def __BitList_to_String(self, data):
+        result = ''
+        pos = 0
+        c = 0
+        while pos < len(data):
+            c += data[pos] << (7 - (pos % 8))
+            if (pos % 8) == 7:
+                result += chr(c)
+                c = 0
+            pos += 1
+        return result
+    def __permutate(self, table, block):
+        return [block[x] for x in table]
+    def __create_sub_keys(self):
+        key = self.__permutate(Des.__pc1, self.__String_to_BitList(self.getKey()))
+        i = 0
+        self.L = key[:28]
+        self.R = key[28:]
+        while i < 16:
+            j = 0
+            while j < Des.__left_rotations[i]:
+                self.L.append(self.L[0])
+                del self.L[0]
+                self.R.append(self.R[0])
+                del self.R[0]
+                j += 1
+            self.Kn[i] = self.__permutate(Des.__pc2, self.L + self.R)
+            i += 1
+    def __des_crypt(self, block, crypt_type):
+        block = self.__permutate(Des.__ip, block)
+        self.L = block[:32]
+        self.R = block[32:]
+        if crypt_type == Des.ENCRYPT:
+            iteration = 0
+            iteration_adjustment = 1
+        else:
+            iteration = 15
+            iteration_adjustment = -1
+        i = 0
+        while i < 16:
+            tempR = self.R[:]
+            self.R = self.__permutate(Des.__expansion_table, self.R)
+            self.R = [x ^ y for x,y in zip(self.R, self.Kn[iteration])]
+            B = [self.R[:6], self.R[6:12], self.R[12:18], self.R[18:24], self.R[24:30], self.R[30:36], self.R[36:42], self.R[42:]]
+            j = 0
+            Bn = [0] * 32
+            pos = 0
+            while j < 8:
+                m = (B[j][0] << 1) + B[j][5]
+                n = (B[j][1] << 3) + (B[j][2] << 2) + (B[j][3] << 1) + B[j][4]
+                v = Des.__sbox[j][(m << 4) + n]
+                Bn[pos] = (v & 8) >> 3
+                Bn[pos + 1] = (v & 4) >> 2
+                Bn[pos + 2] = (v & 2) >> 1
+                Bn[pos + 3] = v & 1
+                pos += 4
+                j += 1
+            self.R = self.__permutate(Des.__p, Bn)
+            self.R = [x ^ y for x, y in zip(self.R, self.L)]
+            self.L = tempR
+            i += 1
+            iteration += iteration_adjustment
+        self.final = self.__permutate(Des.__fp, self.R + self.L)
+        return self.final
+    def crypt(self, data, crypt_type):
+        if not data:
+            return ''
+        if len(data) % self.block_size != 0:
+            if crypt_type == Des.DECRYPT: # Decryption must work on 8 byte blocks
+                raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n.")
+            if not self.getPadding():
+                raise ValueError("Invalid data length, data must be a multiple of " + str(self.block_size) + " bytes\n. Try setting the optional padding character")
+            else:
+                data += (self.block_size - (len(data) % self.block_size)) * self.getPadding()
+        if self.getMode() == CBC:
+            if self.getIV():
+                iv = self.__String_to_BitList(self.getIV())
+            else:
+                raise ValueError("For CBC mode, you must supply the Initial Value (IV) for ciphering")
+        i = 0
+        dict = {}
+        result = []
+        while i < len(data):
+            block = self.__String_to_BitList(data[i:i+8])
+            if self.getMode() == CBC:
+                if crypt_type == Des.ENCRYPT:
+                    block = [x ^ y for x, y in zip(block, iv)]
+                processed_block = self.__des_crypt(block, crypt_type)
+                if crypt_type == Des.DECRYPT:
+                    processed_block = [x ^ y for x, y in zip(processed_block, iv)]
+                    iv = block
+                else:
+                    iv = processed_block
+            else:
+                processed_block = self.__des_crypt(block, crypt_type)
+            result.append(self.__BitList_to_String(processed_block))
+            i += 8
+        if crypt_type == Des.DECRYPT and self.getPadding():
+            s = result[-1]
+            while s[-1] == self.getPadding():
+                s = s[:-1]
+            result[-1] = s
+        return ''.join(result)
+    def encrypt(self, data, pad=''):
+        self.__padding = pad
+        return self.crypt(data, Des.ENCRYPT)
+    def decrypt(self, data, pad=''):
+        self.__padding = pad
+        return self.crypt(data, Des.DECRYPT)
index f1d8574d1bee52c60b39c5215703217336251e2b..4c65719afb5d94ee49ac19a46d5d71576f7a9dc7 100644 (file)
@@ -296,7 +296,7 @@ class TopazBook:
                 break
 
         if not bookKey:
-            raise TpzDRMError('Decryption Unsucessful; No valid pid found')
+            raise TpzDRMError("Topaz Book. No key found in " + str(len(pidlst)) + " keys tried. Please report this failure for help.")
 
         self.setBookKey(bookKey)
         self.createBookDirectory()
diff --git a/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfilerugged.py b/DeDRM_Macintosh_Application/DeDRM.app/Contents/Resources/zipfilerugged.py
new file mode 100644 (file)
index 0000000..adf3c53
--- /dev/null
@@ -0,0 +1,1400 @@
+"""
+Read and write ZIP files.
+"""
+import struct, os, time, sys, shutil
+import binascii, cStringIO, stat
+import io
+import re
+
+try:
+    import zlib # We may need its compression method
+    crc32 = zlib.crc32
+except ImportError:
+    zlib = None
+    crc32 = binascii.crc32
+
+__all__ = ["BadZipfile", "error", "ZIP_STORED", "ZIP_DEFLATED", "is_zipfile",
+           "ZipInfo", "ZipFile", "PyZipFile", "LargeZipFile" ]
+
+class BadZipfile(Exception):
+    pass
+
+
+class LargeZipFile(Exception):
+    """
+    Raised when writing a zipfile, the zipfile requires ZIP64 extensions
+    and those extensions are disabled.
+    """
+
+error = BadZipfile      # The exception raised by this module
+
+ZIP64_LIMIT = (1 << 31) - 1
+ZIP_FILECOUNT_LIMIT = 1 << 16
+ZIP_MAX_COMMENT = (1 << 16) - 1
+
+# constants for Zip file compression methods
+ZIP_STORED = 0
+ZIP_DEFLATED = 8
+# Other ZIP compression methods not supported
+
+# Below are some formats and associated data for reading/writing headers using
+# the struct module.  The names and structures of headers/records are those used
+# in the PKWARE description of the ZIP file format:
+#     http://www.pkware.com/documents/casestudies/APPNOTE.TXT
+# (URL valid as of January 2008)
+
+# The "end of central directory" structure, magic number, size, and indices
+# (section V.I in the format document)
+structEndArchive = "<4s4H2LH"
+stringEndArchive = "PK\005\006"
+sizeEndCentDir = struct.calcsize(structEndArchive)
+
+_ECD_SIGNATURE = 0
+_ECD_DISK_NUMBER = 1
+_ECD_DISK_START = 2
+_ECD_ENTRIES_THIS_DISK = 3
+_ECD_ENTRIES_TOTAL = 4
+_ECD_SIZE = 5
+_ECD_OFFSET = 6
+_ECD_COMMENT_SIZE = 7
+# These last two indices are not part of the structure as defined in the
+# spec, but they are used internally by this module as a convenience
+_ECD_COMMENT = 8
+_ECD_LOCATION = 9
+
+# The "central directory" structure, magic number, size, and indices
+# of entries in the structure (section V.F in the format document)
+structCentralDir = "<4s4B4HL2L5H2L"
+stringCentralDir = "PK\001\002"
+sizeCentralDir = struct.calcsize(structCentralDir)
+
+# indexes of entries in the central directory structure
+_CD_SIGNATURE = 0
+_CD_CREATE_VERSION = 1
+_CD_CREATE_SYSTEM = 2
+_CD_EXTRACT_VERSION = 3
+_CD_EXTRACT_SYSTEM = 4
+_CD_FLAG_BITS = 5
+_CD_COMPRESS_TYPE = 6
+_CD_TIME = 7
+_CD_DATE = 8
+_CD_CRC = 9
+_CD_COMPRESSED_SIZE = 10
+_CD_UNCOMPRESSED_SIZE = 11
+_CD_FILENAME_LENGTH = 12
+_CD_EXTRA_FIELD_LENGTH = 13
+_CD_COMMENT_LENGTH = 14
+_CD_DISK_NUMBER_START = 15
+_CD_INTERNAL_FILE_ATTRIBUTES = 16
+_CD_EXTERNAL_FILE_ATTRIBUTES = 17
+_CD_LOCAL_HEADER_OFFSET = 18
+
+# The "local file header" structure, magic number, size, and indices
+# (section V.A in the format document)
+structFileHeader = "<4s2B4HL2L2H"
+stringFileHeader = "PK\003\004"
+sizeFileHeader = struct.calcsize(structFileHeader)
+
+_FH_SIGNATURE = 0
+_FH_EXTRACT_VERSION = 1
+_FH_EXTRACT_SYSTEM = 2
+_FH_GENERAL_PURPOSE_FLAG_BITS = 3
+_FH_COMPRESSION_METHOD = 4
+_FH_LAST_MOD_TIME = 5
+_FH_LAST_MOD_DATE = 6
+_FH_CRC = 7
+_FH_COMPRESSED_SIZE = 8
+_FH_UNCOMPRESSED_SIZE = 9
+_FH_FILENAME_LENGTH = 10
+_FH_EXTRA_FIELD_LENGTH = 11
+
+# The "Zip64 end of central directory locator" structure, magic number, and size
+structEndArchive64Locator = "<4sLQL"
+stringEndArchive64Locator = "PK\x06\x07"
+sizeEndCentDir64Locator = struct.calcsize(structEndArchive64Locator)
+
+# The "Zip64 end of central directory" record, magic number, size, and indices
+# (section V.G in the format document)
+structEndArchive64 = "<4sQ2H2L4Q"
+stringEndArchive64 = "PK\x06\x06"
+sizeEndCentDir64 = struct.calcsize(structEndArchive64)
+
+_CD64_SIGNATURE = 0
+_CD64_DIRECTORY_RECSIZE = 1
+_CD64_CREATE_VERSION = 2
+_CD64_EXTRACT_VERSION = 3
+_CD64_DISK_NUMBER = 4
+_CD64_DISK_NUMBER_START = 5
+_CD64_NUMBER_ENTRIES_THIS_DISK = 6
+_CD64_NUMBER_ENTRIES_TOTAL = 7
+_CD64_DIRECTORY_SIZE = 8
+_CD64_OFFSET_START_CENTDIR = 9
+
+def _check_zipfile(fp):
+    try:
+        if _EndRecData(fp):
+            return True         # file has correct magic number
+    except IOError:
+        pass
+    return False
+
+def is_zipfile(filename):
+    """Quickly see if a file is a ZIP file by checking the magic number.
+
+    The filename argument may be a file or file-like object too.
+    """
+    result = False
+    try:
+        if hasattr(filename, "read"):
+            result = _check_zipfile(fp=filename)
+        else:
+            with open(filename, "rb") as fp:
+                result = _check_zipfile(fp)
+    except IOError:
+        pass
+    return result
+
+def _EndRecData64(fpin, offset, endrec):
+    """
+    Read the ZIP64 end-of-archive records and use that to update endrec
+    """
+    fpin.seek(offset - sizeEndCentDir64Locator, 2)
+    data = fpin.read(sizeEndCentDir64Locator)
+    sig, diskno, reloff, disks = struct.unpack(structEndArchive64Locator, data)
+    if sig != stringEndArchive64Locator:
+        return endrec
+
+    if diskno != 0 or disks != 1:
+        raise BadZipfile("zipfiles that span multiple disks are not supported")
+
+    # Assume no 'zip64 extensible data'
+    fpin.seek(offset - sizeEndCentDir64Locator - sizeEndCentDir64, 2)
+    data = fpin.read(sizeEndCentDir64)
+    sig, sz, create_version, read_version, disk_num, disk_dir, \
+            dircount, dircount2, dirsize, diroffset = \
+            struct.unpack(structEndArchive64, data)
+    if sig != stringEndArchive64:
+        return endrec
+
+    # Update the original endrec using data from the ZIP64 record
+    endrec[_ECD_SIGNATURE] = sig
+    endrec[_ECD_DISK_NUMBER] = disk_num
+    endrec[_ECD_DISK_START] = disk_dir
+    endrec[_ECD_ENTRIES_THIS_DISK] = dircount
+    endrec[_ECD_ENTRIES_TOTAL] = dircount2
+    endrec[_ECD_SIZE] = dirsize
+    endrec[_ECD_OFFSET] = diroffset
+    return endrec
+
+
+def _EndRecData(fpin):
+    """Return data from the "End of Central Directory" record, or None.
+
+    The data is a list of the nine items in the ZIP "End of central dir"
+    record followed by a tenth item, the file seek offset of this record."""
+
+    # Determine file size
+    fpin.seek(0, 2)
+    filesize = fpin.tell()
+
+    # Check to see if this is ZIP file with no archive comment (the
+    # "end of central directory" structure should be the last item in the
+    # file if this is the case).
+    try:
+        fpin.seek(-sizeEndCentDir, 2)
+    except IOError:
+        return None
+    data = fpin.read()
+    if data[0:4] == stringEndArchive and data[-2:] == "\000\000":
+        # the signature is correct and there's no comment, unpack structure
+        endrec = struct.unpack(structEndArchive, data)
+        endrec=list(endrec)
+
+        # Append a blank comment and record start offset
+        endrec.append("")
+        endrec.append(filesize - sizeEndCentDir)
+
+        # Try to read the "Zip64 end of central directory" structure
+        return _EndRecData64(fpin, -sizeEndCentDir, endrec)
+
+    # Either this is not a ZIP file, or it is a ZIP file with an archive
+    # comment.  Search the end of the file for the "end of central directory"
+    # record signature. The comment is the last item in the ZIP file and may be
+    # up to 64K long.  It is assumed that the "end of central directory" magic
+    # number does not appear in the comment.
+    maxCommentStart = max(filesize - (1 << 16) - sizeEndCentDir, 0)
+    fpin.seek(maxCommentStart, 0)
+    data = fpin.read()
+    start = data.rfind(stringEndArchive)
+    if start >= 0:
+        # found the magic number; attempt to unpack and interpret
+        recData = data[start:start+sizeEndCentDir]
+        endrec = list(struct.unpack(structEndArchive, recData))
+        comment = data[start+sizeEndCentDir:]
+        # check that comment length is correct
+        if endrec[_ECD_COMMENT_SIZE] == len(comment):
+            # Append the archive comment and start offset
+            endrec.append(comment)
+            endrec.append(maxCommentStart + start)
+
+            # Try to read the "Zip64 end of central directory" structure
+            return _EndRecData64(fpin, maxCommentStart + start - filesize,
+                                 endrec)
+
+    # Unable to find a valid end of central directory structure
+    return
+
+
+class ZipInfo (object):
+    """Class with attributes describing each file in the ZIP archive."""
+
+    __slots__ = (
+            'orig_filename',
+            'filename',
+            'date_time',
+            'compress_type',
+            'comment',
+            'extra',
+            'create_system',
+            'create_version',
+            'extract_version',
+            'reserved',
+            'flag_bits',
+            'volume',
+            'internal_attr',
+            'external_attr',
+            'header_offset',
+            'CRC',
+            'compress_size',
+            'file_size',
+            '_raw_time',
+        )
+
+    def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)):
+        self.orig_filename = filename   # Original file name in archive
+
+        # Terminate the file name at the first null byte.  Null bytes in file
+        # names are used as tricks by viruses in archives.
+        null_byte = filename.find(chr(0))
+        if null_byte >= 0:
+            filename = filename[0:null_byte]
+        # This is used to ensure paths in generated ZIP files always use
+        # forward slashes as the directory separator, as required by the
+        # ZIP format specification.
+        if os.sep != "/" and os.sep in filename:
+            filename = filename.replace(os.sep, "/")
+
+        self.filename = filename        # Normalized file name
+        self.date_time = date_time      # year, month, day, hour, min, sec
+        # Standard values:
+        self.compress_type = ZIP_STORED # Type of compression for the file
+        self.comment = ""               # Comment for each file
+        self.extra = ""                 # ZIP extra data
+        if sys.platform == 'win32':
+            self.create_system = 0          # System which created ZIP archive
+        else:
+            # Assume everything else is unix-y
+            self.create_system = 3          # System which created ZIP archive
+        self.create_version = 20        # Version which created ZIP archive
+        self.extract_version = 20       # Version needed to extract archive
+        self.reserved = 0               # Must be zero
+        self.flag_bits = 0              # ZIP flag bits
+        self.volume = 0                 # Volume number of file header
+        self.internal_attr = 0          # Internal attributes
+        self.external_attr = 0          # External file attributes
+        # Other attributes are set by class ZipFile:
+        # header_offset         Byte offset to the file header
+        # CRC                   CRC-32 of the uncompressed file
+        # compress_size         Size of the compressed file
+        # file_size             Size of the uncompressed file
+
+    def FileHeader(self):
+        """Return the per-file header as a string."""
+        dt = self.date_time
+        dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2]
+        dostime = dt[3] << 11 | dt[4] << 5 | (dt[5] // 2)
+        if self.flag_bits & 0x08:
+            # Set these to zero because we write them after the file data
+            CRC = compress_size = file_size = 0
+        else:
+            CRC = self.CRC
+            compress_size = self.compress_size
+            file_size = self.file_size
+
+        extra = self.extra
+
+        if file_size > ZIP64_LIMIT or compress_size > ZIP64_LIMIT:
+            # File is larger than what fits into a 4 byte integer,
+            # fall back to the ZIP64 extension
+            fmt = '<HHQQ'
+            extra = extra + struct.pack(fmt,
+                    1, struct.calcsize(fmt)-4, file_size, compress_size)
+            file_size = 0xffffffff
+            compress_size = 0xffffffff
+            self.extract_version = max(45, self.extract_version)
+            self.create_version = max(45, self.extract_version)
+
+        filename, flag_bits = self._encodeFilenameFlags()
+        header = struct.pack(structFileHeader, stringFileHeader,
+                 self.extract_version, self.reserved, flag_bits,
+                 self.compress_type, dostime, dosdate, CRC,
+                 compress_size, file_size,
+                 len(filename), len(extra))
+        return header + filename + extra
+
+    def _encodeFilenameFlags(self):
+        if isinstance(self.filename, unicode):
+            try:
+                return self.filename.encode('ascii'), self.flag_bits
+            except UnicodeEncodeError:
+                return self.filename.encode('utf-8'), self.flag_bits | 0x800
+        else:
+            return self.filename, self.flag_bits
+
+    def _decodeFilename(self):
+        if self.flag_bits & 0x800:
+            try:
+                print "decoding filename",self.filename
+                return self.filename.decode('utf-8')
+            except:
+                return self.filename
+        else:
+            return self.filename
+
+    def _decodeExtra(self):
+        # Try to decode the extra field.
+        extra = self.extra
+        unpack = struct.unpack
+        while extra:
+            tp, ln = unpack('<HH', extra[:4])
+            if tp == 1:
+                if ln >= 24:
+                    counts = unpack('<QQQ', extra[4:28])
+                elif ln == 16:
+                    counts = unpack('<QQ', extra[4:20])
+                elif ln == 8:
+                    counts = unpack('<Q', extra[4:12])
+                elif ln == 0:
+                    counts = ()
+                else:
+                    raise RuntimeError, "Corrupt extra field %s"%(ln,)
+
+                idx = 0
+
+                # ZIP64 extension (large files and/or large archives)
+                if self.file_size in (0xffffffffffffffffL, 0xffffffffL):
+                    self.file_size = counts[idx]
+                    idx += 1
+
+                if self.compress_size == 0xFFFFFFFFL:
+                    self.compress_size = counts[idx]
+                    idx += 1
+
+                if self.header_offset == 0xffffffffL:
+                    old = self.header_offset
+                    self.header_offset = counts[idx]
+                    idx+=1
+
+            extra = extra[ln+4:]
+
+
+class _ZipDecrypter:
+    """Class to handle decryption of files stored within a ZIP archive.
+
+    ZIP supports a password-based form of encryption. Even though known
+    plaintext attacks have been found against it, it is still useful
+    to be able to get data out of such a file.
+
+    Usage:
+        zd = _ZipDecrypter(mypwd)
+        plain_char = zd(cypher_char)
+        plain_text = map(zd, cypher_text)
+    """
+
+    def _GenerateCRCTable():
+        """Generate a CRC-32 table.
+
+        ZIP encryption uses the CRC32 one-byte primitive for scrambling some
+        internal keys. We noticed that a direct implementation is faster than
+        relying on binascii.crc32().
+        """
+        poly = 0xedb88320
+        table = [0] * 256
+        for i in range(256):
+            crc = i
+            for j in range(8):
+                if crc & 1:
+                    crc = ((crc >> 1) & 0x7FFFFFFF) ^ poly
+                else:
+                    crc = ((crc >> 1) & 0x7FFFFFFF)
+            table[i] = crc
+        return table
+    crctable = _GenerateCRCTable()
+
+    def _crc32(self, ch, crc):
+        """Compute the CRC32 primitive on one byte."""
+        return ((crc >> 8) & 0xffffff) ^ self.crctable[(crc ^ ord(ch)) & 0xff]
+
+    def __init__(self, pwd):
+        self.key0 = 305419896
+        self.key1 = 591751049
+        self.key2 = 878082192
+        for p in pwd:
+            self._UpdateKeys(p)
+
+    def _UpdateKeys(self, c):
+        self.key0 = self._crc32(c, self.key0)
+        self.key1 = (self.key1 + (self.key0 & 255)) & 4294967295
+        self.key1 = (self.key1 * 134775813 + 1) & 4294967295
+        self.key2 = self._crc32(chr((self.key1 >> 24) & 255), self.key2)
+
+    def __call__(self, c):
+        """Decrypt a single character."""
+        c = ord(c)
+        k = self.key2 | 2
+        c = c ^ (((k * (k^1)) >> 8) & 255)
+        c = chr(c)
+        self._UpdateKeys(c)
+        return c
+
+class ZipExtFile(io.BufferedIOBase):
+    """File-like object for reading an archive member.
+       Is returned by ZipFile.open().
+    """
+
+    # Max size supported by decompressor.
+    MAX_N = 1 << 31 - 1
+
+    # Read from compressed files in 4k blocks.
+    MIN_READ_SIZE = 4096
+
+    # Search for universal newlines or line chunks.
+    PATTERN = re.compile(r'^(?P<chunk>[^\r\n]+)|(?P<newline>\n|\r\n?)')
+
+    def __init__(self, fileobj, mode, zipinfo, decrypter=None):
+        self._fileobj = fileobj
+        self._decrypter = decrypter
+
+        self._compress_type = zipinfo.compress_type
+        self._compress_size = zipinfo.compress_size
+        self._compress_left = zipinfo.compress_size
+
+        if self._compress_type == ZIP_DEFLATED:
+            self._decompressor = zlib.decompressobj(-15)
+        self._unconsumed = ''
+
+        self._readbuffer = ''
+        self._offset = 0
+
+        self._universal = 'U' in mode
+        self.newlines = None
+
+        # Adjust read size for encrypted files since the first 12 bytes
+        # are for the encryption/password information.
+        if self._decrypter is not None:
+            self._compress_left -= 12
+
+        self.mode = mode
+        self.name = zipinfo.filename
+
+    def readline(self, limit=-1):
+        """Read and return a line from the stream.
+
+        If limit is specified, at most limit bytes will be read.
+        """
+
+        if not self._universal and limit < 0:
+            # Shortcut common case - newline found in buffer.
+            i = self._readbuffer.find('\n', self._offset) + 1
+            if i > 0:
+                line = self._readbuffer[self._offset: i]
+                self._offset = i
+                return line
+
+        if not self._universal:
+            return io.BufferedIOBase.readline(self, limit)
+
+        line = ''
+        while limit < 0 or len(line) < limit:
+            readahead = self.peek(2)
+            if readahead == '':
+                return line
+
+            #
+            # Search for universal newlines or line chunks.
+            #
+            # The pattern returns either a line chunk or a newline, but not
+            # both. Combined with peek(2), we are assured that the sequence
+            # '\r\n' is always retrieved completely and never split into
+            # separate newlines - '\r', '\n' due to coincidental readaheads.
+            #
+            match = self.PATTERN.search(readahead)
+            newline = match.group('newline')
+            if newline is not None:
+                if self.newlines is None:
+                    self.newlines = []
+                if newline not in self.newlines:
+                    self.newlines.append(newline)
+                self._offset += len(newline)
+                return line + '\n'
+
+            chunk = match.group('chunk')
+            if limit >= 0:
+                chunk = chunk[: limit - len(line)]
+
+            self._offset += len(chunk)
+            line += chunk
+
+        return line
+
+    def peek(self, n=1):
+        """Returns buffered bytes without advancing the position."""
+        if n > len(self._readbuffer) - self._offset:
+            chunk = self.read(n)
+            self._offset -= len(chunk)
+
+        # Return up to 512 bytes to reduce allocation overhead for tight loops.
+        return self._readbuffer[self._offset: self._offset + 512]
+
+    def readable(self):
+        return True
+
+    def read(self, n=-1):
+        """Read and return up to n bytes.
+        If the argument is omitted, None, or negative, data is read and returned until EOF is reached..
+        """
+
+        buf = ''
+        while n < 0 or n is None or n > len(buf):
+            data = self.read1(n)
+            if len(data) == 0:
+                return buf
+
+            buf += data
+
+        return buf
+
+    def read1(self, n):
+        """Read up to n bytes with at most one read() system call."""
+
+        # Simplify algorithm (branching) by transforming negative n to large n.
+        if n < 0 or n is None:
+            n = self.MAX_N
+
+        # Bytes available in read buffer.
+        len_readbuffer = len(self._readbuffer) - self._offset
+
+        # Read from file.
+        if self._compress_left > 0 and n > len_readbuffer + len(self._unconsumed):
+            nbytes = n - len_readbuffer - len(self._unconsumed)
+            nbytes = max(nbytes, self.MIN_READ_SIZE)
+            nbytes = min(nbytes, self._compress_left)
+
+            data = self._fileobj.read(nbytes)
+            self._compress_left -= len(data)
+
+            if data and self._decrypter is not None:
+                data = ''.join(map(self._decrypter, data))
+
+            if self._compress_type == ZIP_STORED:
+                self._readbuffer = self._readbuffer[self._offset:] + data
+                self._offset = 0
+            else:
+                # Prepare deflated bytes for decompression.
+                self._unconsumed += data
+
+        # Handle unconsumed data.
+        if (len(self._unconsumed) > 0 and n > len_readbuffer and
+            self._compress_type == ZIP_DEFLATED):
+            data = self._decompressor.decompress(
+                self._unconsumed,
+                max(n - len_readbuffer, self.MIN_READ_SIZE)
+            )
+
+            self._unconsumed = self._decompressor.unconsumed_tail
+            if len(self._unconsumed) == 0 and self._compress_left == 0:
+                data += self._decompressor.flush()
+
+            self._readbuffer = self._readbuffer[self._offset:] + data
+            self._offset = 0
+
+        # Read from buffer.
+        data = self._readbuffer[self._offset: self._offset + n]
+        self._offset += len(data)
+        return data
+
+
+
+class ZipFile:
+    """ Class with methods to open, read, write, close, list zip files.
+
+    z = ZipFile(file, mode="r", compression=ZIP_STORED, allowZip64=False)
+
+    file: Either the path to the file, or a file-like object.
+          If it is a path, the file will be opened and closed by ZipFile.
+    mode: The mode can be either read "r", write "w" or append "a".
+    compression: ZIP_STORED (no compression) or ZIP_DEFLATED (requires zlib).
+    allowZip64: if True ZipFile will create files with ZIP64 extensions when
+                needed, otherwise it will raise an exception when this would
+                be necessary.
+
+    """
+
+    fp = None                   # Set here since __del__ checks it
+
+    def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=False):
+        """Open the ZIP file with mode read "r", write "w" or append "a"."""
+        if mode not in ("r", "w", "a"):
+            raise RuntimeError('ZipFile() requires mode "r", "w", or "a"')
+
+        if compression == ZIP_STORED:
+            pass
+        elif compression == ZIP_DEFLATED:
+            if not zlib:
+                raise RuntimeError,\
+                      "Compression requires the (missing) zlib module"
+        else:
+            raise RuntimeError, "That compression method is not supported"
+
+        self._allowZip64 = allowZip64
+        self._didModify = False
+        self.debug = 0  # Level of printing: 0 through 3
+        self.NameToInfo = {}    # Find file info given name
+        self.filelist = []      # List of ZipInfo instances for archive
+        self.compression = compression  # Method of compression
+        self.mode = key = mode.replace('b', '')[0]
+        self.pwd = None
+        self.comment = ''
+
+        # Check if we were passed a file-like object
+        if isinstance(file, basestring):
+            self._filePassed = 0
+            self.filename = file
+            modeDict = {'r' : 'rb', 'w': 'wb', 'a' : 'r+b'}
+            try:
+                self.fp = open(file, modeDict[mode])
+            except IOError:
+                if mode == 'a':
+                    mode = key = 'w'
+                    self.fp = open(file, modeDict[mode])
+                else:
+                    raise
+        else:
+            self._filePassed = 1
+            self.fp = file
+            self.filename = getattr(file, 'name', None)
+
+        if key == 'r':
+            self._GetContents()
+        elif key == 'w':
+            pass
+        elif key == 'a':
+            try:                        # See if file is a zip file
+                self._RealGetContents()
+                # seek to start of directory and overwrite
+                self.fp.seek(self.start_dir, 0)
+            except BadZipfile:          # file is not a zip file, just append
+                self.fp.seek(0, 2)
+        else:
+            if not self._filePassed:
+                self.fp.close()
+                self.fp = None
+            raise RuntimeError, 'Mode must be "r", "w" or "a"'
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, traceback):
+        self.close()
+
+    def _GetContents(self):
+        """Read the directory, making sure we close the file if the format
+        is bad."""
+        try:
+            self._RealGetContents()
+        except BadZipfile:
+            if not self._filePassed:
+                self.fp.close()
+                self.fp = None
+            raise
+
+    def _RealGetContents(self):
+        """Read in the table of contents for the ZIP file."""
+        fp = self.fp
+        endrec = _EndRecData(fp)
+        if not endrec:
+            raise BadZipfile, "File is not a zip file"
+        if self.debug > 1:
+            print endrec
+        size_cd = endrec[_ECD_SIZE]             # bytes in central directory
+        offset_cd = endrec[_ECD_OFFSET]         # offset of central directory
+        self.comment = endrec[_ECD_COMMENT]     # archive comment
+
+        # "concat" is zero, unless zip was concatenated to another file
+        concat = endrec[_ECD_LOCATION] - size_cd - offset_cd
+        if endrec[_ECD_SIGNATURE] == stringEndArchive64:
+            # If Zip64 extension structures are present, account for them
+            concat -= (sizeEndCentDir64 + sizeEndCentDir64Locator)
+
+        if self.debug > 2:
+            inferred = concat + offset_cd
+            print "given, inferred, offset", offset_cd, inferred, concat
+        # self.start_dir:  Position of start of central directory
+        self.start_dir = offset_cd + concat
+        fp.seek(self.start_dir, 0)
+        data = fp.read(size_cd)
+        fp = cStringIO.StringIO(data)
+        total = 0
+        while total < size_cd:
+            centdir = fp.read(sizeCentralDir)
+            if centdir[0:4] != stringCentralDir:
+                raise BadZipfile, "Bad magic number for central directory"
+            centdir = struct.unpack(structCentralDir, centdir)
+            if self.debug > 2:
+                print centdir
+            filename = fp.read(centdir[_CD_FILENAME_LENGTH])
+            # Create ZipInfo instance to store file information
+            x = ZipInfo(filename)
+            x.extra = fp.read(centdir[_CD_EXTRA_FIELD_LENGTH])
+            x.comment = fp.read(centdir[_CD_COMMENT_LENGTH])
+            x.header_offset = centdir[_CD_LOCAL_HEADER_OFFSET]
+            (x.create_version, x.create_system, x.extract_version, x.reserved,
+                x.flag_bits, x.compress_type, t, d,
+                x.CRC, x.compress_size, x.file_size) = centdir[1:12]
+            x.volume, x.internal_attr, x.external_attr = centdir[15:18]
+            # Convert date/time code to (year, month, day, hour, min, sec)
+            x._raw_time = t
+            x.date_time = ( (d>>9)+1980, (d>>5)&0xF, d&0x1F,
+                                     t>>11, (t>>5)&0x3F, (t&0x1F) * 2 )
+
+            x._decodeExtra()
+            x.header_offset = x.header_offset + concat
+            x.filename = x._decodeFilename()
+            self.filelist.append(x)
+            self.NameToInfo[x.filename] = x
+
+            # update total bytes read from central directory
+            total = (total + sizeCentralDir + centdir[_CD_FILENAME_LENGTH]
+                     + centdir[_CD_EXTRA_FIELD_LENGTH]
+                     + centdir[_CD_COMMENT_LENGTH])
+
+            if self.debug > 2:
+                print "total", total
+
+
+    def namelist(self):
+        """Return a list of file names in the archive."""
+        l = []
+        for data in self.filelist:
+            l.append(data.filename)
+        return l
+
+    def infolist(self):
+        """Return a list of class ZipInfo instances for files in the
+        archive."""
+        return self.filelist
+
+    def printdir(self):
+        """Print a table of contents for the zip file."""
+        print "%-46s %19s %12s" % ("File Name", "Modified    ", "Size")
+        for zinfo in self.filelist:
+            date = "%d-%02d-%02d %02d:%02d:%02d" % zinfo.date_time[:6]
+            print "%-46s %s %12d" % (zinfo.filename, date, zinfo.file_size)
+
+    def testzip(self):
+        """Read all the files and check the CRC."""
+        chunk_size = 2 ** 20
+        for zinfo in self.filelist:
+            try:
+                # Read by chunks, to avoid an OverflowError or a
+                # MemoryError with very large embedded files.
+                f = self.open(zinfo.filename, "r")
+                while f.read(chunk_size):     # Check CRC-32
+                    pass
+            except BadZipfile:
+                return zinfo.filename
+
+    def getinfo(self, name):
+        """Return the instance of ZipInfo given 'name'."""
+        info = self.NameToInfo.get(name)
+        if info is None:
+            raise KeyError(
+                'There is no item named %r in the archive' % name)
+
+        return info
+
+    def setpassword(self, pwd):
+        """Set default password for encrypted files."""
+        self.pwd = pwd
+
+    def read(self, name, pwd=None):
+        """Return file bytes (as a string) for name."""
+        return self.open(name, "r", pwd).read()
+
+    def open(self, name, mode="r", pwd=None):
+        """Return file-like object for 'name'."""
+        if mode not in ("r", "U", "rU"):
+            raise RuntimeError, 'open() requires mode "r", "U", or "rU"'
+        if not self.fp:
+            raise RuntimeError, \
+                  "Attempt to read ZIP archive that was already closed"
+
+        # Only open a new file for instances where we were not
+        # given a file object in the constructor
+        if self._filePassed:
+            zef_file = self.fp
+        else:
+            zef_file = open(self.filename, 'rb')
+
+        # Make sure we have an info object
+        if isinstance(name, ZipInfo):
+            # 'name' is already an info object
+            zinfo = name
+        else:
+            # Get info object for name
+            zinfo = self.getinfo(name)
+
+        zef_file.seek(zinfo.header_offset, 0)
+
+        # Skip the file header:
+        fheader = zef_file.read(sizeFileHeader)
+        if fheader[0:4] != stringFileHeader:
+            raise BadZipfile, "Bad magic number for file header"
+
+        fheader = struct.unpack(structFileHeader, fheader)
+        fname = zef_file.read(fheader[_FH_FILENAME_LENGTH])
+        if fheader[_FH_EXTRA_FIELD_LENGTH]:
+            zef_file.read(fheader[_FH_EXTRA_FIELD_LENGTH])
+
+        if fname != zinfo.orig_filename:
+            raise BadZipfile, \
+                      'File name in directory "%s" and header "%s" differ.' % (
+                          zinfo.orig_filename, fname)
+
+        # check for encrypted flag & handle password
+        is_encrypted = zinfo.flag_bits & 0x1
+        zd = None
+        if is_encrypted:
+            if not pwd:
+                pwd = self.pwd
+            if not pwd:
+                raise RuntimeError, "File %s is encrypted, " \
+                      "password required for extraction" % name
+
+            zd = _ZipDecrypter(pwd)
+            # The first 12 bytes in the cypher stream is an encryption header
+            #  used to strengthen the algorithm. The first 11 bytes are
+            #  completely random, while the 12th contains the MSB of the CRC,
+            #  or the MSB of the file time depending on the header type
+            #  and is used to check the correctness of the password.
+            bytes = zef_file.read(12)
+            h = map(zd, bytes[0:12])
+            if zinfo.flag_bits & 0x8:
+                # compare against the file type from extended local headers
+                check_byte = (zinfo._raw_time >> 8) & 0xff
+            else:
+                # compare against the CRC otherwise
+                check_byte = (zinfo.CRC >> 24) & 0xff
+            if ord(h[11]) != check_byte:
+                raise RuntimeError("Bad password for file", name)
+
+        return  ZipExtFile(zef_file, mode, zinfo, zd)
+
+    def extract(self, member, path=None, pwd=None):
+        """Extract a member from the archive to the current working directory,
+           using its full name. Its file information is extracted as accurately
+           as possible. `member' may be a filename or a ZipInfo object. You can
+           specify a different directory using `path'.
+        """
+        if not isinstance(member, ZipInfo):
+            member = self.getinfo(member)
+
+        if path is None:
+            path = os.getcwd()
+
+        return self._extract_member(member, path, pwd)
+
+    def extractall(self, path=None, members=None, pwd=None):
+        """Extract all members from the archive to the current working
+           directory. `path' specifies a different directory to extract to.
+           `members' is optional and must be a subset of the list returned
+           by namelist().
+        """
+        if members is None:
+            members = self.namelist()
+
+        for zipinfo in members:
+            self.extract(zipinfo, path, pwd)
+
+    def _extract_member(self, member, targetpath, pwd):
+        """Extract the ZipInfo object 'member' to a physical
+           file on the path targetpath.
+        """
+        # build the destination pathname, replacing
+        # forward slashes to platform specific separators.
+        # Strip trailing path separator, unless it represents the root.
+        if (targetpath[-1:] in (os.path.sep, os.path.altsep)
+            and len(os.path.splitdrive(targetpath)[1]) > 1):
+            targetpath = targetpath[:-1]
+
+        # don't include leading "/" from file name if present
+        if member.filename[0] == '/':
+            targetpath = os.path.join(targetpath, member.filename[1:])
+        else:
+            targetpath = os.path.join(targetpath, member.filename)
+
+        targetpath = os.path.normpath(targetpath)
+
+        # Create all upper directories if necessary.
+        upperdirs = os.path.dirname(targetpath)
+        if upperdirs and not os.path.exists(upperdirs):
+            os.makedirs(upperdirs)
+
+        if member.filename[-1] == '/':
+            if not os.path.isdir(targetpath):
+                os.mkdir(targetpath)
+            return targetpath
+
+        source = self.open(member, pwd=pwd)
+        target = file(targetpath, "wb")
+        shutil.copyfileobj(source, target)
+        source.close()
+        target.close()
+
+        return targetpath
+
+    def _writecheck(self, zinfo):
+        """Check for errors before writing a file to the archive."""
+        if zinfo.filename in self.NameToInfo:
+            if self.debug:      # Warning for duplicate names
+                print "Duplicate name:", zinfo.filename
+        if self.mode not in ("w", "a"):
+            raise RuntimeError, 'write() requires mode "w" or "a"'
+        if not self.fp:
+            raise RuntimeError, \
+                  "Attempt to write ZIP archive that was already closed"
+        if zinfo.compress_type == ZIP_DEFLATED and not zlib:
+            raise RuntimeError, \
+                  "Compression requires the (missing) zlib module"
+        if zinfo.compress_type not in (ZIP_STORED, ZIP_DEFLATED):
+            raise RuntimeError, \
+                  "That compression method is not supported"
+        if zinfo.file_size > ZIP64_LIMIT:
+            if not self._allowZip64:
+                raise LargeZipFile("Filesize would require ZIP64 extensions")
+        if zinfo.header_offset > ZIP64_LIMIT:
+            if not self._allowZip64:
+                raise LargeZipFile("Zipfile size would require ZIP64 extensions")
+
+    def write(self, filename, arcname=None, compress_type=None):
+        """Put the bytes from filename into the archive under the name
+        arcname."""
+        if not self.fp:
+            raise RuntimeError(
+                  "Attempt to write to ZIP archive that was already closed")
+
+        st = os.stat(filename)
+        isdir = stat.S_ISDIR(st.st_mode)
+        mtime = time.localtime(st.st_mtime)
+        date_time = mtime[0:6]
+        # Create ZipInfo instance to store file information
+        if arcname is None:
+            arcname = filename
+        arcname = os.path.normpath(os.path.splitdrive(arcname)[1])
+        while arcname[0] in (os.sep, os.altsep):
+            arcname = arcname[1:]
+        if isdir:
+            arcname += '/'
+        zinfo = ZipInfo(arcname, date_time)
+        zinfo.external_attr = (st[0] & 0xFFFF) << 16L      # Unix attributes
+        if compress_type is None:
+            zinfo.compress_type = self.compression
+        else:
+            zinfo.compress_type = compress_type
+
+        zinfo.file_size = st.st_size
+        zinfo.flag_bits = 0x00
+        zinfo.header_offset = self.fp.tell()    # Start of header bytes
+
+        self._writecheck(zinfo)
+        self._didModify = True
+
+        if isdir:
+            zinfo.file_size = 0
+            zinfo.compress_size = 0
+            zinfo.CRC = 0
+            self.filelist.append(zinfo)
+            self.NameToInfo[zinfo.filename] = zinfo
+            self.fp.write(zinfo.FileHeader())
+            return
+
+        with open(filename, "rb") as fp:
+            # Must overwrite CRC and sizes with correct data later
+            zinfo.CRC = CRC = 0
+            zinfo.compress_size = compress_size = 0
+            zinfo.file_size = file_size = 0
+            self.fp.write(zinfo.FileHeader())
+            if zinfo.compress_type == ZIP_DEFLATED:
+                cmpr = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION,
+                     zlib.DEFLATED, -15)
+            else:
+                cmpr = None
+            while 1:
+                buf = fp.read(1024 * 8)
+                if not buf:
+                    break
+                file_size = file_size + len(buf)
+                CRC = crc32(buf, CRC) & 0xffffffff
+                if cmpr:
+                    buf = cmpr.compress(buf)
+                    compress_size = compress_size + len(buf)
+                self.fp.write(buf)
+        if cmpr:
+            buf = cmpr.flush()
+            compress_size = compress_size + len(buf)
+            self.fp.write(buf)
+            zinfo.compress_size = compress_size
+        else:
+            zinfo.compress_size = file_size
+        zinfo.CRC = CRC
+        zinfo.file_size = file_size
+        # Seek backwards and write CRC and file sizes
+        position = self.fp.tell()       # Preserve current position in file
+        self.fp.seek(zinfo.header_offset + 14, 0)
+        self.fp.write(struct.pack("<LLL", zinfo.CRC, zinfo.compress_size,
+              zinfo.file_size))
+        self.fp.seek(position, 0)
+        self.filelist.append(zinfo)
+        self.NameToInfo[zinfo.filename] = zinfo
+
+    def writestr(self, zinfo_or_arcname, bytes, compress_type=None):
+        """Write a file into the archive.  The contents is the string
+        'bytes'.  'zinfo_or_arcname' is either a ZipInfo instance or
+        the name of the file in the archive."""
+        if not isinstance(zinfo_or_arcname, ZipInfo):
+            zinfo = ZipInfo(filename=zinfo_or_arcname,
+                            date_time=time.localtime(time.time())[:6])
+
+            zinfo.compress_type = self.compression
+            zinfo.external_attr = 0600 << 16
+        else:
+            zinfo = zinfo_or_arcname
+
+        if not self.fp:
+            raise RuntimeError(
+                  "Attempt to write to ZIP archive that was already closed")
+
+        if compress_type is not None:
+            zinfo.compress_type = compress_type
+
+        zinfo.file_size = len(bytes)            # Uncompressed size
+        zinfo.header_offset = self.fp.tell()    # Start of header bytes
+        self._writecheck(zinfo)
+        self._didModify = True
+        zinfo.CRC = crc32(bytes) & 0xffffffff       # CRC-32 checksum
+        if zinfo.compress_type == ZIP_DEFLATED:
+            co = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION,
+                 zlib.DEFLATED, -15)
+            bytes = co.compress(bytes) + co.flush()
+            zinfo.compress_size = len(bytes)    # Compressed size
+        else:
+            zinfo.compress_size = zinfo.file_size
+        zinfo.header_offset = self.fp.tell()    # Start of header bytes
+        self.fp.write(zinfo.FileHeader())
+        self.fp.write(bytes)
+        self.fp.flush()
+        if zinfo.flag_bits & 0x08:
+            # Write CRC and file sizes after the file data
+            self.fp.write(struct.pack("<LLL", zinfo.CRC, zinfo.compress_size,
+                  zinfo.file_size))
+        self.filelist.append(zinfo)
+        self.NameToInfo[zinfo.filename] = zinfo
+
+    def __del__(self):
+        """Call the "close()" method in case the user forgot."""
+        self.close()
+
+    def close(self):
+        """Close the file, and for mode "w" and "a" write the ending
+        records."""
+        if self.fp is None:
+            return
+
+        if self.mode in ("w", "a") and self._didModify: # write ending records
+            count = 0
+            pos1 = self.fp.tell()
+            for zinfo in self.filelist:         # write central directory
+                count = count + 1
+                dt = zinfo.date_time
+                dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2]
+                dostime = dt[3] << 11 | dt[4] << 5 | (dt[5] // 2)
+                extra = []
+                if zinfo.file_size > ZIP64_LIMIT \
+                        or zinfo.compress_size > ZIP64_LIMIT:
+                    extra.append(zinfo.file_size)
+                    extra.append(zinfo.compress_size)
+                    file_size = 0xffffffff
+                    compress_size = 0xffffffff
+                else:
+                    file_size = zinfo.file_size
+                    compress_size = zinfo.compress_size
+
+                if zinfo.header_offset > ZIP64_LIMIT:
+                    extra.append(zinfo.header_offset)
+                    header_offset = 0xffffffffL
+                else:
+                    header_offset = zinfo.header_offset
+
+                extra_data = zinfo.extra
+                if extra:
+                    # Append a ZIP64 field to the extra's
+                    extra_data = struct.pack(
+                            '<HH' + 'Q'*len(extra),
+                            1, 8*len(extra), *extra) + extra_data
+
+                    extract_version = max(45, zinfo.extract_version)
+                    create_version = max(45, zinfo.create_version)
+                else:
+                    extract_version = zinfo.extract_version
+                    create_version = zinfo.create_version
+
+                try:
+                    filename, flag_bits = zinfo._encodeFilenameFlags()
+                    centdir = struct.pack(structCentralDir,
+                     stringCentralDir, create_version,
+                     zinfo.create_system, extract_version, zinfo.reserved,
+                     flag_bits, zinfo.compress_type, dostime, dosdate,
+                     zinfo.CRC, compress_size, file_size,
+                     len(filename), len(extra_data), len(zinfo.comment),
+                     0, zinfo.internal_attr, zinfo.external_attr,
+                     header_offset)
+                except DeprecationWarning:
+                    print >>sys.stderr, (structCentralDir,
+                     stringCentralDir, create_version,
+                     zinfo.create_system, extract_version, zinfo.reserved,
+                     zinfo.flag_bits, zinfo.compress_type, dostime, dosdate,
+                     zinfo.CRC, compress_size, file_size,
+                     len(zinfo.filename), len(extra_data), len(zinfo.comment),
+                     0, zinfo.internal_attr, zinfo.external_attr,
+                     header_offset)
+                    raise
+                self.fp.write(centdir)
+                self.fp.write(filename)
+                self.fp.write(extra_data)
+                self.fp.write(zinfo.comment)
+
+            pos2 = self.fp.tell()
+            # Write end-of-zip-archive record
+            centDirCount = count
+            centDirSize = pos2 - pos1
+            centDirOffset = pos1
+            if (centDirCount >= ZIP_FILECOUNT_LIMIT or
+                centDirOffset > ZIP64_LIMIT or
+                centDirSize > ZIP64_LIMIT):
+                # Need to write the ZIP64 end-of-archive records
+                zip64endrec = struct.pack(
+                        structEndArchive64, stringEndArchive64,
+                        44, 45, 45, 0, 0, centDirCount, centDirCount,
+                        centDirSize, centDirOffset)
+                self.fp.write(zip64endrec)
+
+                zip64locrec = struct.pack(
+                        structEndArchive64Locator,
+                        stringEndArchive64Locator, 0, pos2, 1)
+                self.fp.write(zip64locrec)
+                centDirCount = min(centDirCount, 0xFFFF)
+                centDirSize = min(centDirSize, 0xFFFFFFFF)
+                centDirOffset = min(centDirOffset, 0xFFFFFFFF)
+
+            # check for valid comment length
+            if len(self.comment) >= ZIP_MAX_COMMENT:
+                if self.debug > 0:
+                    msg = 'Archive comment is too long; truncating to %d bytes' \
+                          % ZIP_MAX_COMMENT
+                self.comment = self.comment[:ZIP_MAX_COMMENT]
+
+            endrec = struct.pack(structEndArchive, stringEndArchive,
+                                 0, 0, centDirCount, centDirCount,
+                                 centDirSize, centDirOffset, len(self.comment))
+            self.fp.write(endrec)
+            self.fp.write(self.comment)
+            self.fp.flush()
+
+        if not self._filePassed:
+            self.fp.close()
+        self.fp = None
+
+
+class PyZipFile(ZipFile):
+    """Class to create ZIP archives with Python library files and packages."""
+
+    def writepy(self, pathname, basename = ""):
+        """Add all files from "pathname" to the ZIP archive.
+
+        If pathname is a package directory, search the directory and
+        all package subdirectories recursively for all *.py and enter
+        the modules into the archive.  If pathname is a plain
+        directory, listdir *.py and enter all modules.  Else, pathname
+        must be a Python *.py file and the module will be put into the
+        archive.  Added modules are always module.pyo or module.pyc.
+        This method will compile the module.py into module.pyc if
+        necessary.
+        """
+        dir, name = os.path.split(pathname)
+        if os.path.isdir(pathname):
+            initname = os.path.join(pathname, "__init__.py")
+            if os.path.isfile(initname):
+                # This is a package directory, add it
+                if basename:
+                    basename = "%s/%s" % (basename, name)
+                else:
+                    basename = name
+                if self.debug:
+                    print "Adding package in", pathname, "as", basename
+                fname, arcname = self._get_codename(initname[0:-3], basename)
+                if self.debug:
+                    print "Adding", arcname
+                self.write(fname, arcname)
+                dirlist = os.listdir(pathname)
+                dirlist.remove("__init__.py")
+                # Add all *.py files and package subdirectories
+                for filename in dirlist:
+                    path = os.path.join(pathname, filename)
+                    root, ext = os.path.splitext(filename)
+                    if os.path.isdir(path):
+                        if os.path.isfile(os.path.join(path, "__init__.py")):
+                            # This is a package directory, add it
+                            self.writepy(path, basename)  # Recursive call
+                    elif ext == ".py":
+                        fname, arcname = self._get_codename(path[0:-3],
+                                         basename)
+                        if self.debug:
+                            print "Adding", arcname
+                        self.write(fname, arcname)
+            else:
+                # This is NOT a package directory, add its files at top level
+                if self.debug:
+                    print "Adding files from directory", pathname
+                for filename in os.listdir(pathname):
+                    path = os.path.join(pathname, filename)
+                    root, ext = os.path.splitext(filename)
+                    if ext == ".py":
+                        fname, arcname = self._get_codename(path[0:-3],
+                                         basename)
+                        if self.debug:
+                            print "Adding", arcname
+                        self.write(fname, arcname)
+        else:
+            if pathname[-3:] != ".py":
+                raise RuntimeError, \
+                      'Files added with writepy() must end with ".py"'
+            fname, arcname = self._get_codename(pathname[0:-3], basename)
+            if self.debug:
+                print "Adding file", arcname
+            self.write(fname, arcname)
+
+    def _get_codename(self, pathname, basename):
+        """Return (filename, archivename) for the path.
+
+        Given a module name path, return the correct file path and
+        archive name, compiling if necessary.  For example, given
+        /python/lib/string, return (/python/lib/string.pyc, string).
+        """
+        file_py  = pathname + ".py"
+        file_pyc = pathname + ".pyc"
+        file_pyo = pathname + ".pyo"
+        if os.path.isfile(file_pyo) and \
+                            os.stat(file_pyo).st_mtime >= os.stat(file_py).st_mtime:
+            fname = file_pyo    # Use .pyo file
+        elif not os.path.isfile(file_pyc) or \
+             os.stat(file_pyc).st_mtime < os.stat(file_py).st_mtime:
+            import py_compile
+            if self.debug:
+                print "Compiling", file_py
+            try:
+                py_compile.compile(file_py, file_pyc, None, True)
+            except py_compile.PyCompileError,err:
+                print err.msg
+            fname = file_pyc
+        else:
+            fname = file_pyc
+        archivename = os.path.split(fname)[1]
+        if basename:
+            archivename = "%s/%s" % (basename, archivename)
+        return (fname, archivename)
+
+
+def main(args = None):
+    import textwrap
+    USAGE=textwrap.dedent("""\
+        Usage:
+            zipfile.py -l zipfile.zip        # Show listing of a zipfile
+            zipfile.py -t zipfile.zip        # Test if a zipfile is valid
+            zipfile.py -e zipfile.zip target # Extract zipfile into target dir
+            zipfile.py -c zipfile.zip src ... # Create zipfile from sources
+        """)
+    if args is None:
+        args = sys.argv[1:]
+
+    if not args or args[0] not in ('-l', '-c', '-e', '-t'):
+        print USAGE
+        sys.exit(1)
+
+    if args[0] == '-l':
+        if len(args) != 2:
+            print USAGE
+            sys.exit(1)
+        zf = ZipFile(args[1], 'r')
+        zf.printdir()
+        zf.close()
+
+    elif args[0] == '-t':
+        if len(args) != 2:
+            print USAGE
+            sys.exit(1)
+        zf = ZipFile(args[1], 'r')
+        zf.testzip()
+        print "Done testing"
+
+    elif args[0] == '-e':
+        if len(args) != 3:
+            print USAGE
+            sys.exit(1)
+
+        zf = ZipFile(args[1], 'r')
+        out = args[2]
+        for path in zf.namelist():
+            if path.startswith('./'):
+                tgt = os.path.join(out, path[2:])
+            else:
+                tgt = os.path.join(out, path)
+
+            tgtdir = os.path.dirname(tgt)
+            if not os.path.exists(tgtdir):
+                os.makedirs(tgtdir)
+            with open(tgt, 'wb') as fp:
+                fp.write(zf.read(path))
+        zf.close()
+
+    elif args[0] == '-c':
+        if len(args) < 3:
+            print USAGE
+            sys.exit(1)
+
+        def addToZip(zf, path, zippath):
+            if os.path.isfile(path):
+                zf.write(path, zippath, ZIP_DEFLATED)
+            elif os.path.isdir(path):
+                for nm in os.listdir(path):
+                    addToZip(zf,
+                            os.path.join(path, nm), os.path.join(zippath, nm))
+            # else: ignore
+
+        zf = ZipFile(args[1], 'w', allowZip64=True)
+        for src in args[2:]:
+            addToZip(zf, src, os.path.basename(src))
+
+        zf.close()
+
+if __name__ == "__main__":
+    main()
index 523ef1a2109cb6d41dcaaec8175672484cccff9c..c7921f2485e5189fd25f6dbf9e90a3c630fbbd96 100644 (file)
@@ -2,7 +2,7 @@
 
 import sys
 import zlib
-import zipfile
+import zipfilerugged
 import os
 import os.path
 import getopt
@@ -15,7 +15,7 @@ _FILENAME_OFFSET = 30
 _MAX_SIZE = 64 * 1024
 _MIMETYPE = 'application/epub+zip'
 
-class ZipInfo(zipfile.ZipInfo):
+class ZipInfo(zipfilerugged.ZipInfo):
     def __init__(self, *args, **kwargs):
         if 'compress_type' in kwargs:
             compress_type = kwargs.pop('compress_type')
@@ -27,10 +27,14 @@ class fixZip:
         self.ztype = 'zip'
         if zinput.lower().find('.epub') >= 0 :
             self.ztype = 'epub'
-        self.inzip = zipfile.ZipFile(zinput,'r')
-        self.outzip = zipfile.ZipFile(zoutput,'w')
+        print "opening input"
+        self.inzip = zipfilerugged.ZipFile(zinput,'r')
+        print "opening outout"
+        self.outzip = zipfilerugged.ZipFile(zoutput,'w')
+        print "opening input as raw file"
         # open the input zip for reading only as a raw file
         self.bzf = file(zinput,'rb')
+        print "finished initialising"
 
     def getlocalname(self, zi):
         local_header_offset = zi.header_offset
@@ -76,11 +80,11 @@ class fixZip:
         data = None
 
         # if not compressed we are good to go
-        if zi.compress_type == zipfile.ZIP_STORED:
+        if zi.compress_type == zipfilerugged.ZIP_STORED:
             data = self.bzf.read(zi.file_size)
 
         # if compressed we must decompress it using zlib
-        if zi.compress_type == zipfile.ZIP_DEFLATED:
+        if zi.compress_type == zipfilerugged.ZIP_DEFLATED:
             cmpdata = self.bzf.read(zi.compress_size)
             data = self.uncompress(cmpdata)
 
@@ -95,7 +99,7 @@ class fixZip:
 
         # if epub write mimetype file first, with no compression
         if self.ztype == 'epub':
-            nzinfo = ZipInfo('mimetype', compress_type=zipfile.ZIP_STORED)
+            nzinfo = ZipInfo('mimetype', compress_type=zipfilerugged.ZIP_STORED)
             self.outzip.writestr(nzinfo, _MIMETYPE)
 
         # write the rest of the files
@@ -105,7 +109,7 @@ class fixZip:
                 nzinfo = zinfo
                 try:
                     data = self.inzip.read(zinfo.filename)
-                except zipfile.BadZipfile or zipfile.error:
+                except zipfilerugged.BadZipfile or zipfilerugged.error:
                     local_name = self.getlocalname(zinfo)
                     data = self.getfiledata(zinfo)
                     nzinfo.filename = local_name
index 2e1abee40ca388f7f6ec8f2f5c9973d0e089084a..c11eabe7930877844c9a3d75a1c0bca44769b6a0 100644 (file)
@@ -21,7 +21,7 @@ import re
 import simpleprefs
 
 
-__version__ = '5.2'
+__version__ = '5.4'
 
 class DrmException(Exception):
     pass
@@ -142,21 +142,21 @@ class PrefsDialog(Toplevel):
         button = Tkinter.Button(body, text="...", command=self.get_altinfopath)
         button.grid(row=2, column=2)
 
-        Tkinter.Label(body, text='PID list (10 characters, no spaces, comma separated)').grid(row=3, sticky=Tkconstants.E)
+        Tkinter.Label(body, text='Mobipocket PID list\n(8 or 10 characters, comma separated)').grid(row=3, sticky=Tkconstants.E)
         self.pidnums = Tkinter.StringVar()
         self.pidinfo = Tkinter.Entry(body, width=50, textvariable=self.pidnums)
         if 'pids' in self.prefs_array:
             self.pidnums.set(self.prefs_array['pids'])
         self.pidinfo.grid(row=3, column=1, sticky=sticky)
 
-        Tkinter.Label(body, text='Kindle Serial Number list (16 characters, no spaces, comma separated)').grid(row=4, sticky=Tkconstants.E)
+        Tkinter.Label(body, text='eInk Kindle Serial Number list\n(16 characters, first character B, comma separated)').grid(row=4, sticky=Tkconstants.E)
         self.sernums = Tkinter.StringVar()
         self.serinfo = Tkinter.Entry(body, width=50, textvariable=self.sernums)
         if 'serials' in self.prefs_array:
             self.sernums.set(self.prefs_array['serials'])
         self.serinfo.grid(row=4, column=1, sticky=sticky)
 
-        Tkinter.Label(body, text='eReader data list (name:last 8 digits on credit card, comma separated)').grid(row=5, sticky=Tkconstants.E)
+        Tkinter.Label(body, text='eReader data list\n(name:last 8 digits on credit card, comma separated)').grid(row=5, sticky=Tkconstants.E)
         self.sdrmnums = Tkinter.StringVar()
         self.sdrminfo = Tkinter.Entry(body, width=50, textvariable=self.sdrmnums)
         if 'sdrms' in self.prefs_array:
@@ -287,9 +287,9 @@ class PrefsDialog(Toplevel):
         new_prefs = {}
         prefdir = self.prefs_array['dir']
         new_prefs['dir'] = prefdir
-        new_prefs['pids'] = self.pidinfo.get().strip()
-        new_prefs['serials'] = self.serinfo.get().strip()
-        new_prefs['sdrms'] = self.sdrminfo.get().strip()
+        new_prefs['pids'] = self.pidinfo.get().replace(" ","")
+        new_prefs['serials'] = self.serinfo.get().replace(" ","")
+        new_prefs['sdrms'] = self.sdrminfo.get().strip().replace(", ",",")
         new_prefs['outdir'] = self.outpath.get().strip()
         adkpath = self.adkpath.get()
         if os.path.dirname(adkpath) != prefdir:
diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/alfcrypto.exp b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/alfcrypto.exp
deleted file mode 100644 (file)
index 08b8cdb..0000000
Binary files a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/alfcrypto.exp and /dev/null differ
diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/cmbtc_v2.2.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/cmbtc_v2.2.py
deleted file mode 100644 (file)
index 7be7a8a..0000000
+++ /dev/null
@@ -1,899 +0,0 @@
-#! /usr/bin/python
-
-"""
-
-Comprehensive Mazama Book DRM with Topaz Cryptography V2.2
-
------BEGIN PUBLIC KEY-----
-MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdBHJ4CNc6DNFCw4MRCw4SWAK6
-M8hYfnNEI0yQmn5Ti+W8biT7EatpauE/5jgQMPBmdNrDr1hbHyHBSP7xeC2qlRWC
-B62UCxeu/fpfnvNHDN/wPWWH4jynZ2M6cdcnE5LQ+FfeKqZn7gnG2No1U9h7oOHx
-y2/pHuYme7U1TsgSjwIDAQAB
------END PUBLIC KEY-----
-
-"""
-
-from __future__ import with_statement
-
-import csv
-import sys
-import os
-import getopt
-import zlib
-from struct import pack
-from struct import unpack
-from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
-    create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
-    string_at, Structure, c_void_p, cast
-import _winreg as winreg
-import Tkinter
-import Tkconstants
-import tkMessageBox
-import traceback
-import hashlib
-
-MAX_PATH = 255
-
-kernel32 = windll.kernel32
-advapi32 = windll.advapi32
-crypt32 = windll.crypt32
-
-global kindleDatabase
-global bookFile
-global bookPayloadOffset
-global bookHeaderRecords
-global bookMetadata
-global bookKey
-global command
-
-#
-# Various character maps used to decrypt books. Probably supposed to act as obfuscation
-#
-
-charMap1 = "n5Pr6St7Uv8Wx9YzAb0Cd1Ef2Gh3Jk4M"
-charMap2 = "AaZzB0bYyCc1XxDdW2wEeVv3FfUuG4g-TtHh5SsIiR6rJjQq7KkPpL8lOoMm9Nn_"
-charMap3 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
-charMap4 = "ABCDEFGHIJKLMNPQRSTUVWXYZ123456789"
-
-#
-# Exceptions for all the problems that might happen during the script
-#
-
-class CMBDTCError(Exception):
-    pass
-
-class CMBDTCFatal(Exception):
-    pass
-
-#
-# Stolen stuff
-#
-
-class DataBlob(Structure):
-    _fields_ = [('cbData', c_uint),
-                ('pbData', c_void_p)]
-DataBlob_p = POINTER(DataBlob)
-
-def GetSystemDirectory():
-    GetSystemDirectoryW = kernel32.GetSystemDirectoryW
-    GetSystemDirectoryW.argtypes = [c_wchar_p, c_uint]
-    GetSystemDirectoryW.restype = c_uint
-    def GetSystemDirectory():
-        buffer = create_unicode_buffer(MAX_PATH + 1)
-        GetSystemDirectoryW(buffer, len(buffer))
-        return buffer.value
-    return GetSystemDirectory
-GetSystemDirectory = GetSystemDirectory()
-
-
-def GetVolumeSerialNumber():
-    GetVolumeInformationW = kernel32.GetVolumeInformationW
-    GetVolumeInformationW.argtypes = [c_wchar_p, c_wchar_p, c_uint,
-                                      POINTER(c_uint), POINTER(c_uint),
-                                      POINTER(c_uint), c_wchar_p, c_uint]
-    GetVolumeInformationW.restype = c_uint
-    def GetVolumeSerialNumber(path):
-        vsn = c_uint(0)
-        GetVolumeInformationW(path, None, 0, byref(vsn), None, None, None, 0)
-        return vsn.value
-    return GetVolumeSerialNumber
-GetVolumeSerialNumber = GetVolumeSerialNumber()
-
-
-def GetUserName():
-    GetUserNameW = advapi32.GetUserNameW
-    GetUserNameW.argtypes = [c_wchar_p, POINTER(c_uint)]
-    GetUserNameW.restype = c_uint
-    def GetUserName():
-        buffer = create_unicode_buffer(32)
-        size = c_uint(len(buffer))
-        while not GetUserNameW(buffer, byref(size)):
-            buffer = create_unicode_buffer(len(buffer) * 2)
-            size.value = len(buffer)
-        return buffer.value.encode('utf-16-le')[::2]
-    return GetUserName
-GetUserName = GetUserName()
-
-
-def CryptUnprotectData():
-    _CryptUnprotectData = crypt32.CryptUnprotectData
-    _CryptUnprotectData.argtypes = [DataBlob_p, c_wchar_p, DataBlob_p,
-                                   c_void_p, c_void_p, c_uint, DataBlob_p]
-    _CryptUnprotectData.restype = c_uint
-    def CryptUnprotectData(indata, entropy):
-        indatab = create_string_buffer(indata)
-        indata = DataBlob(len(indata), cast(indatab, c_void_p))
-        entropyb = create_string_buffer(entropy)
-        entropy = DataBlob(len(entropy), cast(entropyb, c_void_p))
-        outdata = DataBlob()
-        if not _CryptUnprotectData(byref(indata), None, byref(entropy),
-                                   None, None, 0, byref(outdata)):
-            raise CMBDTCFatal("Failed to Unprotect Data")
-        return string_at(outdata.pbData, outdata.cbData)
-    return CryptUnprotectData
-CryptUnprotectData = CryptUnprotectData()
-
-#
-# Returns the MD5 digest of "message"
-#
-
-def MD5(message):
-    ctx = hashlib.md5()
-    ctx.update(message)
-    return ctx.digest()
-
-#
-# Returns the MD5 digest of "message"
-#
-
-def SHA1(message):
-    ctx = hashlib.sha1()
-    ctx.update(message)
-    return ctx.digest()
-
-#
-# Open the book file at path
-#
-
-def openBook(path):
-    try:
-        return open(path,'rb')
-    except:
-        raise CMBDTCFatal("Could not open book file: " + path)
-#
-# Encode the bytes in data with the characters in map
-#
-
-def encode(data, map):
-    result = ""
-    for char in data:
-        value = ord(char)
-        Q = (value ^ 0x80) // len(map)
-        R = value % len(map)
-        result += map[Q]
-        result += map[R]
-    return result
-
-#
-# Hash the bytes in data and then encode the digest with the characters in map
-#
-
-def encodeHash(data,map):
-    return encode(MD5(data),map)
-
-#
-# Decode the string in data with the characters in map. Returns the decoded bytes
-#
-
-def decode(data,map):
-    result = ""
-    for i in range (0,len(data),2):
-        high = map.find(data[i])
-        low = map.find(data[i+1])
-        value = (((high * 0x40) ^ 0x80) & 0xFF) + low
-        result += pack("B",value)
-    return result
-
-#
-# Locate and open the Kindle.info file (Hopefully in the way it is done in the Kindle application)
-#
-
-def openKindleInfo():
-    regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
-    path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
-    return open(path+'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info','r')
-
-#
-# Parse the Kindle.info file and return the records as a list of key-values
-#
-
-def parseKindleInfo():
-    DB = {}
-    infoReader = openKindleInfo()
-    infoReader.read(1)
-    data = infoReader.read()
-    items = data.split('{')
-
-    for item in items:
-        splito = item.split(':')
-        DB[splito[0]] =splito[1]
-    return DB
-
-#
-# Find if the original string for a hashed/encoded string is known. If so return the original string othwise return an empty string. (Totally not optimal)
-#
-
-def findNameForHash(hash):
-    names = ["kindle.account.tokens","kindle.cookie.item","eulaVersionAccepted","login_date","kindle.token.item","login","kindle.key.item","kindle.name.info","kindle.device.info", "MazamaRandomNumber"]
-    result = ""
-    for name in names:
-        if hash == encodeHash(name, charMap2):
-            result = name
-            break
-    return name
-
-#
-# Print all the records from the kindle.info file (option -i)
-#
-
-def printKindleInfo():
-    for record in kindleDatabase:
-        name = findNameForHash(record)
-        if name != "" :
-            print (name)
-            print ("--------------------------\n")
-        else :
-            print ("Unknown Record")
-        print getKindleInfoValueForHash(record)
-        print "\n"
-#
-# Get a record from the Kindle.info file for the key "hashedKey" (already hashed and encoded). Return the decoded and decrypted record
-#
-
-def getKindleInfoValueForHash(hashedKey):
-    global kindleDatabase
-    encryptedValue = decode(kindleDatabase[hashedKey],charMap2)
-    return CryptUnprotectData(encryptedValue,"")
-
-#
-#  Get a record from the Kindle.info file for the string in "key" (plaintext). Return the decoded and decrypted record
-#
-
-def getKindleInfoValueForKey(key):
-    return getKindleInfoValueForHash(encodeHash(key,charMap2))
-
-#
-# Get a 7 bit encoded number from the book file
-#
-
-def bookReadEncodedNumber():
-    flag = False
-    data = ord(bookFile.read(1))
-
-    if data == 0xFF:
-        flag = True
-        data = ord(bookFile.read(1))
-
-    if data >= 0x80:
-        datax = (data & 0x7F)
-        while data >= 0x80 :
-            data = ord(bookFile.read(1))
-            datax = (datax <<7) + (data & 0x7F)
-        data = datax
-
-    if flag:
-        data = -data
-    return data
-
-#
-# Encode a number in 7 bit format
-#
-
-def encodeNumber(number):
-    result = ""
-    negative = False
-    flag = 0
-
-    if number < 0 :
-        number = -number + 1
-        negative = True
-
-    while True:
-        byte = number & 0x7F
-        number = number >> 7
-        byte += flag
-        result += chr(byte)
-        flag = 0x80
-        if number == 0 :
-            if (byte == 0xFF and negative == False) :
-                result += chr(0x80)
-            break
-
-    if negative:
-        result += chr(0xFF)
-
-    return result[::-1]
-
-#
-# Get a length prefixed string from the file
-#
-
-def bookReadString():
-    stringLength = bookReadEncodedNumber()
-    return unpack(str(stringLength)+"s",bookFile.read(stringLength))[0]
-
-#
-# Returns a length prefixed string
-#
-
-def lengthPrefixString(data):
-    return encodeNumber(len(data))+data
-
-
-#
-# Read and return the data of one header record at the current book file position [[offset,compressedLength,decompressedLength],...]
-#
-
-def bookReadHeaderRecordData():
-    nbValues = bookReadEncodedNumber()
-    values = []
-    for i in range (0,nbValues):
-        values.append([bookReadEncodedNumber(),bookReadEncodedNumber(),bookReadEncodedNumber()])
-    return values
-
-#
-# Read and parse one header record at the current book file position and return the associated data [[offset,compressedLength,decompressedLength],...]
-#
-
-def parseTopazHeaderRecord():
-    if ord(bookFile.read(1)) != 0x63:
-        raise CMBDTCFatal("Parse Error : Invalid Header")
-
-    tag = bookReadString()
-    record = bookReadHeaderRecordData()
-    return [tag,record]
-
-#
-# Parse the header of a Topaz file, get all the header records and the offset for the payload
-#
-
-def parseTopazHeader():
-    global bookHeaderRecords
-    global bookPayloadOffset
-    magic = unpack("4s",bookFile.read(4))[0]
-
-    if magic != 'TPZ0':
-        raise CMBDTCFatal("Parse Error : Invalid Header, not a Topaz file")
-
-    nbRecords = bookReadEncodedNumber()
-    bookHeaderRecords = {}
-
-    for i in range (0,nbRecords):
-        result = parseTopazHeaderRecord()
-        bookHeaderRecords[result[0]] = result[1]
-
-    if ord(bookFile.read(1))  != 0x64 :
-        raise CMBDTCFatal("Parse Error : Invalid Header")
-
-    bookPayloadOffset = bookFile.tell()
-
-#
-# Get a record in the book payload, given its name and index. If necessary the record is decrypted. The record is not decompressed
-#
-
-def getBookPayloadRecord(name, index):
-    encrypted = False
-
-    try:
-        recordOffset = bookHeaderRecords[name][index][0]
-    except:
-        raise CMBDTCFatal("Parse Error : Invalid Record, record not found")
-
-    bookFile.seek(bookPayloadOffset + recordOffset)
-
-    tag = bookReadString()
-    if tag != name :
-        raise CMBDTCFatal("Parse Error : Invalid Record, record name doesn't match")
-
-    recordIndex = bookReadEncodedNumber()
-
-    if recordIndex < 0 :
-        encrypted = True
-        recordIndex = -recordIndex -1
-
-    if recordIndex != index :
-        raise CMBDTCFatal("Parse Error : Invalid Record, index doesn't match")
-
-    if bookHeaderRecords[name][index][2] != 0 :
-        record = bookFile.read(bookHeaderRecords[name][index][2])
-    else:
-        record = bookFile.read(bookHeaderRecords[name][index][1])
-
-    if encrypted:
-        ctx = topazCryptoInit(bookKey)
-        record = topazCryptoDecrypt(record,ctx)
-
-    return record
-
-#
-# Extract, decrypt and decompress a book record indicated by name and index and print it or save it in "filename"
-#
-
-def extractBookPayloadRecord(name, index, filename):
-    compressed = False
-
-    try:
-        compressed = bookHeaderRecords[name][index][2] != 0
-        record = getBookPayloadRecord(name,index)
-    except:
-        print("Could not find record")
-
-    if compressed:
-        try:
-            record = zlib.decompress(record)
-        except:
-            raise CMBDTCFatal("Could not decompress record")
-
-    if filename != "":
-        try:
-            file = open(filename,"wb")
-            file.write(record)
-            file.close()
-        except:
-            raise CMBDTCFatal("Could not write to destination file")
-    else:
-        print(record)
-
-#
-# return next record [key,value] from the book metadata from the current book position
-#
-
-def readMetadataRecord():
-    return [bookReadString(),bookReadString()]
-
-#
-# Parse the metadata record from the book payload and return a list of [key,values]
-#
-
-def parseMetadata():
-    global bookHeaderRecords
-    global bookPayloadAddress
-    global bookMetadata
-    bookMetadata = {}
-    bookFile.seek(bookPayloadOffset + bookHeaderRecords["metadata"][0][0])
-    tag = bookReadString()
-    if tag != "metadata" :
-        raise CMBDTCFatal("Parse Error : Record Names Don't Match")
-
-    flags = ord(bookFile.read(1))
-    nbRecords = ord(bookFile.read(1))
-
-    for i in range (0,nbRecords) :
-        record =readMetadataRecord()
-        bookMetadata[record[0]] = record[1]
-
-#
-# Returns two bit at offset from a bit field
-#
-
-def getTwoBitsFromBitField(bitField,offset):
-    byteNumber = offset // 4
-    bitPosition = 6 - 2*(offset % 4)
-
-    return ord(bitField[byteNumber]) >> bitPosition & 3
-
-#
-# Returns the six bits at offset from a bit field
-#
-
-def getSixBitsFromBitField(bitField,offset):
-    offset *= 3
-    value = (getTwoBitsFromBitField(bitField,offset) <<4) + (getTwoBitsFromBitField(bitField,offset+1) << 2) +getTwoBitsFromBitField(bitField,offset+2)
-    return value
-
-#
-# 8 bits to six bits encoding from hash to generate PID string
-#
-
-def encodePID(hash):
-    global charMap3
-    PID = ""
-    for position in range (0,8):
-        PID += charMap3[getSixBitsFromBitField(hash,position)]
-    return PID
-
-#
-# Context initialisation for the Topaz Crypto
-#
-
-def topazCryptoInit(key):
-    ctx1 = 0x0CAFFE19E
-
-    for keyChar in key:
-        keyByte = ord(keyChar)
-        ctx2 = ctx1
-        ctx1 = ((((ctx1 >>2) * (ctx1 >>7))&0xFFFFFFFF) ^ (keyByte * keyByte * 0x0F902007)& 0xFFFFFFFF )
-    return [ctx1,ctx2]
-
-#
-# decrypt data with the context prepared by topazCryptoInit()
-#
-
-def topazCryptoDecrypt(data, ctx):
-    ctx1 = ctx[0]
-    ctx2 = ctx[1]
-
-    plainText = ""
-
-    for dataChar in data:
-        dataByte = ord(dataChar)
-        m = (dataByte ^ ((ctx1 >> 3) &0xFF) ^ ((ctx2<<3) & 0xFF)) &0xFF
-        ctx2 = ctx1
-        ctx1 = (((ctx1 >> 2) * (ctx1 >> 7)) &0xFFFFFFFF) ^((m * m * 0x0F902007) &0xFFFFFFFF)
-        plainText += chr(m)
-
-    return plainText
-
-#
-# Decrypt a payload record with the PID
-#
-
-def decryptRecord(data,PID):
-    ctx = topazCryptoInit(PID)
-    return topazCryptoDecrypt(data, ctx)
-
-#
-# Try to decrypt a dkey record (contains the book PID)
-#
-
-def decryptDkeyRecord(data,PID):
-    record = decryptRecord(data,PID)
-    fields = unpack("3sB8sB8s3s",record)
-
-    if fields[0] != "PID" or fields[5] != "pid" :
-        raise CMBDTCError("Didn't find PID magic numbers in record")
-    elif fields[1] != 8 or fields[3] != 8 :
-        raise CMBDTCError("Record didn't contain correct length fields")
-    elif fields[2] != PID :
-        raise CMBDTCError("Record didn't contain PID")
-
-    return fields[4]
-
-#
-# Decrypt all the book's dkey records (contain the book PID)
-#
-
-def decryptDkeyRecords(data,PID):
-    nbKeyRecords = ord(data[0])
-    records = []
-    data = data[1:]
-    for i in range (0,nbKeyRecords):
-        length = ord(data[0])
-        try:
-            key = decryptDkeyRecord(data[1:length+1],PID)
-            records.append(key)
-        except CMBDTCError:
-            pass
-        data = data[1+length:]
-
-    return records
-
-#
-# Encryption table used to generate the device PID
-#
-
-def generatePidEncryptionTable() :
-    table = []
-    for counter1 in range (0,0x100):
-        value = counter1
-        for counter2 in range (0,8):
-            if (value & 1 == 0) :
-                value = value >> 1
-            else :
-                value = value >> 1
-                value = value ^ 0xEDB88320
-        table.append(value)
-    return table
-
-#
-# Seed value used to generate the device PID
-#
-
-def generatePidSeed(table,dsn) :
-    value = 0
-    for counter in range (0,4) :
-        index = (ord(dsn[counter]) ^ value) &0xFF
-        value = (value >> 8) ^ table[index]
-    return value
-
-#
-# Generate the device PID
-#
-
-def generateDevicePID(table,dsn,nbRoll):
-    seed = generatePidSeed(table,dsn)
-    pidAscii = ""
-    pid = [(seed >>24) &0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF,(seed>>24) & 0xFF,(seed >> 16) &0xff,(seed >> 8) &0xFF,(seed) & 0xFF]
-    index = 0
-
-    for counter in range (0,nbRoll):
-        pid[index] = pid[index] ^ ord(dsn[counter])
-        index = (index+1) %8
-
-    for counter in range (0,8):
-        index = ((((pid[counter] >>5) & 3) ^ pid[counter]) & 0x1f) + (pid[counter] >> 7)
-        pidAscii += charMap4[index]
-    return pidAscii
-
-#
-# Create decrypted book payload
-#
-
-def createDecryptedPayload(payload):
-
-    # store data to be able to create the header later
-    headerData= []
-    currentOffset = 0
-
-    # Add social DRM to decrypted files
-
-    try:
-        data = getKindleInfoValueForKey("kindle.name.info")+":"+ getKindleInfoValueForKey("login")
-        if payload!= None:
-            payload.write(lengthPrefixString("sdrm"))
-            payload.write(encodeNumber(0))
-            payload.write(data)
-        else:
-            currentOffset += len(lengthPrefixString("sdrm"))
-            currentOffset += len(encodeNumber(0))
-            currentOffset += len(data)
-    except:
-        pass
-
-    for headerRecord in bookHeaderRecords:
-        name = headerRecord
-        newRecord = []
-
-        if name != "dkey" :
-
-            for index in range (0,len(bookHeaderRecords[name])) :
-                offset = currentOffset
-
-                if payload != None:
-                    # write tag
-                    payload.write(lengthPrefixString(name))
-                    # write data
-                    payload.write(encodeNumber(index))
-                    payload.write(getBookPayloadRecord(name, index))
-
-                else :
-                    currentOffset += len(lengthPrefixString(name))
-                    currentOffset += len(encodeNumber(index))
-                    currentOffset += len(getBookPayloadRecord(name, index))
-                    newRecord.append([offset,bookHeaderRecords[name][index][1],bookHeaderRecords[name][index][2]])
-
-        headerData.append([name,newRecord])
-
-
-
-    return headerData
-
-#
-# Create decrypted book
-#
-
-def createDecryptedBook(outputFile):
-    outputFile = open(outputFile,"wb")
-    # Write the payload in a temporary file
-    headerData = createDecryptedPayload(None)
-    outputFile.write("TPZ0")
-    outputFile.write(encodeNumber(len(headerData)))
-
-    for header in headerData :
-        outputFile.write(chr(0x63))
-        outputFile.write(lengthPrefixString(header[0]))
-        outputFile.write(encodeNumber(len(header[1])))
-        for numbers in header[1] :
-            outputFile.write(encodeNumber(numbers[0]))
-            outputFile.write(encodeNumber(numbers[1]))
-            outputFile.write(encodeNumber(numbers[2]))
-
-    outputFile.write(chr(0x64))
-    createDecryptedPayload(outputFile)
-    outputFile.close()
-
-#
-# Set the command to execute by the programm according to cmdLine parameters
-#
-
-def setCommand(name) :
-    global command
-    if command != "" :
-        raise CMBDTCFatal("Invalid command line parameters")
-    else :
-        command = name
-
-#
-# Program usage
-#
-
-def usage():
-    print("\nUsage:")
-    print("\nCMBDTC.py [options] bookFileName\n")
-    print("-p Adds a PID to the list of PIDs that are tried to decrypt the book key (can be used several times)")
-    print("-d Saves a decrypted copy of the book")
-    print("-r Prints or writes to disk a record indicated in the form name:index (e.g \"img:0\")")
-    print("-o Output file name to write records and decrypted books")
-    print("-v Verbose (can be used several times)")
-    print("-i Prints kindle.info database")
-
-#
-# Main
-#
-
-def main(argv=sys.argv):
-    global kindleDatabase
-    global bookMetadata
-    global bookKey
-    global bookFile
-    global command
-
-    progname = os.path.basename(argv[0])
-
-    verbose = 0
-    recordName = ""
-    recordIndex = 0
-    outputFile = ""
-    PIDs = []
-    kindleDatabase = None
-    command = ""
-
-
-    try:
-        opts, args = getopt.getopt(sys.argv[1:], "vdir:o:p:")
-    except getopt.GetoptError, err:
-        # print help information and exit:
-        print str(err) # will print something like "option -a not recognized"
-        usage()
-        sys.exit(2)
-
-    if len(opts) == 0 and len(args) == 0 :
-        usage()
-        sys.exit(2)
-
-    for o, a in opts:
-        if o == "-v":
-            verbose+=1
-        if o == "-i":
-            setCommand("printInfo")
-        if o =="-o":
-            if a == None :
-                raise CMBDTCFatal("Invalid parameter for -o")
-            outputFile = a
-        if o =="-r":
-            setCommand("printRecord")
-            try:
-                recordName,recordIndex = a.split(':')
-            except:
-                raise CMBDTCFatal("Invalid parameter for -r")
-        if o =="-p":
-            PIDs.append(a)
-        if o =="-d":
-            setCommand("doit")
-
-    if command == "" :
-        raise CMBDTCFatal("No action supplied on command line")
-
-    #
-    # Read the encrypted database
-    #
-
-    try:
-        kindleDatabase = parseKindleInfo()
-    except Exception, message:
-        if verbose>0:
-            print(message)
-
-    if kindleDatabase != None :
-        if command == "printInfo" :
-            printKindleInfo()
-
-    #
-    # Compute the DSN
-    #
-
-    # Get the Mazama Random number
-        MazamaRandomNumber = getKindleInfoValueForKey("MazamaRandomNumber")
-
-    # Get the HDD serial
-        encodedSystemVolumeSerialNumber = encodeHash(str(GetVolumeSerialNumber(GetSystemDirectory().split('\\')[0] + '\\')),charMap1)
-
-    # Get the current user name
-        encodedUsername = encodeHash(GetUserName(),charMap1)
-
-    # concat, hash and encode
-        DSN = encode(SHA1(MazamaRandomNumber+encodedSystemVolumeSerialNumber+encodedUsername),charMap1)
-
-        if verbose >1:
-            print("DSN: " + DSN)
-
-    #
-    # Compute the device PID
-    #
-
-        table =  generatePidEncryptionTable()
-        devicePID = generateDevicePID(table,DSN,4)
-        PIDs.append(devicePID)
-
-        if verbose > 0:
-            print("Device PID: " + devicePID)
-
-    #
-    # Open book and parse metadata
-    #
-
-    if len(args) == 1:
-
-        bookFile = openBook(args[0])
-        parseTopazHeader()
-        parseMetadata()
-
-    #
-    # Compute book PID
-    #
-
-    # Get the account token
-
-        if kindleDatabase != None:
-            kindleAccountToken = getKindleInfoValueForKey("kindle.account.tokens")
-
-            if verbose >1:
-                print("Account Token: " + kindleAccountToken)
-
-            keysRecord = bookMetadata["keys"]
-            keysRecordRecord = bookMetadata[keysRecord]
-
-            pidHash = SHA1(DSN+kindleAccountToken+keysRecord+keysRecordRecord)
-
-            bookPID = encodePID(pidHash)
-            PIDs.append(bookPID)
-
-            if verbose > 0:
-                print ("Book PID: " + bookPID )
-
-    #
-    #  Decrypt book key
-    #
-
-        dkey = getBookPayloadRecord('dkey', 0)
-
-        bookKeys = []
-        for PID in PIDs :
-            bookKeys+=decryptDkeyRecords(dkey,PID)
-
-        if len(bookKeys) == 0 :
-            if verbose > 0 :
-                print ("Book key could not be found. Maybe this book is not registered with this device.")
-        else :
-            bookKey = bookKeys[0]
-            if verbose > 0:
-                print("Book key: " + bookKey.encode('hex'))
-
-
-
-            if command == "printRecord" :
-                extractBookPayloadRecord(recordName,int(recordIndex),outputFile)
-                if outputFile != "" and verbose>0 :
-                    print("Wrote record to file: "+outputFile)
-            elif command == "doit" :
-                if outputFile!="" :
-                    createDecryptedBook(outputFile)
-                    if verbose >0 :
-                        print ("Decrypted book saved. Don't pirate!")
-                elif verbose > 0:
-                    print("Output file name was not supplied.")
-
-    return 0
-
-if __name__ == '__main__':
-    sys.exit(main())
index c029760955bd4d6bed120397667051ddfdde3f1d..98258788c86dbb40a3f2e773d185baad750d4cdf 100644 (file)
@@ -20,7 +20,7 @@ class ConfigWidget(QWidget):
         self.l = QVBoxLayout()
         self.setLayout(self.l)
 
-        self.serialLabel = QLabel('Kindle Serial numbers (separate with commas, no spaces)')
+        self.serialLabel = QLabel('eInk Kindle Serial numbers (First character B, 16 characters, use commas if more than one)')
         self.l.addWidget(self.serialLabel)
 
         self.serials = QLineEdit(self)
@@ -28,7 +28,7 @@ class ConfigWidget(QWidget):
         self.l.addWidget(self.serials)
         self.serialLabel.setBuddy(self.serials)
 
-        self.pidLabel = QLabel('Mobipocket PIDs (separate with commas, no spaces)')
+        self.pidLabel = QLabel('Mobipocket PIDs (8 or 10 characters, use commas if more than one)')
         self.l.addWidget(self.pidLabel)
 
         self.pids = QLineEdit(self)
@@ -50,8 +50,8 @@ class ConfigWidget(QWidget):
         self.wpLabel.setBuddy(self.wineprefix)
 
     def save_settings(self):
-        prefs['pids'] = str(self.pids.text())
-        prefs['serials'] = str(self.serials.text())
+       prefs['pids'] = str(self.pids.text()).replace(" ","")
+        prefs['serials'] = str(self.serials.text()).replace(" ","")
         winepref=str(self.wineprefix.text())
         if winepref.strip() != '':
             prefs['WINEPREFIX'] = winepref
index 917aa4aaa16210a7532e733a4fd667633ad8a4d9..03aa91fe50347c2dcabb08acbffa2ed0b12c5c82 100644 (file)
@@ -2,7 +2,7 @@
 
 from __future__ import with_statement
 
-# ignobleepub.pyw, version 3.4
+# ignobleepub.pyw, version 3.5
 
 # To run this program install Python 2.6 from <http://www.python.org/download/>
 # and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
@@ -17,6 +17,7 @@ from __future__ import with_statement
 #   3.2 - add support for encoding to 'utf-8' when building up list of files to cecrypt from encryption.xml
 #   3.3 - On Windows try PyCrypto first and OpenSSL next
 #   3.4 - Modify interace to allow use with import
+#   3.5 - Fix for potential problem with PyCrypto
 
 
 __license__ = 'GPL v3'
@@ -100,7 +101,7 @@ def _load_crypto_pycrypto():
 
     class AES(object):
         def __init__(self, key):
-            self._aes = _AES.new(key, _AES.MODE_CBC)
+            self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
 
         def decrypt(self, data):
             return self._aes.decrypt(data)
@@ -143,7 +144,7 @@ class ZipInfo(zipfile.ZipInfo):
 class Decryptor(object):
     def __init__(self, bookkey, encryption):
         enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
-        # self._aes = AES.new(bookkey, AES.MODE_CBC)
+        # self._aes = AES.new(bookkey, AES.MODE_CBC, '\x00'*16)
         self._aes = AES(bookkey)
         encryption = etree.fromstring(encryption)
         self._encrypted = encrypted = set()
@@ -271,7 +272,7 @@ def decryptBook(keypath, inpath, outpath):
     with open(keypath, 'rb') as f:
         keyb64 = f.read()
     key = keyb64.decode('base64')[:16]
-    # aes = AES.new(key, AES.MODE_CBC)
+    # aes = AES.new(key, AES.MODE_CBC, '\x00'*16)
     aes = AES(key)
 
     with closing(ZipFile(open(inpath, 'rb'))) as inf:
index e7a78ea19a7d60c2b2679d3b548d5162ca193c12..e2c50e2ebb1788dd784db57ba130a28a05ec2a37 100644 (file)
@@ -2,7 +2,7 @@
 
 from __future__ import with_statement
 
-# ignoblekeygen.pyw, version 2.3
+# ignoblekeygen.pyw, version 2.4
 
 # To run this program install Python 2.6 from <http://www.python.org/download/>
 # and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
@@ -15,6 +15,7 @@ from __future__ import with_statement
 #   2.1 - Allow Windows versions of libcrypto to be found
 #   2.2 - On Windows try PyCrypto first and then OpenSSL next
 #   2.3 - Modify interface to allow use of import
+#   2.4 - Improvements to UI and now works in plugins
 
 """
 Generate Barnes & Noble EPUB user key from name and credit card number.
@@ -25,10 +26,6 @@ __license__ = 'GPL v3'
 import sys
 import os
 import hashlib
-import Tkinter
-import Tkconstants
-import tkFileDialog
-import tkMessageBox
 
 
 
@@ -124,8 +121,10 @@ def normalize_name(name):
 
 
 def generate_keyfile(name, ccn, outpath):
+    # remove spaces and case from name and CC numbers.
     name = normalize_name(name) + '\x00'
-    ccn = ccn + '\x00'
+    ccn = normalize_name(ccn) + '\x00'
+    
     name_sha = hashlib.sha1(name).digest()[:16]
     ccn_sha = hashlib.sha1(ccn).digest()[:16]
     both_sha = hashlib.sha1(name + ccn).digest()
@@ -137,69 +136,6 @@ def generate_keyfile(name, ccn, outpath):
     return userkey
 
 
-class DecryptionDialog(Tkinter.Frame):
-    def __init__(self, root):
-        Tkinter.Frame.__init__(self, root, border=5)
-        self.status = Tkinter.Label(self, text='Enter parameters')
-        self.status.pack(fill=Tkconstants.X, expand=1)
-        body = Tkinter.Frame(self)
-        body.pack(fill=Tkconstants.X, expand=1)
-        sticky = Tkconstants.E + Tkconstants.W
-        body.grid_columnconfigure(1, weight=2)
-        Tkinter.Label(body, text='Name').grid(row=1)
-        self.name = Tkinter.Entry(body, width=30)
-        self.name.grid(row=1, column=1, sticky=sticky)
-        Tkinter.Label(body, text='CC#').grid(row=2)
-        self.ccn = Tkinter.Entry(body, width=30)
-        self.ccn.grid(row=2, column=1, sticky=sticky)
-        Tkinter.Label(body, text='Output file').grid(row=0)
-        self.keypath = Tkinter.Entry(body, width=30)
-        self.keypath.grid(row=0, column=1, sticky=sticky)
-        self.keypath.insert(0, 'bnepubkey.b64')
-        button = Tkinter.Button(body, text="...", command=self.get_keypath)
-        button.grid(row=0, column=2)
-        buttons = Tkinter.Frame(self)
-        buttons.pack()
-        botton = Tkinter.Button(
-            buttons, text="Generate", width=10, command=self.generate)
-        botton.pack(side=Tkconstants.LEFT)
-        Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
-        button = Tkinter.Button(
-            buttons, text="Quit", width=10, command=self.quit)
-        button.pack(side=Tkconstants.RIGHT)
-
-    def get_keypath(self):
-        keypath = tkFileDialog.asksaveasfilename(
-            parent=None, title='Select B&N EPUB key file to produce',
-            defaultextension='.b64',
-            filetypes=[('base64-encoded files', '.b64'),
-                       ('All Files', '.*')])
-        if keypath:
-            keypath = os.path.normpath(keypath)
-            self.keypath.delete(0, Tkconstants.END)
-            self.keypath.insert(0, keypath)
-        return
-
-    def generate(self):
-        name = self.name.get()
-        ccn = self.ccn.get()
-        keypath = self.keypath.get()
-        if not name:
-            self.status['text'] = 'Name not specified'
-            return
-        if not ccn:
-            self.status['text'] = 'Credit card number not specified'
-            return
-        if not keypath:
-            self.status['text'] = 'Output keyfile path not specified'
-            return
-        self.status['text'] = 'Generating...'
-        try:
-            generate_keyfile(name, ccn, keypath)
-        except Exception, e:
-            self.status['text'] = 'Error: ' + str(e)
-            return
-        self.status['text'] = 'Keyfile successfully generated'
 
 
 def cli_main(argv=sys.argv):
@@ -218,6 +154,75 @@ def cli_main(argv=sys.argv):
 
 
 def gui_main():
+    import Tkinter
+    import Tkconstants
+    import tkFileDialog
+    import tkMessageBox
+
+    class DecryptionDialog(Tkinter.Frame):
+        def __init__(self, root):
+            Tkinter.Frame.__init__(self, root, border=5)
+            self.status = Tkinter.Label(self, text='Enter parameters')
+            self.status.pack(fill=Tkconstants.X, expand=1)
+            body = Tkinter.Frame(self)
+            body.pack(fill=Tkconstants.X, expand=1)
+            sticky = Tkconstants.E + Tkconstants.W
+            body.grid_columnconfigure(1, weight=2)
+            Tkinter.Label(body, text='Account Name').grid(row=0)
+            self.name = Tkinter.Entry(body, width=40)
+            self.name.grid(row=0, column=1, sticky=sticky)
+            Tkinter.Label(body, text='CC#').grid(row=1)
+            self.ccn = Tkinter.Entry(body, width=40)
+            self.ccn.grid(row=1, column=1, sticky=sticky)
+            Tkinter.Label(body, text='Output file').grid(row=2)
+            self.keypath = Tkinter.Entry(body, width=40)
+            self.keypath.grid(row=2, column=1, sticky=sticky)
+            self.keypath.insert(2, 'bnepubkey.b64')
+            button = Tkinter.Button(body, text="...", command=self.get_keypath)
+            button.grid(row=2, column=2)
+            buttons = Tkinter.Frame(self)
+            buttons.pack()
+            botton = Tkinter.Button(
+                buttons, text="Generate", width=10, command=self.generate)
+            botton.pack(side=Tkconstants.LEFT)
+            Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
+            button = Tkinter.Button(
+                buttons, text="Quit", width=10, command=self.quit)
+            button.pack(side=Tkconstants.RIGHT)
+    
+        def get_keypath(self):
+            keypath = tkFileDialog.asksaveasfilename(
+                parent=None, title='Select B&N EPUB key file to produce',
+                defaultextension='.b64',
+                filetypes=[('base64-encoded files', '.b64'),
+                           ('All Files', '.*')])
+            if keypath:
+                keypath = os.path.normpath(keypath)
+                self.keypath.delete(0, Tkconstants.END)
+                self.keypath.insert(0, keypath)
+            return
+    
+        def generate(self):
+            name = self.name.get()
+            ccn = self.ccn.get()
+            keypath = self.keypath.get()
+            if not name:
+                self.status['text'] = 'Name not specified'
+                return
+            if not ccn:
+                self.status['text'] = 'Credit card number not specified'
+                return
+            if not keypath:
+                self.status['text'] = 'Output keyfile path not specified'
+                return
+            self.status['text'] = 'Generating...'
+            try:
+                generate_keyfile(name, ccn, keypath)
+            except Exception, e:
+                self.status['text'] = 'Error: ' + str(e)
+                return
+            self.status['text'] = 'Keyfile successfully generated'
+
     root = Tkinter.Tk()
     if AES is None:
         root.withdraw()
index 018736acd74626a3ac075f1659e565b28cfbb2aa..2bb32b10f73c2add5cdeecb0431412456598c650 100644 (file)
@@ -30,6 +30,8 @@ from __future__ import with_statement
 #   5.4 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml
 #   5.5 - On Windows try PyCrypto first, OpenSSL next
 #   5.6 - Modify interface to allow use with import
+#   5.7 - Fix for potential problem with PyCrypto
+
 """
 Decrypt Adobe ADEPT-encrypted EPUB books.
 """
@@ -235,7 +237,7 @@ def _load_crypto_pycrypto():
 
     class AES(object):
         def __init__(self, key):
-            self._aes = _AES.new(key, _AES.MODE_CBC)
+            self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
 
         def decrypt(self, data):
             return self._aes.decrypt(data)
index da61e77eb4ab05593ecd0a6f4a5a1700edf71a09..723b7c64eeab9ead313fc8283196cb5baa32407c 100644 (file)
@@ -3,7 +3,7 @@
 
 from __future__ import with_statement
 
-# ineptkey.pyw, version 5.4
+# ineptkey.pyw, version 5.6
 # Copyright © 2009-2010 i♥cabbages
 
 # Released under the terms of the GNU General Public Licence, version 3 or
@@ -36,6 +36,8 @@ from __future__ import with_statement
 #   5.2 - added support for output of key to a particular file
 #   5.3 - On Windows try PyCrypto first, OpenSSL next
 #   5.4 - Modify interface to allow use of import
+#   5.5 - Fix for potential problem with PyCrypto
+#   5.6 - Revise to allow use in Plugins to eliminate need for duplicate code
 
 """
 Retrieve Adobe ADEPT user key.
@@ -46,15 +48,17 @@ __license__ = 'GPL v3'
 import sys
 import os
 import struct
-import Tkinter
-import Tkconstants
-import tkMessageBox
-import traceback
+
+try:
+    from calibre.constants import iswindows, isosx
+except:
+    iswindows = sys.platform.startswith('win')
+    isosx = sys.platform.startswith('darwin')
 
 class ADEPTError(Exception):
     pass
 
-if sys.platform.startswith('win'):
+if iswindows:
     from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
         create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
         string_at, Structure, c_void_p, cast, c_size_t, memmove, CDLL, c_int, \
@@ -76,13 +80,13 @@ if sys.platform.startswith('win'):
             _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
                         ('rounds', c_int)]
         AES_KEY_p = POINTER(AES_KEY)
-
+    
         def F(restype, name, argtypes):
             func = getattr(libcrypto, name)
             func.restype = restype
             func.argtypes = argtypes
             return func
-
+    
         AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
                                 [c_char_p, c_int, AES_KEY_p])
         AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
@@ -110,7 +114,7 @@ if sys.platform.startswith('win'):
         from Crypto.Cipher import AES as _AES
         class AES(object):
             def __init__(self, key):
-                self._aes = _AES.new(key, _AES.MODE_CBC)
+                self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
             def decrypt(self, data):
                 return self._aes.decrypt(data)
         return AES
@@ -292,13 +296,9 @@ if sys.platform.startswith('win'):
         return CryptUnprotectData
     CryptUnprotectData = CryptUnprotectData()
 
-    def retrieve_key(keypath):
+    def retrieve_keys():
         if AES is None:
-            tkMessageBox.showerror(
-                "ADEPT Key",
-                "This script requires PyCrypto or OpenSSL which must be installed "
-                "separately.  Read the top-of-script comment for details.")
-            return False
+            raise ADEPTError("PyCrypto or OpenSSL must be installed")
         root = GetSystemDirectory().split('\\')[0] + '\\'
         serial = GetVolumeSerialNumber(root)
         vendor = cpuid0()
@@ -313,6 +313,7 @@ if sys.platform.startswith('win'):
         device = winreg.QueryValueEx(regkey, 'key')[0]
         keykey = CryptUnprotectData(device, entropy)
         userkey = None
+        keys = []
         try:
             plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH)
         except WindowsError:
@@ -334,50 +335,43 @@ if sys.platform.startswith('win'):
                 if ktype != 'privateLicenseKey':
                     continue
                 userkey = winreg.QueryValueEx(plkkey, 'value')[0]
-                break
-            if userkey is not None:
-                break
-        if userkey is None:
+                userkey = userkey.decode('base64')
+                aes = AES(keykey)
+                userkey = aes.decrypt(userkey)
+                userkey = userkey[26:-ord(userkey[-1])]
+                keys.append(userkey)
+        if len(keys) == 0:
             raise ADEPTError('Could not locate privateLicenseKey')
-        userkey = userkey.decode('base64')
-        aes = AES(keykey)
-        userkey = aes.decrypt(userkey)
-        userkey = userkey[26:-ord(userkey[-1])]
-        with open(keypath, 'wb') as f:
-            f.write(userkey)
-        return True
-
-elif sys.platform.startswith('darwin'):
+        return keys
+        
+
+elif isosx:
     import xml.etree.ElementTree as etree
-    import Carbon.File
-    import Carbon.Folder
-    import Carbon.Folders
-    import MacOS
+    import subprocess
 
-    ACTIVATION_PATH = 'Adobe/Digital Editions/activation.dat'
     NSMAP = {'adept': 'http://ns.adobe.com/adept',
              'enc': 'http://www.w3.org/2001/04/xmlenc#'}
 
-    def find_folder(domain, dtype):
-        try:
-            fsref = Carbon.Folder.FSFindFolder(domain, dtype, False)
-            return Carbon.File.pathname(fsref)
-        except MacOS.Error:
-            return None
-
-    def find_app_support_file(subpath):
-        dtype = Carbon.Folders.kApplicationSupportFolderType
-        for domain in Carbon.Folders.kUserDomain, Carbon.Folders.kLocalDomain:
-            path = find_folder(domain, dtype)
-            if path is None:
-                continue
-            path = os.path.join(path, subpath)
-            if os.path.isfile(path):
-                return path
+    def findActivationDat():
+        home = os.getenv('HOME')
+        cmdline = 'find "' + home + '/Library/Application Support/Adobe/Digital Editions" -name "activation.dat"'
+        cmdline = cmdline.encode(sys.getfilesystemencoding())
+        p2 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
+        out1, out2 = p2.communicate()
+        reslst = out1.split('\n')
+        cnt = len(reslst)
+        for j in xrange(cnt):
+            resline = reslst[j]
+            pp = resline.find('activation.dat')
+            if pp >= 0:
+                ActDatPath = resline
+                break
+        if os.path.exists(ActDatPath):
+            return ActDatPath
         return None
 
-    def retrieve_key(keypath):
-        actpath = find_app_support_file(ACTIVATION_PATH)
+    def retrieve_keys():
+        actpath = findActivationDat()
         if actpath is None:
             raise ADEPTError("Could not locate ADE activation")
         tree = etree.parse(actpath)
@@ -386,39 +380,18 @@ elif sys.platform.startswith('darwin'):
         userkey = tree.findtext(expr)
         userkey = userkey.decode('base64')
         userkey = userkey[26:]
-        with open(keypath, 'wb') as f:
-            f.write(userkey)
-        return True
-
-elif sys.platform.startswith('cygwin'):
-    def retrieve_key(keypath):
-        tkMessageBox.showerror(
-            "ADEPT Key",
-            "This script requires a Windows-native Python, and cannot be run "
-            "under Cygwin.  Please install a Windows-native Python and/or "
-            "check your file associations.")
-        return False
+        return [userkey]
 
 else:
-    def retrieve_key(keypath):
-        tkMessageBox.showerror(
-            "ADEPT Key",
-            "This script only supports Windows and Mac OS X.  For Linux "
-            "you should be able to run ADE and this script under Wine (with "
-            "an appropriate version of Windows Python installed).")
-        return False
-
-class ExceptionDialog(Tkinter.Frame):
-    def __init__(self, root, text):
-        Tkinter.Frame.__init__(self, root, border=5)
-        label = Tkinter.Label(self, text="Unexpected error:",
-                              anchor=Tkconstants.W, justify=Tkconstants.LEFT)
-        label.pack(fill=Tkconstants.X, expand=0)
-        self.text = Tkinter.Text(self)
-        self.text.pack(fill=Tkconstants.BOTH, expand=1)
-
-        self.text.insert(Tkconstants.END, text)
-
+    def retrieve_keys(keypath):
+        raise ADEPTError("This script only supports Windows and Mac OS X.")
+        return []
+        
+def retrieve_key(keypath):
+    keys = retrieve_keys()
+    with open(keypath, 'wb') as f:
+        f.write(keys[0])
+    return True
 
 def extractKeyfile(keypath):
     try:
@@ -440,10 +413,27 @@ def cli_main(argv=sys.argv):
 
 
 def main(argv=sys.argv):
+    import Tkinter
+    import Tkconstants
+    import tkMessageBox
+    import traceback
+
+    class ExceptionDialog(Tkinter.Frame):
+        def __init__(self, root, text):
+            Tkinter.Frame.__init__(self, root, border=5)
+            label = Tkinter.Label(self, text="Unexpected error:",
+                                  anchor=Tkconstants.W, justify=Tkconstants.LEFT)
+            label.pack(fill=Tkconstants.X, expand=0)
+            self.text = Tkinter.Text(self)
+            self.text.pack(fill=Tkconstants.BOTH, expand=1)
+    
+            self.text.insert(Tkconstants.END, text)
+
+
     root = Tkinter.Tk()
     root.withdraw()
     progname = os.path.basename(argv[0])
-    keypath = 'adeptkey.der'
+    keypath = os.path.abspath("adeptkey.der")
     success = False
     try:
         success = retrieve_key(keypath)
index e51b094ef89dc4c0522b0e7d2d5c4951812d696f..03858390b434858493ae0ab797b54d5f1e5b6954 100644 (file)
@@ -233,7 +233,7 @@ def GetVolumeSerialNumber():
 
 def GetUserHomeAppSupKindleDirParitionName():
     home = os.getenv('HOME')
-    dpath =  home + '/Library/Application Support/Kindle'
+    dpath =  home + '/Library'
     cmdline = '/sbin/mount'
     cmdline = cmdline.encode(sys.getfilesystemencoding())
     p = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
@@ -358,6 +358,10 @@ def isNewInstall():
     # soccer game fan anyone
     dpath = home + '/Library/Application Support/Kindle/storage/.pes2011'
     # print dpath, os.path.exists(dpath)
+    if os.path.exists(dpath):
+        return True
+    dpath = home + '/Library/Containers/com.amazon.Kindle/Data/Library/Application Support/Kindle/storage/.pes2011'
+    # print dpath, os.path.exists(dpath)
     if os.path.exists(dpath):
         return True
     return False
@@ -491,22 +495,21 @@ class CryptUnprotectDataV3(object):
 
 # Locate the .kindle-info files
 def getKindleInfoFiles(kInfoFiles):
-    # first search for current .kindle-info files
+    found = False
     home = os.getenv('HOME')
-    cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"'
+    # search for any .kinf2011 files in new location (Sep 2012)
+    cmdline = 'find "' + home + '/Library/Containers/com.amazon.Kindle/Data/Library/Application Support" -name ".kinf2011"'
     cmdline = cmdline.encode(sys.getfilesystemencoding())
     p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
     out1, out2 = p1.communicate()
     reslst = out1.split('\n')
-    kinfopath = 'NONE'
-    found = False
     for resline in reslst:
         if os.path.isfile(resline):
             kInfoFiles.append(resline)
-            print('Found K4Mac kindle-info file: ' + resline)
+            print('Found k4Mac kinf2011 file: ' + resline)
             found = True
-    # add any .rainier*-kinf files
-    cmdline = 'find "' + home + '/Library/Application Support" -name ".rainier*-kinf"'
+   # search for any .kinf2011 files
+    cmdline = 'find "' + home + '/Library/Application Support" -name ".kinf2011"'
     cmdline = cmdline.encode(sys.getfilesystemencoding())
     p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
     out1, out2 = p1.communicate()
@@ -514,18 +517,30 @@ def getKindleInfoFiles(kInfoFiles):
     for resline in reslst:
         if os.path.isfile(resline):
             kInfoFiles.append(resline)
-            print('Found k4Mac kinf file: ' + resline)
+            print('Found k4Mac kinf2011 file: ' + resline)
             found = True
-    # add any .kinf2011 files
-    cmdline = 'find "' + home + '/Library/Application Support" -name ".kinf2011"'
+    # search for any .kindle-info files
+    cmdline = 'find "' + home + '/Library/Application Support" -name ".kindle-info"'
     cmdline = cmdline.encode(sys.getfilesystemencoding())
     p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
     out1, out2 = p1.communicate()
     reslst = out1.split('\n')
+    kinfopath = 'NONE'
     for resline in reslst:
         if os.path.isfile(resline):
             kInfoFiles.append(resline)
-            print('Found k4Mac kinf2011 file: ' + resline)
+            print('Found K4Mac kindle-info file: ' + resline)
+            found = True
+    # search for any .rainier*-kinf files
+    cmdline = 'find "' + home + '/Library/Application Support" -name ".rainier*-kinf"'
+    cmdline = cmdline.encode(sys.getfilesystemencoding())
+    p1 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
+    out1, out2 = p1.communicate()
+    reslst = out1.split('\n')
+    for resline in reslst:
+        if os.path.isfile(resline):
+            kInfoFiles.append(resline)
+            print('Found k4Mac kinf file: ' + resline)
             found = True
     if not found:
         print('No k4Mac kindle-info/kinf/kinf2011 files have been found.')
index 1bd256234dfd4159fa15113db0efa2993ee80e5d..d491d7d8dd7e52c8fc98b1c3ad2b197e441521b9 100644 (file)
@@ -204,45 +204,62 @@ CryptUnprotectData = CryptUnprotectData()
 
 # Locate all of the kindle-info style files and return as list
 def getKindleInfoFiles(kInfoFiles):
-    regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
-    path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
-
     # some 64 bit machines do not have the proper registry key for some reason
     # or the pythonn interface to the 32 vs 64 bit registry is broken
+    path = ""
     if 'LOCALAPPDATA' in os.environ.keys():
         path = os.environ['LOCALAPPDATA']
-
-    print('searching for kinfoFiles in ' + path)
-    found = False
-
-    # first look for older kindle-info files
-    kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
-    if os.path.isfile(kinfopath):
-        found = True
-        print('Found K4PC kindle.info file: ' + kinfopath)
-        kInfoFiles.append(kinfopath)
-
-    # now look for newer (K4PC 1.5.0 and later rainier.2.1.1.kinf file
-
-    kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf'
-    if os.path.isfile(kinfopath):
-        found = True
-        print('Found K4PC 1.5.X kinf file: ' + kinfopath)
-        kInfoFiles.append(kinfopath)
-
-    # now look for even newer (K4PC 1.6.0 and later) rainier.2.1.1.kinf file
-    kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf'
-    if os.path.isfile(kinfopath):
-        found = True
-        print('Found K4PC 1.6.X kinf file: ' + kinfopath)
-        kInfoFiles.append(kinfopath)
-
-    # now look for even newer (K4PC 1.9.0 and later) .kinf2011 file
-    kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2011'
-    if os.path.isfile(kinfopath):
-        found = True
-        print('Found K4PC kinf2011 file: ' + kinfopath)
-        kInfoFiles.append(kinfopath)
+    else:
+        # User Shell Folders show take precedent over Shell Folders if present
+        try:
+            regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\")
+            path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
+            if not os.path.isdir(path):
+                path = ""
+                try:
+                    regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
+                    path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
+                    if not os.path.isdir(path):
+                        path = ""
+                except RegError:
+                    pass
+        except RegError:
+            pass
+
+    found = False    
+    if path == "":
+        print ('Could not find the folder in which to look for kinfoFiles.')
+    else:
+        print('searching for kinfoFiles in ' + path)
+    
+        # first look for older kindle-info files
+        kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
+        if os.path.isfile(kinfopath):
+            found = True
+            print('Found K4PC kindle.info file: ' + kinfopath)
+            kInfoFiles.append(kinfopath)
+    
+        # now look for newer (K4PC 1.5.0 and later rainier.2.1.1.kinf file
+    
+        kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf'
+        if os.path.isfile(kinfopath):
+            found = True
+            print('Found K4PC 1.5.X kinf file: ' + kinfopath)
+            kInfoFiles.append(kinfopath)
+    
+        # now look for even newer (K4PC 1.6.0 and later) rainier.2.1.1.kinf file
+        kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf'
+        if os.path.isfile(kinfopath):
+            found = True
+            print('Found K4PC 1.6.X kinf file: ' + kinfopath)
+            kInfoFiles.append(kinfopath)
+    
+        # now look for even newer (K4PC 1.9.0 and later) .kinf2011 file
+        kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2011'
+        if os.path.isfile(kinfopath):
+            found = True
+            print('Found K4PC kinf2011 file: ' + kinfopath)
+            kInfoFiles.append(kinfopath)
 
     if not found:
         print('No K4PC kindle.info/kinf/kinf2011 files have been found.')
diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/libalfcrypto.dylib b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/libalfcrypto.dylib
new file mode 100644 (file)
index 0000000..01c348c
Binary files /dev/null and b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/libalfcrypto.dylib differ
diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/libalfcrypto32.so b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/libalfcrypto32.so
new file mode 100644 (file)
index 0000000..9a5a442
Binary files /dev/null and b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/libalfcrypto32.so differ
diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/libalfcrypto64.so b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/libalfcrypto64.so
new file mode 100644 (file)
index 0000000..a08ac28
Binary files /dev/null and b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/libalfcrypto64.so differ
diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/pbkdf2.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/pbkdf2.py
deleted file mode 100644 (file)
index 65220a9..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-# A simple implementation of pbkdf2 using stock python modules. See RFC2898
-# for details. Basically, it derives a key from a password and salt.
-
-# Copyright 2004 Matt Johnston <matt @ ucc asn au>
-# Copyright 2009 Daniel Holth <dholth@fastmail.fm>
-# This code may be freely used and modified for any purpose.
-
-# Revision history
-# v0.1  October 2004    - Initial release
-# v0.2  8 March 2007    - Make usable with hashlib in Python 2.5 and use
-# v0.3  ""                 the correct digest_size rather than always 20
-# v0.4  Oct 2009        - Rescue from chandler svn, test and optimize.
-
-import sys
-import hmac
-from struct import pack
-try:
-    # only in python 2.5
-    import hashlib
-    sha = hashlib.sha1
-    md5 = hashlib.md5
-    sha256 = hashlib.sha256
-except ImportError: # pragma: NO COVERAGE
-    # fallback
-    import sha
-    import md5
-
-# this is what you want to call.
-def pbkdf2( password, salt, itercount, keylen, hashfn = sha ):
-    try:
-        # depending whether the hashfn is from hashlib or sha/md5
-        digest_size = hashfn().digest_size
-    except TypeError: # pragma: NO COVERAGE
-        digest_size = hashfn.digest_size
-    # l - number of output blocks to produce
-    l = keylen / digest_size
-    if keylen % digest_size != 0:
-        l += 1
-
-    h = hmac.new( password, None, hashfn )
-
-    T = ""
-    for i in range(1, l+1):
-        T += pbkdf2_F( h, salt, itercount, i )
-
-    return T[0: keylen]
-
-def xorstr( a, b ):
-    if len(a) != len(b):
-        raise ValueError("xorstr(): lengths differ")
-    return ''.join((chr(ord(x)^ord(y)) for x, y in zip(a, b)))
-
-def prf( h, data ):
-    hm = h.copy()
-    hm.update( data )
-    return hm.digest()
-
-# Helper as per the spec. h is a hmac which has been created seeded with the
-# password, it will be copy()ed and not modified.
-def pbkdf2_F( h, salt, itercount, blocknum ):
-    U = prf( h, salt + pack('>i',blocknum ) )
-    T = U
-
-    for i in range(2, itercount+1):
-        U = prf( h, U )
-        T = xorstr( T, U )
-
-    return T
index f1d8574d1bee52c60b39c5215703217336251e2b..4c65719afb5d94ee49ac19a46d5d71576f7a9dc7 100644 (file)
@@ -296,7 +296,7 @@ class TopazBook:
                 break
 
         if not bookKey:
-            raise TpzDRMError('Decryption Unsucessful; No valid pid found')
+            raise TpzDRMError("Topaz Book. No key found in " + str(len(pidlst)) + " keys tried. Please report this failure for help.")
 
         self.setBookKey(bookKey)
         self.createBookDirectory()
diff --git a/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/zipfilerugged.py b/DeDRM_Windows_Application/DeDRM_App/DeDRM_lib/lib/zipfilerugged.py
new file mode 100644 (file)
index 0000000..adf3c53
--- /dev/null
@@ -0,0 +1,1400 @@
+"""
+Read and write ZIP files.
+"""
+import struct, os, time, sys, shutil
+import binascii, cStringIO, stat
+import io
+import re
+
+try:
+    import zlib # We may need its compression method
+    crc32 = zlib.crc32
+except ImportError:
+    zlib = None
+    crc32 = binascii.crc32
+
+__all__ = ["BadZipfile", "error", "ZIP_STORED", "ZIP_DEFLATED", "is_zipfile",
+           "ZipInfo", "ZipFile", "PyZipFile", "LargeZipFile" ]
+
+class BadZipfile(Exception):
+    pass
+
+
+class LargeZipFile(Exception):
+    """
+    Raised when writing a zipfile, the zipfile requires ZIP64 extensions
+    and those extensions are disabled.
+    """
+
+error = BadZipfile      # The exception raised by this module
+
+ZIP64_LIMIT = (1 << 31) - 1
+ZIP_FILECOUNT_LIMIT = 1 << 16
+ZIP_MAX_COMMENT = (1 << 16) - 1
+
+# constants for Zip file compression methods
+ZIP_STORED = 0
+ZIP_DEFLATED = 8
+# Other ZIP compression methods not supported
+
+# Below are some formats and associated data for reading/writing headers using
+# the struct module.  The names and structures of headers/records are those used
+# in the PKWARE description of the ZIP file format:
+#     http://www.pkware.com/documents/casestudies/APPNOTE.TXT
+# (URL valid as of January 2008)
+
+# The "end of central directory" structure, magic number, size, and indices
+# (section V.I in the format document)
+structEndArchive = "<4s4H2LH"
+stringEndArchive = "PK\005\006"
+sizeEndCentDir = struct.calcsize(structEndArchive)
+
+_ECD_SIGNATURE = 0
+_ECD_DISK_NUMBER = 1
+_ECD_DISK_START = 2
+_ECD_ENTRIES_THIS_DISK = 3
+_ECD_ENTRIES_TOTAL = 4
+_ECD_SIZE = 5
+_ECD_OFFSET = 6
+_ECD_COMMENT_SIZE = 7
+# These last two indices are not part of the structure as defined in the
+# spec, but they are used internally by this module as a convenience
+_ECD_COMMENT = 8
+_ECD_LOCATION = 9
+
+# The "central directory" structure, magic number, size, and indices
+# of entries in the structure (section V.F in the format document)
+structCentralDir = "<4s4B4HL2L5H2L"
+stringCentralDir = "PK\001\002"
+sizeCentralDir = struct.calcsize(structCentralDir)
+
+# indexes of entries in the central directory structure
+_CD_SIGNATURE = 0
+_CD_CREATE_VERSION = 1
+_CD_CREATE_SYSTEM = 2
+_CD_EXTRACT_VERSION = 3
+_CD_EXTRACT_SYSTEM = 4
+_CD_FLAG_BITS = 5
+_CD_COMPRESS_TYPE = 6
+_CD_TIME = 7
+_CD_DATE = 8
+_CD_CRC = 9
+_CD_COMPRESSED_SIZE = 10
+_CD_UNCOMPRESSED_SIZE = 11
+_CD_FILENAME_LENGTH = 12
+_CD_EXTRA_FIELD_LENGTH = 13
+_CD_COMMENT_LENGTH = 14
+_CD_DISK_NUMBER_START = 15
+_CD_INTERNAL_FILE_ATTRIBUTES = 16
+_CD_EXTERNAL_FILE_ATTRIBUTES = 17
+_CD_LOCAL_HEADER_OFFSET = 18
+
+# The "local file header" structure, magic number, size, and indices
+# (section V.A in the format document)
+structFileHeader = "<4s2B4HL2L2H"
+stringFileHeader = "PK\003\004"
+sizeFileHeader = struct.calcsize(structFileHeader)
+
+_FH_SIGNATURE = 0
+_FH_EXTRACT_VERSION = 1
+_FH_EXTRACT_SYSTEM = 2
+_FH_GENERAL_PURPOSE_FLAG_BITS = 3
+_FH_COMPRESSION_METHOD = 4
+_FH_LAST_MOD_TIME = 5
+_FH_LAST_MOD_DATE = 6
+_FH_CRC = 7
+_FH_COMPRESSED_SIZE = 8
+_FH_UNCOMPRESSED_SIZE = 9
+_FH_FILENAME_LENGTH = 10
+_FH_EXTRA_FIELD_LENGTH = 11
+
+# The "Zip64 end of central directory locator" structure, magic number, and size
+structEndArchive64Locator = "<4sLQL"
+stringEndArchive64Locator = "PK\x06\x07"
+sizeEndCentDir64Locator = struct.calcsize(structEndArchive64Locator)
+
+# The "Zip64 end of central directory" record, magic number, size, and indices
+# (section V.G in the format document)
+structEndArchive64 = "<4sQ2H2L4Q"
+stringEndArchive64 = "PK\x06\x06"
+sizeEndCentDir64 = struct.calcsize(structEndArchive64)
+
+_CD64_SIGNATURE = 0
+_CD64_DIRECTORY_RECSIZE = 1
+_CD64_CREATE_VERSION = 2
+_CD64_EXTRACT_VERSION = 3
+_CD64_DISK_NUMBER = 4
+_CD64_DISK_NUMBER_START = 5
+_CD64_NUMBER_ENTRIES_THIS_DISK = 6
+_CD64_NUMBER_ENTRIES_TOTAL = 7
+_CD64_DIRECTORY_SIZE = 8
+_CD64_OFFSET_START_CENTDIR = 9
+
+def _check_zipfile(fp):
+    try:
+        if _EndRecData(fp):
+            return True         # file has correct magic number
+    except IOError:
+        pass
+    return False
+
+def is_zipfile(filename):
+    """Quickly see if a file is a ZIP file by checking the magic number.
+
+    The filename argument may be a file or file-like object too.
+    """
+    result = False
+    try:
+        if hasattr(filename, "read"):
+            result = _check_zipfile(fp=filename)
+        else:
+            with open(filename, "rb") as fp:
+                result = _check_zipfile(fp)
+    except IOError:
+        pass
+    return result
+
+def _EndRecData64(fpin, offset, endrec):
+    """
+    Read the ZIP64 end-of-archive records and use that to update endrec
+    """
+    fpin.seek(offset - sizeEndCentDir64Locator, 2)
+    data = fpin.read(sizeEndCentDir64Locator)
+    sig, diskno, reloff, disks = struct.unpack(structEndArchive64Locator, data)
+    if sig != stringEndArchive64Locator:
+        return endrec
+
+    if diskno != 0 or disks != 1:
+        raise BadZipfile("zipfiles that span multiple disks are not supported")
+
+    # Assume no 'zip64 extensible data'
+    fpin.seek(offset - sizeEndCentDir64Locator - sizeEndCentDir64, 2)
+    data = fpin.read(sizeEndCentDir64)
+    sig, sz, create_version, read_version, disk_num, disk_dir, \
+            dircount, dircount2, dirsize, diroffset = \
+            struct.unpack(structEndArchive64, data)
+    if sig != stringEndArchive64:
+        return endrec
+
+    # Update the original endrec using data from the ZIP64 record
+    endrec[_ECD_SIGNATURE] = sig
+    endrec[_ECD_DISK_NUMBER] = disk_num
+    endrec[_ECD_DISK_START] = disk_dir
+    endrec[_ECD_ENTRIES_THIS_DISK] = dircount
+    endrec[_ECD_ENTRIES_TOTAL] = dircount2
+    endrec[_ECD_SIZE] = dirsize
+    endrec[_ECD_OFFSET] = diroffset
+    return endrec
+
+
+def _EndRecData(fpin):
+    """Return data from the "End of Central Directory" record, or None.
+
+    The data is a list of the nine items in the ZIP "End of central dir"
+    record followed by a tenth item, the file seek offset of this record."""
+
+    # Determine file size
+    fpin.seek(0, 2)
+    filesize = fpin.tell()
+
+    # Check to see if this is ZIP file with no archive comment (the
+    # "end of central directory" structure should be the last item in the
+    # file if this is the case).
+    try:
+        fpin.seek(-sizeEndCentDir, 2)
+    except IOError:
+        return None
+    data = fpin.read()
+    if data[0:4] == stringEndArchive and data[-2:] == "\000\000":
+        # the signature is correct and there's no comment, unpack structure
+        endrec = struct.unpack(structEndArchive, data)
+        endrec=list(endrec)
+
+        # Append a blank comment and record start offset
+        endrec.append("")
+        endrec.append(filesize - sizeEndCentDir)
+
+        # Try to read the "Zip64 end of central directory" structure
+        return _EndRecData64(fpin, -sizeEndCentDir, endrec)
+
+    # Either this is not a ZIP file, or it is a ZIP file with an archive
+    # comment.  Search the end of the file for the "end of central directory"
+    # record signature. The comment is the last item in the ZIP file and may be
+    # up to 64K long.  It is assumed that the "end of central directory" magic
+    # number does not appear in the comment.
+    maxCommentStart = max(filesize - (1 << 16) - sizeEndCentDir, 0)
+    fpin.seek(maxCommentStart, 0)
+    data = fpin.read()
+    start = data.rfind(stringEndArchive)
+    if start >= 0:
+        # found the magic number; attempt to unpack and interpret
+        recData = data[start:start+sizeEndCentDir]
+        endrec = list(struct.unpack(structEndArchive, recData))
+        comment = data[start+sizeEndCentDir:]
+        # check that comment length is correct
+        if endrec[_ECD_COMMENT_SIZE] == len(comment):
+            # Append the archive comment and start offset
+            endrec.append(comment)
+            endrec.append(maxCommentStart + start)
+
+            # Try to read the "Zip64 end of central directory" structure
+            return _EndRecData64(fpin, maxCommentStart + start - filesize,
+                                 endrec)
+
+    # Unable to find a valid end of central directory structure
+    return
+
+
+class ZipInfo (object):
+    """Class with attributes describing each file in the ZIP archive."""
+
+    __slots__ = (
+            'orig_filename',
+            'filename',
+            'date_time',
+            'compress_type',
+            'comment',
+            'extra',
+            'create_system',
+            'create_version',
+            'extract_version',
+            'reserved',
+            'flag_bits',
+            'volume',
+            'internal_attr',
+            'external_attr',
+            'header_offset',
+            'CRC',
+            'compress_size',
+            'file_size',
+            '_raw_time',
+        )
+
+    def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)):
+        self.orig_filename = filename   # Original file name in archive
+
+        # Terminate the file name at the first null byte.  Null bytes in file
+        # names are used as tricks by viruses in archives.
+        null_byte = filename.find(chr(0))
+        if null_byte >= 0:
+            filename = filename[0:null_byte]
+        # This is used to ensure paths in generated ZIP files always use
+        # forward slashes as the directory separator, as required by the
+        # ZIP format specification.
+        if os.sep != "/" and os.sep in filename:
+            filename = filename.replace(os.sep, "/")
+
+        self.filename = filename        # Normalized file name
+        self.date_time = date_time      # year, month, day, hour, min, sec
+        # Standard values:
+        self.compress_type = ZIP_STORED # Type of compression for the file
+        self.comment = ""               # Comment for each file
+        self.extra = ""                 # ZIP extra data
+        if sys.platform == 'win32':
+            self.create_system = 0          # System which created ZIP archive
+        else:
+            # Assume everything else is unix-y
+            self.create_system = 3          # System which created ZIP archive
+        self.create_version = 20        # Version which created ZIP archive
+        self.extract_version = 20       # Version needed to extract archive
+        self.reserved = 0               # Must be zero
+        self.flag_bits = 0              # ZIP flag bits
+        self.volume = 0                 # Volume number of file header
+        self.internal_attr = 0          # Internal attributes
+        self.external_attr = 0          # External file attributes
+        # Other attributes are set by class ZipFile:
+        # header_offset         Byte offset to the file header
+        # CRC                   CRC-32 of the uncompressed file
+        # compress_size         Size of the compressed file
+        # file_size             Size of the uncompressed file
+
+    def FileHeader(self):
+        """Return the per-file header as a string."""
+        dt = self.date_time
+        dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2]
+        dostime = dt[3] << 11 | dt[4] << 5 | (dt[5] // 2)
+        if self.flag_bits & 0x08:
+            # Set these to zero because we write them after the file data
+            CRC = compress_size = file_size = 0
+        else:
+            CRC = self.CRC
+            compress_size = self.compress_size
+            file_size = self.file_size
+
+        extra = self.extra
+
+        if file_size > ZIP64_LIMIT or compress_size > ZIP64_LIMIT:
+            # File is larger than what fits into a 4 byte integer,
+            # fall back to the ZIP64 extension
+            fmt = '<HHQQ'
+            extra = extra + struct.pack(fmt,
+                    1, struct.calcsize(fmt)-4, file_size, compress_size)
+            file_size = 0xffffffff
+            compress_size = 0xffffffff
+            self.extract_version = max(45, self.extract_version)
+            self.create_version = max(45, self.extract_version)
+
+        filename, flag_bits = self._encodeFilenameFlags()
+        header = struct.pack(structFileHeader, stringFileHeader,
+                 self.extract_version, self.reserved, flag_bits,
+                 self.compress_type, dostime, dosdate, CRC,
+                 compress_size, file_size,
+                 len(filename), len(extra))
+        return header + filename + extra
+
+    def _encodeFilenameFlags(self):
+        if isinstance(self.filename, unicode):
+            try:
+                return self.filename.encode('ascii'), self.flag_bits
+            except UnicodeEncodeError:
+                return self.filename.encode('utf-8'), self.flag_bits | 0x800
+        else:
+            return self.filename, self.flag_bits
+
+    def _decodeFilename(self):
+        if self.flag_bits & 0x800:
+            try:
+                print "decoding filename",self.filename
+                return self.filename.decode('utf-8')
+            except:
+                return self.filename
+        else:
+            return self.filename
+
+    def _decodeExtra(self):
+        # Try to decode the extra field.
+        extra = self.extra
+        unpack = struct.unpack
+        while extra:
+            tp, ln = unpack('<HH', extra[:4])
+            if tp == 1:
+                if ln >= 24:
+                    counts = unpack('<QQQ', extra[4:28])
+                elif ln == 16:
+                    counts = unpack('<QQ', extra[4:20])
+                elif ln == 8:
+                    counts = unpack('<Q', extra[4:12])
+                elif ln == 0:
+                    counts = ()
+                else:
+                    raise RuntimeError, "Corrupt extra field %s"%(ln,)
+
+                idx = 0
+
+                # ZIP64 extension (large files and/or large archives)
+                if self.file_size in (0xffffffffffffffffL, 0xffffffffL):
+                    self.file_size = counts[idx]
+                    idx += 1
+
+                if self.compress_size == 0xFFFFFFFFL:
+                    self.compress_size = counts[idx]
+                    idx += 1
+
+                if self.header_offset == 0xffffffffL:
+                    old = self.header_offset
+                    self.header_offset = counts[idx]
+                    idx+=1
+
+            extra = extra[ln+4:]
+
+
+class _ZipDecrypter:
+    """Class to handle decryption of files stored within a ZIP archive.
+
+    ZIP supports a password-based form of encryption. Even though known
+    plaintext attacks have been found against it, it is still useful
+    to be able to get data out of such a file.
+
+    Usage:
+        zd = _ZipDecrypter(mypwd)
+        plain_char = zd(cypher_char)
+        plain_text = map(zd, cypher_text)
+    """
+
+    def _GenerateCRCTable():
+        """Generate a CRC-32 table.
+
+        ZIP encryption uses the CRC32 one-byte primitive for scrambling some
+        internal keys. We noticed that a direct implementation is faster than
+        relying on binascii.crc32().
+        """
+        poly = 0xedb88320
+        table = [0] * 256
+        for i in range(256):
+            crc = i
+            for j in range(8):
+                if crc & 1:
+                    crc = ((crc >> 1) & 0x7FFFFFFF) ^ poly
+                else:
+                    crc = ((crc >> 1) & 0x7FFFFFFF)
+            table[i] = crc
+        return table
+    crctable = _GenerateCRCTable()
+
+    def _crc32(self, ch, crc):
+        """Compute the CRC32 primitive on one byte."""
+        return ((crc >> 8) & 0xffffff) ^ self.crctable[(crc ^ ord(ch)) & 0xff]
+
+    def __init__(self, pwd):
+        self.key0 = 305419896
+        self.key1 = 591751049
+        self.key2 = 878082192
+        for p in pwd:
+            self._UpdateKeys(p)
+
+    def _UpdateKeys(self, c):
+        self.key0 = self._crc32(c, self.key0)
+        self.key1 = (self.key1 + (self.key0 & 255)) & 4294967295
+        self.key1 = (self.key1 * 134775813 + 1) & 4294967295
+        self.key2 = self._crc32(chr((self.key1 >> 24) & 255), self.key2)
+
+    def __call__(self, c):
+        """Decrypt a single character."""
+        c = ord(c)
+        k = self.key2 | 2
+        c = c ^ (((k * (k^1)) >> 8) & 255)
+        c = chr(c)
+        self._UpdateKeys(c)
+        return c
+
+class ZipExtFile(io.BufferedIOBase):
+    """File-like object for reading an archive member.
+       Is returned by ZipFile.open().
+    """
+
+    # Max size supported by decompressor.
+    MAX_N = 1 << 31 - 1
+
+    # Read from compressed files in 4k blocks.
+    MIN_READ_SIZE = 4096
+
+    # Search for universal newlines or line chunks.
+    PATTERN = re.compile(r'^(?P<chunk>[^\r\n]+)|(?P<newline>\n|\r\n?)')
+
+    def __init__(self, fileobj, mode, zipinfo, decrypter=None):
+        self._fileobj = fileobj
+        self._decrypter = decrypter
+
+        self._compress_type = zipinfo.compress_type
+        self._compress_size = zipinfo.compress_size
+        self._compress_left = zipinfo.compress_size
+
+        if self._compress_type == ZIP_DEFLATED:
+            self._decompressor = zlib.decompressobj(-15)
+        self._unconsumed = ''
+
+        self._readbuffer = ''
+        self._offset = 0
+
+        self._universal = 'U' in mode
+        self.newlines = None
+
+        # Adjust read size for encrypted files since the first 12 bytes
+        # are for the encryption/password information.
+        if self._decrypter is not None:
+            self._compress_left -= 12
+
+        self.mode = mode
+        self.name = zipinfo.filename
+
+    def readline(self, limit=-1):
+        """Read and return a line from the stream.
+
+        If limit is specified, at most limit bytes will be read.
+        """
+
+        if not self._universal and limit < 0:
+            # Shortcut common case - newline found in buffer.
+            i = self._readbuffer.find('\n', self._offset) + 1
+            if i > 0:
+                line = self._readbuffer[self._offset: i]
+                self._offset = i
+                return line
+
+        if not self._universal:
+            return io.BufferedIOBase.readline(self, limit)
+
+        line = ''
+        while limit < 0 or len(line) < limit:
+            readahead = self.peek(2)
+            if readahead == '':
+                return line
+
+            #
+            # Search for universal newlines or line chunks.
+            #
+            # The pattern returns either a line chunk or a newline, but not
+            # both. Combined with peek(2), we are assured that the sequence
+            # '\r\n' is always retrieved completely and never split into
+            # separate newlines - '\r', '\n' due to coincidental readaheads.
+            #
+            match = self.PATTERN.search(readahead)
+            newline = match.group('newline')
+            if newline is not None:
+                if self.newlines is None:
+                    self.newlines = []
+                if newline not in self.newlines:
+                    self.newlines.append(newline)
+                self._offset += len(newline)
+                return line + '\n'
+
+            chunk = match.group('chunk')
+            if limit >= 0:
+                chunk = chunk[: limit - len(line)]
+
+            self._offset += len(chunk)
+            line += chunk
+
+        return line
+
+    def peek(self, n=1):
+        """Returns buffered bytes without advancing the position."""
+        if n > len(self._readbuffer) - self._offset:
+            chunk = self.read(n)
+            self._offset -= len(chunk)
+
+        # Return up to 512 bytes to reduce allocation overhead for tight loops.
+        return self._readbuffer[self._offset: self._offset + 512]
+
+    def readable(self):
+        return True
+
+    def read(self, n=-1):
+        """Read and return up to n bytes.
+        If the argument is omitted, None, or negative, data is read and returned until EOF is reached..
+        """
+
+        buf = ''
+        while n < 0 or n is None or n > len(buf):
+            data = self.read1(n)
+            if len(data) == 0:
+                return buf
+
+            buf += data
+
+        return buf
+
+    def read1(self, n):
+        """Read up to n bytes with at most one read() system call."""
+
+        # Simplify algorithm (branching) by transforming negative n to large n.
+        if n < 0 or n is None:
+            n = self.MAX_N
+
+        # Bytes available in read buffer.
+        len_readbuffer = len(self._readbuffer) - self._offset
+
+        # Read from file.
+        if self._compress_left > 0 and n > len_readbuffer + len(self._unconsumed):
+            nbytes = n - len_readbuffer - len(self._unconsumed)
+            nbytes = max(nbytes, self.MIN_READ_SIZE)
+            nbytes = min(nbytes, self._compress_left)
+
+            data = self._fileobj.read(nbytes)
+            self._compress_left -= len(data)
+
+            if data and self._decrypter is not None:
+                data = ''.join(map(self._decrypter, data))
+
+            if self._compress_type == ZIP_STORED:
+                self._readbuffer = self._readbuffer[self._offset:] + data
+                self._offset = 0
+            else:
+                # Prepare deflated bytes for decompression.
+                self._unconsumed += data
+
+        # Handle unconsumed data.
+        if (len(self._unconsumed) > 0 and n > len_readbuffer and
+            self._compress_type == ZIP_DEFLATED):
+            data = self._decompressor.decompress(
+                self._unconsumed,
+                max(n - len_readbuffer, self.MIN_READ_SIZE)
+            )
+
+            self._unconsumed = self._decompressor.unconsumed_tail
+            if len(self._unconsumed) == 0 and self._compress_left == 0:
+                data += self._decompressor.flush()
+
+            self._readbuffer = self._readbuffer[self._offset:] + data
+            self._offset = 0
+
+        # Read from buffer.
+        data = self._readbuffer[self._offset: self._offset + n]
+        self._offset += len(data)
+        return data
+
+
+
+class ZipFile:
+    """ Class with methods to open, read, write, close, list zip files.
+
+    z = ZipFile(file, mode="r", compression=ZIP_STORED, allowZip64=False)
+
+    file: Either the path to the file, or a file-like object.
+          If it is a path, the file will be opened and closed by ZipFile.
+    mode: The mode can be either read "r", write "w" or append "a".
+    compression: ZIP_STORED (no compression) or ZIP_DEFLATED (requires zlib).
+    allowZip64: if True ZipFile will create files with ZIP64 extensions when
+                needed, otherwise it will raise an exception when this would
+                be necessary.
+
+    """
+
+    fp = None                   # Set here since __del__ checks it
+
+    def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=False):
+        """Open the ZIP file with mode read "r", write "w" or append "a"."""
+        if mode not in ("r", "w", "a"):
+            raise RuntimeError('ZipFile() requires mode "r", "w", or "a"')
+
+        if compression == ZIP_STORED:
+            pass
+        elif compression == ZIP_DEFLATED:
+            if not zlib:
+                raise RuntimeError,\
+                      "Compression requires the (missing) zlib module"
+        else:
+            raise RuntimeError, "That compression method is not supported"
+
+        self._allowZip64 = allowZip64
+        self._didModify = False
+        self.debug = 0  # Level of printing: 0 through 3
+        self.NameToInfo = {}    # Find file info given name
+        self.filelist = []      # List of ZipInfo instances for archive
+        self.compression = compression  # Method of compression
+        self.mode = key = mode.replace('b', '')[0]
+        self.pwd = None
+        self.comment = ''
+
+        # Check if we were passed a file-like object
+        if isinstance(file, basestring):
+            self._filePassed = 0
+            self.filename = file
+            modeDict = {'r' : 'rb', 'w': 'wb', 'a' : 'r+b'}
+            try:
+                self.fp = open(file, modeDict[mode])
+            except IOError:
+                if mode == 'a':
+                    mode = key = 'w'
+                    self.fp = open(file, modeDict[mode])
+                else:
+                    raise
+        else:
+            self._filePassed = 1
+            self.fp = file
+            self.filename = getattr(file, 'name', None)
+
+        if key == 'r':
+            self._GetContents()
+        elif key == 'w':
+            pass
+        elif key == 'a':
+            try:                        # See if file is a zip file
+                self._RealGetContents()
+                # seek to start of directory and overwrite
+                self.fp.seek(self.start_dir, 0)
+            except BadZipfile:          # file is not a zip file, just append
+                self.fp.seek(0, 2)
+        else:
+            if not self._filePassed:
+                self.fp.close()
+                self.fp = None
+            raise RuntimeError, 'Mode must be "r", "w" or "a"'
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, traceback):
+        self.close()
+
+    def _GetContents(self):
+        """Read the directory, making sure we close the file if the format
+        is bad."""
+        try:
+            self._RealGetContents()
+        except BadZipfile:
+            if not self._filePassed:
+                self.fp.close()
+                self.fp = None
+            raise
+
+    def _RealGetContents(self):
+        """Read in the table of contents for the ZIP file."""
+        fp = self.fp
+        endrec = _EndRecData(fp)
+        if not endrec:
+            raise BadZipfile, "File is not a zip file"
+        if self.debug > 1:
+            print endrec
+        size_cd = endrec[_ECD_SIZE]             # bytes in central directory
+        offset_cd = endrec[_ECD_OFFSET]         # offset of central directory
+        self.comment = endrec[_ECD_COMMENT]     # archive comment
+
+        # "concat" is zero, unless zip was concatenated to another file
+        concat = endrec[_ECD_LOCATION] - size_cd - offset_cd
+        if endrec[_ECD_SIGNATURE] == stringEndArchive64:
+            # If Zip64 extension structures are present, account for them
+            concat -= (sizeEndCentDir64 + sizeEndCentDir64Locator)
+
+        if self.debug > 2:
+            inferred = concat + offset_cd
+            print "given, inferred, offset", offset_cd, inferred, concat
+        # self.start_dir:  Position of start of central directory
+        self.start_dir = offset_cd + concat
+        fp.seek(self.start_dir, 0)
+        data = fp.read(size_cd)
+        fp = cStringIO.StringIO(data)
+        total = 0
+        while total < size_cd:
+            centdir = fp.read(sizeCentralDir)
+            if centdir[0:4] != stringCentralDir:
+                raise BadZipfile, "Bad magic number for central directory"
+            centdir = struct.unpack(structCentralDir, centdir)
+            if self.debug > 2:
+                print centdir
+            filename = fp.read(centdir[_CD_FILENAME_LENGTH])
+            # Create ZipInfo instance to store file information
+            x = ZipInfo(filename)
+            x.extra = fp.read(centdir[_CD_EXTRA_FIELD_LENGTH])
+            x.comment = fp.read(centdir[_CD_COMMENT_LENGTH])
+            x.header_offset = centdir[_CD_LOCAL_HEADER_OFFSET]
+            (x.create_version, x.create_system, x.extract_version, x.reserved,
+                x.flag_bits, x.compress_type, t, d,
+                x.CRC, x.compress_size, x.file_size) = centdir[1:12]
+            x.volume, x.internal_attr, x.external_attr = centdir[15:18]
+            # Convert date/time code to (year, month, day, hour, min, sec)
+            x._raw_time = t
+            x.date_time = ( (d>>9)+1980, (d>>5)&0xF, d&0x1F,
+                                     t>>11, (t>>5)&0x3F, (t&0x1F) * 2 )
+
+            x._decodeExtra()
+            x.header_offset = x.header_offset + concat
+            x.filename = x._decodeFilename()
+            self.filelist.append(x)
+            self.NameToInfo[x.filename] = x
+
+            # update total bytes read from central directory
+            total = (total + sizeCentralDir + centdir[_CD_FILENAME_LENGTH]
+                     + centdir[_CD_EXTRA_FIELD_LENGTH]
+                     + centdir[_CD_COMMENT_LENGTH])
+
+            if self.debug > 2:
+                print "total", total
+
+
+    def namelist(self):
+        """Return a list of file names in the archive."""
+        l = []
+        for data in self.filelist:
+            l.append(data.filename)
+        return l
+
+    def infolist(self):
+        """Return a list of class ZipInfo instances for files in the
+        archive."""
+        return self.filelist
+
+    def printdir(self):
+        """Print a table of contents for the zip file."""
+        print "%-46s %19s %12s" % ("File Name", "Modified    ", "Size")
+        for zinfo in self.filelist:
+            date = "%d-%02d-%02d %02d:%02d:%02d" % zinfo.date_time[:6]
+            print "%-46s %s %12d" % (zinfo.filename, date, zinfo.file_size)
+
+    def testzip(self):
+        """Read all the files and check the CRC."""
+        chunk_size = 2 ** 20
+        for zinfo in self.filelist:
+            try:
+                # Read by chunks, to avoid an OverflowError or a
+                # MemoryError with very large embedded files.
+                f = self.open(zinfo.filename, "r")
+                while f.read(chunk_size):     # Check CRC-32
+                    pass
+            except BadZipfile:
+                return zinfo.filename
+
+    def getinfo(self, name):
+        """Return the instance of ZipInfo given 'name'."""
+        info = self.NameToInfo.get(name)
+        if info is None:
+            raise KeyError(
+                'There is no item named %r in the archive' % name)
+
+        return info
+
+    def setpassword(self, pwd):
+        """Set default password for encrypted files."""
+        self.pwd = pwd
+
+    def read(self, name, pwd=None):
+        """Return file bytes (as a string) for name."""
+        return self.open(name, "r", pwd).read()
+
+    def open(self, name, mode="r", pwd=None):
+        """Return file-like object for 'name'."""
+        if mode not in ("r", "U", "rU"):
+            raise RuntimeError, 'open() requires mode "r", "U", or "rU"'
+        if not self.fp:
+            raise RuntimeError, \
+                  "Attempt to read ZIP archive that was already closed"
+
+        # Only open a new file for instances where we were not
+        # given a file object in the constructor
+        if self._filePassed:
+            zef_file = self.fp
+        else:
+            zef_file = open(self.filename, 'rb')
+
+        # Make sure we have an info object
+        if isinstance(name, ZipInfo):
+            # 'name' is already an info object
+            zinfo = name
+        else:
+            # Get info object for name
+            zinfo = self.getinfo(name)
+
+        zef_file.seek(zinfo.header_offset, 0)
+
+        # Skip the file header:
+        fheader = zef_file.read(sizeFileHeader)
+        if fheader[0:4] != stringFileHeader:
+            raise BadZipfile, "Bad magic number for file header"
+
+        fheader = struct.unpack(structFileHeader, fheader)
+        fname = zef_file.read(fheader[_FH_FILENAME_LENGTH])
+        if fheader[_FH_EXTRA_FIELD_LENGTH]:
+            zef_file.read(fheader[_FH_EXTRA_FIELD_LENGTH])
+
+        if fname != zinfo.orig_filename:
+            raise BadZipfile, \
+                      'File name in directory "%s" and header "%s" differ.' % (
+                          zinfo.orig_filename, fname)
+
+        # check for encrypted flag & handle password
+        is_encrypted = zinfo.flag_bits & 0x1
+        zd = None
+        if is_encrypted:
+            if not pwd:
+                pwd = self.pwd
+            if not pwd:
+                raise RuntimeError, "File %s is encrypted, " \
+                      "password required for extraction" % name
+
+            zd = _ZipDecrypter(pwd)
+            # The first 12 bytes in the cypher stream is an encryption header
+            #  used to strengthen the algorithm. The first 11 bytes are
+            #  completely random, while the 12th contains the MSB of the CRC,
+            #  or the MSB of the file time depending on the header type
+            #  and is used to check the correctness of the password.
+            bytes = zef_file.read(12)
+            h = map(zd, bytes[0:12])
+            if zinfo.flag_bits & 0x8:
+                # compare against the file type from extended local headers
+                check_byte = (zinfo._raw_time >> 8) & 0xff
+            else:
+                # compare against the CRC otherwise
+                check_byte = (zinfo.CRC >> 24) & 0xff
+            if ord(h[11]) != check_byte:
+                raise RuntimeError("Bad password for file", name)
+
+        return  ZipExtFile(zef_file, mode, zinfo, zd)
+
+    def extract(self, member, path=None, pwd=None):
+        """Extract a member from the archive to the current working directory,
+           using its full name. Its file information is extracted as accurately
+           as possible. `member' may be a filename or a ZipInfo object. You can
+           specify a different directory using `path'.
+        """
+        if not isinstance(member, ZipInfo):
+            member = self.getinfo(member)
+
+        if path is None:
+            path = os.getcwd()
+
+        return self._extract_member(member, path, pwd)
+
+    def extractall(self, path=None, members=None, pwd=None):
+        """Extract all members from the archive to the current working
+           directory. `path' specifies a different directory to extract to.
+           `members' is optional and must be a subset of the list returned
+           by namelist().
+        """
+        if members is None:
+            members = self.namelist()
+
+        for zipinfo in members:
+            self.extract(zipinfo, path, pwd)
+
+    def _extract_member(self, member, targetpath, pwd):
+        """Extract the ZipInfo object 'member' to a physical
+           file on the path targetpath.
+        """
+        # build the destination pathname, replacing
+        # forward slashes to platform specific separators.
+        # Strip trailing path separator, unless it represents the root.
+        if (targetpath[-1:] in (os.path.sep, os.path.altsep)
+            and len(os.path.splitdrive(targetpath)[1]) > 1):
+            targetpath = targetpath[:-1]
+
+        # don't include leading "/" from file name if present
+        if member.filename[0] == '/':
+            targetpath = os.path.join(targetpath, member.filename[1:])
+        else:
+            targetpath = os.path.join(targetpath, member.filename)
+
+        targetpath = os.path.normpath(targetpath)
+
+        # Create all upper directories if necessary.
+        upperdirs = os.path.dirname(targetpath)
+        if upperdirs and not os.path.exists(upperdirs):
+            os.makedirs(upperdirs)
+
+        if member.filename[-1] == '/':
+            if not os.path.isdir(targetpath):
+                os.mkdir(targetpath)
+            return targetpath
+
+        source = self.open(member, pwd=pwd)
+        target = file(targetpath, "wb")
+        shutil.copyfileobj(source, target)
+        source.close()
+        target.close()
+
+        return targetpath
+
+    def _writecheck(self, zinfo):
+        """Check for errors before writing a file to the archive."""
+        if zinfo.filename in self.NameToInfo:
+            if self.debug:      # Warning for duplicate names
+                print "Duplicate name:", zinfo.filename
+        if self.mode not in ("w", "a"):
+            raise RuntimeError, 'write() requires mode "w" or "a"'
+        if not self.fp:
+            raise RuntimeError, \
+                  "Attempt to write ZIP archive that was already closed"
+        if zinfo.compress_type == ZIP_DEFLATED and not zlib:
+            raise RuntimeError, \
+                  "Compression requires the (missing) zlib module"
+        if zinfo.compress_type not in (ZIP_STORED, ZIP_DEFLATED):
+            raise RuntimeError, \
+                  "That compression method is not supported"
+        if zinfo.file_size > ZIP64_LIMIT:
+            if not self._allowZip64:
+                raise LargeZipFile("Filesize would require ZIP64 extensions")
+        if zinfo.header_offset > ZIP64_LIMIT:
+            if not self._allowZip64:
+                raise LargeZipFile("Zipfile size would require ZIP64 extensions")
+
+    def write(self, filename, arcname=None, compress_type=None):
+        """Put the bytes from filename into the archive under the name
+        arcname."""
+        if not self.fp:
+            raise RuntimeError(
+                  "Attempt to write to ZIP archive that was already closed")
+
+        st = os.stat(filename)
+        isdir = stat.S_ISDIR(st.st_mode)
+        mtime = time.localtime(st.st_mtime)
+        date_time = mtime[0:6]
+        # Create ZipInfo instance to store file information
+        if arcname is None:
+            arcname = filename
+        arcname = os.path.normpath(os.path.splitdrive(arcname)[1])
+        while arcname[0] in (os.sep, os.altsep):
+            arcname = arcname[1:]
+        if isdir:
+            arcname += '/'
+        zinfo = ZipInfo(arcname, date_time)
+        zinfo.external_attr = (st[0] & 0xFFFF) << 16L      # Unix attributes
+        if compress_type is None:
+            zinfo.compress_type = self.compression
+        else:
+            zinfo.compress_type = compress_type
+
+        zinfo.file_size = st.st_size
+        zinfo.flag_bits = 0x00
+        zinfo.header_offset = self.fp.tell()    # Start of header bytes
+
+        self._writecheck(zinfo)
+        self._didModify = True
+
+        if isdir:
+            zinfo.file_size = 0
+            zinfo.compress_size = 0
+            zinfo.CRC = 0
+            self.filelist.append(zinfo)
+            self.NameToInfo[zinfo.filename] = zinfo
+            self.fp.write(zinfo.FileHeader())
+            return
+
+        with open(filename, "rb") as fp:
+            # Must overwrite CRC and sizes with correct data later
+            zinfo.CRC = CRC = 0
+            zinfo.compress_size = compress_size = 0
+            zinfo.file_size = file_size = 0
+            self.fp.write(zinfo.FileHeader())
+            if zinfo.compress_type == ZIP_DEFLATED:
+                cmpr = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION,
+                     zlib.DEFLATED, -15)
+            else:
+                cmpr = None
+            while 1:
+                buf = fp.read(1024 * 8)
+                if not buf:
+                    break
+                file_size = file_size + len(buf)
+                CRC = crc32(buf, CRC) & 0xffffffff
+                if cmpr:
+                    buf = cmpr.compress(buf)
+                    compress_size = compress_size + len(buf)
+                self.fp.write(buf)
+        if cmpr:
+            buf = cmpr.flush()
+            compress_size = compress_size + len(buf)
+            self.fp.write(buf)
+            zinfo.compress_size = compress_size
+        else:
+            zinfo.compress_size = file_size
+        zinfo.CRC = CRC
+        zinfo.file_size = file_size
+        # Seek backwards and write CRC and file sizes
+        position = self.fp.tell()       # Preserve current position in file
+        self.fp.seek(zinfo.header_offset + 14, 0)
+        self.fp.write(struct.pack("<LLL", zinfo.CRC, zinfo.compress_size,
+              zinfo.file_size))
+        self.fp.seek(position, 0)
+        self.filelist.append(zinfo)
+        self.NameToInfo[zinfo.filename] = zinfo
+
+    def writestr(self, zinfo_or_arcname, bytes, compress_type=None):
+        """Write a file into the archive.  The contents is the string
+        'bytes'.  'zinfo_or_arcname' is either a ZipInfo instance or
+        the name of the file in the archive."""
+        if not isinstance(zinfo_or_arcname, ZipInfo):
+            zinfo = ZipInfo(filename=zinfo_or_arcname,
+                            date_time=time.localtime(time.time())[:6])
+
+            zinfo.compress_type = self.compression
+            zinfo.external_attr = 0600 << 16
+        else:
+            zinfo = zinfo_or_arcname
+
+        if not self.fp:
+            raise RuntimeError(
+                  "Attempt to write to ZIP archive that was already closed")
+
+        if compress_type is not None:
+            zinfo.compress_type = compress_type
+
+        zinfo.file_size = len(bytes)            # Uncompressed size
+        zinfo.header_offset = self.fp.tell()    # Start of header bytes
+        self._writecheck(zinfo)
+        self._didModify = True
+        zinfo.CRC = crc32(bytes) & 0xffffffff       # CRC-32 checksum
+        if zinfo.compress_type == ZIP_DEFLATED:
+            co = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION,
+                 zlib.DEFLATED, -15)
+            bytes = co.compress(bytes) + co.flush()
+            zinfo.compress_size = len(bytes)    # Compressed size
+        else:
+            zinfo.compress_size = zinfo.file_size
+        zinfo.header_offset = self.fp.tell()    # Start of header bytes
+        self.fp.write(zinfo.FileHeader())
+        self.fp.write(bytes)
+        self.fp.flush()
+        if zinfo.flag_bits & 0x08:
+            # Write CRC and file sizes after the file data
+            self.fp.write(struct.pack("<LLL", zinfo.CRC, zinfo.compress_size,
+                  zinfo.file_size))
+        self.filelist.append(zinfo)
+        self.NameToInfo[zinfo.filename] = zinfo
+
+    def __del__(self):
+        """Call the "close()" method in case the user forgot."""
+        self.close()
+
+    def close(self):
+        """Close the file, and for mode "w" and "a" write the ending
+        records."""
+        if self.fp is None:
+            return
+
+        if self.mode in ("w", "a") and self._didModify: # write ending records
+            count = 0
+            pos1 = self.fp.tell()
+            for zinfo in self.filelist:         # write central directory
+                count = count + 1
+                dt = zinfo.date_time
+                dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2]
+                dostime = dt[3] << 11 | dt[4] << 5 | (dt[5] // 2)
+                extra = []
+                if zinfo.file_size > ZIP64_LIMIT \
+                        or zinfo.compress_size > ZIP64_LIMIT:
+                    extra.append(zinfo.file_size)
+                    extra.append(zinfo.compress_size)
+                    file_size = 0xffffffff
+                    compress_size = 0xffffffff
+                else:
+                    file_size = zinfo.file_size
+                    compress_size = zinfo.compress_size
+
+                if zinfo.header_offset > ZIP64_LIMIT:
+                    extra.append(zinfo.header_offset)
+                    header_offset = 0xffffffffL
+                else:
+                    header_offset = zinfo.header_offset
+
+                extra_data = zinfo.extra
+                if extra:
+                    # Append a ZIP64 field to the extra's
+                    extra_data = struct.pack(
+                            '<HH' + 'Q'*len(extra),
+                            1, 8*len(extra), *extra) + extra_data
+
+                    extract_version = max(45, zinfo.extract_version)
+                    create_version = max(45, zinfo.create_version)
+                else:
+                    extract_version = zinfo.extract_version
+                    create_version = zinfo.create_version
+
+                try:
+                    filename, flag_bits = zinfo._encodeFilenameFlags()
+                    centdir = struct.pack(structCentralDir,
+                     stringCentralDir, create_version,
+                     zinfo.create_system, extract_version, zinfo.reserved,
+                     flag_bits, zinfo.compress_type, dostime, dosdate,
+                     zinfo.CRC, compress_size, file_size,
+                     len(filename), len(extra_data), len(zinfo.comment),
+                     0, zinfo.internal_attr, zinfo.external_attr,
+                     header_offset)
+                except DeprecationWarning:
+                    print >>sys.stderr, (structCentralDir,
+                     stringCentralDir, create_version,
+                     zinfo.create_system, extract_version, zinfo.reserved,
+                     zinfo.flag_bits, zinfo.compress_type, dostime, dosdate,
+                     zinfo.CRC, compress_size, file_size,
+                     len(zinfo.filename), len(extra_data), len(zinfo.comment),
+                     0, zinfo.internal_attr, zinfo.external_attr,
+                     header_offset)
+                    raise
+                self.fp.write(centdir)
+                self.fp.write(filename)
+                self.fp.write(extra_data)
+                self.fp.write(zinfo.comment)
+
+            pos2 = self.fp.tell()
+            # Write end-of-zip-archive record
+            centDirCount = count
+            centDirSize = pos2 - pos1
+            centDirOffset = pos1
+            if (centDirCount >= ZIP_FILECOUNT_LIMIT or
+                centDirOffset > ZIP64_LIMIT or
+                centDirSize > ZIP64_LIMIT):
+                # Need to write the ZIP64 end-of-archive records
+                zip64endrec = struct.pack(
+                        structEndArchive64, stringEndArchive64,
+                        44, 45, 45, 0, 0, centDirCount, centDirCount,
+                        centDirSize, centDirOffset)
+                self.fp.write(zip64endrec)
+
+                zip64locrec = struct.pack(
+                        structEndArchive64Locator,
+                        stringEndArchive64Locator, 0, pos2, 1)
+                self.fp.write(zip64locrec)
+                centDirCount = min(centDirCount, 0xFFFF)
+                centDirSize = min(centDirSize, 0xFFFFFFFF)
+                centDirOffset = min(centDirOffset, 0xFFFFFFFF)
+
+            # check for valid comment length
+            if len(self.comment) >= ZIP_MAX_COMMENT:
+                if self.debug > 0:
+                    msg = 'Archive comment is too long; truncating to %d bytes' \
+                          % ZIP_MAX_COMMENT
+                self.comment = self.comment[:ZIP_MAX_COMMENT]
+
+            endrec = struct.pack(structEndArchive, stringEndArchive,
+                                 0, 0, centDirCount, centDirCount,
+                                 centDirSize, centDirOffset, len(self.comment))
+            self.fp.write(endrec)
+            self.fp.write(self.comment)
+            self.fp.flush()
+
+        if not self._filePassed:
+            self.fp.close()
+        self.fp = None
+
+
+class PyZipFile(ZipFile):
+    """Class to create ZIP archives with Python library files and packages."""
+
+    def writepy(self, pathname, basename = ""):
+        """Add all files from "pathname" to the ZIP archive.
+
+        If pathname is a package directory, search the directory and
+        all package subdirectories recursively for all *.py and enter
+        the modules into the archive.  If pathname is a plain
+        directory, listdir *.py and enter all modules.  Else, pathname
+        must be a Python *.py file and the module will be put into the
+        archive.  Added modules are always module.pyo or module.pyc.
+        This method will compile the module.py into module.pyc if
+        necessary.
+        """
+        dir, name = os.path.split(pathname)
+        if os.path.isdir(pathname):
+            initname = os.path.join(pathname, "__init__.py")
+            if os.path.isfile(initname):
+                # This is a package directory, add it
+                if basename:
+                    basename = "%s/%s" % (basename, name)
+                else:
+                    basename = name
+                if self.debug:
+                    print "Adding package in", pathname, "as", basename
+                fname, arcname = self._get_codename(initname[0:-3], basename)
+                if self.debug:
+                    print "Adding", arcname
+                self.write(fname, arcname)
+                dirlist = os.listdir(pathname)
+                dirlist.remove("__init__.py")
+                # Add all *.py files and package subdirectories
+                for filename in dirlist:
+                    path = os.path.join(pathname, filename)
+                    root, ext = os.path.splitext(filename)
+                    if os.path.isdir(path):
+                        if os.path.isfile(os.path.join(path, "__init__.py")):
+                            # This is a package directory, add it
+                            self.writepy(path, basename)  # Recursive call
+                    elif ext == ".py":
+                        fname, arcname = self._get_codename(path[0:-3],
+                                         basename)
+                        if self.debug:
+                            print "Adding", arcname
+                        self.write(fname, arcname)
+            else:
+                # This is NOT a package directory, add its files at top level
+                if self.debug:
+                    print "Adding files from directory", pathname
+                for filename in os.listdir(pathname):
+                    path = os.path.join(pathname, filename)
+                    root, ext = os.path.splitext(filename)
+                    if ext == ".py":
+                        fname, arcname = self._get_codename(path[0:-3],
+                                         basename)
+                        if self.debug:
+                            print "Adding", arcname
+                        self.write(fname, arcname)
+        else:
+            if pathname[-3:] != ".py":
+                raise RuntimeError, \
+                      'Files added with writepy() must end with ".py"'
+            fname, arcname = self._get_codename(pathname[0:-3], basename)
+            if self.debug:
+                print "Adding file", arcname
+            self.write(fname, arcname)
+
+    def _get_codename(self, pathname, basename):
+        """Return (filename, archivename) for the path.
+
+        Given a module name path, return the correct file path and
+        archive name, compiling if necessary.  For example, given
+        /python/lib/string, return (/python/lib/string.pyc, string).
+        """
+        file_py  = pathname + ".py"
+        file_pyc = pathname + ".pyc"
+        file_pyo = pathname + ".pyo"
+        if os.path.isfile(file_pyo) and \
+                            os.stat(file_pyo).st_mtime >= os.stat(file_py).st_mtime:
+            fname = file_pyo    # Use .pyo file
+        elif not os.path.isfile(file_pyc) or \
+             os.stat(file_pyc).st_mtime < os.stat(file_py).st_mtime:
+            import py_compile
+            if self.debug:
+                print "Compiling", file_py
+            try:
+                py_compile.compile(file_py, file_pyc, None, True)
+            except py_compile.PyCompileError,err:
+                print err.msg
+            fname = file_pyc
+        else:
+            fname = file_pyc
+        archivename = os.path.split(fname)[1]
+        if basename:
+            archivename = "%s/%s" % (basename, archivename)
+        return (fname, archivename)
+
+
+def main(args = None):
+    import textwrap
+    USAGE=textwrap.dedent("""\
+        Usage:
+            zipfile.py -l zipfile.zip        # Show listing of a zipfile
+            zipfile.py -t zipfile.zip        # Test if a zipfile is valid
+            zipfile.py -e zipfile.zip target # Extract zipfile into target dir
+            zipfile.py -c zipfile.zip src ... # Create zipfile from sources
+        """)
+    if args is None:
+        args = sys.argv[1:]
+
+    if not args or args[0] not in ('-l', '-c', '-e', '-t'):
+        print USAGE
+        sys.exit(1)
+
+    if args[0] == '-l':
+        if len(args) != 2:
+            print USAGE
+            sys.exit(1)
+        zf = ZipFile(args[1], 'r')
+        zf.printdir()
+        zf.close()
+
+    elif args[0] == '-t':
+        if len(args) != 2:
+            print USAGE
+            sys.exit(1)
+        zf = ZipFile(args[1], 'r')
+        zf.testzip()
+        print "Done testing"
+
+    elif args[0] == '-e':
+        if len(args) != 3:
+            print USAGE
+            sys.exit(1)
+
+        zf = ZipFile(args[1], 'r')
+        out = args[2]
+        for path in zf.namelist():
+            if path.startswith('./'):
+                tgt = os.path.join(out, path[2:])
+            else:
+                tgt = os.path.join(out, path)
+
+            tgtdir = os.path.dirname(tgt)
+            if not os.path.exists(tgtdir):
+                os.makedirs(tgtdir)
+            with open(tgt, 'wb') as fp:
+                fp.write(zf.read(path))
+        zf.close()
+
+    elif args[0] == '-c':
+        if len(args) < 3:
+            print USAGE
+            sys.exit(1)
+
+        def addToZip(zf, path, zippath):
+            if os.path.isfile(path):
+                zf.write(path, zippath, ZIP_DEFLATED)
+            elif os.path.isdir(path):
+                for nm in os.listdir(path):
+                    addToZip(zf,
+                            os.path.join(path, nm), os.path.join(zippath, nm))
+            # else: ignore
+
+        zf = ZipFile(args[1], 'w', allowZip64=True)
+        for src in args[2:]:
+            addToZip(zf, src, os.path.basename(src))
+
+        zf.close()
+
+if __name__ == "__main__":
+    main()
index 523ef1a2109cb6d41dcaaec8175672484cccff9c..c7921f2485e5189fd25f6dbf9e90a3c630fbbd96 100644 (file)
@@ -2,7 +2,7 @@
 
 import sys
 import zlib
-import zipfile
+import zipfilerugged
 import os
 import os.path
 import getopt
@@ -15,7 +15,7 @@ _FILENAME_OFFSET = 30
 _MAX_SIZE = 64 * 1024
 _MIMETYPE = 'application/epub+zip'
 
-class ZipInfo(zipfile.ZipInfo):
+class ZipInfo(zipfilerugged.ZipInfo):
     def __init__(self, *args, **kwargs):
         if 'compress_type' in kwargs:
             compress_type = kwargs.pop('compress_type')
@@ -27,10 +27,14 @@ class fixZip:
         self.ztype = 'zip'
         if zinput.lower().find('.epub') >= 0 :
             self.ztype = 'epub'
-        self.inzip = zipfile.ZipFile(zinput,'r')
-        self.outzip = zipfile.ZipFile(zoutput,'w')
+        print "opening input"
+        self.inzip = zipfilerugged.ZipFile(zinput,'r')
+        print "opening outout"
+        self.outzip = zipfilerugged.ZipFile(zoutput,'w')
+        print "opening input as raw file"
         # open the input zip for reading only as a raw file
         self.bzf = file(zinput,'rb')
+        print "finished initialising"
 
     def getlocalname(self, zi):
         local_header_offset = zi.header_offset
@@ -76,11 +80,11 @@ class fixZip:
         data = None
 
         # if not compressed we are good to go
-        if zi.compress_type == zipfile.ZIP_STORED:
+        if zi.compress_type == zipfilerugged.ZIP_STORED:
             data = self.bzf.read(zi.file_size)
 
         # if compressed we must decompress it using zlib
-        if zi.compress_type == zipfile.ZIP_DEFLATED:
+        if zi.compress_type == zipfilerugged.ZIP_DEFLATED:
             cmpdata = self.bzf.read(zi.compress_size)
             data = self.uncompress(cmpdata)
 
@@ -95,7 +99,7 @@ class fixZip:
 
         # if epub write mimetype file first, with no compression
         if self.ztype == 'epub':
-            nzinfo = ZipInfo('mimetype', compress_type=zipfile.ZIP_STORED)
+            nzinfo = ZipInfo('mimetype', compress_type=zipfilerugged.ZIP_STORED)
             self.outzip.writestr(nzinfo, _MIMETYPE)
 
         # write the rest of the files
@@ -105,7 +109,7 @@ class fixZip:
                 nzinfo = zinfo
                 try:
                     data = self.inzip.read(zinfo.filename)
-                except zipfile.BadZipfile or zipfile.error:
+                except zipfilerugged.BadZipfile or zipfilerugged.error:
                     local_name = self.getlocalname(zinfo)
                     data = self.getfiledata(zinfo)
                     nzinfo.filename = local_name
index e5e7774e737bf1f115556423e18ce7e2b8f65a6c..6f961d9d61bb8b68b3f49407e3c4ab51e6896ef1 100644 (file)
@@ -1,7 +1,7 @@
-ReadMe_DeDRM_v5.3_WinApp
+ReadMe_DeDRM_v5.4_WinApp
 -----------------------
 
-DeDRM_v5.3_WinApp is a pure python drag and drop application that allows users to drag and drop ebooks or folders of ebooks onto the DeDRM_Drop_Target to have the DRM removed.  It repackages the"tools" python software in one easy to use program that remembers preferences and settings.
+DeDRM_v5.4_WinApp is a pure python drag and drop application that allows users to drag and drop ebooks or folders of ebooks onto the DeDRM_Drop_Target to have the DRM removed.  It repackages the"tools" python software in one easy to use program that remembers preferences and settings.
 
 It should work out of the box with Kindle for PC ebooks and Adobe Adept epub and pdf ebooks.
 
@@ -14,20 +14,19 @@ MobiPocket: 10 digit PID
 
 Once these preferences have been set, the user can simply drag and drop ebooks onto the DeDRM_Drop_Target to remove the DRM.
 
-This program requires that the proper 32 bit version of Python 2.X (tested with Python 2.5 through Python 2.7) and PyCrypto be installed on your computer before it will work.  See below for where to get theese programs for Windows.
+This program requires that a 32 bit version of Python 2.X (tested with Python 2.5 through Python 2.7) and PyCrypto be installed on your computer before it will work.  See below for where to get theese programs for Windows.
 
 Installation
 ------------
 
-1. In tools_v5.3\DeDRM_Applications\Windows, right click on DeDRM_5.3_Win.zip and fully extract its contents using "Extract All...", saving to your "My Documents" folder.
+0. If you don't already have a correct version of Python and PyCrypto installed, follow the "Installing Python on Windows" and "Installing PyCrypto on Windows" sections below before continuing.
 
-2. Open the DeDRM_5.3_Win folder you've just created, and make a short-cut of the DeDRM_Drop_Target.bat file (right-click/Create Shortcut). Drag the shortcut file onto your Desktop.
+1. Drag the DeDRM_5.4 folder from tools_v5.4/DeDRM_Applications/Windows to your "My Documents" folder.
 
-3. To set the preferences simply double-click on your just created short-cut.
+2. Open the DeDRM_5.4 folder you've just dragged, and make a short-cut of the DeDRM_Drop_Target.bat file (right-click/Create Shortcut). Drag the shortcut file onto your Desktop.
 
-If you already have a correct version of Python and PyCrypto installed and in your path, you are ready to go!
+3. To set the preferences simply double-click on your just created short-cut.
 
-If not, see below.
 
 
 Installing Python on Windows
diff --git a/Kindle_for_Android_Patches/kindle version 3.7.0.108/ReadMe_K4Android.txt b/Kindle_for_Android_Patches/kindle version 3.7.0.108/ReadMe_K4Android.txt
new file mode 100644 (file)
index 0000000..95cc013
--- /dev/null
@@ -0,0 +1,66 @@
+Kindle for Android
+------------------
+
+Kindle for Android uses a different scheme to generate its books specific PIDs than Kindle for PC, Kindle for iPhone/iPad, and standalone Kindles.
+
+Unfortunately, K4Android uses an "account secrets" file that would only be available if the device were jail-broken and even then someone would have to figure out how to decode this secret information in order to reverse the process.
+
+Instead of trying to calculate the correct PIDs for each book from this primary data, "Me" (a commenter who posted to the ApprenticeAlf site)  came up with a wonderful idea to simply modify the Kindle 3 for Android application to store the PIDs it uses to decode each book in its "about activity" window.  This list of PIDS can then be provided to MobiDeDRM.py,  which in its latest incarnations allows a comma separated list of pids to be passed in, to successfully remove the DRM from that book.   Effectively "Me" has created an "Unswindle" for the Kindle for Android 3 application!
+
+"Me"'s original patch was for Kindle for Android version 3.0.1.70. Now "Me II" has created a patch for Kindle for Android version 3.7.0.108 and new instructions, since some of the previous steps are no longer necessary.
+
+From the ApprenticeAlf Comments:
+
+"Me II" writes:
+
+Since “Me”‘s old method for getting PIDs from Kindle for Android is outdated and no longer works with newer versions of the app, I decided I’d take a stab at bringing it up to date. It took a little fiddling to get everything working, considering how much has changed since the last patch, but I managed to do it. The process is pretty much identical to “Me”‘s original instructions, with a few minor changes.
+
+1) You don’t need to build apktool from source. You can just grab the binaries from here for whatever OS you’re running: http://code.google.com/p/android-apktool/
+2) When you sign the rebuilt APK, use the following command instead of the one in the instructions:
+jarsigner -verbose -sigalg MD5withRSA -digestalg SHA1 -keystore kindle.keystore kindle3_patched.apk kindle
+3) It no longer logs the PIDs, only displays them within the app.
+
+You can get the new patch, for version 3.7.0.108, here: http://pastebin.com/6FN2cTSN
+
+And here’s a screenshot of the updated menu: http://imgur.com/BbFVF (sorry for the Japanese, I was too lazy to change my phone’s language).
+
+
+
+
+"Me"'s original instructions, from the ApprenticeAlf Comments:
+
+"Me" writes:
+
+A better solution seems to create a patched version of the Kindle apk which either logs or displays it’s PID. I created a patch to both log the pid list and show it in the Kindle application in the about activity screen. The pid list isn’t available until the DRMed book has been opened (and the list seem to differ for different books).
+
+To create the patched kindle apk a certificate must be created (http://developer.android.com/guide/publishing/app-signing.html#cert) and the apktool must be build from source (all subprojects) as long as version 1.4.2 isn’t released (http://code.google.com/p/android-apktool/wiki/BuildApktool).
+
+These are the steps to pull the original apk from the Android device, uninstall it, create a patched apk and install that (tested on a rooted device, but I think all steps should also work on non-rooted devices):
+
+adb pull /data/app/com.amazon.kindle-1.apk kindle3.apk
+adb uninstall com.amazon.kindle
+apktool d kindle3.apk kindle3
+cd kindle3
+patch -p1 < ../kindle3.patch
+cd ..
+apktool b kindle3 kindle3_patched.apk
+jarsigner -verbose -keystore kindle.keystore kindle3_patched.apk kindle
+zipalign -v 4 kindle3_patched.apk kindle3_signed.apk
+adb install kindle3_signed.apk
+
+kindle3.patch (based on kindle version 3.0.1.70) is available on pastebin:
+http://pastebin.com/LNpgkcpP
+
+Have fun!
+
+Comment by me — June 9, 2011 @ 9:01 pm | Reply
+
+Hi me,
+Wow! Great work!!!!
+
+With your patch, you have created the equivalent of Unswindle for the Kindle for Android app and it does not even require jailbreaking!
+
+Very nice work indeed!
+
+Comment by some_updates — June 10, 2011 @ 4:28 am | Reply
+
diff --git a/Kindle_for_Android_Patches/kindle version 3.7.0.108/kindle3.7.0.108.patch b/Kindle_for_Android_Patches/kindle version 3.7.0.108/kindle3.7.0.108.patch
new file mode 100644 (file)
index 0000000..53ed5c3
--- /dev/null
@@ -0,0 +1,155 @@
+diff -ru kindle3.7.0.108_orig/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali kindle3.7.0.108/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali
+--- kindle3.7.0.108_orig/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali
++++ kindle3.7.0.108/smali/com/amazon/kcp/application/AndroidDeviceInformationProvider.smali
+@@ -43,6 +43,8 @@
+ .field private security:Lcom/mobipocket/android/library/reader/AndroidSecurity;
++.field private pidList:Ljava/lang/String;
++
+ .field private totalMemory:J
+@@ -78,6 +80,10 @@
+     .line 132
+     iput-object p2, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->deviceType:Lcom/amazon/kcp/application/AmazonDeviceType;
++    
++    const-string v0, "Open DRMed book to show PID list."
++
++    iput-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String;
+     .line 133
+     sget-object v0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->TAG:Ljava/lang/String;
+@@ -1242,3 +1248,25 @@
+     return-wide v0
+ .end method
++
++.method public getPidList()Ljava/lang/String;
++    .locals 1
++
++    .prologue
++    .line 15
++    iget-object v0, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String;
++
++    return-object v0
++.end method
++
++.method public setPidList(Ljava/lang/String;)V
++    .locals 0
++    .parameter "value"
++
++    .prologue
++    .line 11
++    iput-object p1, p0, Lcom/amazon/kcp/application/AndroidDeviceInformationProvider;->pidList:Ljava/lang/String;
++
++    .line 12
++    return-void
++.end method
+\ No newline at end of file
+diff -ru kindle3.7.0.108_orig/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali kindle3.7.0.108/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali
+--- kindle3.7.0.108_orig/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali
++++ kindle3.7.0.108/smali/com/amazon/kcp/application/IDeviceInformationProvider.smali
+@@ -30,3 +30,9 @@
+ .method public abstract getPid()Ljava/lang/String;
+ .end method
++
++.method public abstract getPidList()Ljava/lang/String;
++.end method
++
++.method public abstract setPidList(Ljava/lang/String;)V
++.end method
+\ No newline at end of file
+diff -ru kindle3.7.0.108_orig/smali/com/amazon/kcp/info/AboutActivity.smali kindle3.7.0.108/smali/com/amazon/kcp/info/AboutActivity.smali
+--- kindle3.7.0.108_orig/smali/com/amazon/kcp/info/AboutActivity.smali
++++ kindle3.7.0.108/smali/com/amazon/kcp/info/AboutActivity.smali
+@@ -493,6 +493,57 @@
+     return-void
+ .end method
++.method private populatePIDList()V
++    .locals 7
++    
++    .prologue
++    .line 313
++    invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider;
++    
++    move-result-object v0
++    
++    invoke-interface {v0}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->getPidList()Ljava/lang/String;
++    
++    move-result-object v1
++
++    .line 314
++    .local v1, PidList:Ljava/lang/String;
++    iget-object v3, p0, Lcom/amazon/kcp/info/AboutActivity;->groupItemList:Ljava/util/List;
++    
++    new-instance v4, Lcom/amazon/kcp/info/AboutActivity$GroupItem;
++    
++    const-string v5, "PID List"
++    
++    const v6, 0x1
++
++    invoke-direct {v4, p0, v5, v6}, Lcom/amazon/kcp/info/AboutActivity$GroupItem;-><init>(Lcom/amazon/kcp/info/AboutActivity;Ljava/lang/String;Z)V
++
++    invoke-interface {v3, v4}, Ljava/util/List;->add(Ljava/lang/Object;)Z
++    
++    .line 315
++    new-instance v2, Ljava/util/ArrayList;
++
++    invoke-direct {v2}, Ljava/util/ArrayList;-><init>()V
++
++    .line 316
++    .local v2, children:Ljava/util/List;,"Ljava/util/List<Lcom/amazon/kcp/info/AboutActivity$DetailItem;>;"
++    new-instance v3, Lcom/amazon/kcp/info/AboutActivity$DetailItem;
++    
++    const-string v4, "PIDs"
++
++    invoke-direct {v3, p0, v4, v1}, Lcom/amazon/kcp/info/AboutActivity$DetailItem;-><init>(Lcom/amazon/kcp/info/AboutActivity;Ljava/lang/String;Ljava/lang/String;)V
++
++    invoke-interface {v2, v3}, Ljava/util/List;->add(Ljava/lang/Object;)Z
++
++    .line 317
++    iget-object v3, p0, Lcom/amazon/kcp/info/AboutActivity;->detailItemList:Ljava/util/List;
++
++    invoke-interface {v3, v2}, Ljava/util/List;->add(Ljava/lang/Object;)Z
++
++    .line 318
++    return-void
++.end method
++
+ .method private populateDisplayItems()V
+     .locals 1
+@@ -539,6 +590,9 @@
+     invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populateDisplayInformation()V
+     .line 190
++    invoke-direct {p0}, Lcom/amazon/kcp/info/AboutActivity;->populatePIDList()V
++    
++    .line 191
+     return-void
+     .line 172
+diff -ru kindle3.7.0.108_orig/smali/com/amazon/system/security/Security.smali kindle3.7.0.108/smali/com/amazon/system/security/Security.smali
+--- kindle3.7.0.108_orig/smali/com/amazon/system/security/Security.smali
++++ kindle3.7.0.108/smali/com/amazon/system/security/Security.smali
+@@ -926,6 +926,16 @@
+     sget-object v0, Lcom/amazon/system/security/Security;->CUSTOM_PID_FOR_BUNDLED_DICTIONARY_DRM:Ljava/lang/String;
+     aput-object v0, v6, v8
++    
++    invoke-static {}, Lcom/amazon/kcp/application/DeviceInformationProviderFactory;->getProvider()Lcom/amazon/kcp/application/IDeviceInformationProvider;
++    
++    move-result-object v5
++    
++    invoke-static {v6}, Ljava/util/Arrays;->toString([Ljava/lang/Object;)Ljava/lang/String;
++    
++    move-result-object v2
++    
++    invoke-interface {v5, v2}, Lcom/amazon/kcp/application/IDeviceInformationProvider;->setPidList(Ljava/lang/String;)V
+     .line 353
+     return-object v6
\ No newline at end of file
index da61e77eb4ab05593ecd0a6f4a5a1700edf71a09..daa988932e4ab4e895f94f2d49b476dfb193b08d 100644 (file)
@@ -3,7 +3,7 @@
 
 from __future__ import with_statement
 
-# ineptkey.pyw, version 5.4
+# ineptkey.pyw, version 5.5
 # Copyright © 2009-2010 i♥cabbages
 
 # Released under the terms of the GNU General Public Licence, version 3 or
@@ -36,6 +36,7 @@ from __future__ import with_statement
 #   5.2 - added support for output of key to a particular file
 #   5.3 - On Windows try PyCrypto first, OpenSSL next
 #   5.4 - Modify interface to allow use of import
+#   5.5 - Fix for potential problem with PyCrypto
 
 """
 Retrieve Adobe ADEPT user key.
@@ -110,7 +111,7 @@ if sys.platform.startswith('win'):
         from Crypto.Cipher import AES as _AES
         class AES(object):
             def __init__(self, key):
-                self._aes = _AES.new(key, _AES.MODE_CBC)
+                self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
             def decrypt(self, data):
                 return self._aes.decrypt(data)
         return AES
index 018736acd74626a3ac075f1659e565b28cfbb2aa..829f1b22ee2cb391af1d93814efdfb5b58787016 100644 (file)
@@ -3,7 +3,7 @@
 
 from __future__ import with_statement
 
-# ineptepub.pyw, version 5.6
+# ineptepub.pyw, version 5.7
 # Copyright © 2009-2010 i♥cabbages
 
 # Released under the terms of the GNU General Public Licence, version 3 or
@@ -30,6 +30,8 @@ from __future__ import with_statement
 #   5.4 - add support for encoding to 'utf-8' when building up list of files to decrypt from encryption.xml
 #   5.5 - On Windows try PyCrypto first, OpenSSL next
 #   5.6 - Modify interface to allow use with import
+#   5.7 - Fix for potential problem with PyCrypto
+
 """
 Decrypt Adobe ADEPT-encrypted EPUB books.
 """
@@ -235,7 +237,7 @@ def _load_crypto_pycrypto():
 
     class AES(object):
         def __init__(self, key):
-            self._aes = _AES.new(key, _AES.MODE_CBC)
+            self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
 
         def decrypt(self, data):
             return self._aes.decrypt(data)
index da61e77eb4ab05593ecd0a6f4a5a1700edf71a09..723b7c64eeab9ead313fc8283196cb5baa32407c 100644 (file)
@@ -3,7 +3,7 @@
 
 from __future__ import with_statement
 
-# ineptkey.pyw, version 5.4
+# ineptkey.pyw, version 5.6
 # Copyright © 2009-2010 i♥cabbages
 
 # Released under the terms of the GNU General Public Licence, version 3 or
@@ -36,6 +36,8 @@ from __future__ import with_statement
 #   5.2 - added support for output of key to a particular file
 #   5.3 - On Windows try PyCrypto first, OpenSSL next
 #   5.4 - Modify interface to allow use of import
+#   5.5 - Fix for potential problem with PyCrypto
+#   5.6 - Revise to allow use in Plugins to eliminate need for duplicate code
 
 """
 Retrieve Adobe ADEPT user key.
@@ -46,15 +48,17 @@ __license__ = 'GPL v3'
 import sys
 import os
 import struct
-import Tkinter
-import Tkconstants
-import tkMessageBox
-import traceback
+
+try:
+    from calibre.constants import iswindows, isosx
+except:
+    iswindows = sys.platform.startswith('win')
+    isosx = sys.platform.startswith('darwin')
 
 class ADEPTError(Exception):
     pass
 
-if sys.platform.startswith('win'):
+if iswindows:
     from ctypes import windll, c_char_p, c_wchar_p, c_uint, POINTER, byref, \
         create_unicode_buffer, create_string_buffer, CFUNCTYPE, addressof, \
         string_at, Structure, c_void_p, cast, c_size_t, memmove, CDLL, c_int, \
@@ -76,13 +80,13 @@ if sys.platform.startswith('win'):
             _fields_ = [('rd_key', c_long * (4 * (AES_MAXNR + 1))),
                         ('rounds', c_int)]
         AES_KEY_p = POINTER(AES_KEY)
-
+    
         def F(restype, name, argtypes):
             func = getattr(libcrypto, name)
             func.restype = restype
             func.argtypes = argtypes
             return func
-
+    
         AES_set_decrypt_key = F(c_int, 'AES_set_decrypt_key',
                                 [c_char_p, c_int, AES_KEY_p])
         AES_cbc_encrypt = F(None, 'AES_cbc_encrypt',
@@ -110,7 +114,7 @@ if sys.platform.startswith('win'):
         from Crypto.Cipher import AES as _AES
         class AES(object):
             def __init__(self, key):
-                self._aes = _AES.new(key, _AES.MODE_CBC)
+                self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
             def decrypt(self, data):
                 return self._aes.decrypt(data)
         return AES
@@ -292,13 +296,9 @@ if sys.platform.startswith('win'):
         return CryptUnprotectData
     CryptUnprotectData = CryptUnprotectData()
 
-    def retrieve_key(keypath):
+    def retrieve_keys():
         if AES is None:
-            tkMessageBox.showerror(
-                "ADEPT Key",
-                "This script requires PyCrypto or OpenSSL which must be installed "
-                "separately.  Read the top-of-script comment for details.")
-            return False
+            raise ADEPTError("PyCrypto or OpenSSL must be installed")
         root = GetSystemDirectory().split('\\')[0] + '\\'
         serial = GetVolumeSerialNumber(root)
         vendor = cpuid0()
@@ -313,6 +313,7 @@ if sys.platform.startswith('win'):
         device = winreg.QueryValueEx(regkey, 'key')[0]
         keykey = CryptUnprotectData(device, entropy)
         userkey = None
+        keys = []
         try:
             plkroot = winreg.OpenKey(cuser, PRIVATE_LICENCE_KEY_PATH)
         except WindowsError:
@@ -334,50 +335,43 @@ if sys.platform.startswith('win'):
                 if ktype != 'privateLicenseKey':
                     continue
                 userkey = winreg.QueryValueEx(plkkey, 'value')[0]
-                break
-            if userkey is not None:
-                break
-        if userkey is None:
+                userkey = userkey.decode('base64')
+                aes = AES(keykey)
+                userkey = aes.decrypt(userkey)
+                userkey = userkey[26:-ord(userkey[-1])]
+                keys.append(userkey)
+        if len(keys) == 0:
             raise ADEPTError('Could not locate privateLicenseKey')
-        userkey = userkey.decode('base64')
-        aes = AES(keykey)
-        userkey = aes.decrypt(userkey)
-        userkey = userkey[26:-ord(userkey[-1])]
-        with open(keypath, 'wb') as f:
-            f.write(userkey)
-        return True
-
-elif sys.platform.startswith('darwin'):
+        return keys
+        
+
+elif isosx:
     import xml.etree.ElementTree as etree
-    import Carbon.File
-    import Carbon.Folder
-    import Carbon.Folders
-    import MacOS
+    import subprocess
 
-    ACTIVATION_PATH = 'Adobe/Digital Editions/activation.dat'
     NSMAP = {'adept': 'http://ns.adobe.com/adept',
              'enc': 'http://www.w3.org/2001/04/xmlenc#'}
 
-    def find_folder(domain, dtype):
-        try:
-            fsref = Carbon.Folder.FSFindFolder(domain, dtype, False)
-            return Carbon.File.pathname(fsref)
-        except MacOS.Error:
-            return None
-
-    def find_app_support_file(subpath):
-        dtype = Carbon.Folders.kApplicationSupportFolderType
-        for domain in Carbon.Folders.kUserDomain, Carbon.Folders.kLocalDomain:
-            path = find_folder(domain, dtype)
-            if path is None:
-                continue
-            path = os.path.join(path, subpath)
-            if os.path.isfile(path):
-                return path
+    def findActivationDat():
+        home = os.getenv('HOME')
+        cmdline = 'find "' + home + '/Library/Application Support/Adobe/Digital Editions" -name "activation.dat"'
+        cmdline = cmdline.encode(sys.getfilesystemencoding())
+        p2 = subprocess.Popen(cmdline, shell=True, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=False)
+        out1, out2 = p2.communicate()
+        reslst = out1.split('\n')
+        cnt = len(reslst)
+        for j in xrange(cnt):
+            resline = reslst[j]
+            pp = resline.find('activation.dat')
+            if pp >= 0:
+                ActDatPath = resline
+                break
+        if os.path.exists(ActDatPath):
+            return ActDatPath
         return None
 
-    def retrieve_key(keypath):
-        actpath = find_app_support_file(ACTIVATION_PATH)
+    def retrieve_keys():
+        actpath = findActivationDat()
         if actpath is None:
             raise ADEPTError("Could not locate ADE activation")
         tree = etree.parse(actpath)
@@ -386,39 +380,18 @@ elif sys.platform.startswith('darwin'):
         userkey = tree.findtext(expr)
         userkey = userkey.decode('base64')
         userkey = userkey[26:]
-        with open(keypath, 'wb') as f:
-            f.write(userkey)
-        return True
-
-elif sys.platform.startswith('cygwin'):
-    def retrieve_key(keypath):
-        tkMessageBox.showerror(
-            "ADEPT Key",
-            "This script requires a Windows-native Python, and cannot be run "
-            "under Cygwin.  Please install a Windows-native Python and/or "
-            "check your file associations.")
-        return False
+        return [userkey]
 
 else:
-    def retrieve_key(keypath):
-        tkMessageBox.showerror(
-            "ADEPT Key",
-            "This script only supports Windows and Mac OS X.  For Linux "
-            "you should be able to run ADE and this script under Wine (with "
-            "an appropriate version of Windows Python installed).")
-        return False
-
-class ExceptionDialog(Tkinter.Frame):
-    def __init__(self, root, text):
-        Tkinter.Frame.__init__(self, root, border=5)
-        label = Tkinter.Label(self, text="Unexpected error:",
-                              anchor=Tkconstants.W, justify=Tkconstants.LEFT)
-        label.pack(fill=Tkconstants.X, expand=0)
-        self.text = Tkinter.Text(self)
-        self.text.pack(fill=Tkconstants.BOTH, expand=1)
-
-        self.text.insert(Tkconstants.END, text)
-
+    def retrieve_keys(keypath):
+        raise ADEPTError("This script only supports Windows and Mac OS X.")
+        return []
+        
+def retrieve_key(keypath):
+    keys = retrieve_keys()
+    with open(keypath, 'wb') as f:
+        f.write(keys[0])
+    return True
 
 def extractKeyfile(keypath):
     try:
@@ -440,10 +413,27 @@ def cli_main(argv=sys.argv):
 
 
 def main(argv=sys.argv):
+    import Tkinter
+    import Tkconstants
+    import tkMessageBox
+    import traceback
+
+    class ExceptionDialog(Tkinter.Frame):
+        def __init__(self, root, text):
+            Tkinter.Frame.__init__(self, root, border=5)
+            label = Tkinter.Label(self, text="Unexpected error:",
+                                  anchor=Tkconstants.W, justify=Tkconstants.LEFT)
+            label.pack(fill=Tkconstants.X, expand=0)
+            self.text = Tkinter.Text(self)
+            self.text.pack(fill=Tkconstants.BOTH, expand=1)
+    
+            self.text.insert(Tkconstants.END, text)
+
+
     root = Tkinter.Tk()
     root.withdraw()
     progname = os.path.basename(argv[0])
-    keypath = 'adeptkey.der'
+    keypath = os.path.abspath("adeptkey.der")
     success = False
     try:
         success = retrieve_key(keypath)
index ae070063698b22c02936d4012e745cd755aa9ef4..0a67cf2dda5d2fc7b5368c5338aa0291776e292d 100644 (file)
@@ -5,11 +5,16 @@ Barnes and Noble EPUB ebooks use a form of Social DRM which requires information
 For more info, see the author's blog:
 http://i-u2665-cabbages.blogspot.com/2009_12_01_archive.html
 
-The original scripts by IHeartCabbages are available here as well.  These scripts have been modified to allow the use of OpenSSL in place of PyCrypto to make them easier to run on Linux and Mac OS X, as well as to fix some minor bugs/
+The original scripts by IHeartCabbages are available here as well.  These scripts have been modified to allow the use of OpenSSL in place of PyCrypto to make them easier to run on Linux and Mac OS X, as well as to fix some minor bugs.
 
 There are 2 scripts:
 
-The first is ignoblekeygen_vX.X.pyw.  Double-click to launch it and provide the required information, and this program will generate a key file needed to remove the DRM from the books.  This key file need only be generated once unless either you change your credit card number or your name on the credit card (or if you use a different credit card to purchase your book).
+The first is ignoblekeygen_v2.4.pyw.  Double-click to launch it and provide the required information, and this program will generate a key file needed to remove the DRM from the books. The require information is
+
+* Your Name: Your name as set in your Barnes & Noble account, My Account page, directly under PERSONAL INFORMATION. It is usually just your first name and last name separated by a space.
+* Credit Card number: This is the credit card number that was on file with Barnes & Noble at the time of download of the ebooks.
+
+This key file need only be generated once unless either you change the default credit card number or your name on your B&N account.
 
 The second is ignobleepub_vX.X.pyw.  Double-click it and it will ask for your key file and the path to the book to remove the DRM from.
 
index 917aa4aaa16210a7532e733a4fd667633ad8a4d9..6b1a1d282410b0657cd60f648da24a08f5e7bbf1 100644 (file)
@@ -2,7 +2,7 @@
 
 from __future__ import with_statement
 
-# ignobleepub.pyw, version 3.4
+# ignobleepub.pyw, version 3.5
 
 # To run this program install Python 2.6 from <http://www.python.org/download/>
 # and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
@@ -17,7 +17,7 @@ from __future__ import with_statement
 #   3.2 - add support for encoding to 'utf-8' when building up list of files to cecrypt from encryption.xml
 #   3.3 - On Windows try PyCrypto first and OpenSSL next
 #   3.4 - Modify interace to allow use with import
-
+#   3.5 - Fix for potential problem with PyCrypto
 
 __license__ = 'GPL v3'
 
@@ -100,7 +100,7 @@ def _load_crypto_pycrypto():
 
     class AES(object):
         def __init__(self, key):
-            self._aes = _AES.new(key, _AES.MODE_CBC)
+            self._aes = _AES.new(key, _AES.MODE_CBC, '\x00'*16)
 
         def decrypt(self, data):
             return self._aes.decrypt(data)
@@ -143,7 +143,7 @@ class ZipInfo(zipfile.ZipInfo):
 class Decryptor(object):
     def __init__(self, bookkey, encryption):
         enc = lambda tag: '{%s}%s' % (NSMAP['enc'], tag)
-        # self._aes = AES.new(bookkey, AES.MODE_CBC)
+        # self._aes = AES.new(bookkey, AES.MODE_CBC, '\x00'*16)
         self._aes = AES(bookkey)
         encryption = etree.fromstring(encryption)
         self._encrypted = encrypted = set()
@@ -271,7 +271,7 @@ def decryptBook(keypath, inpath, outpath):
     with open(keypath, 'rb') as f:
         keyb64 = f.read()
     key = keyb64.decode('base64')[:16]
-    # aes = AES.new(key, AES.MODE_CBC)
+    # aes = AES.new(key, AES.MODE_CBC, '\x00'*16)
     aes = AES(key)
 
     with closing(ZipFile(open(inpath, 'rb'))) as inf:
index e7a78ea19a7d60c2b2679d3b548d5162ca193c12..e2c50e2ebb1788dd784db57ba130a28a05ec2a37 100644 (file)
@@ -2,7 +2,7 @@
 
 from __future__ import with_statement
 
-# ignoblekeygen.pyw, version 2.3
+# ignoblekeygen.pyw, version 2.4
 
 # To run this program install Python 2.6 from <http://www.python.org/download/>
 # and OpenSSL or PyCrypto from http://www.voidspace.org.uk/python/modules.shtml#pycrypto
@@ -15,6 +15,7 @@ from __future__ import with_statement
 #   2.1 - Allow Windows versions of libcrypto to be found
 #   2.2 - On Windows try PyCrypto first and then OpenSSL next
 #   2.3 - Modify interface to allow use of import
+#   2.4 - Improvements to UI and now works in plugins
 
 """
 Generate Barnes & Noble EPUB user key from name and credit card number.
@@ -25,10 +26,6 @@ __license__ = 'GPL v3'
 import sys
 import os
 import hashlib
-import Tkinter
-import Tkconstants
-import tkFileDialog
-import tkMessageBox
 
 
 
@@ -124,8 +121,10 @@ def normalize_name(name):
 
 
 def generate_keyfile(name, ccn, outpath):
+    # remove spaces and case from name and CC numbers.
     name = normalize_name(name) + '\x00'
-    ccn = ccn + '\x00'
+    ccn = normalize_name(ccn) + '\x00'
+    
     name_sha = hashlib.sha1(name).digest()[:16]
     ccn_sha = hashlib.sha1(ccn).digest()[:16]
     both_sha = hashlib.sha1(name + ccn).digest()
@@ -137,69 +136,6 @@ def generate_keyfile(name, ccn, outpath):
     return userkey
 
 
-class DecryptionDialog(Tkinter.Frame):
-    def __init__(self, root):
-        Tkinter.Frame.__init__(self, root, border=5)
-        self.status = Tkinter.Label(self, text='Enter parameters')
-        self.status.pack(fill=Tkconstants.X, expand=1)
-        body = Tkinter.Frame(self)
-        body.pack(fill=Tkconstants.X, expand=1)
-        sticky = Tkconstants.E + Tkconstants.W
-        body.grid_columnconfigure(1, weight=2)
-        Tkinter.Label(body, text='Name').grid(row=1)
-        self.name = Tkinter.Entry(body, width=30)
-        self.name.grid(row=1, column=1, sticky=sticky)
-        Tkinter.Label(body, text='CC#').grid(row=2)
-        self.ccn = Tkinter.Entry(body, width=30)
-        self.ccn.grid(row=2, column=1, sticky=sticky)
-        Tkinter.Label(body, text='Output file').grid(row=0)
-        self.keypath = Tkinter.Entry(body, width=30)
-        self.keypath.grid(row=0, column=1, sticky=sticky)
-        self.keypath.insert(0, 'bnepubkey.b64')
-        button = Tkinter.Button(body, text="...", command=self.get_keypath)
-        button.grid(row=0, column=2)
-        buttons = Tkinter.Frame(self)
-        buttons.pack()
-        botton = Tkinter.Button(
-            buttons, text="Generate", width=10, command=self.generate)
-        botton.pack(side=Tkconstants.LEFT)
-        Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
-        button = Tkinter.Button(
-            buttons, text="Quit", width=10, command=self.quit)
-        button.pack(side=Tkconstants.RIGHT)
-
-    def get_keypath(self):
-        keypath = tkFileDialog.asksaveasfilename(
-            parent=None, title='Select B&N EPUB key file to produce',
-            defaultextension='.b64',
-            filetypes=[('base64-encoded files', '.b64'),
-                       ('All Files', '.*')])
-        if keypath:
-            keypath = os.path.normpath(keypath)
-            self.keypath.delete(0, Tkconstants.END)
-            self.keypath.insert(0, keypath)
-        return
-
-    def generate(self):
-        name = self.name.get()
-        ccn = self.ccn.get()
-        keypath = self.keypath.get()
-        if not name:
-            self.status['text'] = 'Name not specified'
-            return
-        if not ccn:
-            self.status['text'] = 'Credit card number not specified'
-            return
-        if not keypath:
-            self.status['text'] = 'Output keyfile path not specified'
-            return
-        self.status['text'] = 'Generating...'
-        try:
-            generate_keyfile(name, ccn, keypath)
-        except Exception, e:
-            self.status['text'] = 'Error: ' + str(e)
-            return
-        self.status['text'] = 'Keyfile successfully generated'
 
 
 def cli_main(argv=sys.argv):
@@ -218,6 +154,75 @@ def cli_main(argv=sys.argv):
 
 
 def gui_main():
+    import Tkinter
+    import Tkconstants
+    import tkFileDialog
+    import tkMessageBox
+
+    class DecryptionDialog(Tkinter.Frame):
+        def __init__(self, root):
+            Tkinter.Frame.__init__(self, root, border=5)
+            self.status = Tkinter.Label(self, text='Enter parameters')
+            self.status.pack(fill=Tkconstants.X, expand=1)
+            body = Tkinter.Frame(self)
+            body.pack(fill=Tkconstants.X, expand=1)
+            sticky = Tkconstants.E + Tkconstants.W
+            body.grid_columnconfigure(1, weight=2)
+            Tkinter.Label(body, text='Account Name').grid(row=0)
+            self.name = Tkinter.Entry(body, width=40)
+            self.name.grid(row=0, column=1, sticky=sticky)
+            Tkinter.Label(body, text='CC#').grid(row=1)
+            self.ccn = Tkinter.Entry(body, width=40)
+            self.ccn.grid(row=1, column=1, sticky=sticky)
+            Tkinter.Label(body, text='Output file').grid(row=2)
+            self.keypath = Tkinter.Entry(body, width=40)
+            self.keypath.grid(row=2, column=1, sticky=sticky)
+            self.keypath.insert(2, 'bnepubkey.b64')
+            button = Tkinter.Button(body, text="...", command=self.get_keypath)
+            button.grid(row=2, column=2)
+            buttons = Tkinter.Frame(self)
+            buttons.pack()
+            botton = Tkinter.Button(
+                buttons, text="Generate", width=10, command=self.generate)
+            botton.pack(side=Tkconstants.LEFT)
+            Tkinter.Frame(buttons, width=10).pack(side=Tkconstants.LEFT)
+            button = Tkinter.Button(
+                buttons, text="Quit", width=10, command=self.quit)
+            button.pack(side=Tkconstants.RIGHT)
+    
+        def get_keypath(self):
+            keypath = tkFileDialog.asksaveasfilename(
+                parent=None, title='Select B&N EPUB key file to produce',
+                defaultextension='.b64',
+                filetypes=[('base64-encoded files', '.b64'),
+                           ('All Files', '.*')])
+            if keypath:
+                keypath = os.path.normpath(keypath)
+                self.keypath.delete(0, Tkconstants.END)
+                self.keypath.insert(0, keypath)
+            return
+    
+        def generate(self):
+            name = self.name.get()
+            ccn = self.ccn.get()
+            keypath = self.keypath.get()
+            if not name:
+                self.status['text'] = 'Name not specified'
+                return
+            if not ccn:
+                self.status['text'] = 'Credit card number not specified'
+                return
+            if not keypath:
+                self.status['text'] = 'Output keyfile path not specified'
+                return
+            self.status['text'] = 'Generating...'
+            try:
+                generate_keyfile(name, ccn, keypath)
+            except Exception, e:
+                self.status['text'] = 'Error: ' + str(e)
+                return
+            self.status['text'] = 'Keyfile successfully generated'
+
     root = Tkinter.Tk()
     if AES is None:
         root.withdraw()
index 9ab87bd9d1078e545a3aacc80ec8da60382f072c..59fe31d27ab7a7b8661c636e10aa29a4168c0881 100644 (file)
@@ -1,15 +1,14 @@
 KindleBooks (Originally called K4MobiDeDRM and Topaz_Tools)
 
+Most users will be better off using the DeDRM applications or the calibre plugin. This script is provided more for historical interest than anything else.
+
+
 This tools combines functionality of MobiDeDRM with that of K4PCDeDRM, K4MDeDRM, and K4DeDRM.  Effectively, it provides one-stop shopping for all your Mobipocket, Kindle for iPhone/iPad/iPodTouch, Kindle for PC, and Kindle for Mac needs and should work for both Mobi and Topaz ebooks.
 
 Preliminary Steps:
 
-1. Make sure you have Python 2.X installed (32 bit) and properly set as part of your SYSTEM PATH environment variable (On Windows I recommend ActiveState's ActivePython. See their web pages for instructions on how to install and how to properly set your PATH). On Mac OSX 10.6 everything you need is already installed.
-
+1. Make sure you have Python 2.5, 2.6 or 2.7 installed (32 bit) and properly set as part of your SYSTEM PATH environment variable (On Windows I recommend ActiveState's ActivePython. See their web pages for instructions on how to install and how to properly set your PATH). On Mac OSX 10.5 and later everything you need is already installed.
 
-****
-Please Note: If you a happy user of MobiDeDRM, K4DeDRM, K4PCDeDRM, or K4MUnswindle, please continue to use these programs as there is no additional capability provided by this tool over the others.  In the long run, if you have problems with any of those tools, you might want to try this one as it will continue under development eventually replacing all of those tools.
-****
 
 Instructions:
 
index c029760955bd4d6bed120397667051ddfdde3f1d..98258788c86dbb40a3f2e773d185baad750d4cdf 100644 (file)
@@ -20,7 +20,7 @@ class ConfigWidget(QWidget):
         self.l = QVBoxLayout()
         self.setLayout(self.l)
 
-        self.serialLabel = QLabel('Kindle Serial numbers (separate with commas, no spaces)')
+        self.serialLabel = QLabel('eInk Kindle Serial numbers (First character B, 16 characters, use commas if more than one)')
         self.l.addWidget(self.serialLabel)
 
         self.serials = QLineEdit(self)
@@ -28,7 +28,7 @@ class ConfigWidget(QWidget):
         self.l.addWidget(self.serials)
         self.serialLabel.setBuddy(self.serials)
 
-        self.pidLabel = QLabel('Mobipocket PIDs (separate with commas, no spaces)')
+        self.pidLabel = QLabel('Mobipocket PIDs (8 or 10 characters, use commas if more than one)')
         self.l.addWidget(self.pidLabel)
 
         self.pids = QLineEdit(self)
@@ -50,8 +50,8 @@ class ConfigWidget(QWidget):
         self.wpLabel.setBuddy(self.wineprefix)
 
     def save_settings(self):
-        prefs['pids'] = str(self.pids.text())
-        prefs['serials'] = str(self.serials.text())
+       prefs['pids'] = str(self.pids.text()).replace(" ","")
+        prefs['serials'] = str(self.serials.text()).replace(" ","")
         winepref=str(self.wineprefix.text())
         if winepref.strip() != '':
             prefs['WINEPREFIX'] = winepref
index c4a6322643c97c1c773b25593d38eb88c1348a84..03858390b434858493ae0ab797b54d5f1e5b6954 100644 (file)
@@ -495,6 +495,7 @@ class CryptUnprotectDataV3(object):
 
 # Locate the .kindle-info files
 def getKindleInfoFiles(kInfoFiles):
+    found = False
     home = os.getenv('HOME')
     # search for any .kinf2011 files in new location (Sep 2012)
     cmdline = 'find "' + home + '/Library/Containers/com.amazon.Kindle/Data/Library/Application Support" -name ".kinf2011"'
@@ -525,7 +526,6 @@ def getKindleInfoFiles(kInfoFiles):
     out1, out2 = p1.communicate()
     reslst = out1.split('\n')
     kinfopath = 'NONE'
-    found = False
     for resline in reslst:
         if os.path.isfile(resline):
             kInfoFiles.append(resline)
index 1bd256234dfd4159fa15113db0efa2993ee80e5d..d491d7d8dd7e52c8fc98b1c3ad2b197e441521b9 100644 (file)
@@ -204,45 +204,62 @@ CryptUnprotectData = CryptUnprotectData()
 
 # Locate all of the kindle-info style files and return as list
 def getKindleInfoFiles(kInfoFiles):
-    regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
-    path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
-
     # some 64 bit machines do not have the proper registry key for some reason
     # or the pythonn interface to the 32 vs 64 bit registry is broken
+    path = ""
     if 'LOCALAPPDATA' in os.environ.keys():
         path = os.environ['LOCALAPPDATA']
-
-    print('searching for kinfoFiles in ' + path)
-    found = False
-
-    # first look for older kindle-info files
-    kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
-    if os.path.isfile(kinfopath):
-        found = True
-        print('Found K4PC kindle.info file: ' + kinfopath)
-        kInfoFiles.append(kinfopath)
-
-    # now look for newer (K4PC 1.5.0 and later rainier.2.1.1.kinf file
-
-    kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf'
-    if os.path.isfile(kinfopath):
-        found = True
-        print('Found K4PC 1.5.X kinf file: ' + kinfopath)
-        kInfoFiles.append(kinfopath)
-
-    # now look for even newer (K4PC 1.6.0 and later) rainier.2.1.1.kinf file
-    kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf'
-    if os.path.isfile(kinfopath):
-        found = True
-        print('Found K4PC 1.6.X kinf file: ' + kinfopath)
-        kInfoFiles.append(kinfopath)
-
-    # now look for even newer (K4PC 1.9.0 and later) .kinf2011 file
-    kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2011'
-    if os.path.isfile(kinfopath):
-        found = True
-        print('Found K4PC kinf2011 file: ' + kinfopath)
-        kInfoFiles.append(kinfopath)
+    else:
+        # User Shell Folders show take precedent over Shell Folders if present
+        try:
+            regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\User Shell Folders\\")
+            path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
+            if not os.path.isdir(path):
+                path = ""
+                try:
+                    regkey = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders\\")
+                    path = winreg.QueryValueEx(regkey, 'Local AppData')[0]
+                    if not os.path.isdir(path):
+                        path = ""
+                except RegError:
+                    pass
+        except RegError:
+            pass
+
+    found = False    
+    if path == "":
+        print ('Could not find the folder in which to look for kinfoFiles.')
+    else:
+        print('searching for kinfoFiles in ' + path)
+    
+        # first look for older kindle-info files
+        kinfopath = path +'\\Amazon\\Kindle For PC\\{AMAwzsaPaaZAzmZzZQzgZCAkZ3AjA_AY}\\kindle.info'
+        if os.path.isfile(kinfopath):
+            found = True
+            print('Found K4PC kindle.info file: ' + kinfopath)
+            kInfoFiles.append(kinfopath)
+    
+        # now look for newer (K4PC 1.5.0 and later rainier.2.1.1.kinf file
+    
+        kinfopath = path +'\\Amazon\\Kindle For PC\\storage\\rainier.2.1.1.kinf'
+        if os.path.isfile(kinfopath):
+            found = True
+            print('Found K4PC 1.5.X kinf file: ' + kinfopath)
+            kInfoFiles.append(kinfopath)
+    
+        # now look for even newer (K4PC 1.6.0 and later) rainier.2.1.1.kinf file
+        kinfopath = path +'\\Amazon\\Kindle\\storage\\rainier.2.1.1.kinf'
+        if os.path.isfile(kinfopath):
+            found = True
+            print('Found K4PC 1.6.X kinf file: ' + kinfopath)
+            kInfoFiles.append(kinfopath)
+    
+        # now look for even newer (K4PC 1.9.0 and later) .kinf2011 file
+        kinfopath = path +'\\Amazon\\Kindle\\storage\\.kinf2011'
+        if os.path.isfile(kinfopath):
+            found = True
+            print('Found K4PC kinf2011 file: ' + kinfopath)
+            kInfoFiles.append(kinfopath)
 
     if not found:
         print('No K4PC kindle.info/kinf/kinf2011 files have been found.')
index f1d8574d1bee52c60b39c5215703217336251e2b..4c65719afb5d94ee49ac19a46d5d71576f7a9dc7 100644 (file)
@@ -296,7 +296,7 @@ class TopazBook:
                 break
 
         if not bookKey:
-            raise TpzDRMError('Decryption Unsucessful; No valid pid found')
+            raise TpzDRMError("Topaz Book. No key found in " + str(len(pidlst)) + " keys tried. Please report this failure for help.")
 
         self.setBookKey(bookKey)
         self.createBookDirectory()
index ed13aa1b7917bd7923efa09367d1ef2304b472e7..21d6d2c5fc50f293a14815082971a772e56ac619 100644 (file)
@@ -52,7 +52,7 @@ class Process(object):
             self.__stdout_thread = threading.Thread(
                 name="stdout-thread",
                 target=self.__reader, args=(self.__collected_outdata,
-                                           self.__process.stdout))
+                                            self.__process.stdout))
             self.__stdout_thread.setDaemon(True)
             self.__stdout_thread.start()
 
@@ -60,7 +60,7 @@ class Process(object):
             self.__stderr_thread = threading.Thread(
                 name="stderr-thread",
                 target=self.__reader, args=(self.__collected_errdata,
-                                           self.__process.stderr))
+                                            self.__process.stderr))
             self.__stderr_thread.setDaemon(True)
             self.__stderr_thread.start()
 
diff --git a/Other_Tools/ePub_Fixer/lib/zipfilerugged.py b/Other_Tools/ePub_Fixer/lib/zipfilerugged.py
new file mode 100644 (file)
index 0000000..adf3c53
--- /dev/null
@@ -0,0 +1,1400 @@
+"""
+Read and write ZIP files.
+"""
+import struct, os, time, sys, shutil
+import binascii, cStringIO, stat
+import io
+import re
+
+try:
+    import zlib # We may need its compression method
+    crc32 = zlib.crc32
+except ImportError:
+    zlib = None
+    crc32 = binascii.crc32
+
+__all__ = ["BadZipfile", "error", "ZIP_STORED", "ZIP_DEFLATED", "is_zipfile",
+           "ZipInfo", "ZipFile", "PyZipFile", "LargeZipFile" ]
+
+class BadZipfile(Exception):
+    pass
+
+
+class LargeZipFile(Exception):
+    """
+    Raised when writing a zipfile, the zipfile requires ZIP64 extensions
+    and those extensions are disabled.
+    """
+
+error = BadZipfile      # The exception raised by this module
+
+ZIP64_LIMIT = (1 << 31) - 1
+ZIP_FILECOUNT_LIMIT = 1 << 16
+ZIP_MAX_COMMENT = (1 << 16) - 1
+
+# constants for Zip file compression methods
+ZIP_STORED = 0
+ZIP_DEFLATED = 8
+# Other ZIP compression methods not supported
+
+# Below are some formats and associated data for reading/writing headers using
+# the struct module.  The names and structures of headers/records are those used
+# in the PKWARE description of the ZIP file format:
+#     http://www.pkware.com/documents/casestudies/APPNOTE.TXT
+# (URL valid as of January 2008)
+
+# The "end of central directory" structure, magic number, size, and indices
+# (section V.I in the format document)
+structEndArchive = "<4s4H2LH"
+stringEndArchive = "PK\005\006"
+sizeEndCentDir = struct.calcsize(structEndArchive)
+
+_ECD_SIGNATURE = 0
+_ECD_DISK_NUMBER = 1
+_ECD_DISK_START = 2
+_ECD_ENTRIES_THIS_DISK = 3
+_ECD_ENTRIES_TOTAL = 4
+_ECD_SIZE = 5
+_ECD_OFFSET = 6
+_ECD_COMMENT_SIZE = 7
+# These last two indices are not part of the structure as defined in the
+# spec, but they are used internally by this module as a convenience
+_ECD_COMMENT = 8
+_ECD_LOCATION = 9
+
+# The "central directory" structure, magic number, size, and indices
+# of entries in the structure (section V.F in the format document)
+structCentralDir = "<4s4B4HL2L5H2L"
+stringCentralDir = "PK\001\002"
+sizeCentralDir = struct.calcsize(structCentralDir)
+
+# indexes of entries in the central directory structure
+_CD_SIGNATURE = 0
+_CD_CREATE_VERSION = 1
+_CD_CREATE_SYSTEM = 2
+_CD_EXTRACT_VERSION = 3
+_CD_EXTRACT_SYSTEM = 4
+_CD_FLAG_BITS = 5
+_CD_COMPRESS_TYPE = 6
+_CD_TIME = 7
+_CD_DATE = 8
+_CD_CRC = 9
+_CD_COMPRESSED_SIZE = 10
+_CD_UNCOMPRESSED_SIZE = 11
+_CD_FILENAME_LENGTH = 12
+_CD_EXTRA_FIELD_LENGTH = 13
+_CD_COMMENT_LENGTH = 14
+_CD_DISK_NUMBER_START = 15
+_CD_INTERNAL_FILE_ATTRIBUTES = 16
+_CD_EXTERNAL_FILE_ATTRIBUTES = 17
+_CD_LOCAL_HEADER_OFFSET = 18
+
+# The "local file header" structure, magic number, size, and indices
+# (section V.A in the format document)
+structFileHeader = "<4s2B4HL2L2H"
+stringFileHeader = "PK\003\004"
+sizeFileHeader = struct.calcsize(structFileHeader)
+
+_FH_SIGNATURE = 0
+_FH_EXTRACT_VERSION = 1
+_FH_EXTRACT_SYSTEM = 2
+_FH_GENERAL_PURPOSE_FLAG_BITS = 3
+_FH_COMPRESSION_METHOD = 4
+_FH_LAST_MOD_TIME = 5
+_FH_LAST_MOD_DATE = 6
+_FH_CRC = 7
+_FH_COMPRESSED_SIZE = 8
+_FH_UNCOMPRESSED_SIZE = 9
+_FH_FILENAME_LENGTH = 10
+_FH_EXTRA_FIELD_LENGTH = 11
+
+# The "Zip64 end of central directory locator" structure, magic number, and size
+structEndArchive64Locator = "<4sLQL"
+stringEndArchive64Locator = "PK\x06\x07"
+sizeEndCentDir64Locator = struct.calcsize(structEndArchive64Locator)
+
+# The "Zip64 end of central directory" record, magic number, size, and indices
+# (section V.G in the format document)
+structEndArchive64 = "<4sQ2H2L4Q"
+stringEndArchive64 = "PK\x06\x06"
+sizeEndCentDir64 = struct.calcsize(structEndArchive64)
+
+_CD64_SIGNATURE = 0
+_CD64_DIRECTORY_RECSIZE = 1
+_CD64_CREATE_VERSION = 2
+_CD64_EXTRACT_VERSION = 3
+_CD64_DISK_NUMBER = 4
+_CD64_DISK_NUMBER_START = 5
+_CD64_NUMBER_ENTRIES_THIS_DISK = 6
+_CD64_NUMBER_ENTRIES_TOTAL = 7
+_CD64_DIRECTORY_SIZE = 8
+_CD64_OFFSET_START_CENTDIR = 9
+
+def _check_zipfile(fp):
+    try:
+        if _EndRecData(fp):
+            return True         # file has correct magic number
+    except IOError:
+        pass
+    return False
+
+def is_zipfile(filename):
+    """Quickly see if a file is a ZIP file by checking the magic number.
+
+    The filename argument may be a file or file-like object too.
+    """
+    result = False
+    try:
+        if hasattr(filename, "read"):
+            result = _check_zipfile(fp=filename)
+        else:
+            with open(filename, "rb") as fp:
+                result = _check_zipfile(fp)
+    except IOError:
+        pass
+    return result
+
+def _EndRecData64(fpin, offset, endrec):
+    """
+    Read the ZIP64 end-of-archive records and use that to update endrec
+    """
+    fpin.seek(offset - sizeEndCentDir64Locator, 2)
+    data = fpin.read(sizeEndCentDir64Locator)
+    sig, diskno, reloff, disks = struct.unpack(structEndArchive64Locator, data)
+    if sig != stringEndArchive64Locator:
+        return endrec
+
+    if diskno != 0 or disks != 1:
+        raise BadZipfile("zipfiles that span multiple disks are not supported")
+
+    # Assume no 'zip64 extensible data'
+    fpin.seek(offset - sizeEndCentDir64Locator - sizeEndCentDir64, 2)
+    data = fpin.read(sizeEndCentDir64)
+    sig, sz, create_version, read_version, disk_num, disk_dir, \
+            dircount, dircount2, dirsize, diroffset = \
+            struct.unpack(structEndArchive64, data)
+    if sig != stringEndArchive64:
+        return endrec
+
+    # Update the original endrec using data from the ZIP64 record
+    endrec[_ECD_SIGNATURE] = sig
+    endrec[_ECD_DISK_NUMBER] = disk_num
+    endrec[_ECD_DISK_START] = disk_dir
+    endrec[_ECD_ENTRIES_THIS_DISK] = dircount
+    endrec[_ECD_ENTRIES_TOTAL] = dircount2
+    endrec[_ECD_SIZE] = dirsize
+    endrec[_ECD_OFFSET] = diroffset
+    return endrec
+
+
+def _EndRecData(fpin):
+    """Return data from the "End of Central Directory" record, or None.
+
+    The data is a list of the nine items in the ZIP "End of central dir"
+    record followed by a tenth item, the file seek offset of this record."""
+
+    # Determine file size
+    fpin.seek(0, 2)
+    filesize = fpin.tell()
+
+    # Check to see if this is ZIP file with no archive comment (the
+    # "end of central directory" structure should be the last item in the
+    # file if this is the case).
+    try:
+        fpin.seek(-sizeEndCentDir, 2)
+    except IOError:
+        return None
+    data = fpin.read()
+    if data[0:4] == stringEndArchive and data[-2:] == "\000\000":
+        # the signature is correct and there's no comment, unpack structure
+        endrec = struct.unpack(structEndArchive, data)
+        endrec=list(endrec)
+
+        # Append a blank comment and record start offset
+        endrec.append("")
+        endrec.append(filesize - sizeEndCentDir)
+
+        # Try to read the "Zip64 end of central directory" structure
+        return _EndRecData64(fpin, -sizeEndCentDir, endrec)
+
+    # Either this is not a ZIP file, or it is a ZIP file with an archive
+    # comment.  Search the end of the file for the "end of central directory"
+    # record signature. The comment is the last item in the ZIP file and may be
+    # up to 64K long.  It is assumed that the "end of central directory" magic
+    # number does not appear in the comment.
+    maxCommentStart = max(filesize - (1 << 16) - sizeEndCentDir, 0)
+    fpin.seek(maxCommentStart, 0)
+    data = fpin.read()
+    start = data.rfind(stringEndArchive)
+    if start >= 0:
+        # found the magic number; attempt to unpack and interpret
+        recData = data[start:start+sizeEndCentDir]
+        endrec = list(struct.unpack(structEndArchive, recData))
+        comment = data[start+sizeEndCentDir:]
+        # check that comment length is correct
+        if endrec[_ECD_COMMENT_SIZE] == len(comment):
+            # Append the archive comment and start offset
+            endrec.append(comment)
+            endrec.append(maxCommentStart + start)
+
+            # Try to read the "Zip64 end of central directory" structure
+            return _EndRecData64(fpin, maxCommentStart + start - filesize,
+                                 endrec)
+
+    # Unable to find a valid end of central directory structure
+    return
+
+
+class ZipInfo (object):
+    """Class with attributes describing each file in the ZIP archive."""
+
+    __slots__ = (
+            'orig_filename',
+            'filename',
+            'date_time',
+            'compress_type',
+            'comment',
+            'extra',
+            'create_system',
+            'create_version',
+            'extract_version',
+            'reserved',
+            'flag_bits',
+            'volume',
+            'internal_attr',
+            'external_attr',
+            'header_offset',
+            'CRC',
+            'compress_size',
+            'file_size',
+            '_raw_time',
+        )
+
+    def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)):
+        self.orig_filename = filename   # Original file name in archive
+
+        # Terminate the file name at the first null byte.  Null bytes in file
+        # names are used as tricks by viruses in archives.
+        null_byte = filename.find(chr(0))
+        if null_byte >= 0:
+            filename = filename[0:null_byte]
+        # This is used to ensure paths in generated ZIP files always use
+        # forward slashes as the directory separator, as required by the
+        # ZIP format specification.
+        if os.sep != "/" and os.sep in filename:
+            filename = filename.replace(os.sep, "/")
+
+        self.filename = filename        # Normalized file name
+        self.date_time = date_time      # year, month, day, hour, min, sec
+        # Standard values:
+        self.compress_type = ZIP_STORED # Type of compression for the file
+        self.comment = ""               # Comment for each file
+        self.extra = ""                 # ZIP extra data
+        if sys.platform == 'win32':
+            self.create_system = 0          # System which created ZIP archive
+        else:
+            # Assume everything else is unix-y
+            self.create_system = 3          # System which created ZIP archive
+        self.create_version = 20        # Version which created ZIP archive
+        self.extract_version = 20       # Version needed to extract archive
+        self.reserved = 0               # Must be zero
+        self.flag_bits = 0              # ZIP flag bits
+        self.volume = 0                 # Volume number of file header
+        self.internal_attr = 0          # Internal attributes
+        self.external_attr = 0          # External file attributes
+        # Other attributes are set by class ZipFile:
+        # header_offset         Byte offset to the file header
+        # CRC                   CRC-32 of the uncompressed file
+        # compress_size         Size of the compressed file
+        # file_size             Size of the uncompressed file
+
+    def FileHeader(self):
+        """Return the per-file header as a string."""
+        dt = self.date_time
+        dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2]
+        dostime = dt[3] << 11 | dt[4] << 5 | (dt[5] // 2)
+        if self.flag_bits & 0x08:
+            # Set these to zero because we write them after the file data
+            CRC = compress_size = file_size = 0
+        else:
+            CRC = self.CRC
+            compress_size = self.compress_size
+            file_size = self.file_size
+
+        extra = self.extra
+
+        if file_size > ZIP64_LIMIT or compress_size > ZIP64_LIMIT:
+            # File is larger than what fits into a 4 byte integer,
+            # fall back to the ZIP64 extension
+            fmt = '<HHQQ'
+            extra = extra + struct.pack(fmt,
+                    1, struct.calcsize(fmt)-4, file_size, compress_size)
+            file_size = 0xffffffff
+            compress_size = 0xffffffff
+            self.extract_version = max(45, self.extract_version)
+            self.create_version = max(45, self.extract_version)
+
+        filename, flag_bits = self._encodeFilenameFlags()
+        header = struct.pack(structFileHeader, stringFileHeader,
+                 self.extract_version, self.reserved, flag_bits,
+                 self.compress_type, dostime, dosdate, CRC,
+                 compress_size, file_size,
+                 len(filename), len(extra))
+        return header + filename + extra
+
+    def _encodeFilenameFlags(self):
+        if isinstance(self.filename, unicode):
+            try:
+                return self.filename.encode('ascii'), self.flag_bits
+            except UnicodeEncodeError:
+                return self.filename.encode('utf-8'), self.flag_bits | 0x800
+        else:
+            return self.filename, self.flag_bits
+
+    def _decodeFilename(self):
+        if self.flag_bits & 0x800:
+            try:
+                print "decoding filename",self.filename
+                return self.filename.decode('utf-8')
+            except:
+                return self.filename
+        else:
+            return self.filename
+
+    def _decodeExtra(self):
+        # Try to decode the extra field.
+        extra = self.extra
+        unpack = struct.unpack
+        while extra:
+            tp, ln = unpack('<HH', extra[:4])
+            if tp == 1:
+                if ln >= 24:
+                    counts = unpack('<QQQ', extra[4:28])
+                elif ln == 16:
+                    counts = unpack('<QQ', extra[4:20])
+                elif ln == 8:
+                    counts = unpack('<Q', extra[4:12])
+                elif ln == 0:
+                    counts = ()
+                else:
+                    raise RuntimeError, "Corrupt extra field %s"%(ln,)
+
+                idx = 0
+
+                # ZIP64 extension (large files and/or large archives)
+                if self.file_size in (0xffffffffffffffffL, 0xffffffffL):
+                    self.file_size = counts[idx]
+                    idx += 1
+
+                if self.compress_size == 0xFFFFFFFFL:
+                    self.compress_size = counts[idx]
+                    idx += 1
+
+                if self.header_offset == 0xffffffffL:
+                    old = self.header_offset
+                    self.header_offset = counts[idx]
+                    idx+=1
+
+            extra = extra[ln+4:]
+
+
+class _ZipDecrypter:
+    """Class to handle decryption of files stored within a ZIP archive.
+
+    ZIP supports a password-based form of encryption. Even though known
+    plaintext attacks have been found against it, it is still useful
+    to be able to get data out of such a file.
+
+    Usage:
+        zd = _ZipDecrypter(mypwd)
+        plain_char = zd(cypher_char)
+        plain_text = map(zd, cypher_text)
+    """
+
+    def _GenerateCRCTable():
+        """Generate a CRC-32 table.
+
+        ZIP encryption uses the CRC32 one-byte primitive for scrambling some
+        internal keys. We noticed that a direct implementation is faster than
+        relying on binascii.crc32().
+        """
+        poly = 0xedb88320
+        table = [0] * 256
+        for i in range(256):
+            crc = i
+            for j in range(8):
+                if crc & 1:
+                    crc = ((crc >> 1) & 0x7FFFFFFF) ^ poly
+                else:
+                    crc = ((crc >> 1) & 0x7FFFFFFF)
+            table[i] = crc
+        return table
+    crctable = _GenerateCRCTable()
+
+    def _crc32(self, ch, crc):
+        """Compute the CRC32 primitive on one byte."""
+        return ((crc >> 8) & 0xffffff) ^ self.crctable[(crc ^ ord(ch)) & 0xff]
+
+    def __init__(self, pwd):
+        self.key0 = 305419896
+        self.key1 = 591751049
+        self.key2 = 878082192
+        for p in pwd:
+            self._UpdateKeys(p)
+
+    def _UpdateKeys(self, c):
+        self.key0 = self._crc32(c, self.key0)
+        self.key1 = (self.key1 + (self.key0 & 255)) & 4294967295
+        self.key1 = (self.key1 * 134775813 + 1) & 4294967295
+        self.key2 = self._crc32(chr((self.key1 >> 24) & 255), self.key2)
+
+    def __call__(self, c):
+        """Decrypt a single character."""
+        c = ord(c)
+        k = self.key2 | 2
+        c = c ^ (((k * (k^1)) >> 8) & 255)
+        c = chr(c)
+        self._UpdateKeys(c)
+        return c
+
+class ZipExtFile(io.BufferedIOBase):
+    """File-like object for reading an archive member.
+       Is returned by ZipFile.open().
+    """
+
+    # Max size supported by decompressor.
+    MAX_N = 1 << 31 - 1
+
+    # Read from compressed files in 4k blocks.
+    MIN_READ_SIZE = 4096
+
+    # Search for universal newlines or line chunks.
+    PATTERN = re.compile(r'^(?P<chunk>[^\r\n]+)|(?P<newline>\n|\r\n?)')
+
+    def __init__(self, fileobj, mode, zipinfo, decrypter=None):
+        self._fileobj = fileobj
+        self._decrypter = decrypter
+
+        self._compress_type = zipinfo.compress_type
+        self._compress_size = zipinfo.compress_size
+        self._compress_left = zipinfo.compress_size
+
+        if self._compress_type == ZIP_DEFLATED:
+            self._decompressor = zlib.decompressobj(-15)
+        self._unconsumed = ''
+
+        self._readbuffer = ''
+        self._offset = 0
+
+        self._universal = 'U' in mode
+        self.newlines = None
+
+        # Adjust read size for encrypted files since the first 12 bytes
+        # are for the encryption/password information.
+        if self._decrypter is not None:
+            self._compress_left -= 12
+
+        self.mode = mode
+        self.name = zipinfo.filename
+
+    def readline(self, limit=-1):
+        """Read and return a line from the stream.
+
+        If limit is specified, at most limit bytes will be read.
+        """
+
+        if not self._universal and limit < 0:
+            # Shortcut common case - newline found in buffer.
+            i = self._readbuffer.find('\n', self._offset) + 1
+            if i > 0:
+                line = self._readbuffer[self._offset: i]
+                self._offset = i
+                return line
+
+        if not self._universal:
+            return io.BufferedIOBase.readline(self, limit)
+
+        line = ''
+        while limit < 0 or len(line) < limit:
+            readahead = self.peek(2)
+            if readahead == '':
+                return line
+
+            #
+            # Search for universal newlines or line chunks.
+            #
+            # The pattern returns either a line chunk or a newline, but not
+            # both. Combined with peek(2), we are assured that the sequence
+            # '\r\n' is always retrieved completely and never split into
+            # separate newlines - '\r', '\n' due to coincidental readaheads.
+            #
+            match = self.PATTERN.search(readahead)
+            newline = match.group('newline')
+            if newline is not None:
+                if self.newlines is None:
+                    self.newlines = []
+                if newline not in self.newlines:
+                    self.newlines.append(newline)
+                self._offset += len(newline)
+                return line + '\n'
+
+            chunk = match.group('chunk')
+            if limit >= 0:
+                chunk = chunk[: limit - len(line)]
+
+            self._offset += len(chunk)
+            line += chunk
+
+        return line
+
+    def peek(self, n=1):
+        """Returns buffered bytes without advancing the position."""
+        if n > len(self._readbuffer) - self._offset:
+            chunk = self.read(n)
+            self._offset -= len(chunk)
+
+        # Return up to 512 bytes to reduce allocation overhead for tight loops.
+        return self._readbuffer[self._offset: self._offset + 512]
+
+    def readable(self):
+        return True
+
+    def read(self, n=-1):
+        """Read and return up to n bytes.
+        If the argument is omitted, None, or negative, data is read and returned until EOF is reached..
+        """
+
+        buf = ''
+        while n < 0 or n is None or n > len(buf):
+            data = self.read1(n)
+            if len(data) == 0:
+                return buf
+
+            buf += data
+
+        return buf
+
+    def read1(self, n):
+        """Read up to n bytes with at most one read() system call."""
+
+        # Simplify algorithm (branching) by transforming negative n to large n.
+        if n < 0 or n is None:
+            n = self.MAX_N
+
+        # Bytes available in read buffer.
+        len_readbuffer = len(self._readbuffer) - self._offset
+
+        # Read from file.
+        if self._compress_left > 0 and n > len_readbuffer + len(self._unconsumed):
+            nbytes = n - len_readbuffer - len(self._unconsumed)
+            nbytes = max(nbytes, self.MIN_READ_SIZE)
+            nbytes = min(nbytes, self._compress_left)
+
+            data = self._fileobj.read(nbytes)
+            self._compress_left -= len(data)
+
+            if data and self._decrypter is not None:
+                data = ''.join(map(self._decrypter, data))
+
+            if self._compress_type == ZIP_STORED:
+                self._readbuffer = self._readbuffer[self._offset:] + data
+                self._offset = 0
+            else:
+                # Prepare deflated bytes for decompression.
+                self._unconsumed += data
+
+        # Handle unconsumed data.
+        if (len(self._unconsumed) > 0 and n > len_readbuffer and
+            self._compress_type == ZIP_DEFLATED):
+            data = self._decompressor.decompress(
+                self._unconsumed,
+                max(n - len_readbuffer, self.MIN_READ_SIZE)
+            )
+
+            self._unconsumed = self._decompressor.unconsumed_tail
+            if len(self._unconsumed) == 0 and self._compress_left == 0:
+                data += self._decompressor.flush()
+
+            self._readbuffer = self._readbuffer[self._offset:] + data
+            self._offset = 0
+
+        # Read from buffer.
+        data = self._readbuffer[self._offset: self._offset + n]
+        self._offset += len(data)
+        return data
+
+
+
+class ZipFile:
+    """ Class with methods to open, read, write, close, list zip files.
+
+    z = ZipFile(file, mode="r", compression=ZIP_STORED, allowZip64=False)
+
+    file: Either the path to the file, or a file-like object.
+          If it is a path, the file will be opened and closed by ZipFile.
+    mode: The mode can be either read "r", write "w" or append "a".
+    compression: ZIP_STORED (no compression) or ZIP_DEFLATED (requires zlib).
+    allowZip64: if True ZipFile will create files with ZIP64 extensions when
+                needed, otherwise it will raise an exception when this would
+                be necessary.
+
+    """
+
+    fp = None                   # Set here since __del__ checks it
+
+    def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=False):
+        """Open the ZIP file with mode read "r", write "w" or append "a"."""
+        if mode not in ("r", "w", "a"):
+            raise RuntimeError('ZipFile() requires mode "r", "w", or "a"')
+
+        if compression == ZIP_STORED:
+            pass
+        elif compression == ZIP_DEFLATED:
+            if not zlib:
+                raise RuntimeError,\
+                      "Compression requires the (missing) zlib module"
+        else:
+            raise RuntimeError, "That compression method is not supported"
+
+        self._allowZip64 = allowZip64
+        self._didModify = False
+        self.debug = 0  # Level of printing: 0 through 3
+        self.NameToInfo = {}    # Find file info given name
+        self.filelist = []      # List of ZipInfo instances for archive
+        self.compression = compression  # Method of compression
+        self.mode = key = mode.replace('b', '')[0]
+        self.pwd = None
+        self.comment = ''
+
+        # Check if we were passed a file-like object
+        if isinstance(file, basestring):
+            self._filePassed = 0
+            self.filename = file
+            modeDict = {'r' : 'rb', 'w': 'wb', 'a' : 'r+b'}
+            try:
+                self.fp = open(file, modeDict[mode])
+            except IOError:
+                if mode == 'a':
+                    mode = key = 'w'
+                    self.fp = open(file, modeDict[mode])
+                else:
+                    raise
+        else:
+            self._filePassed = 1
+            self.fp = file
+            self.filename = getattr(file, 'name', None)
+
+        if key == 'r':
+            self._GetContents()
+        elif key == 'w':
+            pass
+        elif key == 'a':
+            try:                        # See if file is a zip file
+                self._RealGetContents()
+                # seek to start of directory and overwrite
+                self.fp.seek(self.start_dir, 0)
+            except BadZipfile:          # file is not a zip file, just append
+                self.fp.seek(0, 2)
+        else:
+            if not self._filePassed:
+                self.fp.close()
+                self.fp = None
+            raise RuntimeError, 'Mode must be "r", "w" or "a"'
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, type, value, traceback):
+        self.close()
+
+    def _GetContents(self):
+        """Read the directory, making sure we close the file if the format
+        is bad."""
+        try:
+            self._RealGetContents()
+        except BadZipfile:
+            if not self._filePassed:
+                self.fp.close()
+                self.fp = None
+            raise
+
+    def _RealGetContents(self):
+        """Read in the table of contents for the ZIP file."""
+        fp = self.fp
+        endrec = _EndRecData(fp)
+        if not endrec:
+            raise BadZipfile, "File is not a zip file"
+        if self.debug > 1:
+            print endrec
+        size_cd = endrec[_ECD_SIZE]             # bytes in central directory
+        offset_cd = endrec[_ECD_OFFSET]         # offset of central directory
+        self.comment = endrec[_ECD_COMMENT]     # archive comment
+
+        # "concat" is zero, unless zip was concatenated to another file
+        concat = endrec[_ECD_LOCATION] - size_cd - offset_cd
+        if endrec[_ECD_SIGNATURE] == stringEndArchive64:
+            # If Zip64 extension structures are present, account for them
+            concat -= (sizeEndCentDir64 + sizeEndCentDir64Locator)
+
+        if self.debug > 2:
+            inferred = concat + offset_cd
+            print "given, inferred, offset", offset_cd, inferred, concat
+        # self.start_dir:  Position of start of central directory
+        self.start_dir = offset_cd + concat
+        fp.seek(self.start_dir, 0)
+        data = fp.read(size_cd)
+        fp = cStringIO.StringIO(data)
+        total = 0
+        while total < size_cd:
+            centdir = fp.read(sizeCentralDir)
+            if centdir[0:4] != stringCentralDir:
+                raise BadZipfile, "Bad magic number for central directory"
+            centdir = struct.unpack(structCentralDir, centdir)
+            if self.debug > 2:
+                print centdir
+            filename = fp.read(centdir[_CD_FILENAME_LENGTH])
+            # Create ZipInfo instance to store file information
+            x = ZipInfo(filename)
+            x.extra = fp.read(centdir[_CD_EXTRA_FIELD_LENGTH])
+            x.comment = fp.read(centdir[_CD_COMMENT_LENGTH])
+            x.header_offset = centdir[_CD_LOCAL_HEADER_OFFSET]
+            (x.create_version, x.create_system, x.extract_version, x.reserved,
+                x.flag_bits, x.compress_type, t, d,
+                x.CRC, x.compress_size, x.file_size) = centdir[1:12]
+            x.volume, x.internal_attr, x.external_attr = centdir[15:18]
+            # Convert date/time code to (year, month, day, hour, min, sec)
+            x._raw_time = t
+            x.date_time = ( (d>>9)+1980, (d>>5)&0xF, d&0x1F,
+                                     t>>11, (t>>5)&0x3F, (t&0x1F) * 2 )
+
+            x._decodeExtra()
+            x.header_offset = x.header_offset + concat
+            x.filename = x._decodeFilename()
+            self.filelist.append(x)
+            self.NameToInfo[x.filename] = x
+
+            # update total bytes read from central directory
+            total = (total + sizeCentralDir + centdir[_CD_FILENAME_LENGTH]
+                     + centdir[_CD_EXTRA_FIELD_LENGTH]
+                     + centdir[_CD_COMMENT_LENGTH])
+
+            if self.debug > 2:
+                print "total", total
+
+
+    def namelist(self):
+        """Return a list of file names in the archive."""
+        l = []
+        for data in self.filelist:
+            l.append(data.filename)
+        return l
+
+    def infolist(self):
+        """Return a list of class ZipInfo instances for files in the
+        archive."""
+        return self.filelist
+
+    def printdir(self):
+        """Print a table of contents for the zip file."""
+        print "%-46s %19s %12s" % ("File Name", "Modified    ", "Size")
+        for zinfo in self.filelist:
+            date = "%d-%02d-%02d %02d:%02d:%02d" % zinfo.date_time[:6]
+            print "%-46s %s %12d" % (zinfo.filename, date, zinfo.file_size)
+
+    def testzip(self):
+        """Read all the files and check the CRC."""
+        chunk_size = 2 ** 20
+        for zinfo in self.filelist:
+            try:
+                # Read by chunks, to avoid an OverflowError or a
+                # MemoryError with very large embedded files.
+                f = self.open(zinfo.filename, "r")
+                while f.read(chunk_size):     # Check CRC-32
+                    pass
+            except BadZipfile:
+                return zinfo.filename
+
+    def getinfo(self, name):
+        """Return the instance of ZipInfo given 'name'."""
+        info = self.NameToInfo.get(name)
+        if info is None:
+            raise KeyError(
+                'There is no item named %r in the archive' % name)
+
+        return info
+
+    def setpassword(self, pwd):
+        """Set default password for encrypted files."""
+        self.pwd = pwd
+
+    def read(self, name, pwd=None):
+        """Return file bytes (as a string) for name."""
+        return self.open(name, "r", pwd).read()
+
+    def open(self, name, mode="r", pwd=None):
+        """Return file-like object for 'name'."""
+        if mode not in ("r", "U", "rU"):
+            raise RuntimeError, 'open() requires mode "r", "U", or "rU"'
+        if not self.fp:
+            raise RuntimeError, \
+                  "Attempt to read ZIP archive that was already closed"
+
+        # Only open a new file for instances where we were not
+        # given a file object in the constructor
+        if self._filePassed:
+            zef_file = self.fp
+        else:
+            zef_file = open(self.filename, 'rb')
+
+        # Make sure we have an info object
+        if isinstance(name, ZipInfo):
+            # 'name' is already an info object
+            zinfo = name
+        else:
+            # Get info object for name
+            zinfo = self.getinfo(name)
+
+        zef_file.seek(zinfo.header_offset, 0)
+
+        # Skip the file header:
+        fheader = zef_file.read(sizeFileHeader)
+        if fheader[0:4] != stringFileHeader:
+            raise BadZipfile, "Bad magic number for file header"
+
+        fheader = struct.unpack(structFileHeader, fheader)
+        fname = zef_file.read(fheader[_FH_FILENAME_LENGTH])
+        if fheader[_FH_EXTRA_FIELD_LENGTH]:
+            zef_file.read(fheader[_FH_EXTRA_FIELD_LENGTH])
+
+        if fname != zinfo.orig_filename:
+            raise BadZipfile, \
+                      'File name in directory "%s" and header "%s" differ.' % (
+                          zinfo.orig_filename, fname)
+
+        # check for encrypted flag & handle password
+        is_encrypted = zinfo.flag_bits & 0x1
+        zd = None
+        if is_encrypted:
+            if not pwd:
+                pwd = self.pwd
+            if not pwd:
+                raise RuntimeError, "File %s is encrypted, " \
+                      "password required for extraction" % name
+
+            zd = _ZipDecrypter(pwd)
+            # The first 12 bytes in the cypher stream is an encryption header
+            #  used to strengthen the algorithm. The first 11 bytes are
+            #  completely random, while the 12th contains the MSB of the CRC,
+            #  or the MSB of the file time depending on the header type
+            #  and is used to check the correctness of the password.
+            bytes = zef_file.read(12)
+            h = map(zd, bytes[0:12])
+            if zinfo.flag_bits & 0x8:
+                # compare against the file type from extended local headers
+                check_byte = (zinfo._raw_time >> 8) & 0xff
+            else:
+                # compare against the CRC otherwise
+                check_byte = (zinfo.CRC >> 24) & 0xff
+            if ord(h[11]) != check_byte:
+                raise RuntimeError("Bad password for file", name)
+
+        return  ZipExtFile(zef_file, mode, zinfo, zd)
+
+    def extract(self, member, path=None, pwd=None):
+        """Extract a member from the archive to the current working directory,
+           using its full name. Its file information is extracted as accurately
+           as possible. `member' may be a filename or a ZipInfo object. You can
+           specify a different directory using `path'.
+        """
+        if not isinstance(member, ZipInfo):
+            member = self.getinfo(member)
+
+        if path is None:
+            path = os.getcwd()
+
+        return self._extract_member(member, path, pwd)
+
+    def extractall(self, path=None, members=None, pwd=None):
+        """Extract all members from the archive to the current working
+           directory. `path' specifies a different directory to extract to.
+           `members' is optional and must be a subset of the list returned
+           by namelist().
+        """
+        if members is None:
+            members = self.namelist()
+
+        for zipinfo in members:
+            self.extract(zipinfo, path, pwd)
+
+    def _extract_member(self, member, targetpath, pwd):
+        """Extract the ZipInfo object 'member' to a physical
+           file on the path targetpath.
+        """
+        # build the destination pathname, replacing
+        # forward slashes to platform specific separators.
+        # Strip trailing path separator, unless it represents the root.
+        if (targetpath[-1:] in (os.path.sep, os.path.altsep)
+            and len(os.path.splitdrive(targetpath)[1]) > 1):
+            targetpath = targetpath[:-1]
+
+        # don't include leading "/" from file name if present
+        if member.filename[0] == '/':
+            targetpath = os.path.join(targetpath, member.filename[1:])
+        else:
+            targetpath = os.path.join(targetpath, member.filename)
+
+        targetpath = os.path.normpath(targetpath)
+
+        # Create all upper directories if necessary.
+        upperdirs = os.path.dirname(targetpath)
+        if upperdirs and not os.path.exists(upperdirs):
+            os.makedirs(upperdirs)
+
+        if member.filename[-1] == '/':
+            if not os.path.isdir(targetpath):
+                os.mkdir(targetpath)
+            return targetpath
+
+        source = self.open(member, pwd=pwd)
+        target = file(targetpath, "wb")
+        shutil.copyfileobj(source, target)
+        source.close()
+        target.close()
+
+        return targetpath
+
+    def _writecheck(self, zinfo):
+        """Check for errors before writing a file to the archive."""
+        if zinfo.filename in self.NameToInfo:
+            if self.debug:      # Warning for duplicate names
+                print "Duplicate name:", zinfo.filename
+        if self.mode not in ("w", "a"):
+            raise RuntimeError, 'write() requires mode "w" or "a"'
+        if not self.fp:
+            raise RuntimeError, \
+                  "Attempt to write ZIP archive that was already closed"
+        if zinfo.compress_type == ZIP_DEFLATED and not zlib:
+            raise RuntimeError, \
+                  "Compression requires the (missing) zlib module"
+        if zinfo.compress_type not in (ZIP_STORED, ZIP_DEFLATED):
+            raise RuntimeError, \
+                  "That compression method is not supported"
+        if zinfo.file_size > ZIP64_LIMIT:
+            if not self._allowZip64:
+                raise LargeZipFile("Filesize would require ZIP64 extensions")
+        if zinfo.header_offset > ZIP64_LIMIT:
+            if not self._allowZip64:
+                raise LargeZipFile("Zipfile size would require ZIP64 extensions")
+
+    def write(self, filename, arcname=None, compress_type=None):
+        """Put the bytes from filename into the archive under the name
+        arcname."""
+        if not self.fp:
+            raise RuntimeError(
+                  "Attempt to write to ZIP archive that was already closed")
+
+        st = os.stat(filename)
+        isdir = stat.S_ISDIR(st.st_mode)
+        mtime = time.localtime(st.st_mtime)
+        date_time = mtime[0:6]
+        # Create ZipInfo instance to store file information
+        if arcname is None:
+            arcname = filename
+        arcname = os.path.normpath(os.path.splitdrive(arcname)[1])
+        while arcname[0] in (os.sep, os.altsep):
+            arcname = arcname[1:]
+        if isdir:
+            arcname += '/'
+        zinfo = ZipInfo(arcname, date_time)
+        zinfo.external_attr = (st[0] & 0xFFFF) << 16L      # Unix attributes
+        if compress_type is None:
+            zinfo.compress_type = self.compression
+        else:
+            zinfo.compress_type = compress_type
+
+        zinfo.file_size = st.st_size
+        zinfo.flag_bits = 0x00
+        zinfo.header_offset = self.fp.tell()    # Start of header bytes
+
+        self._writecheck(zinfo)
+        self._didModify = True
+
+        if isdir:
+            zinfo.file_size = 0
+            zinfo.compress_size = 0
+            zinfo.CRC = 0
+            self.filelist.append(zinfo)
+            self.NameToInfo[zinfo.filename] = zinfo
+            self.fp.write(zinfo.FileHeader())
+            return
+
+        with open(filename, "rb") as fp:
+            # Must overwrite CRC and sizes with correct data later
+            zinfo.CRC = CRC = 0
+            zinfo.compress_size = compress_size = 0
+            zinfo.file_size = file_size = 0
+            self.fp.write(zinfo.FileHeader())
+            if zinfo.compress_type == ZIP_DEFLATED:
+                cmpr = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION,
+                     zlib.DEFLATED, -15)
+            else:
+                cmpr = None
+            while 1:
+                buf = fp.read(1024 * 8)
+                if not buf:
+                    break
+                file_size = file_size + len(buf)
+                CRC = crc32(buf, CRC) & 0xffffffff
+                if cmpr:
+                    buf = cmpr.compress(buf)
+                    compress_size = compress_size + len(buf)
+                self.fp.write(buf)
+        if cmpr:
+            buf = cmpr.flush()
+            compress_size = compress_size + len(buf)
+            self.fp.write(buf)
+            zinfo.compress_size = compress_size
+        else:
+            zinfo.compress_size = file_size
+        zinfo.CRC = CRC
+        zinfo.file_size = file_size
+        # Seek backwards and write CRC and file sizes
+        position = self.fp.tell()       # Preserve current position in file
+        self.fp.seek(zinfo.header_offset + 14, 0)
+        self.fp.write(struct.pack("<LLL", zinfo.CRC, zinfo.compress_size,
+              zinfo.file_size))
+        self.fp.seek(position, 0)
+        self.filelist.append(zinfo)
+        self.NameToInfo[zinfo.filename] = zinfo
+
+    def writestr(self, zinfo_or_arcname, bytes, compress_type=None):
+        """Write a file into the archive.  The contents is the string
+        'bytes'.  'zinfo_or_arcname' is either a ZipInfo instance or
+        the name of the file in the archive."""
+        if not isinstance(zinfo_or_arcname, ZipInfo):
+            zinfo = ZipInfo(filename=zinfo_or_arcname,
+                            date_time=time.localtime(time.time())[:6])
+
+            zinfo.compress_type = self.compression
+            zinfo.external_attr = 0600 << 16
+        else:
+            zinfo = zinfo_or_arcname
+
+        if not self.fp:
+            raise RuntimeError(
+                  "Attempt to write to ZIP archive that was already closed")
+
+        if compress_type is not None:
+            zinfo.compress_type = compress_type
+
+        zinfo.file_size = len(bytes)            # Uncompressed size
+        zinfo.header_offset = self.fp.tell()    # Start of header bytes
+        self._writecheck(zinfo)
+        self._didModify = True
+        zinfo.CRC = crc32(bytes) & 0xffffffff       # CRC-32 checksum
+        if zinfo.compress_type == ZIP_DEFLATED:
+            co = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION,
+                 zlib.DEFLATED, -15)
+            bytes = co.compress(bytes) + co.flush()
+            zinfo.compress_size = len(bytes)    # Compressed size
+        else:
+            zinfo.compress_size = zinfo.file_size
+        zinfo.header_offset = self.fp.tell()    # Start of header bytes
+        self.fp.write(zinfo.FileHeader())
+        self.fp.write(bytes)
+        self.fp.flush()
+        if zinfo.flag_bits & 0x08:
+            # Write CRC and file sizes after the file data
+            self.fp.write(struct.pack("<LLL", zinfo.CRC, zinfo.compress_size,
+                  zinfo.file_size))
+        self.filelist.append(zinfo)
+        self.NameToInfo[zinfo.filename] = zinfo
+
+    def __del__(self):
+        """Call the "close()" method in case the user forgot."""
+        self.close()
+
+    def close(self):
+        """Close the file, and for mode "w" and "a" write the ending
+        records."""
+        if self.fp is None:
+            return
+
+        if self.mode in ("w", "a") and self._didModify: # write ending records
+            count = 0
+            pos1 = self.fp.tell()
+            for zinfo in self.filelist:         # write central directory
+                count = count + 1
+                dt = zinfo.date_time
+                dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2]
+                dostime = dt[3] << 11 | dt[4] << 5 | (dt[5] // 2)
+                extra = []
+                if zinfo.file_size > ZIP64_LIMIT \
+                        or zinfo.compress_size > ZIP64_LIMIT:
+                    extra.append(zinfo.file_size)
+                    extra.append(zinfo.compress_size)
+                    file_size = 0xffffffff
+                    compress_size = 0xffffffff
+                else:
+                    file_size = zinfo.file_size
+                    compress_size = zinfo.compress_size
+
+                if zinfo.header_offset > ZIP64_LIMIT:
+                    extra.append(zinfo.header_offset)
+                    header_offset = 0xffffffffL
+                else:
+                    header_offset = zinfo.header_offset
+
+                extra_data = zinfo.extra
+                if extra:
+                    # Append a ZIP64 field to the extra's
+                    extra_data = struct.pack(
+                            '<HH' + 'Q'*len(extra),
+                            1, 8*len(extra), *extra) + extra_data
+
+                    extract_version = max(45, zinfo.extract_version)
+                    create_version = max(45, zinfo.create_version)
+                else:
+                    extract_version = zinfo.extract_version
+                    create_version = zinfo.create_version
+
+                try:
+                    filename, flag_bits = zinfo._encodeFilenameFlags()
+                    centdir = struct.pack(structCentralDir,
+                     stringCentralDir, create_version,
+                     zinfo.create_system, extract_version, zinfo.reserved,
+                     flag_bits, zinfo.compress_type, dostime, dosdate,
+                     zinfo.CRC, compress_size, file_size,
+                     len(filename), len(extra_data), len(zinfo.comment),
+                     0, zinfo.internal_attr, zinfo.external_attr,
+                     header_offset)
+                except DeprecationWarning:
+                    print >>sys.stderr, (structCentralDir,
+                     stringCentralDir, create_version,
+                     zinfo.create_system, extract_version, zinfo.reserved,
+                     zinfo.flag_bits, zinfo.compress_type, dostime, dosdate,
+                     zinfo.CRC, compress_size, file_size,
+                     len(zinfo.filename), len(extra_data), len(zinfo.comment),
+                     0, zinfo.internal_attr, zinfo.external_attr,
+                     header_offset)
+                    raise
+                self.fp.write(centdir)
+                self.fp.write(filename)
+                self.fp.write(extra_data)
+                self.fp.write(zinfo.comment)
+
+            pos2 = self.fp.tell()
+            # Write end-of-zip-archive record
+            centDirCount = count
+            centDirSize = pos2 - pos1
+            centDirOffset = pos1
+            if (centDirCount >= ZIP_FILECOUNT_LIMIT or
+                centDirOffset > ZIP64_LIMIT or
+                centDirSize > ZIP64_LIMIT):
+                # Need to write the ZIP64 end-of-archive records
+                zip64endrec = struct.pack(
+                        structEndArchive64, stringEndArchive64,
+                        44, 45, 45, 0, 0, centDirCount, centDirCount,
+                        centDirSize, centDirOffset)
+                self.fp.write(zip64endrec)
+
+                zip64locrec = struct.pack(
+                        structEndArchive64Locator,
+                        stringEndArchive64Locator, 0, pos2, 1)
+                self.fp.write(zip64locrec)
+                centDirCount = min(centDirCount, 0xFFFF)
+                centDirSize = min(centDirSize, 0xFFFFFFFF)
+                centDirOffset = min(centDirOffset, 0xFFFFFFFF)
+
+            # check for valid comment length
+            if len(self.comment) >= ZIP_MAX_COMMENT:
+                if self.debug > 0:
+                    msg = 'Archive comment is too long; truncating to %d bytes' \
+                          % ZIP_MAX_COMMENT
+                self.comment = self.comment[:ZIP_MAX_COMMENT]
+
+            endrec = struct.pack(structEndArchive, stringEndArchive,
+                                 0, 0, centDirCount, centDirCount,
+                                 centDirSize, centDirOffset, len(self.comment))
+            self.fp.write(endrec)
+            self.fp.write(self.comment)
+            self.fp.flush()
+
+        if not self._filePassed:
+            self.fp.close()
+        self.fp = None
+
+
+class PyZipFile(ZipFile):
+    """Class to create ZIP archives with Python library files and packages."""
+
+    def writepy(self, pathname, basename = ""):
+        """Add all files from "pathname" to the ZIP archive.
+
+        If pathname is a package directory, search the directory and
+        all package subdirectories recursively for all *.py and enter
+        the modules into the archive.  If pathname is a plain
+        directory, listdir *.py and enter all modules.  Else, pathname
+        must be a Python *.py file and the module will be put into the
+        archive.  Added modules are always module.pyo or module.pyc.
+        This method will compile the module.py into module.pyc if
+        necessary.
+        """
+        dir, name = os.path.split(pathname)
+        if os.path.isdir(pathname):
+            initname = os.path.join(pathname, "__init__.py")
+            if os.path.isfile(initname):
+                # This is a package directory, add it
+                if basename:
+                    basename = "%s/%s" % (basename, name)
+                else:
+                    basename = name
+                if self.debug:
+                    print "Adding package in", pathname, "as", basename
+                fname, arcname = self._get_codename(initname[0:-3], basename)
+                if self.debug:
+                    print "Adding", arcname
+                self.write(fname, arcname)
+                dirlist = os.listdir(pathname)
+                dirlist.remove("__init__.py")
+                # Add all *.py files and package subdirectories
+                for filename in dirlist:
+                    path = os.path.join(pathname, filename)
+                    root, ext = os.path.splitext(filename)
+                    if os.path.isdir(path):
+                        if os.path.isfile(os.path.join(path, "__init__.py")):
+                            # This is a package directory, add it
+                            self.writepy(path, basename)  # Recursive call
+                    elif ext == ".py":
+                        fname, arcname = self._get_codename(path[0:-3],
+                                         basename)
+                        if self.debug:
+                            print "Adding", arcname
+                        self.write(fname, arcname)
+            else:
+                # This is NOT a package directory, add its files at top level
+                if self.debug:
+                    print "Adding files from directory", pathname
+                for filename in os.listdir(pathname):
+                    path = os.path.join(pathname, filename)
+                    root, ext = os.path.splitext(filename)
+                    if ext == ".py":
+                        fname, arcname = self._get_codename(path[0:-3],
+                                         basename)
+                        if self.debug:
+                            print "Adding", arcname
+                        self.write(fname, arcname)
+        else:
+            if pathname[-3:] != ".py":
+                raise RuntimeError, \
+                      'Files added with writepy() must end with ".py"'
+            fname, arcname = self._get_codename(pathname[0:-3], basename)
+            if self.debug:
+                print "Adding file", arcname
+            self.write(fname, arcname)
+
+    def _get_codename(self, pathname, basename):
+        """Return (filename, archivename) for the path.
+
+        Given a module name path, return the correct file path and
+        archive name, compiling if necessary.  For example, given
+        /python/lib/string, return (/python/lib/string.pyc, string).
+        """
+        file_py  = pathname + ".py"
+        file_pyc = pathname + ".pyc"
+        file_pyo = pathname + ".pyo"
+        if os.path.isfile(file_pyo) and \
+                            os.stat(file_pyo).st_mtime >= os.stat(file_py).st_mtime:
+            fname = file_pyo    # Use .pyo file
+        elif not os.path.isfile(file_pyc) or \
+             os.stat(file_pyc).st_mtime < os.stat(file_py).st_mtime:
+            import py_compile
+            if self.debug:
+                print "Compiling", file_py
+            try:
+                py_compile.compile(file_py, file_pyc, None, True)
+            except py_compile.PyCompileError,err:
+                print err.msg
+            fname = file_pyc
+        else:
+            fname = file_pyc
+        archivename = os.path.split(fname)[1]
+        if basename:
+            archivename = "%s/%s" % (basename, archivename)
+        return (fname, archivename)
+
+
+def main(args = None):
+    import textwrap
+    USAGE=textwrap.dedent("""\
+        Usage:
+            zipfile.py -l zipfile.zip        # Show listing of a zipfile
+            zipfile.py -t zipfile.zip        # Test if a zipfile is valid
+            zipfile.py -e zipfile.zip target # Extract zipfile into target dir
+            zipfile.py -c zipfile.zip src ... # Create zipfile from sources
+        """)
+    if args is None:
+        args = sys.argv[1:]
+
+    if not args or args[0] not in ('-l', '-c', '-e', '-t'):
+        print USAGE
+        sys.exit(1)
+
+    if args[0] == '-l':
+        if len(args) != 2:
+            print USAGE
+            sys.exit(1)
+        zf = ZipFile(args[1], 'r')
+        zf.printdir()
+        zf.close()
+
+    elif args[0] == '-t':
+        if len(args) != 2:
+            print USAGE
+            sys.exit(1)
+        zf = ZipFile(args[1], 'r')
+        zf.testzip()
+        print "Done testing"
+
+    elif args[0] == '-e':
+        if len(args) != 3:
+            print USAGE
+            sys.exit(1)
+
+        zf = ZipFile(args[1], 'r')
+        out = args[2]
+        for path in zf.namelist():
+            if path.startswith('./'):
+                tgt = os.path.join(out, path[2:])
+            else:
+                tgt = os.path.join(out, path)
+
+            tgtdir = os.path.dirname(tgt)
+            if not os.path.exists(tgtdir):
+                os.makedirs(tgtdir)
+            with open(tgt, 'wb') as fp:
+                fp.write(zf.read(path))
+        zf.close()
+
+    elif args[0] == '-c':
+        if len(args) < 3:
+            print USAGE
+            sys.exit(1)
+
+        def addToZip(zf, path, zippath):
+            if os.path.isfile(path):
+                zf.write(path, zippath, ZIP_DEFLATED)
+            elif os.path.isdir(path):
+                for nm in os.listdir(path):
+                    addToZip(zf,
+                            os.path.join(path, nm), os.path.join(zippath, nm))
+            # else: ignore
+
+        zf = ZipFile(args[1], 'w', allowZip64=True)
+        for src in args[2:]:
+            addToZip(zf, src, os.path.basename(src))
+
+        zf.close()
+
+if __name__ == "__main__":
+    main()
index 523ef1a2109cb6d41dcaaec8175672484cccff9c..c7921f2485e5189fd25f6dbf9e90a3c630fbbd96 100644 (file)
@@ -2,7 +2,7 @@
 
 import sys
 import zlib
-import zipfile
+import zipfilerugged
 import os
 import os.path
 import getopt
@@ -15,7 +15,7 @@ _FILENAME_OFFSET = 30
 _MAX_SIZE = 64 * 1024
 _MIMETYPE = 'application/epub+zip'
 
-class ZipInfo(zipfile.ZipInfo):
+class ZipInfo(zipfilerugged.ZipInfo):
     def __init__(self, *args, **kwargs):
         if 'compress_type' in kwargs:
             compress_type = kwargs.pop('compress_type')
@@ -27,10 +27,14 @@ class fixZip:
         self.ztype = 'zip'
         if zinput.lower().find('.epub') >= 0 :
             self.ztype = 'epub'
-        self.inzip = zipfile.ZipFile(zinput,'r')
-        self.outzip = zipfile.ZipFile(zoutput,'w')
+        print "opening input"
+        self.inzip = zipfilerugged.ZipFile(zinput,'r')
+        print "opening outout"
+        self.outzip = zipfilerugged.ZipFile(zoutput,'w')
+        print "opening input as raw file"
         # open the input zip for reading only as a raw file
         self.bzf = file(zinput,'rb')
+        print "finished initialising"
 
     def getlocalname(self, zi):
         local_header_offset = zi.header_offset
@@ -76,11 +80,11 @@ class fixZip:
         data = None
 
         # if not compressed we are good to go
-        if zi.compress_type == zipfile.ZIP_STORED:
+        if zi.compress_type == zipfilerugged.ZIP_STORED:
             data = self.bzf.read(zi.file_size)
 
         # if compressed we must decompress it using zlib
-        if zi.compress_type == zipfile.ZIP_DEFLATED:
+        if zi.compress_type == zipfilerugged.ZIP_DEFLATED:
             cmpdata = self.bzf.read(zi.compress_size)
             data = self.uncompress(cmpdata)
 
@@ -95,7 +99,7 @@ class fixZip:
 
         # if epub write mimetype file first, with no compression
         if self.ztype == 'epub':
-            nzinfo = ZipInfo('mimetype', compress_type=zipfile.ZIP_STORED)
+            nzinfo = ZipInfo('mimetype', compress_type=zipfilerugged.ZIP_STORED)
             self.outzip.writestr(nzinfo, _MIMETYPE)
 
         # write the rest of the files
@@ -105,7 +109,7 @@ class fixZip:
                 nzinfo = zinfo
                 try:
                     data = self.inzip.read(zinfo.filename)
-                except zipfile.BadZipfile or zipfile.error:
+                except zipfilerugged.BadZipfile or zipfilerugged.error:
                     local_name = self.getlocalname(zinfo)
                     data = self.getfiledata(zinfo)
                     nzinfo.filename = local_name
index 4d1ae44543db6e18195f4607c6373ae5f266ffe3..8f5e1b919b403c80185f282743b802fee0aaa108 100644 (file)
@@ -14,7 +14,7 @@ The second script, Pml2HTML.pyw, converts the PML file extracted by the first sc
 
 The last script is eReaderPDB2PMLZ.pyw and it removes the DRM and extracts the PML and images from the ebook into a zip archive that can be directly imported into Calibre.
 
-All of these scripts are gui python programs.   Python 2.X (32 bit) is already installed in Mac OSX.  We recommend ActiveState's Active Python Version 2.X (32 bit) for Windows users.
+All of these scripts are gui python programs.   Python 2.5 or later (32 bit) is already installed in Mac OSX 10.5 and later.  We recommend ActiveState's Active Python Version 2.5 or later (32 bit) for Windows users.
 
 Simply double-click to launch these applications and follow along.
 
index c798ae68206db452e1a38455d2931df13e57254e..1dc6b734543e89c79976cc37c620cf8d594e4737 100644 (file)
@@ -1,29 +1,43 @@
 Welcome to the tools!
 =====================
 
-This ReadMe_First.txt is meant to give users a quick overview of what is available and how to get started. This document is part of the Tools v5.3.1 archive.
+This ReadMe_First.txt is meant to give users a quick overview of what is available and how to get started. This document is part of the Tools v5.4 archive.
 
 The is archive includes tools to remove DRM from:
 
- - Kindle ebooks (including Mobi, Topaz, Print Replica and KF8).
+ - Kindle ebooks (Mobi, Topaz, Print Replica and KF8).
  - Barnes and Noble ePubs
- - Adobe Digital Editions ePubs
+ - Adobe Digital Editions ePubs (including Sony and Kobo ePubs downloaded to ADE)
  - Adobe Digital Editions PDFs
  - Mobipocket ebooks
  - eReader PDB books
 
-These tools do NOT work with Apple's iBooks FairPlay DRM.
+These tools do NOT work with Apple's iBooks FairPlay DRM (see end of this file.)
 
-The only tool that removes Apple's iBooks Fairplay DRM that is Requiem by Brahms version 3.3 or later. Requiem is NOT included in this tools package. It is under active development because Apple constantly updates its DRM scheme to stop Requiem from working.
-The latest version as of September 2012 is 3.3.5 and works with iTunes 10.5 and above.
 
-Requiem has a Tor website: http://tag3ulp55xczs3pn.onion. To reach the site using Tor, you will need to install Tor (http://www.torproject.org). If you're willing to sacrifice your anonymity, you can use the regular web with tor2web. Just go to http://tag3ulp55xczs3pn.tor2web.com.
+About the tools
+---------------
+These tools have been updated and maintained by Apprentice Alf, DiapDealer and some_updates.
+
+You can find the latest updates and get support at Apprentice Alf's blog: http://www.apprenticealf.wordpress.com/
+
+If you re-post these tools, a link to the blog would be appreciated.
+
+The original inept and ignoble scripts were by I♥cabbages
+The original mobidedrm and erdr2pml scripts were by The Dark Reverser
+The original topaz DRM removal script was by CMBDTC
+The original topaz format conversion scripts were by some_updates, clarknova and Bart Simpson
+
+The calibre plugin conversions were originally by DiapDealer
+The DeDRM AppleScript application was by Apprentice Alf
+The DeDRM python GUI was by some_updates
 
+Many fixes, updates and enhancements to the scripts and applicatons have been by Apprentice Alf, some_updates and DiapDealer.
 
 
 Calibre Users (Mac OS X, Windows, and Linux)
 --------------------------------------------
-If you are a calibre user, the quickest and easiest way to remove DRM from your ebooks is to install each of the plugins in the Calibre_Plugins folder, following the instructions and configuration directions provided in each plugin's ReadMe file.
+If you are a calibre user, the quickest and easiest way, especially on Windows, to remove DRM from your ebooks is to install each of the plugins in the Calibre_Plugins folder, following the instructions and configuration directions provided in each plugin's ReadMe file.
 
 Once installed and configured, you can simply add a DRM book to calibre and the DeDRMed version will be imported into the calibre database. Note that DRM removal ONLY occurs on import. If you have already imported DRM books you'll need to remove them from calibre and re-import them.
 
@@ -31,13 +45,16 @@ These plugins work for Windows, Mac OS X and Linux. For ebooks from Kindle 4 PC
 
 
 
-DeDRM application for Mac OS X users: (Mac OS X 10.5 and above)
+DeDRM application for Mac OS X users: (Mac OS X 10.4 and above)
 ----------------------------------------------------------------------
-Drag the "DeDRM 5.3.1.app" application from the DeDRM_Applications/Macintosh folder to your Desktop (or your Applications Folder, or anywhere else you find convenient). Double-click on the application to run it and it will guide you through collecting the data it needs to remove the DRM from any of the kinds of DRMed ebook listed in the first section of this ReadMe.
+This application combines all the tools into one easy-to-use tool for Mac OS X users.
+
+Drag the "DeDRM 5.4.app" application from the DeDRM_Applications/Macintosh folder to your Desktop (or your Applications Folder, or anywhere else you find convenient). Double-click on the application to run it and it will guide you through collecting the data it needs to remove the DRM from any of the kinds of DRMed ebook listed in the first section of this ReadMe.
 
 To use the DeDRM application, simply drag ebooks, or folders containing ebooks, onto the DeDRM application and it will remove the DRM of the kinds listed above.
 
-For more detailed instructions, see the "DeDRM ReadMe.rtf" file in the DeDRM_Applications/Macintosh folder.
+For more detailed instructions, see the "DeDRM ReadMe.rtf" file in the DeDRM_Applications/Macintosh folder, including details of the extra step that Mac OS X 10.4 users need to take to use the application.
+
 
 
 
@@ -46,7 +63,9 @@ DeDRM application for Windows users: (Windows XP through Windows 7)
 ***This program requires that Python and PyCrypto be properly installed.***
 ***See below for details on recommended versions are where to get them.***
 
-Unzip the DeDRM_5.3_Win.zip archive that's in the DeDRM_Applications/Windows folder, saving the resulting DeDRM_5.3_Win folder in your "My Documents" folder (or anywhere else you find convenient). Make a short-cut on your Desktop of the DeDRM_Drop_Target.bat file that's in the DeDRM_5.3_Win folder. Double-click on the shortcut and the DeDRM application will run and guide you through collecting the data it needs to remove the DRM from any of the kinds of DRMed ebook listed in the first section of this ReadMe.
+This application combines all the tools into one easy-to-use tool for Windows users.
+
+Drag the DeDRM_5.4 folder that's in the DeDRM_Applications/Windows folder, to your "My Documents" folder (or anywhere else you find convenient). Make a short-cut on your Desktop of the DeDRM_Drop_Target.bat file that's in the DeDRM_5.4 folder. Double-click on the shortcut and the DeDRM application will run and guide you through collecting the data it needs to remove the DRM from any of the kinds of DRMed ebook listed in the first section of this ReadMe.
 
 To use the DeDRM application, simply drag ebooks, or folders containing ebooks, onto the DeDRM_Drop_Target.bat shortcut and it will remove the DRM of the kinds listed above.
 
@@ -54,27 +73,32 @@ For more detailed instructions, see the DeDRM_ReadMe.txt file in the DeDRM_Appli
 
 
 
+Kindle_for_Android_Patches
+--------------------------
+Definitely only for the adventurous, this folder contains information on how to modify the Kindel for Android app to b able to get a PID for use with the other Kindle tools (DeDRM apps and calibre plugin).
+
+
 Other_Tools
 -----------
 There are a number of other python based tools that have graphical user interfaces to make them easy to use. To use any of these tools, you need to have Python 2.5, 2.6, or 2.7 for 32 bits installed on your machine as well as a matching PyCrypto or OpenSSL for some tools.
 
 On Mac OS X (10.5, 10.6 and 10.7), your systems already have the proper Python and OpenSSL installed. So nothing need be done, you can already run these tools by double-clicking on the .pyw python scripts.
 
-Users of Mac OS X 10.3 and 10.4, need to download and install the "32-bit Mac Installer disk Image (2.7.X) for OS X 10.3 and later from http://www.python.org/download/releases/2.7.1/
+Users of Mac OS X 10.3 and 10.4, need to download and install the "32-bit Mac Installer disk Image (2.7.3) for OS X 10.3 and later from http://www.python.org/ftp/python/2.7.3/python-2.7.3-macosx10.3.dmg.
 
 On Windows, you need to install a 32 bit version of Python (even on Windows 64) plus a matching 32 bit version of PyCrypto *OR* OpenSSL. We ***strongly*** recommend the free community edition of ActiveState's Active Python version. See the end of this document for details.
 
-Linux users should have python 2.7, and openssl installed. but may need to run some of these tools under recent versions of Wine. See the Linux_Users section below:
+Linux users should have python 2.7, and openssl installed, but may need to run some of these tools under recent versions of Wine. See the Linux_Users section below:
 
 The scripts in the Other_Tools folder are organized by type of ebook you need to remove the DRM from. Choose from among:
 
   "Adobe_ePub_Tools"
   "Adobe_PDF_Tools"
   "Barnes_and_Noble_ePub_Tools"
+  "ePub_Fixer" (for fixing incorrectly made Adobe and Barnes and Noble ePubs)
   "eReader_PDB_Tools"
+  "Kindle/Mobi_Tools"
   "KindleBooks"
-  "Kindle_for_Android_Patch"
-  "ePub_Fixer" (for fixing incorrectly made Adobe and Barnes and Noble ePubs)
 
 by simply opening that folder.
 
@@ -262,3 +286,24 @@ so if you want you can use calibre in Linux:
 
 12. copy the adeptkey.der into the config dir of calibre (~/.config/calibre in debian). Every book imported to calibre will automaticly freed from DRM.
 
+
+Apple's iBooks FairPlay DRM
+---------------------------
+
+The only tool that removes Apple's iBooks Fairplay DRM that is Requiem by Brahms version 3.3 or later. Requiem is NOT included in this tools package. It is under active development because Apple constantly updates its DRM scheme to stop Requiem from working.
+The latest version as of October 2012 is 3.3.5 and works with iTunes 10.5 and above.
+
+Requiem has a Tor website: http://tag3ulp55xczs3pn.onion. To reach the site using Tor, you will need to install Tor (http://www.torproject.org). If you're willing to sacrifice your anonymity, you can use the regular web with tor2web. Just go to http://tag3ulp55xczs3pn.tor2web.com.
+
+Alternatively, you can download the 3.3.5 version from the following locationss:
+
+Requiem Windows application: http://www.datafilehost.com/download-b015485b.html
+MD5: 954f9ecf42635fae77afbc3a24489004
+
+Requiem Mac OS X application: http://www.datafilehost.com/download-50608ba6.html
+MD5: 4e7dc46ad7e0b54bea6182c5ad024ffe
+
+Requiem source code: http://www.datafilehost.com/download-af8f91a1.html
+MD5: e175560590a154859c0344e30870ac73
+
+No support for requiem is provided at Apprentice Alf's blog.
\ No newline at end of file