dumphd-0.61/0000775000175000017500000000000011211612216013110 5ustar marillatmarillatdumphd-0.61/KEYDB.cfg0000664000175000017500000001141611211606154014436 0ustar marillatmarillat; DumpHD key database file, format 1.4 ; ------------------------------------ ; ; This file is provided as documentation and example of the key database file ; used by DumpHD, it contains no valid keys. ; ; The encoding of this file is UTF-8 without BOM (if the BOM is present the ; first line will not get recognized). Each line is treated as one key entry. ; As line delimiter CR+LF (Windows style) is used, however LF only (Unix style) ; is also recognized. ; ; Lines starting with a ; are comment lines and are ignored. ; If a line contains a ; after the title the remainings of that line are ; treated as comment. ; ; ; Key entry format ; ---------------- ; ; = | <ENTRY ID> | <ENTRY DATA> [ | <ENTRY ID> | <ENTRY DATA> ... ] ; ; DISCID is the calculated identifier of the disc content type, a physical disc ; may have multiple DiscID's (e.g. the disc contains HD-DVD Standard Content and ; HD-DVD Advanced Content). The value is stored as a hexadecimal string and is ; 40 characters long. The DiscID is the SHA-1 hash calculated from the ; following file: ; - HD-DVD Standard Content Audio : AACS\ATKF.AACS ; - HD-DVD Standard Content Video : AACS\VTKF.AACS ; - HD-DVD Advanced Content Audio : AACS\ATKF000.AACS ; - HD-DVD Advanced Content Video : AACS\VTKF000.AACS ; - BD-ROM BDMV : AACS\Unit_Key_RO.inf ; - BD-Recordable BDMV : AACS_mv\Unit_Key_RW.inf ; - BD-Recordable BDAV : AACS\AACS_av\Unit_Key_RW.inf ; ; TITLE is the title of the content. ; ; ENTRY ID describes the type of the following ENTRY DATA, these IDs are valid: ; - D : Date of the DiscID file ; - M : Media Key ; - I : Volume ID ; - B : Binding Nonce ; - V : Volume Unique Key ; - P : Protected Area Key ; - T : Title Key ; - U : CPS Unit Key ; ; ENTRY DATA is specific for each ENTRY ID: ; - D : <YEAR> - <MONTH> - <DAY> ; YEAR must be 4 digits, MONTH and DAY 2 digits. An invalid / non present ; date has the special value 0000-00-00 ; - M : <MEK> ; Hexadecimal string of the Media Key, 32 characters long ; - I : <VID> ; Hexadecimal string of the Volume ID, 32 characters long ; This entry applies to prerecorded media only ; - B : <BN NUMBER> - <BN> [ | <BN NUMBER> - <BN> ... ] ; BN NUMBER is the number of the Binding Nonce, it must be 1 to 5 decimal ; digits long and starts at 0 ; BN is the hexadecimal string of the Binding Nonce, 32 characters long ; This entry applies to recordable media only, there may be multiple ; Binding Nonces ; For BD recordables BN NUMBER is 0 ; - V : <VUK> ; Hexadecimal string of the Volume Unique Key, 32 characters long ; This entry applies to prerecorded media only ; - P : <PAK NUMBER> - <PAK> [ | <PAK NUMBER> - <PAK> ... ] ; PAK NUMBER is the number of the Protected Area Key, it must be 1 to 5 ; decimal digits long and starts at 0 ; PAK is the hexadecimal string of the Protected Area Key, 32 characters ; long ; This entry applies to recordable media only, there my be multiple ; Protected Area Keys ; For BD recordables PAK NUMBER is 0 ; - T : <TK NUMBER> - <TK> [ | <TK NUMBER> - <TK> ... ] ; TK NUMBER is the number of the Title Key, it must be 1 to 5 decimal ; digits long and starts at 1 ; TK is the hexadecimal string of the Title Key, 32 characters long ; This entry applies to HD-DVD only, there may be multiple Title Keys ; - U : <UK NUMBER> - <UK> [ | <UK NUMBER> - <UK> ... ] ; UK NUMBER is the number of the CPS Unit Key, it must be 1 to 5 decimal ; digits long and starts at 1 ; UK is the hexadecimal string of the CPS Unit Key, 32 characters long ; This entry applies to BD only, there may be multiple CPS Unit Keys ; ; All entries are treated case insensitive, whitespace between the ; values / delimiters may be present / omitted. ; There must be a line delimiter behind the last line. ; ; ; Examples ; -------- ; 0000000000000000000000000000000000000000 = Movie Title | D | 0000-00-00 | V | 00000000000000000000000000000000 ; I am a comment 0000000000000000000000000000000000000000 = Movie Title ; I am NOT a comment | D | 1337-08-15 | T | 1-00000000000000000000000000000000 | 2-00000000000000000000000000000000 | 3-00000000000000000000000000000000 0000000000000000000000000000000000000000 = Movie Title | D | 2007-04-01 | V | 00000000000000000000000000000000 | T | 1-00000000000000000000000000000000 0000000000000000000000000000000000000000 = Movie Title | D | 1111-11-11 | U | 1-00000000000000000000000000000000 | 2-00000000000000000000000000000000 | I | 00000000000000000000000000000000 ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/acapacker.sh����������������������������������������������������������������������������0000664�0001750�0001750�00000000132�11133460656�015366� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/bash java -server -Djava.library.path=. -cp DumpHD.jar dumphd.core.ACAPacker "$@" ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/���������������������������������������������������������������������������������0000775�0001750�0001750�00000000000�11211611342�014407� 5����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/Source.txt�����������������������������������������������������������������������0000664�0001750�0001750�00000000567�11132225672�016431� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������DumpHD Source Code ------------------ DumpHD is written in J2SE 1.6 and should compile under JDK 1.6.0 or later. It is developed with Eclipse 3.2, the procject files are included. Because DumpHD accesses resource files through a classloader the directory \resources must be copied manually into the root directory of the binary files or it cannot be run from Eclipse.�����������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/Copying.txt����������������������������������������������������������������������0000664�0001750�0001750�00000105755�11132225672�016606� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������� GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. <one line to give the program's name and a brief idea of what it does.> Copyright (C) <year> <name of author> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: <program> Copyright (C) <year> <name of author> This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see <http://www.gnu.org/licenses/>. The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read <http://www.gnu.org/philosophy/why-not-lgpl.html>. �������������������dumphd-0.61/source/.classpath�����������������������������������������������������������������������0000664�0001750�0001750�00000000350�11132223736�016400� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?xml version="1.0" encoding="UTF-8"?> <classpath> <classpathentry kind="src" path="src"/> <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/> <classpathentry kind="output" path="bin"/> </classpath> ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/.project�������������������������������������������������������������������������0000664�0001750�0001750�00000000576�11132223736�016076� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������<?xml version="1.0" encoding="UTF-8"?> <projectDescription> <name>DumpHD</name> <comment></comment> <projects> </projects> <buildSpec> <buildCommand> <name>org.eclipse.jdt.core.javabuilder</name> <arguments> </arguments> </buildCommand> </buildSpec> <natures> <nature>org.eclipse.jdt.core.javanature</nature> </natures> </projectDescription> ����������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/�����������������������������������������������������������������������������0000775�0001750�0001750�00000000000�11211611342�015176� 5����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/bdvm/������������������������������������������������������������������������0000775�0001750�0001750�00000000000�11211611342�016126� 5����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/bdvm/vm/���������������������������������������������������������������������0000775�0001750�0001750�00000000000�11211611342�016550� 5����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/bdvm/vm/BDVMException.java���������������������������������������������������0000664�0001750�0001750�00000001346�11132225672�022037� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������package bdvm.vm; /** * General purpose exception for the BDVM. * * @author KenD00 */ public class BDVMException extends Exception { /** * The SerialVersionUID of this class, automatically generated, just here to avoid stupid warnings. * Should be replaced by some better defined value if serialization is used someday. */ private static final long serialVersionUID = -2292581931925100157L; public BDVMException() { super(); } public BDVMException(String message) { super(message); } public BDVMException(String message, Throwable cause) { super(message, cause); } public BDVMException(Throwable cause) { super(cause); } } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/bdvm/vm/BDVMInterface.java���������������������������������������������������0000664�0001750�0001750�00000003275�11132225672�022004� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������package bdvm.vm; import java.io.File; /** * This Interface exposes the public methods which can be used by external entities to interact with the BDVM. * * The BDVM is NOT thread safe, external synchronization is needed if multiple thread access the same BDVM. * * @author KenD00 */ public interface BDVMInterface { /** * Returns a String describing the version of the BDVM. * * @return The version of the BDVM */ public String getVersionString(); /** * Initializes the VM to be ready to run. * * The VM needs to know the mountpath of the BD+ protected disc to load the Content Code and execute it. * It is highly recommended to supply the Volume ID of the disc too because the Content Code may use it too. * * @param mountpath The mountpath of the disc, needs to be the root directory of the disc * @param vid The Volume ID of the disc, may be null * @throws BDVMException An error occurred */ public void initVM(File mountpath, byte[] vid) throws BDVMException; /** * Starts the VM. * * It runs until the content code finished. To restart the VM it has to be initialized again first! * * TODO: Maybe throw BDVMException on error? */ public void run(); /** * Returns the generated raw conversion table, as specified by the BDVM documentation. * * If the VM failed to properly execute the Content Code null is returned. If this method is called before * the VM was run the result is unspecified. * * @return The raw conversion table, null if it could not be created */ public byte[] getRAWConversionTable(); } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/����������������������������������������������������������������������0000775�0001750�0001750�00000000000�11211611342�016457� 5����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/core/�����������������������������������������������������������������0000775�0001750�0001750�00000000000�11211611342�017407� 5����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/core/ACAException.java������������������������������������������������0000664�0001750�0001750�00000002272�11211610500�022513� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.core; /** * Exception thrown when an error occurs during ACA processing. * * @author KenD00 */ public class ACAException extends Exception { public ACAException() { super(); } public ACAException(String message) { super(message); } public ACAException(String message, Throwable cause) { super(message, cause); } public ACAException(Throwable cause) { super(cause); } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/core/KeyDataFile.java�������������������������������������������������0000664�0001750�0001750�00000175316�11211610536�022415� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.core; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.FileChannel; import java.nio.charset.*; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Iterator; import java.util.regex.*; import dumphd.util.Utils; /** * Class for accessing the key database in the format DumpHD 1.3. * For reading there is backward compatibility to BackupHDDVD 1.0 but not BackupBluRay 0.21. * All data gets written in DumpHD 1.3 format. * * The file is stored as UTF-8 without BOM and uses Windows line terminators (CR+LF). However, a file with only LF can be read, * but editing such lines will convert these line terminators to CR+LF. * * TODO: Current setKeyData can damage the database if not enough diskspace is left (nothing can be done about this), optional allow 2 file writing? * TODO: Support ignoring of unknown ENTRY ID's? * * @author KenD00 */ public class KeyDataFile { /** * Size of the buffers used for reading the key database file. This value must be at least as big to hold one decoded entry line. */ private final static int LINEBUFFER_SIZE = 16 * 1024; //private final static int LINEBUFFER_SIZE = 5; /** * This pattern matches one line, line terminator is system dependent, CR+LF and LF are supported */ private final static Pattern linePattern = Pattern.compile("(.*)\r?\n"); /* * The following Strings identify various data type identifiers and the corresponding data. * They always start with the separator char | and go just before the next separator char. * Some of them may be applied multiple times, e.g. Title Keys */ /** * This String identifies the data type id's * * Group 1: The identifier */ private final static String dataIdPatternString = "\\|[ \\t]*([DMIBVPTU])[ \\t]*"; /** * This String identifies the date data, old and new format * * Group 1: New date format (nd) * Group 2: nd year * Group 3: nd month * Group 4: nd day * Group 5: Old date format (od) * Group 6: od month * Group 7: od day * Group 8: od year */ private final static String dPatternString = "\\|[ \\t]*(?:(([0-9]{4})[ \\t]*-[ \\t]*([0-9]{2})[ \\t]*-[ \\t]*([0-9]{2}))|(([0-9M]{2})[ \\t]*/[ \\t]*([0-9D]{2})[ \\t]*/[ \\t]*([0-9Y]{2})))[ \\t]*"; /** * This String identifies the data for the MEK, VID and VUK * * Group 1: The key */ private final static String ivPatternString = "\\|[ \\t]*([0-9ABCDEF]{32})[ \\t]*"; /** * This String identifies the data for the Title Key and CPS Unit Key. There may be more than entry. * This String is also used for Binding Nonces and Protected Area Keys. * * Group 1: The key number * Group 2: The key */ private final static String tuPatternString = "\\|[ \\t]*([0-9]{1,5})[ \\t]*-[ \\t]*([0-9ABCDEF]{32})[ \\t]*"; /* * End of data (type) identifiers */ /** * This Pattern identifies the beginning of the dataset line, that is the DiscID, Title, the first Content Type identifier and optional the old-style date field following it * * Group 1 : The DiscID * Group 2 : The title * Group x : Groups from dataIdPatternString * Group x + 1 : Date data * Group x + 1 + y: Groups from dPatternString */ //private final static Pattern datasetIdPattern = Pattern.compile("[ \\t]*([0-9ABCDEF]{40})[ \\t]*=[ \\t]*(.*?)[ \\t]*" + dataIdPatternString + "(" + dPatternString + ")?", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); // TODO: This expression "adds" all whitespace after the title up to the first pipe to the title but the previous expression // will "add" broken Data Entries to the title until it finds a correct one private final static Pattern datasetIdPattern = Pattern.compile("[ \\t]*([0-9ABCDEF]{40})[ \\t]*=[ \\t]*([^|]*)" + dataIdPatternString + "(" + dPatternString + ")?", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); /** * This Pattern identifies the data type id * * Groups are from dataIdPatternString */ private final static Pattern dataIdPattern = Pattern.compile("(?:;(.*))|(?:" + dataIdPatternString + ")", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); /** * This Pattern identifies the data * * Groups are from the following Strings in the following order: * dPatternString * ivPatternString * tuPatternString */ private final static Pattern dataPattern = Pattern.compile("(?:" + dPatternString + ")|(?:" + ivPatternString + ")|(?:" + tuPatternString + ")", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); /** * The file object used to open the physical file */ private RandomAccessFile raf = null; /** * Acquired FileChannel from raf. All reading and writing to the file is done through this channel. */ private FileChannel fc = null; /** * Direct buffer to read into from file */ private ByteBuffer bb = null; /** * Helper buffer, used for temporary encoding of cb's contents to determine its byte size */ private ByteBuffer bbh = null; /** * Buffer used for encoding and decoding chars from / to file */ private CharBuffer cb = null; /** * Decodes bytes to chars */ private CharsetDecoder decoder = null; /** * Encodes chars to bytes */ private CharsetEncoder encoder = null; /** * Opens the specified file and initializes the object. * * @param src The key database file to open. If the file is not present, it gets created. * @throws IOException An I/O error occurred */ public KeyDataFile(File src) throws IOException { raf = new RandomAccessFile(src, "rw"); fc = raf.getChannel(); bb = ByteBuffer.allocateDirect(LINEBUFFER_SIZE); bbh = ByteBuffer.allocate(LINEBUFFER_SIZE); cb = CharBuffer.allocate(LINEBUFFER_SIZE); decoder = Charset.forName("UTF-8").newDecoder(); encoder = Charset.forName("UTF-8").newEncoder(); } /** * Determines which parameters should be written. Avoids redundant data and uses the highest level of data. * E.g. if a VUK is present the VUK gets written instead of the TUKs. * * TODO: Make this private? * * @param kd The KeyData object to determine the mode for * @return The write mode */ public int getSmartMode(KeyData kd) { int mode = 0; // Always write the title mode |= KeyData.DATA_TITLE; // Always write the date mode |= KeyData.DATA_DATE; // Always write the comment mode |= KeyData.DATA_COMMENT; // Write VUK if present, otherwise write the MEK/VID or TUKs if ((kd.getVuk() != null) || (kd.pakCount() > 0)) { mode |= KeyData.DATA_VUKPAK; } else if ((kd.getMek() != null) && ((kd.getVid() != null) || (kd.bnCount() > 0))) { mode |= KeyData.DATA_MEK | KeyData.DATA_VIDBN; } else { mode |= KeyData.DATA_TUK; } return mode; } /** * Builds an entry string for the key database file from the given KeyData object. This string includes the line terminator. * * TODO: Make this private? * * @param kd The KeyData to build the entry for * @param bluRay Set this to true if the KeyData is from a BD * @param recordable Set this to true if the KeyData is from a recordable * @param mode The write mode to use. This is a binary OR of KeyData.DATA_*-Types, every supplied entry type gets written if present (except date, an empty date will be written if none is present) * @return The entry for the key database, the string contains the trailing newline */ public String buildEntry(KeyData kd, boolean bluRay, boolean recordable, int mode) { StringBuffer sb = new StringBuffer(8192); byte[] temp = kd.getDiscId(); sb.append(Utils.toHexString(temp, 0, temp.length)); sb.append(" = "); String title = null; if ((mode & KeyData.DATA_TITLE) != 0) { title = kd.getTitle(); if (title != null) { // Remove all | and cr/lf characters, they will break the entry because they have a special meaning title = title.replaceAll("(?:\\|)|(?:\r?\n)", ""); // Finally remove leading and trailing whitespace // This must be done after the special characters have been removed to ensure that not a whitespace-only string remains title = title.trim(); } } if (title == null) { title = ""; } // Format the title that it has a minimum width //TODO: Longer minwidth? if (title.length() < 48) { title = String.format("%1$-48s", title); } sb.append(title); // Special case for date, allow an empty value if ((mode & KeyData.DATA_DATE) != 0) { sb.append(" | D | "); Date date = kd.getDate(); if (date != null) { sb.append(String.format("%1$tY-%1$tm-%1$td", date)); } else { sb.append("0000-00-00"); } } // For all other entries, write them only if data is available for them if ((mode & KeyData.DATA_MEK) != 0) { temp = kd.getMek(); if (temp != null) { sb.append(" | M | "); sb.append(Utils.toHexString(temp, 0, temp.length)); } } if ((mode & KeyData.DATA_VIDBN) != 0) { if (recordable) { if (kd.bnCount() > 0) { sb.append(" | B"); Iterator<Integer> it = kd.bnIdx().iterator(); while (it.hasNext()) { int bnIndex = it.next(); temp = kd.getBn(bnIndex); sb.append(" | "); sb.append(bnIndex); sb.append("-"); sb.append(Utils.toHexString(temp, 0, temp.length)); } } } else { temp = kd.getVid(); if (temp != null) { sb.append(" | I | "); sb.append(Utils.toHexString(temp, 0, temp.length)); } } } if ((mode & KeyData.DATA_VUKPAK) != 0) { if (recordable) { if (kd.pakCount() > 0) { sb.append(" | P"); Iterator<Integer> it = kd.pakIdx().iterator(); while (it.hasNext()) { int pakIndex = it.next(); temp = kd.getPak(pakIndex); sb.append(" | "); sb.append(pakIndex); sb.append("-"); sb.append(Utils.toHexString(temp, 0, temp.length)); } } } else { temp = kd.getVuk(); if (temp != null) { sb.append(" | V | "); sb.append(Utils.toHexString(temp, 0, temp.length)); } } } if ((mode & KeyData.DATA_TUK) != 0) { if (kd.tukCount() > 0) { if (bluRay) { sb.append(" | U"); } else { sb.append(" | T"); } Iterator<Integer> it = kd.tukIdx().iterator(); while (it.hasNext()) { int tukIndex = it.next(); temp = kd.getTuk(tukIndex); sb.append(" | "); sb.append(tukIndex); sb.append("-"); sb.append(Utils.toHexString(temp, 0, temp.length)); } } } if ((mode & KeyData.DATA_COMMENT) != 0) { String comment = kd.getComment(); if (comment != null) { sb.append(" ;"); sb.append(comment); } } // Because most people will use Windows and will use stupid editors which will mess up the line endings write Windows-Newlines sb.append("\r\n"); return sb.toString(); } /** * Searches the key database for the given entry and returns it if found. * * @param discId The DiscID to search for * @param offset Offset of the DiscID in the given array * @return The KeyData object for the queried DiscID, null if no data was found * @throws IOException An I/O error occurred */ public KeyData getKeyData(byte[] discId, int offset) throws IOException { return getSetKeyData(discId, offset, null, false, false, false, 0); } /** * Updates the entry denoted by the given KeyData object in the key database with the given KeyData object. * The parameters to written are automatically determined by the method getSmartMode(KeyData). * * @param kd The entry with the DiscId from this object gets updated with the data from this object * @param bluRay Set this to true if the KeyData is from a BD * @param recordable Set this to true if the KeyData is from a recordable * @param keepDataTypes If true, all present data types of the entry in the key database will be preserved (if possible, they must be present in kd) * @return If the entry has been found and was updated the old KeyData object from the key database is returned, otherwise null * @throws IOException An I/O error occurred */ public KeyData setKeyData(KeyData kd, boolean bluRay, boolean recordable, boolean keepDataTypes) throws IOException { return getSetKeyData(kd.getDiscId(), 0, kd, bluRay, recordable, keepDataTypes, getSmartMode(kd)); } /** * Updates the entry denoted by the given KeyData object in the key database with the given KeyData object. * * @param kd The entry with the DiscId from this object gets updated with the data from this object * @param bluRay Set this to true if the KeyData is from a BD * @param recordable Set this to true if the KeyData is from a recordable * @param keepDataTypes If true, all present data types of the entry in the key database will be preserved (if possible, they must be present in kd) * @param mode The write mode to use * @return If the entry has been found and was updated the old KeyData object from the key database is returned, otherwise null * @throws IOException An I/O error occurred */ public KeyData setKeyData(KeyData kd, boolean bluRay, boolean recordable, boolean keepDataTypes, int mode) throws IOException { return getSetKeyData(kd.getDiscId(), 0, kd, bluRay, recordable, keepDataTypes, mode); } /** * Appends the given KeyData object to the key database. It is not checked if the entry is already present. * The parameters to written are automatically determined by the method getSmartMode(KeyData). * * @param kd The entry with the DiscId from this object gets updated with the data from this object * @param bluRay Set this to true if the KeyData is from a BD * @param recordable Set this to true if the KeyData is from a recordable * @return kd in case of success, null otherwise * @throws IOException An I/O error occurred */ public KeyData appendKeyData(KeyData kd, boolean bluRay, boolean recordable) throws IOException { return appendKeyDataImpl(kd, bluRay, recordable, getSmartMode(kd)); } /** * Appends the given KeyData object to the key database. It is not checked if the entry is already present. * * @param kd The entry with the DiscId from this object gets updated with the data from this object * @param bluRay Set this to true if the KeyData is from a BD * @param recordable Set this to true if the KeyData is from a recordable * @param mode The write mode to use * @return kd in case of success, null otherwise * @throws IOException An I/O error occurred */ public KeyData appendKeyData(KeyData kd, boolean bluRay, boolean recordable, int mode) throws IOException { return appendKeyDataImpl(kd, bluRay, recordable, mode); } /** * Closes the KeyDataFile. * * @throws IOException An I/O error occurred */ public void close() throws IOException { raf.close(); } /** * Actual implementation that appends the given KeyData. * * @param kd KeyData to append to the key database file * @param bluRay Set this to true if the KeyData is from a BD * @param recordable Set this to true if the KeyData is from a recordable * @param mode The write mode to use * @return kd in case of success, null otherwise * @throws IOException An I/O error occurred */ private KeyData appendKeyDataImpl(KeyData kd, boolean bluRay, boolean recordable, int mode) throws IOException { // Clear cb to store the new value cb.clear(); // Try to put the new entry in the line buffer, catch the runtime exception to throw it as checked IOException try { //System.out.println("Entry: " + buildEntry(kd, bluRay, recordable, mode)); cb.put(buildEntry(kd, bluRay, recordable, mode)); } catch (BufferOverflowException e) { Utils.getMessagePrinter().println("Keyentry exceeds linebuffer size"); return null; } // Flib cb to read from it cb.flip(); // Set the channels position to the end fc.position(fc.size()); // Write cb writeCb(); // Forces all changes to be physically written fc.force(true); return kd; } /** * Actual implementation that either retrieves the KeyData for the given DiscID or replaces the entry with the given replacement string. * * TODO: Don't calculate the bufferOffset when retrieving the KeyData? * * @param discId The DiscID of the entry to retrieve or replace * @param offset The offset into discId to start reading from. 20 bytes get read. * @param replacement If not null, the found entry gets replaced with this * @param bluRay Set this to true if the KeyData is from a BD. Only used if replacement != null. * @param recordable Set this to true if the KeyData is from a recordable * @param keepDataTypes If true, all present data types of the entry in the key database will be preserved (if possible, they must be present in replacement). Only used if replacement != null. * @param mode The write mode to use. Only used if replacement != null. * @return The found entry in the key database, null if it was not found / replaced * @throws IOException An I/O error occurred */ private KeyData getSetKeyData(byte[] discId, int offset, KeyData replacement, boolean bluRay, boolean recordable, boolean keepDataTypes, int mode) throws IOException { //System.out.println("GetSetKeyData"); // *************************************************** // *** Variables for reading and decoding the file *** // *************************************************** // The first position of cb is this position inside the file long bufferOffset = 0; // Counts the number of bytes from which the current contents of cb was decoded long bufferBytes = 0; // If true, then EOF of the input file has been reached boolean eofReached = false; // If true, then the final deocde operation and EOF has been done boolean eofProcessed = false; // If true, the decoder has been finally flushed boolean flushed = false; // CoderResult object used by the decoder / encoder CoderResult coderResult = null; // ************************************************************* // *** Variables for processing the decoded input (== lines) *** // ************************************************************* // Matches a line in the key database file Matcher lineMatcher = linePattern.matcher(cb); // Matches the beginning of a dataset Matcher datasetIdMatcher = datasetIdPattern.matcher(cb); // Matches an ENTRY ID Matcher dataIdMatcher = dataIdPattern.matcher(cb); // Matches an ENTRY DATA Matcher dataMatcher = dataPattern.matcher(cb); // Number of the current line, only used for printing error messages int lineCounter = 0; // Offset of the current line in cb, -1 if no line was found int lineStart = -1; // End of the current line in cb (one position behind the last character) int lineEnd = 0; // If true, the parser ignores the next line. Used to discard the remainings of an overflowed line. boolean ignoreNextLine = false; // Backup of cb's position, created before the Inner Loop because this will modify cb's position and limit to the found lines int positionBackup = 0; // Backup of cb's limit, created before the Inner Loop because this will modify cb's position and limit to the found lines int limitBackup = 0; // ************************************************** // *** Variables for processing the detected data *** // ************************************************** // Stores the date object created by the dataParser Date date = null; // Used to parse the datestring created from the found date values SimpleDateFormat dateParser = new SimpleDateFormat("yyyyMMdd"); dateParser.setLenient(false); // Temporary buffer to decode the keystrings into byte[] temp = new byte[20]; // ************************************************ // *** Initialize everything for the outer loop *** // ************************************************ // Reset all used objects decoder.reset(); bb.clear(); cb.clear(); // Jump to the beginning of the file fc.position(0L); // **************************************** // *** Outer loop for reading from file *** // **************************************** while (!flushed) { // Fill the complete buffer //TODO: Read incremental? while (!eofReached && bb.hasRemaining()) { //System.out.println("Reading"); if (fc.read(bb) == -1) { //System.out.println("Reading: reached EOF"); eofReached = true; break; } } // Flip bb for reading from it!! bb.flip(); // If EOF has been reached, check if the final decode operation has been already done if (eofReached) { if (!eofProcessed) { // Final decode operation pending, do it //System.out.println("Decoder: Processing EOF"); coderResult = decoder.decode(bb, cb, true); if (coderResult.isUnderflow()) { // The final decode (not flush!) operation was successful, now set the flag so that the decoder can get flushed eofProcessed = true; } } // Check again if eof has been processed to flush at once (saves an iteration) if (eofProcessed) { // Finally flush the buffer //System.out.println("Decoder: Flushing EOF"); coderResult = decoder.flush(cb); if (coderResult.isUnderflow()) { // Buffer has been successfully flushed, no more processing required, set the exit flag to true flushed = true; } } } else { // EOF has not been reached, normal, incremental decode operation //System.out.println("Decoder: decoding"); coderResult = decoder.decode(bb, cb, false); } // Check the decoder result, throw an exception in case of an error if (coderResult.isError()) { coderResult.throwException(); } // The current position inside bb reflects the number of bytes which have been decoded to cb // It is possible that bb has been compacted without the bufferOffset beeing changed, therefor the postion must be added to the bufferCount bufferBytes += (long)bb.position(); // ************************************************* // *** Prepare objects for inner processing loop *** // ************************************************* // Every time the outer loop has finished reading, the beginning of cb contains the next unprocessed (partial) line // Flip the cb for reading from it!! cb.flip(); //System.out.println("cb contents: " + cb.toString()); lineStart = -1; lineEnd = 0; lineMatcher.reset(); positionBackup = cb.position(); limitBackup = cb.limit(); //System.out.println("Saved position: " + positionBackup + ", limit: " + limitBackup); // ************************************************ // *** Inner loop for processing decoding input *** // ************************************************ while (lineMatcher.find()) { lineCounter++; //System.out.println("Line " + lineCounter + ": " + lineMatcher.group(1)); // Update the line offsets lineStart = lineMatcher.start(); lineEnd = lineMatcher.end(); // End of the line contents (== end of line excluding line terminator) // ATTENTION: Here this value is in cb coordinates! int lineContentEnd = lineMatcher.end(1); // Check if this line is the end of an overflowed line and skip it in that case // This MUST be the first check to be made or the ignore flag my not be reset correctly if by chance one of the other conditions is also true if (ignoreNextLine) { //System.out.println("End of overflowed line found, ignoring line"); ignoreNextLine = false; continue; } // Check if the line is a comment line and skip it if it is if (cb.charAt(lineStart) == ';') { //System.out.println("Comment line found, skipping line"); continue; } // Check if an empty line is found and skip it if it is if (lineStart == lineContentEnd) { //System.out.println("Empty line found, skipping line"); continue; } // Limit and position cb to the found line // ATTENTION! After these lines of code the backup restore code at the end of the while body MUST be reached to restore the original values! //System.out.println("Setting line " + lineCounter + " position: " + lineStart + ", limit: " + lineContentEnd); cb.limit(lineContentEnd); cb.position(lineStart); // ATTENTION: Here the value is changed to the following matchers coordinates! lineContentEnd -= lineStart; //System.out.println("Converted lineContentEnd: " + lineContentEnd); // Reset the matchers datasetIdMatcher.reset(); dataIdMatcher.reset(); dataMatcher.reset(); // Check if the line is the one we are looking for if (datasetIdMatcher.lookingAt()) { //System.out.println("datasetIdMatcher-result: " + datasetIdMatcher.group()); // Check if this is the entry we are looking for and retrieve it // ****************************** // *** Process datasetIdMatch *** // ****************************** // Decode the DiscID to compare it with the one we are looking for Utils.decodeHexString(datasetIdMatcher.group(1), temp, 0); // Compare the decoded DiscID with the one we are looking for boolean discIdMatch = true; for (int i = 0; i < 20; i++) { if (temp[i] != discId[offset + i]) { discIdMatch = false; break; } } if (discIdMatch) { // ******************** // *** DiscID match *** // ******************** // The KeyData to build and return KeyData kd = new KeyData(discId, offset); //System.out.println("Title: " + datasetIdMatcher.group(2)); // The title must be trimmed because of the changed regular expression //kd.setTitle(datasetIdMatcher.group(2)); kd.setTitle(datasetIdMatcher.group(2).trim()); // Set the current id to the found one String currentDataId = datasetIdMatcher.group(3).toUpperCase(); // Check if the old BackupHDDVD format is present, that is the date is following the id char which identifies the data after the date field if (datasetIdMatcher.group(4) != null) { // Old BackupHDDVD format or new DumpHD format with the date data as first entry if (!currentDataId.equals("D")) { // Old BackupHDVD format, parse the date data date = null; try { if (datasetIdMatcher.group(5) != null) { // New date format date = dateParser.parse(datasetIdMatcher.group(6) + datasetIdMatcher.group(7) + datasetIdMatcher.group(8)); } else { // Old date format date = dateParser.parse("20" + datasetIdMatcher.group(12) + datasetIdMatcher.group(10) + datasetIdMatcher.group(11)); } } catch (ParseException e) { // Ignore the exception, the date parser allows "invalid" dates } if (date != null) { kd.setDate(date); } // Set region of dataMatcher to start just behind the datasetIdMatcher match dataMatcher.region(datasetIdMatcher.end(), lineContentEnd); } else { // New DumpHD format, set region of dataMatcher just behind the id so that the date data will be found again dataMatcher.region(datasetIdMatcher.start(4), lineContentEnd); } } else { // New DumpHD format, set the region of dataMatcher to start just behind this match dataMatcher.region(datasetIdMatcher.end(), lineContentEnd); } // *************************************** // *** Parse all following data entrys *** // *************************************** // For-ever, the loop will get a appropriate break for(;;) { // ************************* // *** Process dataMatch *** // ************************* //System.out.println("Current dataId: " + currentDataId); if (dataMatcher.lookingAt()) { // End offset of the current dataMatch int dataEnd = dataMatcher.end(); //System.out.println("dataMatcher-result: " + dataMatcher.group()); // In case of an error kd is set to null as an error flag if (currentDataId.equals("D")) { //System.out.println("Date: " + dataMatcher.group(1) + ", " + dataMatcher.group(5)); date = null; try { if (dataMatcher.group(1) != null) { // New date format date = dateParser.parse(dataMatcher.group(2) + dataMatcher.group(3) + dataMatcher.group(4)); } else if (dataMatcher.group(5) != null) { // Old date format date = dateParser.parse("20" + dataMatcher.group(8) + dataMatcher.group(6) + dataMatcher.group(7)); } else { // Invalid data Utils.getMessagePrinter().println("Error at line " + lineCounter + ", position " + (dataMatcher.start() + 1) + ": Invalid DATE data"); kd = null; } } catch (ParseException e) { // Ignore the exception, the date parser allows "invalid" dates } // In case of error (kd == null) date is also null, therefor this does work without an extra check if (date != null) { kd.setDate(date); } } else if (currentDataId.equals("M")) { //System.out.println("MEK: " + dataMatcher.group(9)); if (dataMatcher.group(9) != null) { Utils.decodeHexString(dataMatcher.group(9), temp, 0); kd.setMek(temp, 0); } else { // Invalid data Utils.getMessagePrinter().println("Error at line " + lineCounter + ", position " + (dataMatcher.start() + 1) + ": Invalid MEK data"); kd = null; } } else if (currentDataId.equals("I")) { //System.out.println("VID: " + dataMatcher.group(9)); if (dataMatcher.group(9) != null) { Utils.decodeHexString(dataMatcher.group(9), temp, 0); kd.setVid(temp, 0); } else { // Invalid data Utils.getMessagePrinter().println("Error at line " + lineCounter + ", position " + (dataMatcher.start() + 1) + ": Invalid VID data"); kd = null; } } else if (currentDataId.equals("B")) { do { //System.out.println("BN: " + dataMatcher.group(10) + ", " + dataMatcher.group(11)); dataEnd = dataMatcher.end(); if (dataMatcher.group(10) != null) { //TODO: Catch exception? int keyNumber = Integer.parseInt(dataMatcher.group(10)); //TODO: More checks? if (keyNumber >= 0) { // Valid BN number Utils.decodeHexString(dataMatcher.group(11), temp, 0); kd.setBn(keyNumber, temp, 0); } else { // Invalid key number Utils.getMessagePrinter().println("Error at line " + lineCounter + ", position " + (dataMatcher.start(10) + 1) + ": Invalid BN number"); kd = null; break; } //TODO: Check for line end? dataMatcher.region(dataMatcher.end(), lineContentEnd); } else { // Invalid data Utils.getMessagePrinter().println("Error at line " + lineCounter + ", position " + (dataMatcher.start() + 1) + ": Invalid BN data"); kd = null; break; } } while (dataMatcher.lookingAt()); } else if (currentDataId.equals("V")) { //System.out.println("VUK: " + dataMatcher.group(9)); if (dataMatcher.group(9) != null) { Utils.decodeHexString(dataMatcher.group(9), temp, 0); kd.setVuk(temp, 0); } else { // Invalid data Utils.getMessagePrinter().println("Error at line " + lineCounter + ", position " + (dataMatcher.start() + 1) + ": Invalid VUK data"); kd = null; } } else if (currentDataId.equals("P")) { do { //System.out.println("PAK: " + dataMatcher.group(10) + ", " + dataMatcher.group(11)); dataEnd = dataMatcher.end(); if (dataMatcher.group(10) != null) { //TODO: Catch exception? int keyNumber = Integer.parseInt(dataMatcher.group(10)); //TODO: More checks? if (keyNumber >= 0) { // Valid PAK number Utils.decodeHexString(dataMatcher.group(11), temp, 0); kd.setPak(keyNumber, temp, 0); } else { // Invalid key number Utils.getMessagePrinter().println("Error at line " + lineCounter + ", position " + (dataMatcher.start(10) + 1) + ": Invalid PAK number"); kd = null; break; } //TODO: Check for line end? dataMatcher.region(dataMatcher.end(), lineContentEnd); } else { // Invalid data Utils.getMessagePrinter().println("Error at line " + lineCounter + ", position " + (dataMatcher.start() + 1) + ": Invalid PAK data"); kd = null; break; } } while (dataMatcher.lookingAt()); } else if (currentDataId.equals("T")) { do { //System.out.println("TK: " + dataMatcher.group(10) + ", " + dataMatcher.group(11)); dataEnd = dataMatcher.end(); if (dataMatcher.group(10) != null) { //TODO: Catch exception? int keyNumber = Integer.parseInt(dataMatcher.group(10)); //TODO: More checks? if (keyNumber > 0) { // Valid TK number Utils.decodeHexString(dataMatcher.group(11), temp, 0); kd.setTuk(keyNumber, temp, 0); } else { // Invalid key number Utils.getMessagePrinter().println("Error at line " + lineCounter + ", position " + (dataMatcher.start(10) + 1) + ": Invalid TK number"); kd = null; break; } //TODO: Check for line end? dataMatcher.region(dataMatcher.end(), lineContentEnd); } else { // Invalid data Utils.getMessagePrinter().println("Error at line " + lineCounter + ", position " + (dataMatcher.start() + 1) + ": Invalid TK data"); kd = null; break; } } while (dataMatcher.lookingAt()); } else if (currentDataId.equals("U")) { do { //System.out.println("UK: " + dataMatcher.group(10) + ", " + dataMatcher.group(11)); dataEnd = dataMatcher.end(); if (dataMatcher.group(10) != null) { //TODO: Catch exception? int keyNumber = Integer.parseInt(dataMatcher.group(10)); //TODO: More checks? if (keyNumber > 0) { // Valid UK number Utils.decodeHexString(dataMatcher.group(11), temp, 0); kd.setTuk(keyNumber, temp, 0); } else { // Invalid key number Utils.getMessagePrinter().println("Error at line " + lineCounter + ", position " + (dataMatcher.start(10) + 1) + ": Invalid UK number"); kd = null; break; } //TODO: Check for line end? dataMatcher.region(dataMatcher.end(), lineContentEnd); } else { // Invalid data Utils.getMessagePrinter().println("Error at line " + lineCounter + ", position " + (dataMatcher.start() + 1) + ": Invalid UK data"); kd = null; break; } } while (dataMatcher.lookingAt()); } // Check if error flag is set, break out if it is if (kd == null) { break; } // ************************************************** // *** dataMatch processed, check for new data id *** // ************************************************** if (dataEnd != lineContentEnd) { // There is more data to parse // Set the region of the dataIdMatcher to start just behind the last dataMatch dataIdMatcher.region(dataEnd, lineContentEnd); if (dataIdMatcher.lookingAt()) { // New data id or comment found String comment = dataIdMatcher.group(1); if (comment == null) { // New data id found currentDataId = dataIdMatcher.group(2).toUpperCase(); // Set the region of the dataMatcher to start just behind the data id dataMatcher.region(dataIdMatcher.end(), lineContentEnd); // A next run in the loop continue; } else { // Comment found, the parsing is finished kd.setComment(comment); // Fall through to continue with the action after the parsing } } else { // Invalid data Utils.getMessagePrinter().println("Error at line " + lineCounter + ", position " + (dataIdMatcher.regionStart() + 1) + ": Invalid ENTRY ID"); // Set kd to null to be consistent kd = null; break; } } // Parsing finished // Check if the result should be returned or replaced if (replacement != null) { // ******************************* // *** Replace the found entry *** // ******************************* // WARNING!! The current content of cb and bb will get lost, this section MUST return from the method, the outer loop MUST NOT take a next loop // Determine the bytes from the beginning of the buffer to the line start to calculate the offset of the line into the file cb.position(0); cb.limit(lineStart); // Position in file to start writing the replacement long writeOffset = bufferOffset + (long)getRemainingCbBytes(); // Determine the size in bytes of the line cb.position(lineStart); cb.limit(lineEnd); // The length in bytes of the present entry int entrySize = getRemainingCbBytes(); // If the present data types should be preserved add them to mode if (keepDataTypes) { mode |= kd.dataMask(); } // Encode the replacement just to know how many bytes it consumes //TODO: Make bb big enough to guarantee that a full cb would fit in it? cb.position(0); cb.limit(cb.capacity()); try { cb.put(buildEntry(replacement, bluRay, recordable, mode)); } catch (BufferOverflowException e) { Utils.getMessagePrinter().println("Keyentry exceeds linebuffer size"); return null; } cb.flip(); int replacementSize = getRemainingCbBytes(); // Reset cb's position cb.position(0); // Depending on the size difference of found entry and replacement either write only the replacement or also shift the present data if (replacementSize == entrySize) { // Old and new entry have the same size //System.out.println("Replace entry: Both entrys have the same size, old: " + entrySize + ", new: " + replacementSize); // Simply write the new entry // Set fc's positon to the write position fc.position(writeOffset); writeCb(); } else if (replacementSize < entrySize) { // New entry is smaller than old entry //System.out.println("Replace entry: New entry is smaller than old entry, old: " + entrySize + ", new: " + replacementSize); // Write new entry fc.position(writeOffset); writeCb(); // Shift the remaining data left // The position where the next read should be made from, initially the end of the present entry long readPos = writeOffset + (long)entrySize; // The position where the next write should be made to, initially the end of the new entry (which has just been written) long writePos = fc.position(); // Set fc's position to readPos fc.position(readPos); // Use bb as read / write buffer bb.clear(); // Read / write incrementally until EOF while (fc.read(bb) != -1) { // New readPos starts at current position readPos = fc.position(); // flip buffer to read from it bb.flip(); // Jump to the writePos and write the buffer fc.position(writePos); fc.write(bb); // New writePos starts at current position writePos = fc.position(); // Compact buffer to read into it bb.compact(); // Jump to readPos and start next read-loop fc.position(readPos); } // Shifting finished // Because the data was shifted left, truncate after the last written position fc.truncate(writePos); } else { // New entry is bigger than old entry //System.out.println("Replace entry: New entry is bigger than old entry, old: " + entrySize + ", new: " + replacementSize); // First shift the data // Use bb as read / write buffer bb.clear(); // Store the fileSize to have a fixed value for it to use it multiple times during the initial position calculation // Do this to avoid unpredictable behavior if the filesize gets modified during the calculation from outside // (this itself is not allowed but at least this code will not run amok) long fileSize = fc.size(); // The end of the entry to be replaced long entryEnd = writeOffset + (long)entrySize; // Check if the file has been illegally truncated if (fileSize < entryEnd) { Utils.getMessagePrinter().println("Fatal error: Key database has been illegally truncated from outside beyond the entry to update"); return null; } // Set readPos just one bb left from the end of file to read up a maximum of data long readPos = fileSize - (long)bb.capacity(); // If readPos is left of the end postion of the entry (there is less data to shift than a full bb) set readPos // to this position and limit bb accordingly if (readPos < entryEnd) { bb.limit(bb.capacity() - (int)(entryEnd - readPos)); readPos = entryEnd; } // Set writePos the shift difference behind the end of file // The buffer limit (== buffer size) must be subtracted because writePos is the starting write position! long writePos = fileSize + (long)(replacementSize - entrySize) - (long)bb.limit(); // As long as bb has a limit > 0 there is data to move while (bb.limit() > 0) { //System.out.println("Shift loop"); // Jump to readPos and fill bb completely fc.position(readPos); while (bb.hasRemaining()) { fc.read(bb); } // Flip bb to read from it! bb.flip(); // Jump to writePos and write bb fully fc.position(writePos); while (bb.hasRemaining()) { fc.write(bb); } // Clear bb for a new run bb.clear(); // Move readPos one bb capacity to the left readPos -= (long)bb.capacity(); // Check if readPos is left of entryEnd and move it to there and adjust bb's limit if (readPos < entryEnd) { bb.limit(bb.capacity() - (int)(entryEnd - readPos)); readPos = entryEnd; } // Shift writePos by the amount that will get written writePos -= (long)bb.limit(); } // Now write the entry fc.position(writeOffset); writeCb(); } // Forces all changes to be physically written fc.force(true); // Return the found entry return kd; } else { // ****************************** // *** Return the found entry *** // ****************************** return kd; } } else { // No data present Utils.getMessagePrinter().println("Error at line " + lineCounter + ", position " + (dataMatcher.regionStart() + 1) + ": Invalid ENTRY DATA"); // Set kd to null to be consistent kd = null; break; } // End if dataMatcher.lookingAt() } // End for ever entry parse loop } // End if discIdMatch } else { // Line is not a key entry Utils.getMessagePrinter().println("Error at line " + lineCounter + ": Line is not a key entry"); } // End if datasetIdMatcher.loockingAt() // ************************************ // *** Restore saved cb constraints *** // ************************************ cb.position(positionBackup); cb.limit(limitBackup); } // End while lineMatcher.find() //System.out.println("After lineMatcher bb.remaining(): " + bb.remaining() + ", cb.reamining(): " + cb.remaining() + ", cb.position(): " + cb.position()); // ************************************* // *** Prepare buffers for a new run *** // ************************************* if (!flushed) { // Buffer not flushed, prepare for a new loop if (lineStart == -1) { // No line found // Because the buffer was flipped, the limit marks the max position the buffer is filled up to if (cb.limit() == cb.capacity()) { // Line buffer overflow Utils.getMessagePrinter().println("Error at line " + (lineCounter + 1) + ": Linebuffer overflow, ignoring line"); // Clear cb and compact bb to continue reading // -> the next found line will be the end of the too big line, set flag to ignore the next found line // lineCounter + 1 because the overflowed line has not been counted // Because cb gets cleared, the current bufferOffset is advanced by the current bufferBytes which must be reset to 0 bufferOffset += bufferBytes; bufferBytes = 0L; cb.clear(); bb.compact(); ignoreNextLine = true; } else { // There is still space left in cb, set position and limit to allow appending, compact bb and let more data be read //System.out.println("No line found, adding more data to cb"); // No need to update bufferOffset because cb gets only appended cb.position(cb.limit()); cb.limit(cb.capacity()); bb.compact(); } } else { // At least one line found //System.out.println("Line(s) found, starting new run"); // Set cb's position to the end of the last line so that after compacting the buffer it starts with the beginning of the new line cb.position(lineEnd); // Now cb's position is the correct one to determine the size of the remaining chars long remainingBytes = (long)getRemainingCbBytes(); // Reset cb's position cb.position(lineEnd); // Subtract the remaining bytes from the current bufferBytes to get the size of the already processed lines to update the bufferOffset correctly bufferOffset += bufferBytes - remainingBytes; // The buffer contains already the remaining bytes bufferBytes = remainingBytes; //System.out.println("New run, bufferOffset: " + bufferOffset); cb.compact(); bb.compact(); } } else { // Buffer flushed, check for remaining (invalid) data if (lineEnd != cb.limit()) { // There is data behind the last found line Utils.getMessagePrinter().println("Error behind line " + lineCounter + ": Invalid data at end"); } } } // End while file processing loop return null; } /** * Helper method! * * Encodes the chars between cb's current position and its limit and returns the size of the encoding in bytes. * Uses encoder and bbh, cb's position is advanced to its limit. * * @return The number of bytes which are needed to store the chars between cb's current position and its limit * @throws IOException An error occured during encoding */ private int getRemainingCbBytes() throws IOException { // Total number of bytes the encoded chars need int encodedSize = 0; // Flag indicating if the data has been fully encoded boolean encoded = false; // Flag indicating if the encoder has been flushed boolean flushed = false; // Return value of the encoder CoderResult coderResult = null; // Reset all used objects encoder.reset(); bbh.clear(); // Encode the data while (!flushed) { if (!encoded) { // Output has not been fully encoded yet coderResult = encoder.encode(cb, bbh, true); if (coderResult.isUnderflow()) { // The encode was fully done encoded = true; } } else { // We have everything encoded, now flush the encoder // No need to check if we have already flushed, we wont get here if we had coderResult = encoder.flush(bbh); if (coderResult.isUnderflow()) { flushed = true; } } if (coderResult.isError()) { coderResult.throwException(); } // The current position reflects how many bytes the current encode loop produced encodedSize += bbh.position(); // Just clear bbh, the encoded output is not needed bbh.clear(); } return encodedSize; } /** * Helper method! * * Writes the contents of cb using its current position and limit to the current position of fc. * Uses encoder and bb, cb's position is advanced to its limit and fc's position is advanced by the bytes written. * * @throws IOException An I/O error occured */ private void writeCb() throws IOException { // Flag indicating if the data has been fully encoded boolean encoded = false; // Flag indicating if the encoder has been flushed boolean flushed = false; // Return value of the encoder CoderResult coderResult = null; // Reset all used objects encoder.reset(); bb.clear(); // Encode the data while (!flushed) { if (!encoded) { // Output has not been fully encoded yet coderResult = encoder.encode(cb, bb, true); if (coderResult.isUnderflow()) { // The encode was fully done encoded = true; } } else { // We have everything encoded, now flush the encoder // No need to check if we have already flushed, we wont get here if we had coderResult = encoder.flush(bb); if (coderResult.isUnderflow()) { flushed = true; } } if (coderResult.isError()) { coderResult.throwException(); } // Flip bb to read from it bb.flip(); // Write bb fully and reset it for a new run //TODO: Write incremental? while (bb.hasRemaining()) { fc.write(bb); } bb.clear(); } } } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/core/DiscSet.java�����������������������������������������������������0000664�0001750�0001750�00000014204�11211610514�021611� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.core; import java.io.File; import java.util.ArrayList; import java.util.Iterator; import java.util.TreeMap; import dumphd.bdplus.ConversionTable; /** * This class holds all information about a specific disc set. A disc set contains all files from a specific content type on that disc (e.g. HD-DVD Advanced Content Video). * Additionally it is used to store different information used to process the disc set, e.g. source- and destination-directory, KeyData. * * Objects of this class are created by the DumpHD class and are used as information exchange objects between the core and the GUI. * * @author KenD00 */ public class DiscSet { public final static int HD_STANDARD_V = 1; public final static int HD_STANDARD_A = 2; public final static int HD_ADVANCED_V = 3; public final static int HD_ADVANCED_A = 4; public final static int BD_MV = 5; public final static int BDR_MV = 6; public final static int BDR_AV = 7; public final static String[] DISCTYPE_NAMES = { "None", "HD-DVD Standard Content Video", "HD-DVD Standard Content Audio", "HD-DVD Advanced Content Video", "HD-DVD Advanced Content Audio", "Blu-Ray BDMV", "Blu-Ray Recordable BDMV", "Blu-Ray Recordable BDAV" }; /** * Type of this DiscSet */ public final int contentType; /** * The directory that contains the files / directories to process. This is an absolute path. */ public final File srcDir; /** * The destination directory, all files will be put there. This is an absolute path */ public File dstDir = null; /** * The directory with the AACS files. This points to the directory with the Title / Unit Key file(s), * in case of BDAV the MKB is in the parent directory! This is an absolute path. */ public File aacsDir = null; /** * This holds all key information about this DiscSet */ public KeyData keyData = null; /** * If true, this DiscSet is additionally BD+ protected. Only valid for Blu-Ray types. */ public boolean bdplus = false; /** * The BD+ ConversionTable. Only valid for Blu-Ray types. */ public ConversionTable convTable = null; /** * This TreeMap contains all "stream" source files, with a relative path starting from srcDir. * Currently for all media except BDAV this TreeMap contains only one ArrayList with the key 0 with the * files from the "base" content directory, HVDVD_TS for HD-DVD and BDMV for Blu-Ray. For BDAV there * can be multiple entries, the keys are corresponding to the BDAV directory number. */ public final TreeMap<Integer, ArrayList<String>> streamSets = new TreeMap<Integer, ArrayList<String>>(); /** * This ArrayList contains additional files, for Blu-Ray this is the Certificate folder, for HD-DVD Advanced Content * the ADV_OBJ directory. */ public final ArrayList<String> fileSet = new ArrayList<String>(4096); /** * If true, this DiscSet will be processed, otherwise not. */ public boolean selected = false; /** * Creates a new DiscSet of the given type with the given source directory. * * @param contentType The type of the DiscSet * @param srcDir The source directory of the DiscSet. This will also be set as the root directory. */ public DiscSet(int contentType, File srcDir) { this.contentType = contentType; this.srcDir = srcDir; } public boolean isBluRay() { return (contentType >= 5); } public boolean isRecordable() { //TODO: This won't work for HD-DVD recordables (which should reside below the BluRay-ID's) return (contentType >= 6); } public String toString() { StringBuffer sb = new StringBuffer(2048); sb.append("DiscSet:\n"); sb.append("Type : "); sb.append(DiscSet.DISCTYPE_NAMES[contentType]); sb.append("\nSource directory : "); sb.append(srcDir.toString()); sb.append("\nDestination directory: "); if (dstDir != null) { sb.append(dstDir.toString()); } else { sb.append("NONE"); } sb.append("\nAACS directory : "); if (aacsDir != null) { sb.append(aacsDir.toString()); } else { sb.append("NONE"); } sb.append("\nKey data : "); if (keyData != null) { sb.append(keyData.toString()); } else { sb.append("NONE"); } if (isBluRay()) { sb.append("\nBD+ protection : "); if (bdplus) { sb.append("YES"); sb.append("\nConversion Table : "); if (convTable != null) { sb.append("PRESENT"); } else { sb.append("MISSING"); } } else { sb.append("NO"); } } sb.append("\nStreamsets :"); Iterator<ArrayList<String>> setIt = streamSets.values().iterator(); while (setIt.hasNext()) { Iterator<String> it = setIt.next().iterator(); while (it.hasNext()) { sb.append("\n"); sb.append(it.next()); } } sb.append("\nOther files :"); Iterator<String> it = fileSet.iterator(); while (it.hasNext()) { sb.append("\n"); sb.append(it.next()); } return sb.toString(); } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/core/KeyData.java�����������������������������������������������������0000664�0001750�0001750�00000043117�11211610530�021600� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.core; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.Set; import java.util.Map; import java.util.TreeMap; import java.util.ArrayList; import dumphd.util.Utils; /** * A class holding all keys to decrypt a disc. * * The constructor and the set methodes actually make a copy of their given arguments, therefore it is safe to modify them after a method call. * The get methodes return a reference to the internal members, so don't modify these! All getters may return null if not noted otherwise. * * @author KenD00 */ public class KeyData { public final static int DATA_TITLE = 2; public final static int DATA_DATE = 4; public final static int DATA_MEK = 8; public final static int DATA_VIDBN = 16; public final static int DATA_VUKPAK = 32; public final static int DATA_TUK = 64; public final static int DATA_UM = 128; public final static int DATA_COMMENT = 256; public final static int DATA_ALL = 0xFFFFFFFF; /** * The DiscID */ private byte[] discId = null; /** * The Title */ private String title = null; /** * The Date */ private Date date = null; /** * An optional comment */ private String comment = null; /** * The Media Key */ private byte[] mek = null; /** * The VolumeID or Binding Nonces. * A Volume ID has the Key 0. */ private TreeMap<Integer, byte[]> vids = new TreeMap<Integer, byte[]>(); /** * The Volume Unique Key or Protected Area Keys. * A Volume Unique Key has the Key 0. */ private TreeMap<Integer, byte[]> vuks = new TreeMap<Integer, byte[]>(); /** * The Title Keys / CPS Unit Keys. There is no difference made between Title Keys and CPS Unit Keys. * Key is the key number. */ private TreeMap<Integer, byte[]> tuks = new TreeMap<Integer, byte[]>(); /** * Head / Title/Clip Unit mappings for BD directories */ private ArrayList<UnitMappingSet> umss = new ArrayList<UnitMappingSet>(5); /** * Constructs a new KeyData Object with the given DiscID. * * @param discId The DiscID * @param offset Offset into the byte array to start reading from. 20 Bytes get read. */ public KeyData(byte[] discId, int offset) { this.discId = new byte[20]; System.arraycopy(discId, offset, this.discId, 0, this.discId.length); } /** * Returns the DiscID. * * @return The DiscID, this is never null */ public byte[] getDiscId() { return discId; } /** * Returns the Title. * * @return The Title */ public String getTitle() { return title; } /** * Returns the Date. * * @return The Date */ public Date getDate() { return date; } /** * Returns the Comment. * * @return The Comment */ public String getComment() { return comment; } /** * Returns the Media Key. * * @return The Media Key */ public byte[] getMek() { return mek; } /** * Returns the Volume ID. * * This method is the same as getBn(0). * * @return The Volume ID */ public byte[] getVid() { return vids.get(0); } /** * Returns a Binding Nonce. * * @param index The index of the Binding Nonce, counting starts at 0 * @return The Binding Nonce with the given index */ public byte[] getBn(int index) { return vids.get(index); } /** * Returns the Volume Unique Key. * * This method is the same as getPak(0). * * @return The Volume Unique Key */ public byte[] getVuk() { return vuks.get(0); } /** * Returns a Protected Area Key. * * @param index The index of the Protected Area Key, counting starts at 0 * @return The Protected Area Key with the given index */ public byte[] getPak(int index) { return vuks.get(index); } /** * Returns a Title Key / CPS Unit Key. There is no difference made between Title Keys and CPS Unit Keys. * * @param index The index of the key, counting starts at 1 * @return The Key with the given index */ public byte[] getTuk(int index) { return tuks.get(index); } /** * Returns the CPS Unit Number for the given head element. * * @param unitMappingSet Number of the unitMappingSet to query. For BDMV this is always 0, for BDAV this is the number of the BDAV directory, * the Basic BDAV directory has the number 0 * @param henr BD-ROM: 0 = First Playback, 1 = Top Menu * BR-R : 0 = Menu file, 1 = Mark file * @return CPS Unit number, 0 if no mapping present */ public int getHeadUnit(int unitMappingSet, int henr) { if (unitMappingSet < umss.size()) { UnitMappingSet ums = umss.get(unitMappingSet); if (henr < ums.hm.size()) { return ums.hm.get(henr); } } return 0; } /** * Returns the CPS Unit Number for the Title / Clip number. * * @param unitMappingSet Number of the unitMappingSet to query. For BDMV this is always 0, for BDAV this is the number of the BDAV directory, * the Basic BDAV directory has the number 0 * @param tcnr Title number (BD-ROM) / Clip number (BD-R) * @return CPS Unit number, 0 if no mapping present */ public int getTcUnit(int unitMappingSet, int tcnr) { // The ArrayList starts at 0, the Title/Clip number at 1 tcnr--; if (unitMappingSet < umss.size()) { UnitMappingSet kis = umss.get(unitMappingSet); if (tcnr < kis.tcm.size()) { return kis.tcm.get(tcnr); } } return 0; } /** * Sets the Title. * * @param title The Title, null clears the Title */ public void setTitle(String title) { this.title = title; } /** * Sets the Date. * * @param date The Date, null clears the Date */ public void setDate(Date date) { if (date != null) { this.date = (Date)date.clone(); } else { this.date = null; } } /** * Sets the Comment. * * @param comment The Comment, null clears the Comment */ public void setComment(String comment) { this.comment = comment; } /** * Sets the Media Key. * * @param mek The Media Key, null clears the Media Key. * @param offset Offset into the byte array to start reading from. 16 Bytes get read. */ public void setMek(byte[] mek, int offset) { if (mek != null) { this.mek = new byte[16]; System.arraycopy(mek, offset, this.mek, 0, this.mek.length); } else { this.mek = null; } } /** * Sets the Volume ID. * * This method is the same as setBn(0, vid, offset). * * @param vid The Volume ID, null clears the Volume ID * @param offset Offset into the byte array to start reading from. 16 Bytes get read. */ public void setVid(byte[] vid, int offset) { setKey(vids, 0, vid, offset); } /** * Sets a Binding Nonce. Do either set Binding Nonces or a Volume ID but not both! * * @param index The index of the Binding Nonce * @param bn The Binding Nonce, null clears the Binding Nonce * @param offset Offset into the byte array to start reading from. 16 Bytes get read. */ public void setBn(int index, byte[] bn, int offset) { setKey(vids, index, bn, offset); } /** * Sets the Volume Unique Key. * * This method is the same as setPak(0, pak, offset). * * @param vuk The Volume Unique Key, null clears the Volume Unique Key * @param offset Offset into the byte array to start reading from. 16 Bytes get read. */ public void setVuk(byte[] vuk, int offset) { setKey(vuks, 0, vuk, offset); } /** * Sets a Protected Area Key. Do either set Protected Area Keys or a Volume Unique Key but not both! * * @param index The index of the Protected Area Key * @param pak The Protected Area Key, null clears the Protected Area Key * @param offset Offset into the byte array to start reading from. 16 Bytes get read. */ public void setPak(int index, byte[] pak, int offset) { setKey(vuks, index, pak, offset); } /** * Sets a Title Key / CPS Unit Key. There is no difference made between Title Keys and CPS Unit Keys. * * @param index The index of the key, must be > 0 * @param tuk The Title Key / CPS Unit Key, null removes the Title Key / CPS Unit Key * @param offset Offset into the byte array to start reading from. 16 Bytes get read. */ public void setTuk(int index, byte[] tuk, int offset) { setKey(tuks, index, tuk, offset); } /** * Sets a Head unit. * * TODO: There is no way to remove such a mapping * * @param unitMappingSet unitMappingSet to use * @param henr Head element number * @param unit Number of the CPS Unit */ public void setHeadUnit(int unitMappingSet, int henr, int unit) { // Add empty unitMappingSets until we have enough to access the requested one while (umss.size() <= unitMappingSet) { umss.add(new UnitMappingSet()); } // Now our unitMappingSet is present or unitMappingSet is negative UnitMappingSet ums = umss.get(unitMappingSet); // Add empty mappings until we have one less than the requested one while (ums.hm.size() < henr) { ums.hm.add(0); } // This checks implicit if henr is negative if (ums.hm.size() == henr) { ums.hm.add(unit); } else { ums.hm.set(henr, unit); } } /** * Sets a Title / Clip unit. * * TODO: There is no way to remove such a mapping * * @param unitMappingSet unitMappingSet to use * @param tcnr Title / Clip number * @param unit Number of the CPS Unit */ public void setTcUnit(int unitMappingSet, int tcnr, int unit) { // The ArrayList starts at 0, the Title/Clip number at 1 tcnr--; // Add empty unitMappingSets until we have enough to access the requested one while (umss.size() <= unitMappingSet) { umss.add(new UnitMappingSet()); } // Now our unitMappingSet is present or unitMappingSet is negative UnitMappingSet ums = umss.get(unitMappingSet); // Add empty mappings until we have one less than the requested one while (ums.tcm.size() < tcnr) { ums.tcm.add(0); } // This checks implicit if tcnr is negative if (ums.tcm.size() == tcnr) { ums.tcm.add(unit); } else { ums.tcm.set(tcnr, unit); } } /** * Returns a bitmask of all present data entries. This is a binary OR of all present DATA_* values. * * @return Bitmask of all present data entries */ public int dataMask() { int mask = 0; if (title != null) { mask |= DATA_TITLE; } if (date != null) { mask |= DATA_DATE; } if (comment != null) { mask |= DATA_COMMENT; } if (mek != null) { mask |= DATA_MEK; } if (vids.size() > 0) { mask |= DATA_VIDBN; } if (vuks.size() > 0) { mask |= DATA_VUKPAK; } if (tuks.size() > 0) { mask |= DATA_TUK; } if (umss.size() > 0) { mask |= DATA_UM; } return mask; } /** * Returns the number of present Binding Nonces. * * @return The number of present Binding Nonces, 1 if a VID is set */ public int bnCount() { return vids.size(); } /** * Returns the number of present Protected Area Keys. * * @return The number of present Protected Area Keys, 1 if a VUK is set */ public int pakCount() { return vuks.size(); } /** * Returns the number of present Title Keys / CPS Unit Keys. * * @return The number of present Title Keys / CPS Unit Keys */ public int tukCount() { return tuks.size(); } /** * Returns an unmodifiable set view of the indices from the present Binding Nonces. * * @return The indices of the present Binding Nonces */ public Set<Integer> bnIdx() { return Collections.unmodifiableSet(vids.keySet()); } /** * Returns an unmodifiable set view of the indices from the present Protected Area Keys. * * @return The indices of the present Protected Area Keys */ public Set<Integer> pakIdx() { return Collections.unmodifiableSet(vuks.keySet()); } /** * Returns an unmodifiable set view of the indices from the present Title Keys / CPS Unit Keys. * * @return The indices of the present Title Keys / CPS Unit Keys */ public Set<Integer> tukIdx() { return Collections.unmodifiableSet(tuks.keySet()); } public String toString() { StringBuffer out = new StringBuffer(8 * 1024); out.append("DiscID : "); out.append(Utils.toHexString(discId, 0, discId.length)); out.append("\nTitle : "); if (title != null) { out.append(title); } else { out.append("N/A"); } out.append("\nDate : "); if (date != null) { out.append(String.format("%1$tY-%1$tm-%1$td", date)); } else { out.append("N/A"); } out.append("\nComment : "); if (comment != null) { out.append(comment); } else { out.append("N/A"); } out.append("\nMEK : "); if (mek != null) { out.append(Utils.toHexString(mek, 0, mek.length)); } else { out.append("N/A"); } out.append("\nVID / BN's : "); printKeys(out, vids); out.append("\nVUK / PAK's : "); printKeys(out, vuks); out.append("\nTUK's : "); printKeys(out, tuks); if (umss.size() > 0) { out.append("\nUM's : "); out.append(umss.size()); for (int set = 0; set < umss.size(); set++) { out.append(String.format("\n Set %1$d", set)); UnitMappingSet ums = umss.get(set); for (int i = 0; i < ums.hm.size(); i++) { out.append(String.format("\n%1$12dH-%2$d", i, ums.hm.get(i))); } for (int i = 0; i < ums.tcm.size(); i++) { out.append(String.format("\n%1$13d-%2$d", i + 1, ums.tcm.get(i))); } } } return out.toString(); } /** * Helper Method, puts / removes the given key from the given TreeMap. * * @param tm The TreeMap to use * @param index The index of the key * @param key The key. If null the key at index gets removed * @param offset Offset into key where the actual key starts. 16 bytes get read. */ private void setKey(TreeMap<Integer, byte[]> tm, int index, byte[] key, int offset) { if (key != null) { // Its more common that a new key gets inserted than an old one updated, therefore don't check for an old entry byte[] tmp = new byte[16]; System.arraycopy(key, offset, tmp, 0, tmp.length); tm.put(index, tmp); } else { tm.remove(index); } } /** * Helper Method, prints the keys from the given TreeMap to the given StringBuffer. * * @param out StringBuffer to write to * @param keyMap TreeMap to read from */ private void printKeys(StringBuffer out, TreeMap<Integer, byte[]> keyMap) { int keyCount = keyMap.size(); if (keyCount > 0) { out.append(keyCount); Iterator<Map.Entry<Integer, byte[]>> keyIt = keyMap.entrySet().iterator(); while (keyIt.hasNext()) { Map.Entry<Integer, byte[]> keyEntry = keyIt.next(); out.append(String.format("\n%1$13d-%2$s", keyEntry.getKey(), Utils.toHexString(keyEntry.getValue(), 0, keyEntry.getValue().length))); } } else { out.append("N/A"); } } /** * Small structure to hold Head / Title/Clip mappings for a BD directory */ private class UnitMappingSet { /** * Head Mappings: CPS Unit Number for First Playback and Top Menu (BD-ROM) or Menu and Mark file (BD-R) */ public ArrayList<Integer> hm = new ArrayList<Integer>(2); /** * Title/Clip Mappings: CPS Unit Number for title number (BD-ROM) or clip number (BD-R) */ public ArrayList<Integer> tcm = new ArrayList<Integer>(200); } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/core/DumpHD.java������������������������������������������������������0000664�0001750�0001750�00000141061�11211612000�021366� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.core; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import bdvm.vm.BDVMException; import bdvm.vm.BDVMInterface; import dumphd.aacs.AACSDecrypter; import dumphd.aacs.AACSException; import dumphd.bdplus.BDVMLoader; import dumphd.bdplus.ConversionTable; import dumphd.bdplus.SubTable; import dumphd.util.ByteSource; import dumphd.util.PrintStreamPrinter; import dumphd.util.FileSource; import dumphd.util.MessagePrinter; import dumphd.util.SimpleFilenameFilter; import dumphd.util.StreamSource; import dumphd.util.Utils; import dumphd.gui.Manager; /** * Main class of DumpHD. Parses the given source and processes the files. * Acts as interface for the GUI. * * @author KenD00 */ public final class DumpHD { /** * Name of the program */ public final static String PROGRAM_NAME = "DumpHD"; /** * Program version */ public final static String PROGRAM_VERSION = "0.61"; /** * Program author */ public final static String PROGRAM_AUTHOR = "KenD00"; /** * The MessagePrinter used for textual output */ private MessagePrinter out = null; /** * The Key Database */ private KeyDataFile keyDataFile = null; /** * The AACS module */ private AACSDecrypter aacsDec = null; /** * The BDVM module for BD+ removal */ private BDVMInterface bdvm = null; /** * The ACA packer */ private ACAPacker acaPacker = null; /** * Stores all currently present disc types */ private ArrayList<DiscSet> discSets = new ArrayList<DiscSet>(6); /** * If true, streaming mode is used, otherwise normal mode */ private boolean streaming = false; /** * Creates a new DumpHD which writes its textual output to the MessagePrinter received from Utils. * * @param streaming If true, streaming mode will be used. In streaming mode only EVO's / M2TS's files will be played back. * They will be output to stdout, all usual textual output will be output to stderr. Non streamable parts * of the file (currently only ADV_PCKs in EVOs) will not be decrypted and passed through as is. * If false, the files will be normally output. */ public DumpHD(boolean streaming) throws Exception { this.out = Utils.getMessagePrinter(); this.streaming = streaming; createDumpHD(); } /** * Creates a new DumpHD with the given MessagePrinter. * * @param mp The MessagePrinter the DumpHD object will use for textual output * @param streaming {@link dumphd.core.DumpHD#DumpHD(boolean)} */ public DumpHD(MessagePrinter mp, boolean streaming) throws Exception { this.out = mp; this.streaming = streaming; createDumpHD(); } /** * Actually creates the DumpHD object. * * FIXME: Throw a more specific exception type * * @throws Exception An error occurred while creating a required component */ private void createDumpHD() throws Exception { out.println(DumpHD.PROGRAM_NAME + " " + DumpHD.PROGRAM_VERSION + " by " + DumpHD.PROGRAM_AUTHOR); out.println(); out.print("Opening Key Data File... "); try { keyDataFile = new KeyDataFile(new File("KEYDB.cfg")); } catch (FileNotFoundException e1) { out.println("FAILED"); throw new Exception("Key Data File not found"); } catch (IOException e2) { out.println("FAILED"); throw new Exception("Could not open Key Data File"); } out.println("OK"); // Create AACS module try { aacsDec = new AACSDecrypter(out, keyDataFile); } catch (AACSException e) { throw new Exception(e.getMessage()); } // Create BDVM module try { out.print("Loading BDVM... "); BDVMLoader loader = new BDVMLoader(); loader.loadClass(new File("bdvmdbg.jar")); bdvm = loader.newInstance(); out.println("OK"); out.println(bdvm.getVersionString()); } catch (BDVMException e) { out.println("FAILED"); out.println(e.getMessage()); out.println("Automatic BD+ removal disabled, specify a Conversion Table manually to remove BD+ if necessary"); } // Create ACA-Packer acaPacker = new ACAPacker(out, aacsDec); acaPacker.setContentChecking(true); out.println(); } /** * Parses the given source and detects the disc type(s) it contains. * Stores all found disc types in this DumpHD object. * * @param src The source to parse * @param inFiles If not null, only the files listed in these ArrayList will be processed, otherwise the whole disc * The path of the files needs to be relative to the disc source directory. * @param convTable If not null this Conversion Table binary file will be used to remove BD+ (if present) * @throws IOException An I/O error occurred */ public void initSource(File src, ArrayList<String> inFiles, String convTable) throws IOException { boolean HD_STANDARD = false; discSets.clear(); out.println("Initializing source... "); if (!src.exists()) { throw new IOException(src.toString() + " does not exist"); } if (!src.isDirectory()) { throw new IOException(src.toString() + " is not a directory"); } if (inFiles != null) { if (inFiles.isEmpty()) { inFiles = null; } } if (convTable != null) { if (convTable.isEmpty()) { convTable = null; } } // ### Identify content types ### // FIXME: Don't know how to identify HD-DVD Audio content // Check for HD-DVD Standard Content File idFile = new File(src, "HVDVD_TS" + File.separator + "HV000I01.IFO"); if (idFile.isFile()) { out.println("Disc type found: " + DiscSet.DISCTYPE_NAMES[DiscSet.HD_STANDARD_V]); DiscSet ds = new DiscSet(DiscSet.HD_STANDARD_V, src); File aacsDir = new File(src, "AACS"); if (aacsDir.isDirectory()) { ds.aacsDir = aacsDir; } discSets.add(ds); HD_STANDARD = true; } // Check for HD-DVD Advanced Content idFile = new File(src, "ADV_OBJ" + File.separator + "DISCID.DAT"); if (idFile.isFile()) { out.println("Disc type found: " + DiscSet.DISCTYPE_NAMES[DiscSet.HD_ADVANCED_V]); DiscSet ds = new DiscSet(DiscSet.HD_ADVANCED_V, src); File aacsDir = new File(src, "AACS"); if (aacsDir.isDirectory()) { ds.aacsDir = aacsDir; } discSets.add(ds); // FIXME: Temporary, remove when full parsing is integrated! if (HD_STANDARD) { discSets.remove(0); HD_STANDARD = false; } } // Check for BD MV content idFile = new File(src, "BDMV" + File.separator + "index.bdmv"); if (idFile.isFile()) { // TODO: BD Recordable has the same file, currently i don't know any other "high level" way to distinguish both types // than checking for the specific AACS component. Check only for the directory or also for the Unit Key file? File aacsDir = new File(src, "AACS_mv"); if (aacsDir.isDirectory()) { // AACS for BD Recordable MV present, assume BDR_MV out.println("Disc type found: " + DiscSet.DISCTYPE_NAMES[DiscSet.BDR_MV]); DiscSet ds = new DiscSet(DiscSet.BDR_MV, src); ds.aacsDir = aacsDir; discSets.add(ds); } else { // No AACS for BD Recordable MV present, assume BD_MV out.println("Disc type found: " + DiscSet.DISCTYPE_NAMES[DiscSet.BD_MV]); DiscSet ds = new DiscSet(DiscSet.BD_MV, src); aacsDir = new File(src, "AACS"); if (aacsDir.isDirectory()) { ds.aacsDir = aacsDir; } // Check for BD+ if (new File(src, "BDSVM").isDirectory()) { ds.bdplus = true; } discSets.add(ds); } } // Check for BD AV content idFile = new File(src, "BDAV" + File.separator + "info.bdav"); if (idFile.isFile()) { out.println("Disc type found: " + DiscSet.DISCTYPE_NAMES[DiscSet.BDR_AV]); DiscSet ds = new DiscSet(DiscSet.BDR_AV, src); File aacsDir = new File(src, "AACS" + File.separator + "AACS_av"); if (aacsDir.isDirectory()) { ds.aacsDir = aacsDir; } discSets.add(ds); } // Check if anything was found, return if not if (discSets.size() > 0) { if (inFiles == null) { out.println("Collecting input files..."); } else { out.println("Processing input files..."); } } else { out.println("No disc type found"); return; } // Process input files depending on mode if (inFiles == null) { // ***************** // *** Disc mode *** // ***************** ArrayList<String> streamSet = null; Iterator<DiscSet> it = discSets.iterator(); while (it.hasNext()) { DiscSet ds = it.next(); // FIXME: Don't know where to look for HD-DVD Audio files switch (ds.contentType) { case DiscSet.HD_STANDARD_V: streamSet = new ArrayList<String>(8192); if (streaming) { Utils.scanForFilenames(new File(ds.srcDir, "HVDVD_TS"), "HVDVD_TS", streamSet, true, new SimpleFilenameFilter(".*\\.EVO", true, false), true); } else { Utils.scanForFilenames(new File(ds.srcDir, "HVDVD_TS"), "HVDVD_TS", streamSet, true, null, true); } Collections.sort(streamSet); ds.streamSets.put(0, streamSet); break; case DiscSet.HD_ADVANCED_V: streamSet = new ArrayList<String>(8192); if (streaming) { Utils.scanForFilenames(new File(ds.srcDir, "HVDVD_TS"), "HVDVD_TS", streamSet, true, new SimpleFilenameFilter(".*\\.EVO", true, false), true); } else { Utils.scanForFilenames(new File(ds.srcDir, "HVDVD_TS"), "HVDVD_TS", streamSet, true, null, true); Utils.scanForFilenames(new File(ds.srcDir, "ADV_OBJ"), "ADV_OBJ", ds.fileSet, true, null, true); } Collections.sort(streamSet); ds.streamSets.put(0, streamSet); Collections.sort(ds.fileSet); break; case DiscSet.BD_MV: case DiscSet.BDR_MV: streamSet = new ArrayList<String>(8192); if (streaming) { Utils.scanForFilenames(new File(ds.srcDir, "BDMV"), "BDMV", streamSet, true, new SimpleFilenameFilter(".*\\.M2TS", true, false), true); } else { Utils.scanForFilenames(new File(ds.srcDir, "BDMV"), "BDMV", streamSet, true, null, true); // Do recordables also have this? It doesn't hurt if they don't Utils.scanForFilenames(new File(ds.srcDir, "CERTIFICATE"), "CERTIFICATE", ds.fileSet, true, null, true); } Collections.sort(streamSet); ds.streamSets.put(0, streamSet); Collections.sort(ds.fileSet); break; case DiscSet.BDR_AV: int currentStreamSet = 0; String checkString = "BDAV"; File checkDir = new File(ds.srcDir, checkString); while (checkDir.isDirectory()) { streamSet = new ArrayList<String>(8192); if (streaming) { Utils.scanForFilenames(checkDir, checkString, streamSet, true, new SimpleFilenameFilter(".*\\.M2TS", true, false), true); } else { Utils.scanForFilenames(checkDir, checkString, streamSet, true, null, true); } Collections.sort(streamSet); ds.streamSets.put(currentStreamSet, streamSet); currentStreamSet++; checkString = String.format("BDAV%1$d", currentStreamSet); checkDir = new File(ds.srcDir, checkString); } break; } } } else { // ***************** // *** File mode *** // ***************** if (discSets.size() > 0) { DiscSet currentDs = discSets.get(0); Iterator<String> fileIt = inFiles.iterator(); // The currently used fileSet, MUST be set to null after each change of currentDs!! ArrayList<String> fileSet = null; // The number of the currently used fileSet, only used by BDAV int fileSetNumber = 0; while (fileIt.hasNext()) { // TODO: Check if inFile exists? String inFile = fileIt.next(); String upperInFile = inFile.toUpperCase(); // FIXME: Don't know where to look for HD-DVD Audio files if (upperInFile.startsWith("HVDVD_TS")) { // HD-DVD file if (!streaming || upperInFile.endsWith(".EVO")) { if (currentDs.contentType != DiscSet.HD_STANDARD_V && currentDs.contentType != DiscSet.HD_ADVANCED_V) { DiscSet tmp = getFirstDiscSet(DiscSet.HD_STANDARD_V | DiscSet.HD_ADVANCED_V); if (tmp != null) { currentDs = tmp; fileSet = null; } else { out.println("Disc does not contain the required content type, ignoring: " + inFile); continue; } } if (fileSet == null) { fileSet = getStreamSet(currentDs, 0); } fileSet.add(inFile); } else { out.println("Ignored because of streaming: " + inFile); } } else if (upperInFile.startsWith("ADV_OBJ")) { // HD-DVD Advanced Content file if (!streaming) { if (currentDs.contentType != DiscSet.HD_ADVANCED_V) { DiscSet tmp = getFirstDiscSet(DiscSet.HD_ADVANCED_V); if (tmp != null) { currentDs = tmp; fileSet = null; } else { out.println("Disc does not contain the required content type, ignoring: " + inFile); continue; } } currentDs.fileSet.add(inFile); } else { out.println("Ignored because of streaming: " + inFile); } } else if (upperInFile.startsWith("BDMV")) { // Blu-Ray BDMV file if (!streaming || upperInFile.endsWith(".M2TS")) { if (currentDs.contentType != DiscSet.BD_MV && currentDs.contentType != DiscSet.BDR_MV) { DiscSet tmp = getFirstDiscSet(DiscSet.BD_MV | DiscSet.BDR_MV); if (tmp != null) { currentDs = tmp; fileSet = null; } else { out.println("Disc does not contain the required content type, ignoring: " + inFile); continue; } } if (fileSet == null) { fileSet = getStreamSet(currentDs, 0); } fileSet.add(inFile); } else { out.println("Ignored because of streaming: " + inFile); } } else if (upperInFile.startsWith("BDAV")) { // Blu-Ray BDAV file if (!streaming || upperInFile.endsWith(".M2TS")) { if (currentDs.contentType != DiscSet.BDR_AV) { DiscSet tmp = getFirstDiscSet(DiscSet.BDR_AV); if (tmp != null) { currentDs = tmp; fileSet = null; } else { out.println("Disc does not contain the required content type, ignoring: " + inFile); continue; } } // Determine fileSetNumber first if (upperInFile.length() > 4) { try { int tmp = Integer.parseInt(inFile.substring(4, 5)); if (tmp > 0 && tmp < 5) { fileSetNumber = tmp; } else { out.println("Invalid BDAV Aux Directory number, ignoring: " + inFile); continue; } } catch (NumberFormatException e) { // Ignore it, this can happen with the Basic BDAV directory and is not an error } } else if (fileSetNumber != 0) { fileSetNumber = 0; } if (fileSet == null) { fileSet = getStreamSet(currentDs, fileSetNumber); } fileSet.add(inFile); } else { out.println("Ignored because of streaming: " + inFile); } } else if (upperInFile.startsWith("CERTIFICATE")) { if (!streaming) { if (currentDs.contentType != DiscSet.BD_MV && currentDs.contentType != DiscSet.BDR_MV) { DiscSet tmp = getFirstDiscSet(DiscSet.BD_MV | DiscSet.BDR_MV); if (tmp != null) { currentDs = tmp; fileSet = null; } else { out.println("Disc does not contain the required content type, ignoring: " + inFile); continue; } } currentDs.fileSet.add(inFile); } else { out.println("Ignored because of streaming: " + inFile); } } else { // Unknown file out.println("Unknown file type, ignoring: " + inFile); } } } } // Process a given Conversion Table if (convTable != null) { File convTableFile = new File(convTable).getAbsoluteFile(); DiscSet ds = getFirstDiscSet(DiscSet.BD_MV); if (ds != null) { // Check if DiscSet is BD+ protected if (ds.bdplus) { // This may throw an exception but it is OK to abort if something is wrong with the conversion table // TODO: Maybe this is not OK? All code "ignores" errors currently. FileSource convTableSrc = new FileSource(convTableFile, ByteSource.R_MODE); ds.convTable = new ConversionTable(convTableSrc); } else { out.println("Disc is not BD+ protected, ignoring Conversion Table: " + convTableFile); } } else { out.println("Disc does not contain the required content type, ignoring Conversion Table: " + convTableFile); } } out.println("Source initialized"); } /** * Identifies the current present disc types. * If the source has not been initialized or no disc types are present, this method does nothing. * * @return An unmodifyable collection of all present disc types */ public Collection<DiscSet> identifySource() { Iterator<DiscSet> it = discSets.iterator(); while (it.hasNext()) { DiscSet ds = it.next(); try { aacsDec.identifyDisc(ds); } catch (AACSException e) { out.println(e.getMessage()); } } return Collections.unmodifiableList(discSets); } /** * Dumps the present disc types. * The source must have been identified or it will not be dumped. * A destination directory must have been set to all present disc types. * If a disc type is not marked as selected, it will be skipped. */ public void dumpSource() { ByteSource inSource = null; ByteSource outSource = null; HashSet<File> knownDirectories = new HashSet<File>(64); if (streaming) { outSource = new StreamSource(System.out, StreamSource.W_MODE, false); } Iterator<DiscSet> discIt = discSets.iterator(); while (discIt.hasNext()) { DiscSet ds = discIt.next(); if (!ds.selected) { out.println("Skipping disc set:"); out.println(ds.toString()); continue; } out.println("Processing disc set:"); out.println(ds.toString()); if (!streaming) { if (ds.dstDir == null) { out.println("No destination set, skipping disc set"); continue; } else if (ds.dstDir.exists() && !ds.dstDir.isDirectory()) { out.println("Destination is not a directory, skipping disc set"); continue; } else if (ds.srcDir.equals(ds.dstDir)) { out.println("Source and destination are the same, skipping disc set"); continue; } } // Initialize AACS Decrypter boolean aacsInitialized = false; try { aacsDec.initDisc(ds); aacsDec.init(ds); aacsInitialized = true; } catch (AACSException e) { out.println("Error initializing AACS, skipping disc set"); out.println(e.getMessage()); } if (!aacsInitialized) { continue; } // Initialize BD+ Decrypter if (ds.bdplus) { if (ds.convTable == null && bdvm != null) { try { out.print("Initializing the BDVM... "); bdvm.initVM(ds.srcDir, ds.keyData.getVid()); out.println("OK"); out.print("Executing the BDVM... "); bdvm.run(); out.println("OK"); byte[] rawTable = bdvm.getRAWConversionTable(); if (rawTable != null) { try { out.print("Parsing the Conversion Table... "); ConversionTable convTable = new ConversionTable(); convTable.parse(rawTable, 0); out.println("OK"); ds.convTable = convTable; } catch (IndexOutOfBoundsException e) { out.println("FAILED"); out.println("Error! Parsing the Conversion Table failed, dumping may fail!"); } } else { out.println("Error! The BDVM has not produced a Conversion Table, dumping may fail!"); } } catch (BDVMException e) { out.println("ERROR"); out.println(e.getMessage()); out.println("Error! Failed to execute the BDVM, dumping may fail!"); } } else { out.println("Error! BD+ detected and no Conversion Table present, dumping may fail!"); } } File inFile = null; File outFile = null; String filename = null; // First process all streamSets Iterator<Map.Entry<Integer, ArrayList<String>>> setIt = ds.streamSets.entrySet().iterator(); while (setIt.hasNext()) { Map.Entry<Integer, ArrayList<String>> currentEntry = setIt.next(); int currentSet = currentEntry.getKey(); Iterator<String> fileIt = currentEntry.getValue().iterator(); while (fileIt.hasNext()) { // Current filename, this may include a directory part!! filename = fileIt.next(); out.println("Processing: " + filename); inFile = new File(ds.srcDir, filename); try { if (!streaming) { outFile = new File(ds.dstDir, filename); File outDir = outFile.getParentFile(); if (knownDirectories.add(outDir)) { // No need to check if it is a directory, the list only contains files, so this is true if (!outDir.exists()) { try { if (!outDir.mkdirs()) { throw new IOException("Failed creating destination directory: " + outDir); } } catch (SecurityException e) { throw new IOException("Insufficient rights to create destination directory: " + outDir); } } } if (outFile.exists()) { out.println("Destination file already exists, skipping file"); continue; } } inSource = new FileSource(inFile, FileSource.R_MODE); if (!streaming) { outSource = new FileSource(outFile, FileSource.RW_MODE); } switch (ds.contentType) { case DiscSet.HD_STANDARD_V: case DiscSet.HD_STANDARD_A: case DiscSet.HD_ADVANCED_V: case DiscSet.HD_ADVANCED_A: if (filename.toUpperCase().endsWith(".EVO")) { Collection<MultiplexedArf> arfs = aacsDec.decryptEvob(inSource, outSource); if (!streaming && arfs.size() > 0) { out.println("Processing multiplexed ARFs..."); Iterator<MultiplexedArf> arfIt = arfs.iterator(); while (arfIt.hasNext()) { MultiplexedArf arf = arfIt.next(); ByteSource inArf = arf.getReader(); ByteSource outArf = arf.getWriter(); out.println(inArf.getFile().getName()); if (inArf.getFile().getName().toUpperCase().endsWith(".ACA")) { try { acaPacker.rePack(inArf, outArf); } catch (ACAException acae) { out.println("ACA-Exception: " + acae.getMessage()); out.println("Skipping ARF"); } } else { try { aacsDec.decryptArf(inArf, outArf); } catch (AACSException aacse) { out.println("AACS-Exception: " + aacse.getMessage()); out.println("Skipping ARF"); } } arf.close(); } out.println("Multiplexed ARFs processed"); } } else { // No need to check for ARFs, streamSets don't contain ARFs // To be on the safe side, check for streaming if (!streaming) { Utils.copyBs(inSource, outSource, inSource.size()); } } break; case DiscSet.BD_MV: if (filename.toUpperCase().endsWith(".M2TS")) { int clipNr = Utils.getClipNumber(inFile.getName()); if (clipNr < 0) { out.println("Error parsing filename, assuming clip number 0"); clipNr = 0; } SubTable subTable = null; if (ds.bdplus) { // No need to emit warning about missing Conversion Table, this was already be done if (ds.convTable != null) { subTable = ds.convTable.getSubTable(clipNr); } if (subTable == null) { out.println("Error! BD+ SubTable not found, dumping may fail"); } } // TODO: Not nice that the AACS Decrypter has to handle BD+ too, but currently there is no other way to do it // TODO: Uses brute force approach to determine the CPS Unit Key aacsDec.decryptTs(inSource, outSource, null, subTable); } else { // To be on the safe side, check for streaming if (!streaming) { Utils.copyBs(inSource, outSource, inSource.size()); } } break; case DiscSet.BDR_MV: case DiscSet.BDR_AV: { // This is the plain filename without directory String plainFilename = inFile.getName(); // Index of the CPS Unit Key to use int keyIndex = 0; if (filename.toUpperCase().endsWith(".M2TS")) { // The number of the clip int clipNr = Utils.getClipNumber(plainFilename); if (clipNr < 0) { out.println("Error parsing filename, assuming clip number 0"); clipNr = 0; } keyIndex = ds.keyData.getTcUnit(currentSet, clipNr); if (keyIndex != 0) { // Index of the CPS Unit Key found, decrypt the file aacsDec.decryptTs(inSource, outSource, ds.keyData.getTuk(keyIndex), null); } else { // No CPS Unit Key index found, that means the file should not be encrypted, run it // through the decrypter to see if its really unencrypted (emits warnings if not) aacsDec.decryptTs(inSource, outSource, null, null); } } else { // To be on the safe side, check for streaming if (!streaming) { if (ds.contentType == DiscSet.BDR_AV) { // For BDAV, check if we are dealing with a Thumbnail Data file boolean isTdf = false; if (plainFilename.equals("menu.tdt1")) { isTdf = true; keyIndex = ds.keyData.getHeadUnit(currentSet, 0); } else if (plainFilename.equals("mark.tdt1")) { isTdf = true; keyIndex = ds.keyData.getHeadUnit(currentSet, 1); } if (isTdf) { if (keyIndex != 0) { aacsDec.decryptTdf(inSource, outSource, ds.keyData.getTuk(keyIndex)); } else { out.println("Error! CPS Unit Key index for Thumbnail Data file not found, copying file"); Utils.copyBs(inSource, outSource, inSource.size()); } } else { Utils.copyBs(inSource, outSource, inSource.size()); } } else { Utils.copyBs(inSource, outSource, inSource.size()); } } } break; } default: // Unknown disc type, it's broken code if this section is reached!! out.println("Error! Unknown disc type: " + ds.contentType + ", skipping file"); } } catch (IOException ei) { out.println("I/O-Exception: " + ei.getMessage()); out.println("Skipping file"); } catch (AACSException ea) { out.println("AACS-Exception: " + ea.getMessage()); out.println("Skipping file"); } finally { try { if (inSource != null) { inSource.close(); } } catch (IOException e) { out.println("Error closing input file: " + e.getMessage()); } inSource = null; if (!streaming) { try { if (outSource != null) { outSource.close(); } } catch (IOException e) { out.println("Error closing output file: " + e.getMessage()); } outSource = null; } } } } // Now process the fileSet if (streaming) { // In streaming mode the fileSet isn't processed continue; } // TODO: This is partially duplicate code, can this be avoided? Iterator<String> fileIt = ds.fileSet.iterator(); while (fileIt.hasNext()) { // Current filename, this may include a directory part!! filename = fileIt.next(); out.println("Processing: " + filename); inFile = new File(ds.srcDir, filename); try { outFile = new File(ds.dstDir, filename); File outDir = outFile.getParentFile(); if (knownDirectories.add(outDir)) { // No need to check if it is a directory, the list only contains files, so this is true if (!outDir.exists()) { try { if (!outDir.mkdirs()) { throw new IOException("Failed creating destination directory: " + outDir); } } catch (SecurityException e) { throw new IOException("Insufficient rights to create destination directory: " + outDir); } } } if (outFile.exists()) { out.println("Destination file already exists, skipping file"); continue; } inSource = new FileSource(inFile, FileSource.R_MODE); outSource = new FileSource(outFile, FileSource.RW_MODE); switch (ds.contentType) { case DiscSet.HD_ADVANCED_A: case DiscSet.HD_ADVANCED_V: if (filename.toUpperCase().endsWith(".ACA")) { acaPacker.rePack(inSource, outSource); } else { // This copies the file if its not encrypted aacsDec.decryptArf(inSource, outSource); } break; default: // For every other content type just copy the files Utils.copyBs(inSource, outSource, inSource.size()); } } catch (IOException ei) { out.println("I/O-Exception: " + ei.getMessage()); out.println("Skipping file"); } catch (ACAException ea) { out.println("ACA-Exception: " + ea.getMessage()); out.println("Skipping file"); } catch (AACSException eaa) { out.println("AACS-Exception: " + eaa.getMessage()); out.println("Skipping file"); } finally { try { if (inSource != null) { inSource.close(); } } catch (IOException e) { out.println("Error closing input file: " + e.getMessage()); } inSource = null; try { if (outSource != null) { outSource.close(); } } catch (IOException e) { out.println("Error closing output file: " + e.getMessage()); } outSource = null; } } out.println("Disc set processed"); } } /** * Dumps the given source to the destination. * * If source is a file, then only that file gets dumped. It is required that the directory AACS is in the parent directory of that file. * If source is a directory, then the complete HD-DVD / BluRay gets dumped. Source must be the root directory of the HD-DVD / BluRay. * * @param srcStr Source to dump * @param dstStr Destination directory to dump into, this is null in streaming mode * @param inFiles If not null and not empty, only these files will be processed. They must be given relative to srcStr. * @param convTable If not null this Conversion Table binary file will be used to remove BD+ (if present) * @return If true, the dump was successful, if false, it failed */ public boolean dump(String srcStr, String dstStr, ArrayList<String> inFiles, String convTable) { File src = new File(srcStr).getAbsoluteFile(); File dst = null; if (!streaming) { dst = new File(dstStr).getAbsoluteFile(); } out.println("Start time: " + new Date()); out.println(); // *********************** // *** Source checking *** // *********************** out.println("Checking source..."); out.println("Source path: " + src.getPath()); try { initSource(src, inFiles, convTable); if (discSets.size() == 0) { endMsg("Aborting"); return false; } } catch (IOException e) { endMsg(e.getMessage()); return false; } // ***************************** // *** Source identification *** // ***************************** out.println("Identifying source..."); identifySource(); out.println("Finished identifying source"); // ************************* // *** Destination check *** // ************************* if (!streaming) { out.println("Checking destination..."); out.println("Destination path: " + dst.getPath()); if (!dst.exists() || dst.isDirectory()) { Iterator<DiscSet> setIt = discSets.iterator(); while (setIt.hasNext()) { DiscSet ds = setIt.next(); ds.dstDir = dst; } } else { endMsg("Destination is not a directory, aborting"); return false; } } // *************** // *** Dumping *** // *************** out.println("Dumping source..."); dumpSource(); endMsg("Dump complete"); return true; } public void close() { try { keyDataFile.close(); } catch (IOException e) { out.println("Error closing Key Data File: " + e.getMessage()); } keyDataFile = null; } public KeyDataFile getKeyDataFile() { return keyDataFile; } /** * Display the given message followed by the end time string * * @param reason Message to display */ private void endMsg(String reason) { out.println(reason); out.println(); out.println("End time: " + new Date()); out.println(); } private DiscSet getFirstDiscSet(int type) { Iterator<DiscSet> it = discSets.iterator(); while (it.hasNext()) { DiscSet ds = it.next(); if ((ds.contentType & type) != 0) { return ds; } } return null; } private ArrayList<String> getStreamSet(DiscSet ds, int number) { // TODO: I could use ds.streamSets.get(int) alone because there should be no "null" key inside if (!ds.streamSets.containsKey(number)) { ArrayList<String> streamSet = new ArrayList<String>(64); ds.streamSets.put(number, streamSet); return streamSet; } else { return ds.streamSets.get(number); } } /** * Simple cmd parser to start the program * * @param args First argument is the source, second is the destination directory */ public static void main(String[] args) { // Check if parameters are given if (args.length == 0) { // Start the GUI new Manager(); } else { //for (int i = 0; i < args.length; i++) { // System.out.println(args[i]); //} // Simple command line parser boolean streaming = false; ArrayList<String> inFiles = new ArrayList<String>(64); String convTable = null; String input = null;; String output = null; int currentArg = 0; String option = null; // Parse options for(; currentArg < args.length; currentArg++) { if (args[currentArg].startsWith("--")) { // Long option option = args[currentArg].substring(2); if (option.startsWith("infile:")) { // Input file String tmp = option.substring(7); if (!tmp.isEmpty()) { inFiles.add(tmp); } else { System.out.println("No file given for infile"); System.exit(1); } } else if (option.startsWith("convtable:")) { // Conversion Table convTable = option.substring(10); if (convTable.isEmpty()) { System.out.println("No file given for convtable"); System.exit(1); } } else { // Unknown long option System.out.println(String.format("Unknown long option: \"%1$s\"", option)); System.exit(1); } } else if (args[currentArg].startsWith("-")) { // Short option option = args[currentArg].substring(1); // Help if (option.equals("h")) { System.out.println(DumpHD.PROGRAM_NAME + " " + DumpHD.PROGRAM_VERSION + " by " + DumpHD.PROGRAM_AUTHOR); System.out.println(); System.out.println("Usage : DumpHD [options] source [destination]"); System.out.println(); System.out.println("Options:"); System.out.println("-h : This help screen"); System.out.println("--convtable:<file> : Use this Conversion Table to remove BD+"); System.out.println("--infile:<file> : Only process the specified file. It must be given"); System.out.println(" relative to source. This parameter can be specified"); System.out.println(" multiple times, the files will be processed in the"); System.out.println(" given order."); System.out.println(); System.out.println("source : Source to dump, must be the root directory of the disc"); System.out.println("destination : directory where the dump is written"); System.out.println(" If destination is omitted streaming mode is used."); System.out.println(" In streaming mode only EVO / M2TS files are processed,"); System.out.println(" they are decrypted to stdout (EVOs without decrypting"); System.out.println(" ADV_PCKs) in alphabetical order."); System.out.println(" Pipe the output to another program which can read from"); System.out.println(" stdin."); System.out.println(" Textual output is written to stderr."); System.out.println(); System.out.println("If started without arguments, the GUI is loaded"); System.exit(0); } else { System.out.println(String.format("Unknown option: \"%1$s\"", option)); System.exit(1); } } else { // Not an option break; } } // Parse input if (currentArg < args.length) { input = args[currentArg++]; } else { System.out.println("No source specified"); System.exit(1); } // Parse output if (currentArg < args.length) { output = args[currentArg++]; } else { streaming = true; } // Check for invalid following parameters if (currentArg < args.length) { System.out.println(String.format("Invalid parameter: \"%1$s\"", args[currentArg])); System.exit(1); } // Execute DumpHD try { DumpHD dumpHD = null; if (!streaming) { dumpHD = new DumpHD(false); } else { PrintStreamPrinter errPrinter = new PrintStreamPrinter(System.err); Utils.setMessagePrinter(errPrinter); dumpHD = new DumpHD(errPrinter, true); } dumpHD.dump(input, output, inFiles, convTable); dumpHD.close(); } catch (Exception e) { Utils.getMessagePrinter().println(e.getMessage()); System.exit(2); } } } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/core/MultiplexedArf.java����������������������������������������������0000664�0001750�0001750�00000033703�11211610544�023210� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.core; import java.io.File; import java.io.IOException; import java.util.Iterator; import java.util.LinkedList; import dumphd.util.ByteArray; import dumphd.util.ByteSource; import dumphd.util.FileSource; import dumphd.util.PackSource; import dumphd.util.PESPack; import dumphd.util.PESParserException; /** * A MultiplexedArf is used to access an ARF multiplexed into an EVO. This class is used to build a list with the locations of the packets for one ARF. * Then a reader and a writer can be requested from this class which allow reading and writing to the ARF through the ByteSource interface. It is possible to * redirect the writes to demux the ARF while processing it. If the ARF gets not demuxed, the reader and writer use the same underlying ByteSource to access the ARF. * This class automatically pads the resulting ARF in the EVO (if not demuxed) when closed. * * TODO: Don't throw IllegalStateExceptions, or at least write that into the javadoc? * * @author KenD00 */ public final class MultiplexedArf { public final static int START_PACKET = 0x10; public final static int INTERMEDIATE_PACKET = 0x00; public final static int END_PACKET = 0x20; /** * This is the file that gets returned by the getFile() call from the reader and writer */ private File arfFile = null; /** * The underlying ByteSource used to access the EVO */ private ByteSource bs = null; /** * If true, the ARF gets demuxed, otherwise it is rewritten in place */ private boolean demux = false; /** * The list with the records to the packs */ private LinkedList<PackSource.PackRecord> packRecords = new LinkedList<PackSource.PackRecord>(); /** * Current dataOffset, used when creating the packRecord list */ private long currentDataOffset = 0; /** * If the MultiplexedArf if completed, no more data can be added and the reader and writer can get returned */ private boolean complete = false; /** * The created reader returned by this object */ private ByteSource reader = null; /** * The created writer returned by this object */ private ByteSource writer = null; /** * Creates a new MultiplexedArf which is contained in the given EVO. * * @param arfName The filename of the ARF, this get added behind the path of bs * @param bs The ByteSource of the EVO to use * @param demux If true, the writer demuxed the ARF, otherwise it rewrites it into the EVO */ public MultiplexedArf(String arfName, ByteSource bs, boolean demux) { this.arfFile = new File(bs.getFile().getParentFile(), arfName); this.bs = bs; this.demux = demux; } /** * Adds a new data packet to the list. This method can only be called when the ARF is not completed. * * @param packOffset Offset to the pack * @param payloadOffset Relative offset from the pack start to the payload start * @param payloadSize The size of the payload */ public void addData(long packOffset, int payloadOffset, int payloadSize) { if (!complete) { packRecords.add(new PackSource.PackRecord(currentDataOffset, packOffset, payloadOffset, payloadSize)); currentDataOffset += (long)payloadSize; } else throw new IllegalStateException("Cannot add additional data, MultiplexedArf already completed"); } /** * Marks this MultiplexedArf as complete. */ public void markComplete() { if (!complete) { complete = true; // If there are no entries in the list, add a zero-entry, or the PackSource will throw exceptions if (packRecords.size() == 0) { packRecords.add(new PackSource.PackRecord()); } //Iterator<PackSource.PackRecord> it = packRecords.iterator(); //while (it.hasNext()) { // System.out.println(it.next().toString()); //} } } /** * If not already done, creates a reader. The reader then gets returned. * This method may only be called after this MultiplexedArf has been marked as complete. * * @return The reader to read the ARF * @throws IOException An I/O error during the creation of the reader occurred */ public ByteSource getReader() throws IOException { if (complete) { if (reader == null) { reader = new PackSource(bs, ByteSource.R_MODE, arfFile, packRecords); } return reader; } else throw new IllegalStateException("Cannot create reader, MultiplexedArf not completed"); } /** * If not already done, creates a writer. The writer then gets returned. * This method may only be called after this MultiplexedArf has been marked as complete. * * @return The writer to write the ARF. Depending on the creation of this MultiplexedArf, the ARF gets demuxed or rewritten in place * @throws IOException An I/O error during the creation of the writer occurred */ public ByteSource getWriter() throws IOException { if (complete) { if (writer == null) { if (demux) { writer = new FileSource(arfFile, ByteSource.RW_MODE); } else { writer = new PackSource(bs, ByteSource.W_MODE, arfFile, packRecords); } } return writer; } else throw new IllegalStateException("Canot create writer, MultiplexedArf not completed"); } /** * Closes the created reader and writer and pads the EVO if necessary. * * @throws IOException An I/O error occurred */ public void close() throws IOException { StringBuffer exceptionMessage = new StringBuffer(128); if (writer != null) { if (!demux) { try { // Fix the packet structure of the underlying source // The PESPack to parse the pack data PESPack pack = new PESPack(); // The ARF_PCK that gets modified PESPack.PESPacket packet = null; // Iterator over all PackRecords Iterator<PackSource.PackRecord> it = packRecords.iterator(); // The current PackRecord PackSource.PackRecord packRecord = it.next(); // The byte array to read the pack data in byte[] packBuffer = new byte[PESPack.PACK_LENGTH]; // The maximum written position long maxWritten = ((PackSource)writer).maxWrittenPosition(); // Check if the ARF is more than one packet big if (maxWritten <= (long)packRecord.payloadSize) { // The multiplexed ARF occupies only one packet // FIXME: Temporary message until i know how to handle only one packet ARFs System.out.println(String.format("0x%1$010X Warning! Multiplexed ARF is only one packet big!", packRecord.packetOffset)); // Read the pack bs.setPosition(packRecord.packetOffset); bs.read(packBuffer, 0, packBuffer.length); try { // Parse the pack and set the packet type to START_PACKET // FIXME: Is this the correct packet type?? If not change it to the correct one! pack.parse(packBuffer, 0, false); packet = pack.getPacket(0); ByteArray.setByte(packBuffer, packet.payloadOffset(), MultiplexedArf.START_PACKET); // Adjust the payload length and pad the pack. Need to add the size of the header and the filename field packet.setPayloadLength((int)maxWritten + 258); pack.pad(0); // Write the pack back bs.setPosition(packRecord.packetOffset); bs.write(packBuffer, 0, packBuffer.length); } catch (PESParserException e) { if (exceptionMessage.length() > 0) { exceptionMessage.append(", "); } exceptionMessage.append(String.format("0x%1$010X Error padding stream, PES Parser Exception: ", packRecord.packetOffset)); //exceptionMessage.append("Error padding stream, PES Parser Exception: "); exceptionMessage.append(e.getMessage()); } } else { // The multiplexed ARF occupies multiple packets // Search the end packet. Because the decrypted ARF is always smaller than the original one we don't need to adjust the // INTERMEDIATE_PACKETs, we only need to set the new END_PACKET which will be before the original one. //System.out.println("ARF occupies more than one packet, searching end packet"); while (maxWritten > packRecord.dataOffset + (long)packRecord.payloadSize) { if (it.hasNext()) { packRecord = it.next(); } else throw new IOException("Unexpected EOF"); } // Found the new END_PACKET, read it in //System.out.println(String.format("End packet found at position: 0x%1$010X, maxWritten: 0x%2$010X", packRecord.packetOffset, maxWritten)); bs.setPosition(packRecord.packetOffset); bs.read(packBuffer, 0, packBuffer.length); try { // Parse the pack and set the packet type to END_PACKET pack.parse(packBuffer, 0, false); packet = pack.getPacket(0); ByteArray.setByte(packBuffer, packet.payloadOffset(), MultiplexedArf.END_PACKET); // Adjust the payload length and pad the pack. Need to add the size of the header packet.setPayloadLength(3 + (int)(maxWritten - packRecord.dataOffset)); pack.pad(0); // Write the pack back bs.setPosition(packRecord.packetOffset); bs.write(packBuffer, 0, packBuffer.length); } catch (PESParserException e) { if (exceptionMessage.length() > 0) { exceptionMessage.append(", "); } exceptionMessage.append(String.format("0x%1$010X Error padding stream, PES Parser Exception: ", packRecord.packetOffset)); //exceptionMessage.append("Error padding stream, PES Parser Exception: "); exceptionMessage.append(e.getMessage()); } } // Pad the whole packs which follow the new END_PACKET while (it.hasNext()) { packRecord = it.next(); // Read the pack in bs.setPosition(packRecord.packetOffset); bs.read(packBuffer, 0, packBuffer.length); try { // Parse and pad the whole pack pack.parse(packBuffer, 0, false); pack.pad(-1); // Write the pack back bs.setPosition(packRecord.packetOffset); bs.write(packBuffer, 0, packBuffer.length); } catch (PESParserException e) { if (exceptionMessage.length() > 0) { exceptionMessage.append(", "); } exceptionMessage.append(String.format("0x%1$010X Error padding stream, PES Parser Exception: ", packRecord.packetOffset)); //exceptionMessage.append("Error padding stream, PES Parser Exception: "); exceptionMessage.append(e.getMessage()); } } } catch (IOException e) { if (exceptionMessage.length() > 0) { exceptionMessage.append(", "); } exceptionMessage.append("Error padding stream: "); exceptionMessage.append(e.getMessage()); } } try { writer.close(); } catch (IOException e) { if (exceptionMessage.length() > 0) { exceptionMessage.append(", "); } exceptionMessage.append("Error closing writer: "); exceptionMessage.append(e.getMessage()); } } if (reader != null) { try { reader.close(); } catch (IOException e) { if (exceptionMessage.length() > 0) { exceptionMessage.append(", "); } exceptionMessage.append("Error closing reader: "); exceptionMessage.append(e.getMessage()); } } // Release resources reader = null; writer = null; packRecords = null; bs = null; arfFile = null; // If there was an exception, now throw it if (exceptionMessage.length() > 0) { throw new IOException(exceptionMessage.toString()); } } } �������������������������������������������������������������dumphd-0.61/source/src/dumphd/core/ACAPacker.java���������������������������������������������������0000664�0001750�0001750�00000131251�11211610506�021770� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.core; import java.io.*; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import dumphd.aacs.AACSDecrypter; import dumphd.aacs.AACSException; import dumphd.util.ByteArray; import dumphd.util.PrintStreamPrinter; import dumphd.util.CopyResult; import dumphd.util.ByteSource; import dumphd.util.FileSource; import dumphd.util.MessagePrinter; import dumphd.util.Utils; /** * A class for depacking, packing and repacking ACA-Files. * * TODO: CRC-checking? * TODO: Close files in case of I/O exception? Effects only dePack and pack where new FileSources are created. * * @author KenD00 */ public class ACAPacker { /** * ID string of an ACA file */ public final static String idString = "HDDVDACA"; /** * Maximum supported ACA file version */ public final static long maxVersion = 0x00100001; /** * Lookup-table for mime types. Key is the file extension in upper case including the dot */ private final static HashMap<String, Byte> mimeTypes = new HashMap<String, Byte>(); /** * Used to output textual messages */ private MessagePrinter out = null; /** * Used for detecting and processing AACS protection */ private AACSDecrypter aacsDec = null; /** * Used to store the header of an ACA file */ private byte[] header = new byte[32]; /** * Used to store the TOC of an ACA file */ private byte[][] toc = new byte[65536][]; /** * If true, AACS protection is not only identified by the mime field but also by the file contents itself. Used during de- and repacking */ private boolean contentCheck = false; /** * Static constructor to initialize the mime type table */ static { mimeTypes.put(".XPL", new Byte((byte)0x01)); mimeTypes.put(".XMF", new Byte((byte)0x02)); mimeTypes.put(".XMU", new Byte((byte)0x03)); mimeTypes.put(".XTS", new Byte((byte)0x04)); mimeTypes.put(".XAS", new Byte((byte)0x05)); mimeTypes.put(".XSS", new Byte((byte)0x06)); mimeTypes.put(".JS", new Byte((byte)0x07)); mimeTypes.put(".EVO", new Byte((byte)0x08)); mimeTypes.put(".MAP", new Byte((byte)0x09)); mimeTypes.put(".JPG", new Byte((byte)0x0A)); mimeTypes.put(".PNG", new Byte((byte)0x0B)); mimeTypes.put(".MNG", new Byte((byte)0x0C)); mimeTypes.put(".CVI", new Byte((byte)0x0D)); mimeTypes.put(".CDW", new Byte((byte)0x0E)); mimeTypes.put(".WAV", new Byte((byte)0x0F)); mimeTypes.put(".OTF", new Byte((byte)0x10)); mimeTypes.put(".TTC", new Byte((byte)0x10)); mimeTypes.put(".TTF", new Byte((byte)0x10)); // The mime type for encrypted and unknown file types mimeTypes.put(null, new Byte((byte)0xFF)); } /** * Creates a new ACAPacker which does not process AACS and uses the given MessagePrinter for textual output. * * @param mp Used for textual output */ public ACAPacker(MessagePrinter mp) { this.out = mp; for (int i = 0; i < 65536; i++) { toc[i] = null; } } /** * Creates a new ACAPacker which removes AACS and uses the given MessagePrinter for textual output. * * @param mp Used for textual output * @param aacsDec Used to process AACS */ public ACAPacker(MessagePrinter mp, AACSDecrypter aacsDec) { this.out = mp; this.aacsDec = aacsDec; for (int i = 0; i < 65536; i++) { toc[i] = null; } } /** * Lists the information of the ACA file header, prints out the TOC and statistics about the filesize. * Ignores the ACA file version and a possible filesize mismatch. * * @param input ACE file * @throws ACAException There is an error with the ACA file structure * @throws IOException An I/O error occurred */ public void list(ByteSource input) throws ACAException, IOException { // Data fields of the header String fileId = null; int tocEntries = 0; long fileSize = 0; // Data fields of a toc entry byte[] tocEntry = null; long offset = 0; long size = 0; long crc32 = 0; int mimeType = 0; String fileName = null; // Helper variables long tocSize = 0; long dataSize = 0; boolean versionWarning = false; boolean sizeWarning = false; out.print("Reading header... "); if (input.read(header, 0, header.length) == header.length) { fileId = new String(header, 0, idString.length(), "US-ASCII"); if (fileId.equals(idString)) { if (ByteArray.getVarLong(header, 8, 4) > maxVersion) { versionWarning = true; } tocEntries = ByteArray.getUShort(header, 12); fileSize = ByteArray.getVarLong(header, 14, 4); if (fileSize != input.size()) { sizeWarning = true; } out.println("OK"); if (versionWarning) { out.println("WARNING! Unsupported ACA version"); } if (sizeWarning) { out.println("WARNING! ACA filesize does not match physical filesize"); } out.println(String.format("Version : 0x%1$08X", ByteArray.getVarLong(header, 8, 4))); out.println("Number of files : " + ByteArray.getUShort(header, 12)); out.println("Filesize : " + ByteArray.getVarLong(header, 14, 4)); out.println("Reading TOC... "); out.println(String.format("%1$10s %2$10s %3$10s %4$2s %5$s", "Offset", "Size", "CRC32", "Mime", "Name")); // Read the toc into memory for (int i = 0; i < tocEntries; i++) { tocEntry = toc[i]; if (tocEntry == null) { tocEntry = new byte[301]; toc[i] = tocEntry; } // Zero the tocEntry, so that in case of a read error no old values are present Arrays.fill(tocEntry, 0, tocEntry.length, (byte)0); input.read(tocEntry, 0, 14); input.read(tocEntry, 14, (tocEntry[13] & 0xFF) + 32); offset = ByteArray.getVarLong(tocEntry, 0, 4); size = ByteArray.getVarLong(tocEntry, 4, 4); crc32 = ByteArray.getVarLong(tocEntry, 8, 4); mimeType = ByteArray.getUByte(tocEntry, 12); fileName = new String(tocEntry, 14, (tocEntry[13] & 0xFF), "US-ASCII"); tocSize += 46 + (tocEntry[13] & 0xFF); dataSize += size; out.println(String.format("0x%1$08X 0x%2$08X 0x%3$08X 0x%4$02X %5$s", offset, size, crc32, mimeType, fileName)); } long contentSize = header.length + tocSize + dataSize; out.println("Finished TOC reading"); out.println(); out.println("Header size : " + header.length); out.println("TOC size : " + tocSize); out.println("Data size : " + dataSize); out.println("-------------------------------"); out.println("Total content size : " + contentSize); out.println("Filesize : " + fileSize); out.println("-------------------------------"); out.println("Slack : " + (fileSize - contentSize)); } else throw new ACAException("Invalid file id"); } else throw new ACAException("File too small to be an ACA file"); } /** * Depacks the given input into the given output directory. * The input position must be at the starting position of the ACA file. * * @param input The input ACA * @param outDir The output directory * @return The number of written bytes * @throws ACAException There is an error with the ACA file structure * @throws IOException An I/O error occurred */ public long dePack(ByteSource input, File outDir) throws ACAException, IOException { // Data fields of the header String fileId = null; int tocEntries = 0; long fileSize = 0; // Data fields of a toc entry byte[] tocEntry = null; long offset = 0; long size = 0; long crc32 = 0; String fileName = null; // Calculated helper values long oldOffset = 0; long dataCrc32 = 0; long outSize = 0; // For storing return values from aacs-decryption and file copy CopyResult cr = null; out.print("Reading header... "); if (input.read(header, 0, header.length) == header.length) { fileId = new String(header, 0, idString.length(), "US-ASCII"); if (fileId.equals(idString)) { if (ByteArray.getVarLong(header, 8, 4) <= maxVersion) { tocEntries = ByteArray.getUShort(header, 12); fileSize = ByteArray.getVarLong(header, 14, 4); // Allow more bytes at the end of the file if (fileSize <= input.size()) { // Read the toc into memory out.println("OK"); out.print("Reading TOC... "); for (int i = 0; i < tocEntries; i++) { tocEntry = toc[i]; if (tocEntry == null) { tocEntry = new byte[301]; toc[i] = tocEntry; } input.read(tocEntry, 0, 14); input.read(tocEntry, 14, (tocEntry[13] & 0xFF) + 32); } out.println("OK"); // OldOffset contains the end offset of the previous element oldOffset = input.getPosition(); // Process the toc entries, depack every file out.println("Depacking files..."); for (int i = 0; i < tocEntries; i++) { tocEntry = toc[i]; offset = ByteArray.getVarLong(tocEntry, 0, 4); size = ByteArray.getVarLong(tocEntry, 4, 4); crc32 = ByteArray.getVarLong(tocEntry, 8, 4); fileName = new String(tocEntry, 14, tocEntry[13], "US-ASCII"); // Check if this offset doesnt point into the data area of the previous dataset if (offset >= oldOffset) { // Update oldOffset to the end offset of the current dataset oldOffset = offset + size; // Check if the data does not exceed the file size if (oldOffset <= fileSize) { out.print(fileName); FileSource output = new FileSource(new File(outDir, fileName), FileSource.RW_MODE); input.setPosition(offset); input.addWindow(size); try { // Check if AACS should be used, use only mime type or identifiy also by content if (aacsDec != null && (ByteArray.getUByte(tocEntry, 12) == (mimeTypes.get(null) & 0xFF) || contentCheck)) { out.print(", searching AACS... "); cr = aacsDec.decryptArf(input, output); if (cr.size != size) { out.println("REMOVED"); // Updating crc32 to avoid false crc32 error. // TODO: How to check if crc32 was correct? The ARF-Decrypter must do that, or the file must be read in just to check the crc32 crc32 = cr.crc32; } else { out.println("NONE"); } } else { if (contentCheck) { out.print(", searching AACS... "); if (AACSDecrypter.isArfEncrypted(input)) { out.println("FOUND"); } else { out.println("NONE"); } } else { out.println(); } // Just copy the file cr = Utils.copyBs(input, output, size); } } catch (AACSException e) { out.println("ERROR"); out.println(e.getMessage()); // Set to 0 to not update the returned outsize with the size of the error causing file cr.size = 0; // Update the returned crc32 value to avoid a second error message because of crc32 error cr.crc32 = crc32; } input.removeWindow(); outSize += cr.size; dataCrc32 = cr.crc32; output.close(); // Check if crc is correct if (dataCrc32 != crc32) { // TODO: What to do in case of crc error? } } else throw new ACAException("Current file exceeds total file size"); } else throw new ACAException("Offset of current file is inside previous file"); } out.println("Depacking finished"); return outSize; } else throw new ACAException("Physical filesize smaller than ACA filesize"); } else throw new ACAException("Unsupported ACA file version"); } else throw new ACAException("Invalid file id"); } else throw new ACAException("File too small to be an ACA file"); } /** * Packs the given input files into a new ACA file. * * @param inFiles Collection with the input files * @param outFile The output ACA * @return The number of written bytes * @throws ACAException An error occurred creating the ACA file structure. Currently only happens when AACS processing failes for an input file. * @throws IOException An I/O error occurred */ public long pack(Collection<File> inFiles, File outFile) throws ACAException, IOException { // Data fields of the header int tocEntries = 0; int tocSize = 0; // Initial size of the output file, only the size of the header long fileSize = header.length; // Data fields of a toc entry byte[] tocEntry = null; long offset = 0; long size = 0; long crc32 = 0; byte mimeType = 0; String fileName = null; byte[] fileNameBytes = null; // For storing return values from aacs-decryption and file copy CopyResult cr = null; // Parse the input file list out.println("Building TOC... "); Iterator<File> it = inFiles.iterator(); while (it.hasNext()) { File inFile = it.next(); // Only process existent files if (inFile.isFile()) { // Build the toc in memory, only set fields that are known at this point tocEntry = toc[tocEntries]; if (tocEntry == null) { tocEntry = new byte[301]; toc[tocEntries] = tocEntry; } fileName = inFile.getName(); fileNameBytes = fileName.getBytes("US-ASCII"); mimeType = mimeTypes.get(null); int extOffset = fileName.lastIndexOf('.'); if (extOffset >= 0) { Byte mimeTypeObject = mimeTypes.get(fileName.substring(extOffset).toUpperCase()); if (mimeTypeObject != null) { mimeType = mimeTypeObject; } } ByteArray.setByte(tocEntry, 12, mimeType); ByteArray.setByte(tocEntry, 13, (byte)fileNameBytes.length); System.arraycopy(fileNameBytes, 0, tocEntry, 14, fileNameBytes.length); Arrays.fill(tocEntry, 14 + fileNameBytes.length, tocEntry.length, (byte)0); tocEntries += 1; tocSize += 46 + fileNameBytes.length; } else { // File not found / is a directory, skip it out.println(inFile.getPath() + ": File not found, skipping"); it.remove(); } } // Update file size with the size of the toc fileSize += tocSize; // Writing part, write out the aca-archive FileSource output = new FileSource(outFile, FileSource.RW_MODE); // Seek behind the toc //System.out.println("Seeking behind toc"); output.setPosition(fileSize); // Write the every file, update the missing toc fields out.println("Packing files... "); int i = 0; it = inFiles.iterator(); while (it.hasNext()) { File inFile = it.next(); tocEntry = toc[i]; out.print(inFile.getName()); FileSource input = new FileSource(inFile, FileSource.R_MODE); // Set the size here so that we can check if AACS was removed size = input.size(); offset = output.getPosition(); //System.out.println("OutPos is: " + offset); // Setting a output window would be safer, but slower because the buffer will get flushed for every file output.addWindow(size); // If AACS should be processed, decrypt the files if (aacsDec != null) { out.print(", searching AACS... "); try { cr = aacsDec.decryptArf(input, output); if (cr.size != size) { out.println("REMOVED"); } else { out.println("NONE"); } } catch (AACSException e) { out.println("ERROR"); out.println(e.getMessage()); input.close(); output.close(); throw new ACAException("Could not add file to archive", e); } } else { // If content check is enabled, check if the file is encrypted if (contentCheck) { //System.out.println("Checking for encryption in src"); out.print(", looking for AACS protection... "); if (AACSDecrypter.isArfEncrypted(input)) { tocEntry[12] = mimeTypes.get(null); out.println("FOUND"); } else { out.println("NONE"); } } else { out.println(); } //System.out.println("Size is: " + size); // Copy the file cr = Utils.copyBs(input, output, size); } output.removeWindow(); size=cr.size; crc32 = cr.crc32; input.close(); ByteArray.setInt(tocEntry, 0, (int)offset); ByteArray.setInt(tocEntry, 4, (int)size); ByteArray.setInt(tocEntry, 8, (int)crc32); fileSize += size; i += 1; } // Build the header and write it to the output // Check if i == tocEntries? Must be the case, but can it be different? fileNameBytes = idString.getBytes("US-ASCII"); System.arraycopy(fileNameBytes, 0, header, 0, fileNameBytes.length); ByteArray.setInt(header, 8, (int)maxVersion); ByteArray.setShort(header, 12, (short)tocEntries); ByteArray.setInt(header, 14, (int)fileSize); Arrays.fill(header, 18, header.length, (byte)0); //System.out.println("Seeking begin of file"); out.print("Writing header... "); output.setPosition(0); output.write(header, 0, header.length); out.println("OK"); out.print("Writing TOC... "); // Write the toc to the output for (i = 0; i < tocEntries; i++) { tocEntry = toc[i]; output.write(tocEntry, 0, 46 + tocEntry[13]); } output.close(); out.println("OK"); out.println("Packing finished"); return fileSize; } /** * Repacks the given input ACA to a new ACA. Only useful when removing AACS. * The input position must be at the starting position of the ACA file, the output gets written starting at the current output position. * * @param input The input ACA * @param output The output ACA * @return The number of written bytes * @throws ACAException There is an error with the input ACA file structure, or AACS could not be removed for the output ACA * @throws IOException An I/O error occurred */ public long rePack(ByteSource input, ByteSource output) throws ACAException, IOException { // Data fields of the header String fileId = null; int tocEntries = 0; long fileSize = 0; // Data fields of a toc entry byte[] tocEntry = null; long offset = 0; long size = 0; long crc32 = 0; // ** Change from decrypting byte mimeType = 0; // ** End of change String fileName = null; // Calculated helper values long oldOffset = 0; long dataCrc32 = 0; long outSize = 0; // For storing return values from aacs-decryption and file copy CopyResult cr = null; out.print("Reading header... "); if (input.read(header, 0, header.length) == header.length) { fileId = new String(header, 0, idString.length(), "US-ASCII"); if (fileId.equals(idString)) { if (ByteArray.getVarLong(header, 8, 4) <= maxVersion) { tocEntries = ByteArray.getUShort(header, 12); fileSize = ByteArray.getVarLong(header, 14, 4); // Allow more bytes at the end of the file if (fileSize <= input.size()) { // Read the toc into memory out.println("OK"); out.print("Reading TOC... "); for (int i = 0; i < tocEntries; i++) { tocEntry = toc[i]; if (tocEntry == null) { tocEntry = new byte[301]; toc[i] = tocEntry; } input.read(tocEntry, 0, 14); input.read(tocEntry, 14, (tocEntry[13] & 0xFF) + 32); } out.println("OK"); // OldOffset contains the end offset of the previous element oldOffset = input.getPosition(); // ** Change from Decrypting output.setPosition(oldOffset); // OutSize contains the current size of the output, that is also the offset for the current dataset! outSize = oldOffset; // ** End of change // Process the toc entries, repack every file out.println("Repacking files..."); for (int i = 0; i < tocEntries; i++) { tocEntry = toc[i]; offset = ByteArray.getVarLong(tocEntry, 0, 4); size = ByteArray.getVarLong(tocEntry, 4, 4); crc32 = ByteArray.getVarLong(tocEntry, 8, 4); fileName = new String(tocEntry, 14, tocEntry[13], "US-ASCII"); // Check if this offset doesnt point into the data area of the previous dataset if (offset >= oldOffset) { // Update oldOffset to the end offset of the current dataset oldOffset = offset + size; // Check if the data does not exceed the file size if (oldOffset <= fileSize) { out.print(fileName); // ** Change from Decrypting //FileSource output = new FileSource(new File(outDir, fileName), FileSource.RW_MODE); // ** End of change input.setPosition(offset); input.addWindow(size); // ** Change from Decrypting // Setting a output window would be safer, but slower because the buffer will get flushed for every file output.addWindow(size); // ** End of change // Check if AACS should be used, use only mime type or identifiy also by content if (aacsDec != null && (ByteArray.getUByte(tocEntry, 12) == (mimeTypes.get(null) & 0xFF) || contentCheck)) { out.print(", searching AACS... "); try { cr = aacsDec.decryptArf(input, output); if (cr.size != size) { out.println("REMOVED"); } else { out.println("NONE"); } } catch (AACSException e) { out.println("ERROR"); out.println(e.getMessage()); throw new ACAException("Could not add file to archive", e); } // ** Change from decrypting // Updating these values to because thay are later used to update the toc entry (but this can be done another way too) // Mainly the crc32 value must be updated to avoid false crc32 errors. // TODO: How to check if crc32 was correct? The ARF-Decrypter must do that, or the file must be read in just to check the crc32 size = cr.size; crc32 = cr.crc32; mimeType = mimeTypes.get(null); int extOffset = fileName.lastIndexOf('.'); if (extOffset >= 0) { Byte mimeTypeObject = mimeTypes.get(fileName.substring(extOffset).toUpperCase()); if (mimeTypeObject != null) { mimeType = mimeTypeObject; } } // Update offset, size, crc and mime type field in toc entry ByteArray.setInt(tocEntry, 0, (int)outSize); ByteArray.setInt(tocEntry, 4, (int)size); ByteArray.setInt(tocEntry, 8, (int)crc32); ByteArray.setByte(tocEntry, 12, mimeType); // ** End of change } else { if (contentCheck) { out.print(", searching AACS... "); if (AACSDecrypter.isArfEncrypted(input)) { out.println("FOUND"); } else { out.println("NONE"); } } else { out.println(); } // Just copy the file cr = Utils.copyBs(input, output, size); } // ** Change from Decrypting output.removeWindow(); // ** End of change input.removeWindow(); outSize += cr.size; dataCrc32 = cr.crc32; // ** Change from decrypting //out.close(); // ** End of change // Check if crc is correct if (dataCrc32 != crc32) { // TODO: What to do in case of crc error? } } else throw new ACAException("Current file exceeds total file size"); } else throw new ACAException("Offset of current file is inside previous file"); } // ** Change from decrypting int i = 0; // ** Change from encrypting //fileNameBytes = idString.getBytes("US-ASCII"); //System.arraycopy(fileNameBytes, 0, header, 0, fileNameBytes.length); //ByteArray.setInt(header, 8, maxVersion); //ByteArray.setShort(header, 12, (short)tocEntries); ByteArray.setInt(header, 14, (int)outSize); //Arrays.fill(header, 18, 32, (byte)0); out.print("Writing header... "); output.setPosition(0); output.write(header, 0, header.length); out.println("OK"); out.print("Writing TOC... "); for (i = 0; i < tocEntries; i++) { tocEntry = toc[i]; output.write(tocEntry, 0, 46 + tocEntry[13]); } out.println("OK"); // ** End of change out.println("Repacking finished"); return outSize; } else throw new ACAException("Physical filesize smaller than ACA filesize"); } else throw new ACAException("Unsupported ACA file version"); } else throw new ACAException("Invalid file id"); } else throw new ACAException("File too small to be an ACA file"); } public void clearTocTable() { for (int i = 0; i < toc.length; i++) { toc[i] = null; } } public boolean isContentChecking() { return contentCheck; } public void setContentChecking(boolean contentCheck) { this.contentCheck = contentCheck; } public static void main(String[] args) { System.out.println("ACAPacker 0.8 by KenD00"); System.out.println(); int mode = -1; int crypto = 2; int currentArg = 0; if (args.length > 1) { String cli = args[currentArg]; if (cli.startsWith("-")) { for (int i = 1; i < cli.length(); i++) { switch (cli.charAt(i)) { case 'd': case 'D': mode = 0; break; case 'p': case 'P': mode = 1; break; case 'r': case 'R': mode = 2; break; case 'l': case 'L': mode = 3; break; case 'c': case 'C': if (i + 1 < cli.length()) { switch (cli.charAt(i + 1)) { case '0': crypto = 0; break; case '1': crypto = 1; break; case '2': crypto = 2; } i += 1; } break; } } currentArg += 1; } } switch (mode) { case 0: if (currentArg + 2 == args.length) { File in = new File(args[currentArg]).getAbsoluteFile(); File out = new File(args[currentArg + 1]).getAbsoluteFile(); if (in.isFile()) { if (!out.exists()) { out.mkdirs(); } if (out.isDirectory()) { ACAPacker packer = initACAPacker(crypto, in); if (packer != null) { try { FileSource inSrc = new FileSource(in, FileSource.R_MODE); try { long written = packer.dePack(inSrc, out); inSrc.close(); System.out.println("Written: " + written); } catch (ACAException e) { System.out.println("Error: " + e.getMessage()); inSrc.close(); System.exit(2); } System.exit(0); } catch (IOException e) { System.out.println("Error: " + e.getMessage()); e.printStackTrace(); } } } else System.out.println("Error: output is not a directory"); } else System.out.println("Error: input file not found"); } else System.out.println("Error: invalid number of arguments"); System.exit(1); break; case 1: if (currentArg + 2 == args.length) { File in = new File(args[currentArg]).getAbsoluteFile(); File inDirectory = in.getParentFile(); File out = new File(args[currentArg + 1]).getAbsoluteFile(); if (in.isFile()) { if (!out.isDirectory()) { try { LinkedList<File> inList = new LinkedList<File>(); BufferedReader inReader = new BufferedReader(new InputStreamReader(new FileInputStream(in), "US-ASCII")); for (;;) { String inFileName = inReader.readLine(); if (inFileName != null) { File inFile = new File(inFileName); if (!inFile.isAbsolute()) { inList.add(new File(inDirectory, inFileName)); } else { inList.add(inFile); } } else { break; } } inReader.close(); // FIXME: This makes no sense with enabled AACS processing, the input list can never be on the disc! ACAPacker packer = initACAPacker(crypto, in); if (packer != null) { try { long written = packer.pack(inList, out); System.out.println("Written: " + written); } catch (ACAException e) { System.out.println("Error: " + e.getMessage()); System.exit(2); } System.exit(0); } } catch (IOException e) { System.out.println("Error: " + e.getMessage()); e.printStackTrace(); } } else System.out.println("Error: output is a directory"); } else System.out.println("Error: input file not found"); } else System.out.println("Error: invalid number of arguments"); System.exit(1); break; case 2: if (currentArg + 2 == args.length) { File in = new File(args[currentArg]).getAbsoluteFile(); File out = new File(args[currentArg + 1]).getAbsoluteFile(); if (in.isFile()) { if (!out.isDirectory()) { try { ACAPacker packer = initACAPacker(crypto, in); if (packer != null) { FileSource inSource = new FileSource(in, FileSource.R_MODE); FileSource outSource = new FileSource(out, FileSource.RW_MODE); try { long written = packer.rePack(inSource, outSource); // TODO: second close gets lost if first close throws an exception outSource.close(); inSource.close(); System.out.println("Written: " + written); } catch (ACAException e) { System.out.println("Error: " + e.getMessage()); // TODO: second close gets lost if first close throws an exception outSource.close(); inSource.close(); System.exit(2); } System.exit(0); } } catch (IOException e) { System.out.println("Error: " + e.getMessage()); e.printStackTrace(); } } else System.out.println("Error: output is a directory"); } else System.out.println("Error: input file not found"); } else System.out.println("Error: invalid number of arguments"); System.exit(1); break; case 3: if (currentArg + 1 == args.length) { File in = new File(args[currentArg]).getAbsoluteFile(); if (in.isFile()) { ACAPacker packer = new ACAPacker(new PrintStreamPrinter(System.out)); try { FileSource inSrc = new FileSource(in, FileSource.R_MODE); try { packer.list(inSrc); inSrc.close(); System.exit(0); } catch (ACAException e) { System.out.println("Error: " + e.getMessage()); inSrc.close(); System.exit(2); } } catch (IOException e) { System.out.println("Error: " + e.getMessage()); e.printStackTrace(); } System.exit(0); } else System.out.println("Error: input file not found"); } else System.out.println("Error: invalid number of arguments"); System.exit(1); break; } System.out.println("Usage: ACAPacker -parameters input [output]"); System.out.println(); System.out.println("Parameters:"); System.out.println(" -d Depack mode"); System.out.println(" input : input ACA file"); System.out.println(" output : output directory"); System.out.println(" Depacks the contents of the input file into the output directory"); System.out.println(" Existing files in the output directory get overwritten"); System.out.println(); System.out.println(" -p Pack mode"); System.out.println(" input : input file list file"); System.out.println(" A text file with a filename in every line"); System.out.println(" If the filename denotes no / a relative path the path is"); System.out.println(" resolved against the path of the input file list file"); System.out.println(" output : output ACA file"); System.out.println(" Packs the files from the input list into the output ACA file"); System.out.println(" The output ACA file gets overwritten if it already exists"); System.out.println(); System.out.println(" -r Repack mode"); System.out.println(" input : input ACA file"); System.out.println(" output : output ACA file"); System.out.println(" Repacks the input ACA file into the output ACA file,"); System.out.println(" only useful with -c2"); System.out.println(" The output ACA file gets overwritten if it already exists"); System.out.println(); System.out.println(" -l List mode"); System.out.println(" input : input ACA file"); System.out.println(" Lists the header information and prints out the TOC of the"); System.out.println(" input ACA file"); System.out.println(); System.out.println(" -c[x] Cryptography parameters, used in all modes except list mode"); System.out.println(" x = 0 : Do not detect AACS"); System.out.println(" x = 1 : Detect AACS but do not process it"); System.out.println(" x = 2 : Detect and remove AACS (default)"); System.out.println(" This mode requires the AACS directory to be present"); System.out.println(" in the parent directory of the directory of the input file"); } /** * Used by the main method to initialize the ACAPacker. * * @param crypto Crypto mode to initialize * @param srcFile The source file, this must be a file in the ADV_OBJ directory * @return The created ACAPAcker or null if an error occurred */ private static ACAPacker initACAPacker(int crypto, File srcFile) { ACAPacker returnValue = null; if (crypto > 1) { KeyDataFile kdf = null; AACSDecrypter aacsDec = null; try { kdf = new KeyDataFile(new File("KEYDB.cfg")); } catch (IOException e) { System.out.println(e.getMessage()); srcFile = null; } if (srcFile != null) { srcFile = srcFile.getParentFile(); if (srcFile != null) { srcFile = srcFile.getParentFile(); if (srcFile != null) { if (srcFile.isDirectory()) { DiscSet ws = new DiscSet(DiscSet.HD_ADVANCED_V, srcFile); srcFile = new File(srcFile, "AACS"); if (srcFile.isDirectory()) { ws.aacsDir = srcFile; try { aacsDec = new AACSDecrypter(new PrintStreamPrinter(System.out), kdf); aacsDec.identifyDisc(ws); aacsDec.initDisc(ws); aacsDec.init(ws); } catch (AACSException e) { System.out.println(e.getMessage()); srcFile = null; } try { kdf.close(); } catch (IOException e) { System.out.println("Warning: " + e.getMessage()); } } else { System.out.println("AACS directory not found"); srcFile = null; } } else { System.out.println("Root is not a directory"); srcFile = null; } } else { System.out.println("Could not traverse into root directory"); } } else { System.out.println("Source has no parent directory"); } } if (srcFile == null) { System.out.println("Error: AACS not initialized"); return null; } else { returnValue = new ACAPacker(new PrintStreamPrinter(System.out), aacsDec); } } else { returnValue = new ACAPacker(new PrintStreamPrinter(System.out)); } if (crypto > 0) { returnValue.setContentChecking(true); } return returnValue; } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/gui/������������������������������������������������������������������0000775�0001750�0001750�00000000000�11211611342�017243� 5����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/gui/ValidatedInputDialog.java�����������������������������������������0000664�0001750�0001750�00000015025�11211610616�024151� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.gui; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; //import java.awt.event.ComponentAdapter; //import java.awt.event.ComponentEvent; import java.awt.event.WindowEvent; import java.awt.event.WindowAdapter; import java.util.regex.Pattern; import java.util.regex.Matcher; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTextField; /** * Simple Dialog that provides a text box to enter data and the possibility to validate that data before accepting it. * * @author KenD00 */ public class ValidatedInputDialog { private JDialog dialog = null; private JTextField valueField = null; private String validateErrorMsg = null; private boolean dialogConfirmed = false; private Pattern validatePattern = null; public ValidatedInputDialog() { dialog = new JDialog(); createDialog(null, null, null, null, 16, ".*", "", "OK", "Cancel"); } public ValidatedInputDialog(JDialog owner, String title, String message, String valueName, String initialValue, int valueLength, String validatePattern, String validateErrorMsg, String confirmLabel, String cancelLabel) { dialog = new JDialog(owner); createDialog(title, message, valueName, initialValue, valueLength, validatePattern, validateErrorMsg, confirmLabel, cancelLabel); } public ValidatedInputDialog(JFrame owner, String title, String message, String valueName, String initialValue, int valueLength, String validatePattern, String validateErrorMsg, String confirmLabel, String cancelLabel) { dialog = new JDialog(owner); createDialog(title, message, valueName, initialValue, valueLength, validatePattern, validateErrorMsg, confirmLabel, cancelLabel); } private void createDialog(String title, String message, String valueName, String initialValue, int valueLength, String validatePattern, String validateErrorMsg, String confirmLabel, String cancelLabel) { this.validateErrorMsg = validateErrorMsg; this.validatePattern = Pattern.compile(validatePattern); dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); dialog.setModal(true); dialog.setResizable(false); JPanel contentPane = new JPanel(); contentPane.setOpaque(true); contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.PAGE_AXIS)); contentPane.setBorder(Manager.createEmptyBorder()); if (title != null) { dialog.setTitle(title); } if (message != null) { JLabel messageLabel = new JLabel(message); messageLabel.setAlignmentX(JComponent.CENTER_ALIGNMENT); contentPane.add(messageLabel); contentPane.add(Box.createRigidArea(Manager.BOX_VERTICAL_BIG_SPACING)); } JPanel panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS)); panel.setAlignmentX(JComponent.CENTER_ALIGNMENT); if (valueName != null) { JLabel valueLabel = new JLabel(valueName); panel.add(valueLabel); panel.add(Box.createRigidArea(Manager.BOX_HORIZONTAL_SMALL_SPACING)); } if (initialValue != null) { valueField = new JTextField(initialValue, valueLength); valueField.selectAll(); } else { valueField = new JTextField(valueLength); } panel.add(valueField); contentPane.add(panel); contentPane.add(Box.createRigidArea(Manager.BOX_VERTICAL_BIG_SPACING)); panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS)); panel.setAlignmentX(JComponent.CENTER_ALIGNMENT); ButtonListener buttonListener = new ButtonListener(); JButton confirmButton = new JButton(confirmLabel); confirmButton.setActionCommand("confirm"); confirmButton.addActionListener(buttonListener); JButton cancelButton = new JButton(cancelLabel); cancelButton.setActionCommand("cancel"); cancelButton.addActionListener(buttonListener); Manager.equalizeComponentSize(confirmButton, cancelButton); panel.add(confirmButton); panel.add(Box.createRigidArea(Manager.BOX_HORIZONTAL_SMALL_SPACING)); panel.add(cancelButton); contentPane.add(panel); dialog.setContentPane(contentPane); dialog.getRootPane().setDefaultButton(confirmButton); //dialog.addComponentListener(new ComponentAdapter() { // public void componentShown(ComponentEvent e) { // valueField.requestFocusInWindow(); // } //}); dialog.addWindowFocusListener(new WindowAdapter() { public void windowGainedFocus(WindowEvent e) { valueField.requestFocusInWindow(); } }); dialog.pack(); } public boolean show() { dialog.setLocationRelativeTo(dialog.getParent()); dialog.setVisible(true); return dialogConfirmed; } public String getText() { return valueField.getText().trim(); } private class ButtonListener implements ActionListener { public void actionPerformed(ActionEvent e) { String command = e.getActionCommand(); if (command.equals("confirm")) { Matcher validator = validatePattern.matcher(valueField.getText().trim()); if (validator.matches()) { dialogConfirmed = true; dialog.dispose(); } else { JOptionPane.showMessageDialog(dialog, validateErrorMsg, "Error", JOptionPane.ERROR_MESSAGE); } } else if (command.equals("cancel")) { dialog.dispose(); } } } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/gui/MainFrame.java����������������������������������������������������0000664�0001750�0001750�00000072267�11211610562�021766� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.gui; import java.awt.*; import java.awt.event.*; import java.io.File; import java.io.IOException; import java.util.*; import javax.swing.*; import javax.swing.text.*; import dumphd.core.DumpHD; import dumphd.core.DiscSet; import dumphd.util.MessagePrinter; /** * Main window of DumpHD. * * FIXME: Don't use DiscSet.contentType as Index!!!!! A BDMV DiscSet will be overwritten by a BDRMV DiscSet! * * @author KenD00 */ public class MainFrame { private final static String NO_SOURCE_SET = "No source selected"; private final static String NO_DESTINATION_SET = "No destination selected"; private final static String BROWSE = "Browse"; private final static String DUMP = "Dump"; private final static UIManager.LookAndFeelInfo laf[] = UIManager.getInstalledLookAndFeels(); private Manager manager = null; private MessagePrinter out = null; private boolean locked = false; private JFrame mainFrame = null; private JSplitPane horizontalSplitPane = null; private JSplitPane verticalSplitPane = null; private JLabel sourceLabel = null; private JButton sourceButton = null; private JPanel gridPanel = null; private JLabel[] sourceTypeImage = null; private JCheckBox[] sourceTypeCheckBox = null; private boolean[] sourceTypeSelectable = null; private int sourceTypesSelected = 0; private DiscSet[] discSets = null; private JFileChooser sourceChooser = null; private JLabel destinationLabel = null; private JButton destinationButton = null; private JFileChooser destinationChooser = null; private File destinationDirectory = null; private JButton dumpButton = null; private JTabbedPane tabPane = null; private LinkedList<SourceTab> sourceTabs = new LinkedList<SourceTab>(); private JTextArea logArea = null; public MainFrame(Manager manager) { this.manager = manager; JMenuBar menuBar = null; JMenu menu = null; JMenuItem menuItem = null; ButtonGroup buttonGroup = null; JRadioButtonMenuItem radioButtonMenuItem = null; JPanel panel = null; String systemLafClassName = UIManager.getSystemLookAndFeelClassName(); // Set the Look & Feel to the System Look & Feel try { UIManager.setLookAndFeel(systemLafClassName); } catch (UnsupportedLookAndFeelException e1) { // TODO: What to do? } catch (Exception e2) { // TODO: What to do? } // Set up the main JFrame mainFrame = new JFrame(DumpHD.PROGRAM_NAME + " " + DumpHD.PROGRAM_VERSION); mainFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); mainFrame.addWindowListener(new MainFrameWindowAdapter()); // ###################### // ###****************### // ###*** Menu bar ***### // ###****************### // ###################### menuBar = new JMenuBar(); // ***************** // *** File menu *** // ***************** menu = new JMenu("File"); //menuItem = new JMenuItem("Settings"); //menu.add(menuItem); //menu.addSeparator(); menuItem = new JMenuItem("Exit"); menuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { close(); } }); menu.add(menuItem); menuBar.add(menu); // ************************ // *** Look & Feel menu *** // ************************ menu = new JMenu("Look & Feel"); LafActionListener lal = new LafActionListener(); buttonGroup = new ButtonGroup(); for (int i = 0; i < MainFrame.laf.length; i++) { String lafClassName = laf[i].getClassName(); if (lafClassName.equals(systemLafClassName)) { radioButtonMenuItem = new JRadioButtonMenuItem(laf[i].getName(), true); } else { radioButtonMenuItem = new JRadioButtonMenuItem((laf[i].getName()), false); } radioButtonMenuItem.setActionCommand(lafClassName); radioButtonMenuItem.addActionListener(lal); buttonGroup.add(radioButtonMenuItem); menu.add(radioButtonMenuItem); } menuBar.add(menu); // ***************** // *** Help menu *** // ***************** menu = new JMenu("Help"); //menuItem = new JMenuItem("Help"); //menu.add(menuItem); //menu.addSeparator(); menuItem = new JMenuItem("About"); menuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { JOptionPane.showMessageDialog (mainFrame, DumpHD.PROGRAM_NAME + " " + DumpHD.PROGRAM_VERSION + "\n\nThis is the about box.\n" + "\nWhen the time has come, it will be filled with content.\n\n" + DumpHD.PROGRAM_AUTHOR, "About", JOptionPane.INFORMATION_MESSAGE); } }); menu.add(menuItem); menuBar.add(menu); mainFrame.setJMenuBar(menuBar); // ########################### // ###*********************### // ###*** Control panel ***### // ###*********************### // ########################### JPanel controlPanel = new JPanel(); controlPanel.setOpaque(true); controlPanel.setBorder(Manager.createEmptyBorder()); controlPanel.setLayout(new BoxLayout(controlPanel, BoxLayout.PAGE_AXIS)); // ########################## // ###### Source panel ###### // ########################## JPanel sourcePanel = new JPanel(); sourcePanel.setBorder(Manager.createTitledBorder("Source")); sourcePanel.setLayout(new BoxLayout(sourcePanel, BoxLayout.PAGE_AXIS)); // *************************** // *** Source browse panel *** // *************************** panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS)); sourceLabel = new JLabel(MainFrame.NO_SOURCE_SET); sourceLabel.setBorder(Manager.createLabelBorder()); sourceLabel.setToolTipText(MainFrame.NO_SOURCE_SET); sourceButton = new JButton(MainFrame.BROWSE); sourceButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { int returnVal = sourceChooser.showOpenDialog(mainFrame); if (returnVal == JFileChooser.APPROVE_OPTION) { final File selectedFile = sourceChooser.getSelectedFile().getAbsoluteFile(); String selectedFilePath = selectedFile.getPath(); sourceLabel.setText(selectedFilePath); sourceLabel.setToolTipText(selectedFilePath); clear(); ManagedJob workload = new ManagedJob() { public Object run(Object input) { try { MainFrame.this.manager.getDumpHD().initSource(selectedFile, null, null); Collection<DiscSet> result = MainFrame.this.manager.getDumpHD().identifySource(); if (result.size() == 0) { result = null; } return result; } catch (IOException e) { return null; } } }; ManagedJob guiJob = new ManagedJob() { public Object run(Object input) { if (input != null) { MainFrame.this.init((Collection<DiscSet>)input); } MainFrame.this.unlock(); return null; } }; MainFrame.this.lock(); MainFrame.this.manager.execute(workload, guiJob); } } }); Manager.equalizeLabelButtonHeight(sourceLabel, sourceButton); panel.add(sourceLabel); panel.add(Box.createRigidArea(Manager.BOX_HORIZONTAL_BIG_SPACING)); panel.add(sourceButton); sourcePanel.add(panel); sourcePanel.add(Box.createRigidArea(Manager.BOX_VERTICAL_BIG_SPACING)); // ******************************** // *** Source type select panel *** // ******************************** gridPanel = new JPanel(new GridLayout(1, 0, Manager.BIG_SPACING, 0)); JPanel checkBoxPanel = null; SourceTypeCheckBoxActionListener cbal = new SourceTypeCheckBoxActionListener(); sourceTypeImage = new JLabel[3]; sourceTypeCheckBox = new JCheckBox[6]; sourceTypeSelectable = new boolean[6]; discSets = new DiscSet[6]; // ### HD-DVD Standard Content ### panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS)); checkBoxPanel = new JPanel(); checkBoxPanel.setLayout(new BoxLayout(checkBoxPanel, BoxLayout.PAGE_AXIS)); checkBoxPanel.setAlignmentX(JComponent.CENTER_ALIGNMENT); sourceTypeImage[0] = new JLabel(new ImageIcon(Manager.getResource("icons/hdsc_logo.png"))); sourceTypeImage[0].setDisabledIcon(new ImageIcon(Manager.getResource("icons/hdsc_logo_disabled.png"))); sourceTypeImage[0].setAlignmentX(JComponent.CENTER_ALIGNMENT); sourceTypeCheckBox[0] = new JCheckBox("Video"); sourceTypeCheckBox[0].setAlignmentX(JComponent.LEFT_ALIGNMENT); sourceTypeCheckBox[0].addActionListener(cbal); sourceTypeCheckBox[1] = new JCheckBox("Audio"); sourceTypeCheckBox[1].setAlignmentX(JComponent.LEFT_ALIGNMENT); sourceTypeCheckBox[1].addActionListener(cbal); panel.add(sourceTypeImage[0]); panel.add(Box.createRigidArea(Manager.BOX_VERTICAL_SMALL_SPACING)); checkBoxPanel.add(sourceTypeCheckBox[0]); checkBoxPanel.add(sourceTypeCheckBox[1]); panel.add(checkBoxPanel); gridPanel.add(panel); // ### HD-DVD Advanced Content ### panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS)); checkBoxPanel = new JPanel(); checkBoxPanel.setLayout(new BoxLayout(checkBoxPanel, BoxLayout.PAGE_AXIS)); checkBoxPanel.setAlignmentX(JComponent.CENTER_ALIGNMENT); sourceTypeImage[1] = new JLabel(new ImageIcon(Manager.getResource("icons/hdac_logo.png"))); sourceTypeImage[1].setDisabledIcon(new ImageIcon(Manager.getResource("icons/hdac_logo_disabled.png"))); sourceTypeImage[1].setAlignmentX(JComponent.CENTER_ALIGNMENT); sourceTypeCheckBox[2] = new JCheckBox("Video"); sourceTypeCheckBox[2].setAlignmentX(JComponent.LEFT_ALIGNMENT); sourceTypeCheckBox[2].addActionListener(cbal); sourceTypeCheckBox[3] = new JCheckBox("Audio"); sourceTypeCheckBox[3].setAlignmentX(JComponent.LEFT_ALIGNMENT); sourceTypeCheckBox[3].addActionListener(cbal); panel.add(sourceTypeImage[1]); panel.add(Box.createRigidArea(Manager.BOX_VERTICAL_SMALL_SPACING)); checkBoxPanel.add(sourceTypeCheckBox[2]); checkBoxPanel.add(sourceTypeCheckBox[3]); panel.add(checkBoxPanel); gridPanel.add(panel); // ### BluRay ### panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS)); checkBoxPanel = new JPanel(); checkBoxPanel.setLayout(new BoxLayout(checkBoxPanel, BoxLayout.PAGE_AXIS)); checkBoxPanel.setAlignmentX(JComponent.CENTER_ALIGNMENT); sourceTypeImage[2] = new JLabel(new ImageIcon(Manager.getResource("icons/bd_logo.png"))); sourceTypeImage[2].setDisabledIcon(new ImageIcon(Manager.getResource("icons/bd_logo_disabled.png"))); sourceTypeImage[2].setAlignmentX(JComponent.CENTER_ALIGNMENT); sourceTypeCheckBox[4] = new JCheckBox("BDMV"); sourceTypeCheckBox[4].setAlignmentX(JComponent.LEFT_ALIGNMENT); sourceTypeCheckBox[4].addActionListener(cbal); sourceTypeCheckBox[5] = new JCheckBox("BDAV"); sourceTypeCheckBox[5].setAlignmentX(JComponent.LEFT_ALIGNMENT); sourceTypeCheckBox[5].addActionListener(cbal); panel.add(sourceTypeImage[2]); panel.add(Box.createRigidArea(Manager.BOX_VERTICAL_SMALL_SPACING)); checkBoxPanel.add(sourceTypeCheckBox[4]); checkBoxPanel.add(sourceTypeCheckBox[5]); panel.add(checkBoxPanel); gridPanel.add(panel); gridPanel.setPreferredSize(gridPanel.getMinimumSize()); gridPanel.setMaximumSize(new Dimension(gridPanel.getMaximumSize().width, gridPanel.getPreferredSize().height)); sourcePanel.add(gridPanel); // ### Initialize default values ### for (int i = 0; i < sourceTypeImage.length; i++) { sourceTypeImage[i].setEnabled(false); } for (int i = 0; i < sourceTypeCheckBox.length; i++) { sourceTypeCheckBox[i].setEnabled(false); sourceTypeSelectable[i] = false; discSets[i] = null; } controlPanel.add(sourcePanel); // ### Setting up the FileChooser ### sourceChooser = new JFileChooser(); sourceChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); sourceChooser.setMultiSelectionEnabled(false); // ############################### // ###### Destination panel ###### // ############################### panel = new JPanel(); panel.setBorder(Manager.createTitledBorder("Destination")); panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS)); destinationLabel = new JLabel(MainFrame.NO_DESTINATION_SET); destinationLabel.setBorder(Manager.createLabelBorder()); destinationLabel.setToolTipText(MainFrame.NO_DESTINATION_SET); destinationButton = new JButton(MainFrame.BROWSE); destinationButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { int returnVal = destinationChooser.showSaveDialog(mainFrame); if (returnVal == JFileChooser.APPROVE_OPTION) { destinationDirectory = destinationChooser.getSelectedFile().getAbsoluteFile(); String selectedFilePath = destinationDirectory.getPath(); destinationLabel.setText(selectedFilePath); destinationLabel.setToolTipText(selectedFilePath); for (int i = 0; i < discSets.length; i++) { if (discSets[i] != null) { discSets[i].dstDir = destinationDirectory; } } updateDumpButtonEnabledState(); } } }); Manager.equalizeLabelButtonHeight(destinationLabel, destinationButton); panel.add(destinationLabel); panel.add(Box.createRigidArea(Manager.BOX_HORIZONTAL_BIG_SPACING)); panel.add(destinationButton); controlPanel.add(panel); controlPanel.add(Box.createVerticalGlue()); controlPanel.add(Box.createRigidArea(Manager.BOX_VERTICAL_SMALL_SPACING)); // ### Setting up the FileChooser ### destinationChooser = new JFileChooser(); destinationChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); destinationChooser.setMultiSelectionEnabled(false); // ####################### // ### Control buttons ### // ####################### panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS)); dumpButton = new JButton(MainFrame.DUMP); dumpButton.setEnabled(false); dumpButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { ManagedJob workload = new ManagedJob() { public Object run(Object input) { MainFrame.this.manager.getDumpHD().dumpSource(); return null; } }; ManagedJob guiJob = new ManagedJob() { public Object run(Object input) { MainFrame.this.unlock(); return null; } }; MainFrame.this.lock(); MainFrame.this.manager.execute(workload, guiJob); } }); panel.add(dumpButton); controlPanel.add(panel); // ######################### // ###*******************### // ###*** Source view ***### // ###*******************### // ######################### JPanel viewPanel = new JPanel(new BorderLayout()); viewPanel.setOpaque(true); viewPanel.setBorder(Manager.createEmptyBorder()); tabPane = new JTabbedPane(JTabbedPane.TOP, JTabbedPane.SCROLL_TAB_LAYOUT); tabPane.setPreferredSize(new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE)); SourceTab tab = new SourceTab(manager); sourceTabs.add(tab); tabPane.addTab(tab.getTabTitle(), tab.getTabIcon(), tab.getTabComponent()); viewPanel.add(tabPane, BorderLayout.CENTER); // ################# // ###***********### // ###*** Log ***### // ###***********### // ################# JPanel logPanel = new JPanel(new BorderLayout()); logPanel.setOpaque(true); // **************** // *** Menu bar *** // **************** menuBar = new JMenuBar(); menu = new JMenu("Log"); menuItem = new JMenuItem("Clear"); menuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { Document logDocument = logArea.getDocument(); try { logDocument.remove(0, logDocument.getLength()); } catch (BadLocationException ex) { // TODO: What to do? } } }); menu.add(menuItem); menuBar.add(menu); logPanel.add(menuBar, BorderLayout.PAGE_START); // ********************* // *** Log text area *** // ********************* logArea = new JTextArea(); logArea.setEditable(false); JScrollPane scrollPane = new JScrollPane(logArea, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); scrollPane.setMinimumSize(new Dimension(64, 64)); logPanel.add(scrollPane, BorderLayout.CENTER); out = new TextAreaMessagePrinter(); // ############################## // ###************************### // ###*** Mainframe layout ***### // ###************************### // ############################## // ****************** // *** Splitpanes *** // ****************** horizontalSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, true); horizontalSplitPane.setResizeWeight(0.75); verticalSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true); verticalSplitPane.setResizeWeight(0.25); horizontalSplitPane.setLeftComponent(viewPanel); horizontalSplitPane.setRightComponent(controlPanel); verticalSplitPane.setTopComponent(horizontalSplitPane); verticalSplitPane.setBottomComponent(logPanel); // ************************ // *** Mainframe layout *** // ************************ mainFrame.setContentPane(verticalSplitPane); //mainFrame.pack(); mainFrame.setSize(800, 600); } public void show() { mainFrame.setVisible(true); } public void dispose() { mainFrame.dispose(); } public JFrame getFrame() { return mainFrame; } private void close() { if (manager.isWorking()) { int returnVal = JOptionPane.showConfirmDialog(mainFrame, "There is currently an operation running, exit anyway?", "Question", JOptionPane.OK_CANCEL_OPTION , JOptionPane.QUESTION_MESSAGE); if (returnVal != JOptionPane.OK_OPTION) { return; } } manager.exit(); } public MessagePrinter getMessagePrinter() { return out; } // TODO: A clear must have been done before an init! public void init(Collection<DiscSet> discSets) { for (int i = discSets.size() - sourceTabs.size(); i > 0; i--) { sourceTabs.add(new SourceTab(manager)); } boolean firstTab = true; Iterator<DiscSet> dsIt = discSets.iterator(); Iterator<SourceTab> tabIt = sourceTabs.iterator(); while (dsIt.hasNext()) { DiscSet ds = dsIt.next(); SourceTab tab = tabIt.next(); // FIXME: Hack! This makes sure the BDR types won't be put outside the array if (!ds.isRecordable()) { this.discSets[ds.contentType - 1] = ds; } else { this.discSets[ds.contentType - 2] = ds; } if (destinationDirectory != null) { ds.dstDir = destinationDirectory; } // FIXME: Hack! This makes sure that for the BDR types nothing outside the arrays will be accessed if (!ds.isRecordable()) { sourceTypeImage[(ds.contentType - 1) / 2].setEnabled(true); sourceTypeCheckBox[ds.contentType - 1].setSelected(ds.selected); sourceTypeSelectable[ds.contentType - 1] = ds.selected; } else { sourceTypeImage[(ds.contentType - 2) / 2].setEnabled(true); sourceTypeCheckBox[ds.contentType - 2].setSelected(ds.selected); sourceTypeSelectable[ds.contentType - 2] = ds.selected; } if (ds.selected) { sourceTypesSelected++; } tab.init(ds); if (firstTab) { firstTab = false; tabPane.setTitleAt(0, tab.getTabTitle()); tabPane.setIconAt(0, tab.getTabIcon()); } else { tabPane.addTab(tab.getTabTitle(), tab.getTabIcon(), tab.getTabComponent()); } } updateEnabledState(false); updateDumpButtonEnabledState(); } public void clear() { // Clear the tab panel int tabsInPane = tabPane.getTabCount(); Iterator<SourceTab> tabIt = sourceTabs.iterator(); for (int i = 0; i < tabsInPane; i++) { SourceTab tab = tabIt.next(); tab.clear(); if (i != 0) { tabIt.remove(); tabPane.removeTabAt(i); } else { tabPane.setTitleAt(0, tab.getTabTitle()); tabPane.setIconAt(0, tab.getTabIcon()); } } // Clear the select panel for (int i = 0; i < sourceTypeCheckBox.length; i++) { sourceTypeCheckBox[i].setSelected(false); sourceTypeSelectable[i] = false; discSets[i] = null; } sourceTypesSelected = 0; for (int i = 0; i < sourceTypeImage.length; i++) { sourceTypeImage[i].setEnabled(false); } updateEnabledState(false); updateDumpButtonEnabledState(); } private void updateDumpButtonEnabledState() { if (!locked) { if (destinationDirectory != null && sourceTypesSelected > 0) { if (!dumpButton.isEnabled()) { dumpButton.setEnabled(true); } } else { if (dumpButton.isEnabled()) { dumpButton.setEnabled(false); } } } } private void updateEnabledState(boolean forced) { if (!locked) { for (int i = 0; i < sourceTypeCheckBox.length; i++) { sourceTypeCheckBox[i].setEnabled(sourceTypeSelectable[i]); } if (forced) { sourceButton.setEnabled(true); destinationButton.setEnabled(true); updateDumpButtonEnabledState(); } } else { if (forced) { for (int i = 0; i < sourceTypeCheckBox.length; i++) { sourceTypeCheckBox[i].setEnabled(false); } sourceButton.setEnabled(false); destinationButton.setEnabled(false); dumpButton.setEnabled(false); } } } public void lock() { locked = true; updateEnabledState(true); Iterator<SourceTab> it = sourceTabs.iterator(); while (it.hasNext()) { it.next().lock(); } } public void unlock() { locked = false; updateEnabledState(true); Iterator<SourceTab> it = sourceTabs.iterator(); while (it.hasNext()) { it.next().unlock(); } } public boolean isLocked() { return locked; } public void updateFixedSizedComponentsSize() { sourceLabel.setMinimumSize(null); sourceLabel.setMaximumSize(null); sourceLabel.setPreferredSize(null); sourceButton.setMinimumSize(null); sourceButton.setMaximumSize(null); sourceButton.setPreferredSize(null); Manager.equalizeLabelButtonHeight(sourceLabel, sourceButton); gridPanel.setMaximumSize(null); gridPanel.setPreferredSize(null); gridPanel.setPreferredSize(gridPanel.getMinimumSize()); gridPanel.setMaximumSize(new Dimension(gridPanel.getMaximumSize().width, gridPanel.getPreferredSize().height)); destinationLabel.setMinimumSize(null); destinationLabel.setMaximumSize(null); destinationLabel.setPreferredSize(null); destinationButton.setMinimumSize(null); destinationButton.setMaximumSize(null); destinationButton.setPreferredSize(null); Manager.equalizeLabelButtonHeight(destinationLabel, destinationButton); // Invalidate a element in each panel to force it to relayout // FIXME: Somehow the gridPanel does not change its size, the images get sometimes cut sourceButton.invalidate(); destinationButton.invalidate(); gridPanel.invalidate(); Iterator<SourceTab> it = sourceTabs.iterator(); while (it.hasNext()) { it.next().updateFixedSizedComponentsSize(); } } private class TextAreaMessagePrinter implements MessagePrinter { public void print(final String msg) { Runnable runMe = new Runnable() { public void run() { logArea.append(msg); } }; if (SwingUtilities.isEventDispatchThread()) { runMe.run(); } else { SwingUtilities.invokeLater(runMe); } } public void println() { Runnable runMe = new Runnable() { public void run() { logArea.append("\n"); } }; if (SwingUtilities.isEventDispatchThread()) { runMe.run(); } else { SwingUtilities.invokeLater(runMe); } } public void println(final String msg) { Runnable runMe = new Runnable() { public void run() { logArea.append(msg); logArea.append("\n"); } }; if (SwingUtilities.isEventDispatchThread()) { runMe.run(); } else { SwingUtilities.invokeLater(runMe); } } } private class MainFrameWindowAdapter extends WindowAdapter { public void windowClosing(WindowEvent e) { close(); } } private class LafActionListener implements ActionListener { public void actionPerformed(ActionEvent e) { String lafClassName = e.getActionCommand(); try { UIManager.setLookAndFeel(lafClassName); SwingUtilities.updateComponentTreeUI(mainFrame); SwingUtilities.updateComponentTreeUI(sourceChooser); SwingUtilities.updateComponentTreeUI(destinationChooser); logArea.setCaretPosition(logArea.getDocument().getLength()); updateFixedSizedComponentsSize(); horizontalSplitPane.resetToPreferredSizes(); verticalSplitPane.resetToPreferredSizes(); } catch (UnsupportedLookAndFeelException e1) { // TODO: What to do? } catch (Exception e2) { // TODO: What to do? } } } private class SourceTypeCheckBoxActionListener implements ActionListener { public void actionPerformed(ActionEvent e) { JCheckBox source = (JCheckBox)e.getSource(); boolean selected = source.isSelected(); if (selected) { sourceTypesSelected++; } else { sourceTypesSelected--; } for (int i = 0; i < sourceTypeCheckBox.length; i++) { if (source == sourceTypeCheckBox[i]) { discSets[i].selected = selected; break; } } updateDumpButtonEnabledState(); } } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/gui/Manager.java������������������������������������������������������0000664�0001750�0001750�00000021275�11211610576�021477� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.gui; import java.awt.Dimension; import java.net.URL; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.border.Border; import javax.swing.border.EtchedBorder; import dumphd.core.DumpHD; import dumphd.util.MessagePrinter; import dumphd.util.Utils; /** * Main class of the GUI. * It contains some service methods used throughout the GUI and is used to create and destroy the GUI. * * @author KenD00 */ public class Manager { public final static int BIG_SPACING = 10; public final static int SMALL_SPACING = 5; public final static int BORDER_SPACING = 5; public final static int LABEL_SPACING = 5; public final static Dimension BOX_HORIZONTAL_BIG_SPACING = new Dimension(BIG_SPACING, 0); public final static Dimension BOX_HORIZONTAL_SMALL_SPACING = new Dimension(SMALL_SPACING, 0); public final static Dimension BOX_VERTICAL_BIG_SPACING = new Dimension(0, BIG_SPACING); public final static Dimension BOX_VERTICAL_SMALL_SPACING = new Dimension(0, SMALL_SPACING); private DumpHD dumpHD = null; private MainFrame mainFrame = null; private boolean working = false; public static URL getResource(String resource) { return Manager.class.getResource("/resources/" + resource); } /** * Creates an empty border. * * @return The created border */ public static Border createEmptyBorder() { return BorderFactory.createEmptyBorder(BORDER_SPACING, BORDER_SPACING, BORDER_SPACING, BORDER_SPACING); } /** * Creates a titled border with an empty inner border. * * @param title The title * @return The created border */ public static Border createTitledBorder(String title) { return BorderFactory.createCompoundBorder(BorderFactory.createTitledBorder(title), BorderFactory.createEmptyBorder(0, BORDER_SPACING, BORDER_SPACING, BORDER_SPACING)); } /** * Creates a border with LABEL_SPACING borders left and right. * * @return The created Border */ public static Border createLabelBorder() { return BorderFactory.createCompoundBorder(BorderFactory.createEtchedBorder(EtchedBorder.LOWERED), BorderFactory.createEmptyBorder(0, LABEL_SPACING, 0, LABEL_SPACING)); } /** * Makes the label and the button the same height and sets the label maximum width to infinite and its minimum and preferred width to 32. * Uses the preferredSize of each component to calculate a height that is not smaller than the preferred height of any of the both. * * @param label The label * @param button The button */ public static void equalizeLabelButtonHeight(JLabel label, JButton button) { Dimension buttonPreferred = button.getPreferredSize(); Dimension labelPreferred = label.getPreferredSize(); if (buttonPreferred.height > labelPreferred.height) { label.setMinimumSize(new Dimension(32, buttonPreferred.height)); label.setMaximumSize(new Dimension(Integer.MAX_VALUE, buttonPreferred.height)); label.setPreferredSize(label.getMinimumSize()); Dimension buttonDimension = new Dimension(buttonPreferred.width, buttonPreferred.height); button.setMinimumSize(buttonDimension); button.setMaximumSize(buttonDimension); button.setPreferredSize(buttonDimension); } else { label.setMinimumSize(new Dimension(32, labelPreferred.height)); label.setMaximumSize(new Dimension(Integer.MAX_VALUE, labelPreferred.height)); label.setPreferredSize(label.getMinimumSize()); Dimension buttonDimension = new Dimension(buttonPreferred.width, labelPreferred.height); button.setMinimumSize(buttonDimension); button.setMaximumSize(buttonDimension); button.setPreferredSize(buttonDimension); } //System.out.println("Button minimum: " + button.getMinimumSize()); //System.out.println("Button preferred: " + button.getPreferredSize()); //System.out.println("Button maximum: " + button.getMaximumSize()); } /** * Makes the given components the same fixed size. * Uses the preferredSize from each component to calculate a new size that not smaller than the preferred size of any of the both components. * Also sets the minium and maximum size of each component to this size. * * @param component1 The first component * @param component2 The second component */ public static void equalizeComponentSize(JComponent component1, JComponent component2) { Dimension component1Preferred = component1.getPreferredSize(); Dimension component2Preferred = component2.getPreferredSize(); int width = 0; int height = 0; if (component1Preferred.width > component2Preferred.width) { width = component1Preferred.width; } else { width = component2Preferred.width; } if (component1Preferred.height > component2Preferred.height) { height = component1Preferred.height; } else { height = component2Preferred.height; } Dimension newSize = new Dimension(width, height); component1.setMinimumSize(newSize); component1.setMaximumSize(newSize); component1.setPreferredSize(newSize); component2.setMinimumSize(newSize); component2.setMaximumSize(newSize); component2.setPreferredSize(newSize); } public Manager() { try { javax.swing.SwingUtilities.invokeAndWait(new Runnable() { public void run() { System.out.print("Creating GUI... "); mainFrame = new MainFrame(Manager.this); System.out.println("DONE"); mainFrame.lock(); mainFrame.show(); } }); MessagePrinter mainFramePrinter = mainFrame.getMessagePrinter(); Utils.setMessagePrinter(mainFrame.getMessagePrinter()); try { dumpHD = new DumpHD(mainFramePrinter, false); javax.swing.SwingUtilities.invokeLater(new Runnable() { public void run() { mainFrame.unlock(); } }); } catch (Exception e) { mainFramePrinter.println(e.getMessage()); mainFramePrinter.println("Critical error: DumpHD could not be created"); } } catch (Exception e) { System.out.println("\nCritical error occured while creating the GUI: " + e.getMessage()); System.exit(1); } } public MainFrame getMainFrame() { return mainFrame; } public DumpHD getDumpHD() { return dumpHD; } //TODO: When should working be set to false? If set in the EventDispatching thread we cannot wait in the GUI for that event //TODO: Should working be an int? As boolean it is not allowed to execute multiple jobs. Hmm, this is in general not allowed! Wait for finish of a running job? public void execute(final ManagedJob workerJob, final ManagedJob eventDispatcherJob) { SwingWorker worker = new SwingWorker() { public Object construct() { return workerJob.run(null); } public void finished() { working = false; eventDispatcherJob.run(get()); } }; working = true; worker.start(); } /** * Returns true if a call has been passed to the DumpHD object and the process has not been finished yet. * * @return True, if the DumpHD object is currently working, false otherwise */ public boolean isWorking() { return working; } /** * Exits the program. * * TODO: Implement a way to stop DumpHD if its working */ public void exit() { if (dumpHD != null) { dumpHD.close(); } mainFrame.dispose(); System.exit(0); } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/gui/SwingWorker.java��������������������������������������������������0000664�0001750�0001750�00000006713�11132225672�022407� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * The is the SwingWorker 3rd Version, it contains no copyright notice and is therefore assumed to be Public Domain. */ package dumphd.gui; import javax.swing.SwingUtilities; /** * This is the 3rd version of SwingWorker (also known as * SwingWorker 3), an abstract class that you subclass to * perform GUI-related work in a dedicated thread. For * instructions on using this class, see: * * http://java.sun.com/docs/books/tutorial/uiswing/misc/threads.html * * Note that the API changed slightly in the 3rd version: * You must now invoke start() on the SwingWorker after * creating it. */ public abstract class SwingWorker { private Object value; // see getValue(), setValue() private Thread thread; /** * Class to maintain reference to current worker thread * under separate synchronization control. */ private static class ThreadVar { private Thread thread; ThreadVar(Thread t) { thread = t; } synchronized Thread get() { return thread; } synchronized void clear() { thread = null; } } private ThreadVar threadVar; /** * Get the value produced by the worker thread, or null if it * hasn't been constructed yet. */ protected synchronized Object getValue() { return value; } /** * Set the value produced by worker thread */ private synchronized void setValue(Object x) { value = x; } /** * Compute the value to be returned by the <code>get</code> method. */ public abstract Object construct(); /** * Called on the event dispatching thread (not on the worker thread) * after the <code>construct</code> method has returned. */ public void finished() { } /** * A new method that interrupts the worker thread. Call this method * to force the worker to stop what it's doing. */ public void interrupt() { Thread t = threadVar.get(); if (t != null) { t.interrupt(); } threadVar.clear(); } /** * Return the value created by the <code>construct</code> method. * Returns null if either the constructing thread or the current * thread was interrupted before a value was produced. * * @return the value created by the <code>construct</code> method */ public Object get() { while (true) { Thread t = threadVar.get(); if (t == null) { return getValue(); } try { t.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // propagate return null; } } } /** * Start a thread that will call the <code>construct</code> method * and then exit. */ public SwingWorker() { final Runnable doFinished = new Runnable() { public void run() { finished(); } }; Runnable doConstruct = new Runnable() { public void run() { try { setValue(construct()); } finally { threadVar.clear(); } SwingUtilities.invokeLater(doFinished); } }; Thread t = new Thread(doConstruct); threadVar = new ThreadVar(t); } /** * Start the worker thread. */ public void start() { Thread t = threadVar.get(); if (t != null) { t.start(); } } } �����������������������������������������������������dumphd-0.61/source/src/dumphd/gui/FileTableModel.java�����������������������������������������������0000664�0001750�0001750�00000007471�11211610554�022733� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.gui; import java.util.ArrayList; import java.util.Iterator; import java.util.Map; import java.util.TreeMap; import javax.swing.table.AbstractTableModel; import dumphd.core.DiscSet; /** * FileTableModel used by a SourceTab to display the files of its DiscSet in a table. * * Very simple model, does not allow that the file structure of the underlying DiscSet changes. * * @author KenD00 */ public class FileTableModel extends AbstractTableModel { private DiscSet ds = null; private TreeMap<Integer, Integer> streamSetOffsets = new TreeMap<Integer, Integer>(); private int fileSetOffset = 0; public FileTableModel() { } public FileTableModel(DiscSet ds) { this.ds = ds; if (ds != null) { calculateOffsets(); } } public DiscSet getData() { return ds; } public void setData(DiscSet ds) { this.ds = ds; if (ds != null) { calculateOffsets(); } fireTableDataChanged(); } public Class<?> getColumnClass(int columnIndex) { return String.class; } public String getColumnName(int columnIndex) { return "File"; } public boolean isCellEditable(int rowIndex, int columnIndex) { return false; } /* (non-Javadoc) * @see javax.swing.table.TableModel#getColumnCount() */ public int getColumnCount() { return 1; } /* (non-Javadoc) * @see javax.swing.table.TableModel#getRowCount() */ public int getRowCount() { if (ds != null) { return fileSetOffset + ds.fileSet.size(); } else { return 0; } } /* (non-Javadoc) * @see javax.swing.table.TableModel#getValueAt(int, int) */ public Object getValueAt(int rowIndex, int columnIndex) { if (ds != null) { if (rowIndex < fileSetOffset) { // Row inside streamSets int offset = 0; Iterator<Map.Entry<Integer, Integer>> it = streamSetOffsets.entrySet().iterator(); while (it.hasNext()) { Map.Entry<Integer, Integer> entry = it.next(); if (rowIndex < entry.getKey()) { return ds.streamSets.get(entry.getValue()).get(rowIndex - offset); } else { offset = entry.getValue(); } } // This should never happen! return ""; } else { // Row inside fileSet return ds.fileSet.get(rowIndex - fileSetOffset); } } else { return ""; } } /** * Helper Method, calculates the offsets after the DiscSet was changed */ private void calculateOffsets() { fileSetOffset = 0; Iterator<Map.Entry<Integer, ArrayList<String>>> it = ds.streamSets.entrySet().iterator(); while (it.hasNext()) { Map.Entry<Integer, ArrayList<String>> entry = it.next(); fileSetOffset += entry.getValue().size(); streamSetOffsets.put(fileSetOffset, entry.getKey()); } } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/gui/ManagedJob.java���������������������������������������������������0000664�0001750�0001750�00000001665�11211610570�022107� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.gui; /** * Interface for jobs that the Manager runs in a separate thread. * * @author KenD00 */ public interface ManagedJob { public abstract Object run(Object input); } ���������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/gui/SourceTab.java����������������������������������������������������0000664�0001750�0001750�00000046074�11211610604�022010� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.gui; import java.awt.event.ActionListener; import java.awt.event.ActionEvent; import java.io.IOException; import javax.swing.JOptionPane; import javax.swing.ListSelectionModel; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.ScrollPaneConstants; import dumphd.core.DiscSet; import dumphd.core.KeyData; import dumphd.util.Utils; /** * This class represents a tab for a specific disc type. * It holds the reference to the underlying disc set object, all graphical elements make changes directly to that object. * * The JComponent to display this tab and the tab title and icon are also hold by this class. * * FIXME: Don't use DiscSet.contentType as Index!!!!! * * @author KenD00 */ public class SourceTab { private final static String NA_STRING = "N/A"; private final static String OK_STRING = "OK"; private final static String CANCEL_STRING = "Cancel"; private final static String SAVE_STRING = "Save"; /** * The titles of the tabs. Index is the disc type (see DiscSet) */ private final static String[] TAB_TITLES = new String[8]; /** * The icons of the tabs. Index is the disc type (see DiscSet) */ private final static Icon[] TAB_ICONS = new Icon[8]; private Manager manager = null; /** * The underlying disc set object this SourceTab represents */ private DiscSet ds = null; /** * If true, the user can not interact with the object */ private boolean locked = false; /** * The title for this tab */ private String tabTitle = null; /** * The icon for this tab */ private Icon tabIcon = null; /** * The tab contents */ private JPanel tabPanel = null; /** * DiscID button */ private JButton discIdButton = null; /** * DiscID label */ private JLabel discIdLabel = null; /** * Title button */ private JButton titleButton = null; /** * Title label */ private JLabel titleLabel = null; /** * The table that displays the files */ private JTable fileTable = null; /** * If true, it is allowed to change the DiscID */ private boolean discIdEditable = false; /** * If true, it is allowed to change the Title */ private boolean titleEditable = false; /** * If true, the file table can be used to modify the file set */ private boolean fileTableEditable = false; static { SourceTab.TAB_TITLES[0] = SourceTab.NA_STRING; SourceTab.TAB_TITLES[1] = "Standard Video"; SourceTab.TAB_TITLES[2] = "Standard Audio"; SourceTab.TAB_TITLES[3] = "Advanced Video"; SourceTab.TAB_TITLES[4] = "Advanced Audio"; SourceTab.TAB_TITLES[5] = "BDMV"; SourceTab.TAB_TITLES[6] = "BDMV (Recordable)"; SourceTab.TAB_TITLES[7] = "BDAV (Recordable)"; SourceTab.TAB_ICONS[0] = new ImageIcon(Manager.getResource("icons/empty_tab.png")); SourceTab.TAB_ICONS[1] = new ImageIcon(Manager.getResource("icons/hdsc_tab.png")); SourceTab.TAB_ICONS[2] = new ImageIcon(Manager.getResource("icons/hdsc_tab.png")); SourceTab.TAB_ICONS[3] = new ImageIcon(Manager.getResource("icons/hdac_tab.png")); SourceTab.TAB_ICONS[4] = new ImageIcon(Manager.getResource("icons/hdac_tab.png")); SourceTab.TAB_ICONS[5] = new ImageIcon(Manager.getResource("icons/bd_tab.png")); SourceTab.TAB_ICONS[6] = new ImageIcon(Manager.getResource("icons/bd_tab.png")); SourceTab.TAB_ICONS[7] = new ImageIcon(Manager.getResource("icons/bd_tab.png")); } /** * Creates an empty source tab */ public SourceTab(Manager manager) { this.manager = manager; createElements(); } /** * Creates a tab from the given DiscSet. * * @param ws DiscSet to create the tab for */ public SourceTab(Manager manager, DiscSet ws) { this.manager = manager; this.ds = ws; createElements(); init(ws); } /** * Creates all elements for this object. */ private void createElements() { // Tab title and icon tabTitle = SourceTab.TAB_TITLES[0]; tabIcon = SourceTab.TAB_ICONS[0]; // Tab content panel tabPanel = new JPanel(); tabPanel.setOpaque(true); tabPanel.setBorder(Manager.createEmptyBorder()); tabPanel.setLayout(new BoxLayout(tabPanel, BoxLayout.PAGE_AXIS)); JPanel panel = null; // DiscID and Title elements ButtonListener buttonListener = new ButtonListener(); discIdButton = new JButton("DiscID"); discIdButton.setEnabled(discIdEditable); discIdButton.setActionCommand("discid"); discIdButton.addActionListener(buttonListener); titleButton = new JButton("Title"); titleButton.setEnabled(titleEditable); titleButton.setActionCommand("title"); titleButton.addActionListener(buttonListener); discIdLabel = new JLabel(SourceTab.NA_STRING); discIdLabel.setBorder(Manager.createLabelBorder()); discIdLabel.setToolTipText(SourceTab.NA_STRING); titleLabel = new JLabel(SourceTab.NA_STRING); titleLabel.setBorder(Manager.createLabelBorder()); titleLabel.setToolTipText(SourceTab.NA_STRING); Manager.equalizeLabelButtonHeight(discIdLabel, discIdButton); Manager.equalizeLabelButtonHeight(titleLabel, titleButton); Manager.equalizeComponentSize(discIdButton, titleButton); // DiscId and Title panels panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS)); panel.add(discIdButton); panel.add(Box.createRigidArea(Manager.BOX_HORIZONTAL_BIG_SPACING)); panel.add(discIdLabel); tabPanel.add(panel); tabPanel.add(Box.createRigidArea(Manager.BOX_VERTICAL_SMALL_SPACING)); panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.LINE_AXIS)); panel.add(titleButton); panel.add(Box.createRigidArea(Manager.BOX_HORIZONTAL_BIG_SPACING)); panel.add(titleLabel); tabPanel.add(panel); tabPanel.add(Box.createRigidArea(Manager.BOX_VERTICAL_SMALL_SPACING)); // File table fileTable = new JTable(new FileTableModel()); fileTable.setAutoCreateColumnsFromModel(false); fileTable.setAutoResizeMode(JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS); fileTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); fileTable.setShowGrid(false); fileTable.getTableHeader().setReorderingAllowed(false); fileTable.setEnabled(fileTableEditable); JScrollPane fileScroller = new JScrollPane(fileTable, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); tabPanel.add(fileScroller); } /** * Initializes this object with the values from the given DiscSet. * There is no need to clear this object prior calling this method. * * @param ds The DiscSet this tab is initialized for */ public void init(DiscSet ds) { this.ds = ds; tabTitle = SourceTab.TAB_TITLES[ds.contentType]; tabIcon = SourceTab.TAB_ICONS[ds.contentType]; if (ds.keyData != null) { byte[] discId = ds.keyData.getDiscId(); String title = ds.keyData.getTitle(); discIdLabel.setText(Utils.toHexString(discId, 0, discId.length)); discIdLabel.setToolTipText(discIdLabel.getText()); if (title != null) { titleLabel.setText(title); // Check for an empty string and set it to space to avoid an ugly ToolTip-Display bug if (title.equals("")) { titleLabel.setToolTipText(" "); } else { titleLabel.setToolTipText(title); } titleEditable = true; } else { titleLabel.setText(SourceTab.NA_STRING); titleLabel.setToolTipText(SourceTab.NA_STRING); titleEditable = false; } discIdEditable = true; } else { discIdLabel.setText(SourceTab.NA_STRING); discIdLabel.setToolTipText(SourceTab.NA_STRING); titleLabel.setText(SourceTab.NA_STRING); titleLabel.setToolTipText(SourceTab.NA_STRING); discIdEditable = false; titleEditable = false; } ((FileTableModel)fileTable.getModel()).setData(ds); fileTableEditable = true; updateEnabledState(false); } /** * Clears all data from this object. * After this method call this object is an empty tab. */ public void clear() { ds = null; tabTitle = SourceTab.TAB_TITLES[0]; tabIcon = SourceTab.TAB_ICONS[0]; discIdLabel.setText(SourceTab.NA_STRING); discIdLabel.setToolTipText(SourceTab.NA_STRING); titleLabel.setText(SourceTab.NA_STRING); titleLabel.setToolTipText(SourceTab.NA_STRING); discIdEditable = false; titleEditable = false; ((FileTableModel)fileTable.getModel()).setData(null); fileTableEditable = false; updateEnabledState(false); } /** * Updates the enabled state of the elements the user can interact with. * Call this method if the data describing the enabled state of the elements has changed. * This method is also used to lock and unlock this object. * * @param forced If true, the enabled state is set forcibly, even if its not necessary (e.g. the object is locked). Set this to true if the object should be actually locked or unlocked. */ private void updateEnabledState(boolean forced) { if (!locked) { discIdButton.setEnabled(discIdEditable); titleButton.setEnabled(titleEditable); fileTable.setEnabled(fileTableEditable); } else { if (forced) { discIdButton.setEnabled(false); titleButton.setEnabled(false); fileTable.setEnabled(false); } } } /** * Locks this object. The user can not interact with it any more. */ public void lock() { locked = true; updateEnabledState(true); } /** * Unlock this object. The user can interact with this object now. */ public void unlock() { locked = false; updateEnabledState(true); } /** * Returns the locked state of this object. * * @return If true, this object is locked, otherwise not */ public boolean isLocked() { return locked; } /** * Returns the title of this object. * * @return The title for this tab */ public String getTabTitle() { return tabTitle; } /** * Returns the icon of this object. * * @return The Icon for this tab */ public Icon getTabIcon() { return tabIcon; } /** * Returns the content of this object. * * @return The JComponent for this tabs content */ public JComponent getTabComponent() { return tabPanel; } /** * Call this method if the Look & Feel has changed. Resizes all elements that have fixed sizes. */ public void updateFixedSizedComponentsSize() { discIdButton.setMinimumSize(null); discIdButton.setMaximumSize(null); discIdButton.setPreferredSize(null); discIdLabel.setMinimumSize(null); discIdLabel.setMaximumSize(null); discIdLabel.setPreferredSize(null); titleButton.setMinimumSize(null); titleButton.setMaximumSize(null); titleButton.setPreferredSize(null); titleLabel.setMinimumSize(null); titleLabel.setMaximumSize(null); titleLabel.setPreferredSize(null); Manager.equalizeLabelButtonHeight(discIdLabel, discIdButton); Manager.equalizeLabelButtonHeight(titleLabel, titleButton); Manager.equalizeComponentSize(discIdButton, titleButton); // Invalidate a element in each panel to force it to relayout discIdButton.invalidate(); titleButton.invalidate(); fileTable.invalidate(); } private class ButtonListener implements ActionListener { public void actionPerformed(ActionEvent e) { String command = e.getActionCommand(); if (command.equals("discid")) { ValidatedInputDialog dialog = new ValidatedInputDialog(manager.getMainFrame().getFrame(), "Override DiscID", "Warning! Override the DiscID only if the disc contents were wrongly identified!", "DiscID", discIdLabel.getText(), 40, "[0-9a-fA-F]{40}", "The DiscID must be 40 characters long and may only contain the characters 0-9, a-f, A-F", SourceTab.OK_STRING, SourceTab.CANCEL_STRING); if (dialog.show()) { final String discIdString = dialog.getText(); // This ManagedJob returns the found KeyData object or null if not found //TODO: Implement a way to retrieve keys without using AACSKeys? ManagedJob workload = new ManagedJob() { public Object run(Object input) { byte[] discId = new byte[20]; Utils.decodeHexString(discIdString, discId, 0); try { return SourceTab.this.manager.getDumpHD().getKeyDataFile().getKeyData(discId, 0); } catch (IOException e) { SourceTab.this.manager.getMainFrame().getMessagePrinter().println(e.getMessage()); return null; } } }; ManagedJob guiJob = new ManagedJob() { public Object run(Object input) { if (input != null) { SourceTab.this.manager.getMainFrame().getMessagePrinter().println("Disc found in database"); ds.keyData = (KeyData)input; discIdLabel.setText(discIdString); discIdLabel.setToolTipText(discIdString); String title = ds.keyData.getTitle(); //TODO: Title should be always non-null if retrieved from key database if (title != null) { titleLabel.setText(title); // Check for an empty string and set it to space to avoid an ugly ToolTip-Display bug if (title.equals("")) { titleLabel.setToolTipText(" "); } else { titleLabel.setToolTipText(title); } titleEditable = true; } else { titleLabel.setText(SourceTab.NA_STRING); titleLabel.setToolTipText(SourceTab.NA_STRING); titleEditable = false; } updateEnabledState(false); } else { SourceTab.this.manager.getMainFrame().getMessagePrinter().println("Disc not found in database"); JOptionPane.showMessageDialog(SourceTab.this.manager.getMainFrame().getFrame(), "DiscID not found in key database, DiscID not changed!", "Error", JOptionPane.ERROR_MESSAGE); } SourceTab.this.manager.getMainFrame().unlock(); return null; } }; manager.getMainFrame().getMessagePrinter().println("Searching disc in key database..."); SourceTab.this.manager.getMainFrame().lock(); SourceTab.this.manager.execute(workload, guiJob); } } else if (command.equals("title")) { //TODO: allow empty title? ValidatedInputDialog dialog = new ValidatedInputDialog(manager.getMainFrame().getFrame(), "Edit movie title", "Saving the title causes an immediate update of the key database", "Title", titleLabel.getText(), 48, "[^|\\r\\n]+", "The Title must be at least one character long and must not contain the character |", SourceTab.SAVE_STRING, SourceTab.CANCEL_STRING); if (dialog.show()) { final String title = dialog.getText(); ds.keyData.setTitle(title); // This ManagedJob returns the found old KeyData object or null if not found and updated ManagedJob workload = new ManagedJob() { public Object run(Object input) { try { return SourceTab.this.manager.getDumpHD().getKeyDataFile().setKeyData(ds.keyData, ds.isBluRay(), ds.isRecordable(), true); } catch (IOException e) { SourceTab.this.manager.getMainFrame().getMessagePrinter().println(e.getMessage()); return null; } } }; ManagedJob guiJob = new ManagedJob() { public Object run(Object input) { if (input != null) { SourceTab.this.manager.getMainFrame().getMessagePrinter().println("Entry updated"); titleLabel.setText(ds.keyData.getTitle()); titleLabel.setToolTipText(ds.keyData.getTitle()); } else { SourceTab.this.manager.getMainFrame().getMessagePrinter().println("Entry not updated"); ds.keyData.setTitle(titleLabel.getText()); JOptionPane.showMessageDialog(SourceTab.this.manager.getMainFrame().getFrame(), "Error updating entry in key database, see log for details", "Error", JOptionPane.ERROR_MESSAGE); } SourceTab.this.manager.getMainFrame().unlock(); return null; } }; manager.getMainFrame().getMessagePrinter().println("Updating entry in key database..."); SourceTab.this.manager.getMainFrame().lock(); SourceTab.this.manager.execute(workload, guiJob); } } } } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/bdplus/���������������������������������������������������������������0000775�0001750�0001750�00000000000�11211611342�017750� 5����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/bdplus/BDVMLoader.java������������������������������������������������0000664�0001750�0001750�00000007324�11211610424�022500� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.bdplus; import java.io.File; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; import bdvm.vm.BDVMException; import bdvm.vm.BDVMInterface; /** * This class is a loader for the BDVM. * * It can load the BDVM dynamically at runtime from a Jar file and create multiple instances of it. * * To still be able to compile DumpHD without the BDVM present and to not need to use reflection to * call the BDVM methods the known BDVMInterface is used. This interface MUST be identical in DumpHD * and the BDVM or various errors can occur! * * @author KenD00 */ public class BDVMLoader { /** * The default constructor of the BDVM class */ private Constructor<?> bdvmConstructor = null; /** * Constructs a new BDVMLoader. */ public BDVMLoader() { // Nothing to do } /** * Loads the BDVM from the given Jar file. * * This method tries to load the main class of the BDVM from the given Jar file and resolves its default constructor. * No instance of the BDVM is created here. * * @param jarFile Jar archive which contains the BDVM * @throws BDVMException The BDVM class could not be loaded */ public void loadClass(File jarFile) throws BDVMException { try { URL[] urls = { jarFile.toURI().toURL() }; URLClassLoader cl = URLClassLoader.newInstance(urls); Class<?> bdvmClass = cl.loadClass("bdvm.vm.BDVM"); bdvmConstructor = bdvmClass.getConstructor(); } catch (MalformedURLException e) { throw new BDVMException(e.getMessage(), e); } catch (ClassNotFoundException e) { throw new BDVMException(e.getMessage(), e); } catch (SecurityException e) { throw new BDVMException(e.getMessage(), e); } catch (NoSuchMethodException e) { throw new BDVMException(e.getMessage(), e); } } /** * Creates a new instance of the BDVM class. * * @return A new BDVM or null if the class is not loaded. * @throws BDVMException An error occurred while created the instance */ public BDVMInterface newInstance() throws BDVMException { try { if (bdvmConstructor != null) { return (BDVMInterface) bdvmConstructor.newInstance(); } else { return null; } } catch (IllegalArgumentException e) { throw new BDVMException(e.getMessage(), e); } catch (InstantiationException e) { throw new BDVMException(e.getMessage(), e); } catch (IllegalAccessException e) { throw new BDVMException(e.getMessage(), e); } catch (InvocationTargetException e) { throw new BDVMException(e.getMessage(), e); } catch (ClassCastException e) { throw new BDVMException(e.getMessage(), e); } } } ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/bdplus/ConversionTable.java�������������������������������������������0000664�0001750�0001750�00000010027�11211610450�023707� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2008-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.bdplus; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import dumphd.util.ByteArray; import dumphd.util.ByteSource; import dumphd.util.Utils; /** * This class represents a BD+ conversion table. * * TODO: With an ArraySource class the doubled code in parse wouldn't be necessary * * @author KenD00 */ public class ConversionTable { /** * All subtables of the conversion table, in the order as they appear in the conversion table */ private ArrayList<SubTable> tables = new ArrayList<SubTable>(); /** * Index map, maps the subtable id to it's index inside the tables ArrayList */ private HashMap<Long, Integer> indexTable = new HashMap<Long, Integer>(128); /** * Creates an empty ConversionTable. */ public ConversionTable() { // Nothing to do } /** * Creates a ConversionTable from the given source. * * The conversion table must start at position 0 and the read pointer must be located there. * * @param source The conversion table to parse * @throws IOException */ public ConversionTable(ByteSource source) throws IOException { if (source.read(Utils.buffer, 0, 2) != 2) { throw new IOException("Unexpected EOF, conversion table too small for header"); } int tableCount = ByteArray.getUShort(Utils.buffer, 0); tables.ensureCapacity(tableCount); for (int i = 0; i < tableCount; i++) { SubTable subTable = new SubTable(source); tables.add(subTable); indexTable.put(subTable.getTableId(), i); } } /** * Parses the given byte array to create a ConversionTable. * * The content of the byte array gets copied, changes of it after this method call do not affect this ConversionTable. * * @param raw The raw Conversion Table * @param offset Start offset, must be 0 or metadata will get wrong * @return The endoffset of the Conversion Table inside the byte array * @throws IndexOutOfBoundsException */ public int parse(byte[] raw, int offset) throws IndexOutOfBoundsException { int tableCount = ByteArray.getUShort(raw, offset); offset += 2; tables.ensureCapacity(tableCount); for (int i = 0; i < tableCount; i++) { SubTable subTable = new SubTable(); offset = subTable.parse(raw, offset); tables.add(subTable); indexTable.put(subTable.getTableId(), i); } return offset; } /** * @return The number of subtables */ public int size() { return tables.size(); } /** * Returns the subtable with the given table id, or null if its not present. * * @param tableId Table id of the requested subtable * @return The requested subtable or null if its not present */ public SubTable getSubTable(long tableId) { Integer index = indexTable.get(tableId); if (index != null) { return tables.get(index); } else { return null; } } /** * @return An iterator over the subtables, in the order as they appear in the source */ public Iterator<SubTable> iterator() { return tables.iterator(); } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/bdplus/Segment.java���������������������������������������������������0000664�0001750�0001750�00000016220�11211610460�022216� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2008-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.bdplus; import java.io.IOException; import dumphd.util.ByteArray; import dumphd.util.ByteSource; import dumphd.util.Utils; /** * This class represents a segment of a subtable of a BD+ conversion table. * * FIXME: A segment can contain an unsigned 32 bit number of entries. However in java all arrays / collections can take only signed 32 bit * number of elements, so if a segments contains more than a 31 bit number of elements this is a problem! * * @author KenD00 */ public class Segment { /** * Length in bytes of a patch */ public static final int PATCH_LENGTH = 5; /** * The index of this segment in the subtable it comes from */ private int segmentIndex = 0; /** * Offset of this segment from the beginning of the conversion table */ private long segmentOffset = 0; /** * Number of entries */ private long entries = 0; /** * The index section of this segment */ private byte[] indexData = null; /** * The patch entries of this segment */ private byte[] entryData = null; /** * Creates an empty Segment. * * @param index The index of this segment in its parent subtable */ public Segment(int index) { segmentIndex = index; } /** * Creates a segment from the given source. * * The source read pointer must be at the location where the segment starts, the whole conversion table must start at position 0. * * @param source The conversion table to start * @param index The index of this segment in its parent subtable * @throws IOException */ public Segment(ByteSource source, int index) throws IOException { segmentIndex = index; segmentOffset = source.getPosition(); if (source.read(Utils.buffer, 0, 4) != 4) { throw new IOException("Unexpected EOF, segment too small for header"); } entries = ByteArray.getVarLong(Utils.buffer, 0, 4); indexData = new byte[4 * (int)entries]; if (source.read(indexData, 0, indexData.length) != indexData.length) { throw new IOException("Unexpected EOF, segment too small for index data"); } entryData = new byte[16 * (int)entries]; if (source.read(entryData, 0, entryData.length) != entryData.length) { throw new IOException("Unexpected EOF, segment too small for entry data"); } } /** * Parses the given byte array to create a Segment. * * The content of the byte array gets copied, changes of it after this method call do not affect this Segment. * * @param raw The raw Conversion Table * @param offset Start offset, must be the start of the Segment * @return The endoffset of the Segment inside the byte array * @throws IndexOutOfBoundsException */ public int parse(byte[] raw, int offset) throws IndexOutOfBoundsException { segmentOffset = offset; entries = ByteArray.getVarLong(raw, offset, 4); offset += 4; indexData = new byte[4 * (int)entries]; System.arraycopy(raw, offset, indexData, 0, indexData.length); offset += indexData.length; entryData = new byte[16 * (int)entries]; System.arraycopy(raw, offset, entryData, 0, entryData.length); offset += entryData.length; return offset; } /** * @return The index of this segment in the subtable it comes from */ public int getSegmentIndex() { return segmentIndex; } /** * @return Offset of this segment from the beginning of the conversion table */ public long getSegmentOffset() { return segmentOffset; } /** * @return Number of entries */ public int size() { return (int)entries; } /** * @param index Entry index * @return The base address of the entry */ public long getBase(int index) { return ByteArray.getUInt(indexData, index * 4); } /** * @param index Entry index * @return The flags of the entry */ public int getFlags(int index) { return entryData[index * 16] & 0xFF; } /** * @param index Entry index * @return The Adjust 0 value of the entry */ public long getAdjust0(int index) { return (ByteArray.getUShort(entryData, index * 16 + 1) >>> 4) & 0xFFF; } /** * @param index Entry index * @return The Adjust 1 value of the entry */ public long getAdjust1(int index) { return ByteArray.getUShort(entryData, index * 16 + 2) & 0xFFF; } /** * @param index Entry index * @return The Offset 0 value of the entry */ public long getOffset0(int index) { return ByteArray.getUByte(entryData, index * 16 + 4); } /** * @param index Entry index * @return The Offset 1 value of the entry */ public long getOffset1(int index) { return ByteArray.getUByte(entryData, index * 16 + 5); } /** * Write the Patch 0 into the given byte array. * * Segment.PATCH_LENGTH bytes get written. * * @param index Entry index * @param dst Byte array to write to * @param offset Offset to start writing. */ public void getPatch0(int index, byte[] dst, int offset) { System.arraycopy(entryData, index * 16 + 6, dst, offset, PATCH_LENGTH); } /** * Write the Patch 1 into the given byte array. * * Segment.PATCH_LENGTH bytes get written. * * @param index Entry index * @param dst Byte array to write to * @param offset Offset to start writing. */ public void getPatch1(int index, byte[] dst, int offset) { System.arraycopy(entryData, index * 16 + 11, dst, offset, PATCH_LENGTH); } /** * Returns the absolute address of Patch 0. * * This is a calculated value. * * @param index Entry index * @return Absolute address of Patch 0. */ public long getAddress0(int index) { return (getBase(index) + getAdjust0(index)) * 0xC0 + getOffset0(index); } /** * Returns the absolute address of Patch 1. * * This is a calculated value. * * @param index Entry index * @return Absolute address of Patch 1. */ public long getAddress1(int index) { return (getBase(index) + getAdjust0(index) + getAdjust1(index)) * 0xC0 + getOffset1(index); } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/bdplus/SubTable.java��������������������������������������������������0000664�0001750�0001750�00000021461�11211610470�022321� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2008-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.bdplus; import java.io.IOException; import java.lang.IllegalStateException; import java.util.ArrayList; import java.util.Iterator; import dumphd.util.ByteArray; import dumphd.util.ByteSource; import dumphd.util.Utils; /** * This class represents a subtable of a BD+ conversion table. * * @author KenD00 */ public class SubTable { /** * Offset of this subtable from the beginning of the conversion table */ private long tableOffset = 0; /** * The ID of this subtable */ private long tableId = 0; /** * The segments of the subtable in the order they appear in the conversion table */ private ArrayList<Segment> segments = new ArrayList<Segment>(); /** * Creates an empty ConversionTable. */ public SubTable() { // Nothing to do } /** * Creates a subtable from the given source. * * The source read pointer must be at the location where the subtable starts, the whole conversion table must start at position 0. * * @param source The conversion table to parse * @throws IOException */ public SubTable(ByteSource source) throws IOException { tableOffset = source.getPosition(); if (source.read(Utils.buffer, 0, 6) != 6) { throw new IOException("Unexpected EOF, subtable too small for header"); } tableId = ByteArray.getUInt(Utils.buffer, 0); int segmentCount = ByteArray.getUShort(Utils.buffer, 4); segments.ensureCapacity(segmentCount); // Use the index section to jump to every segment. This isn't necessary if all segments are consecutive, it looks like they are, // but you can never know for (int i = 0; i < segmentCount; i++) { // Jump to the current index source.setPosition(tableOffset + 6 + i * 4); // Read it if (source.read(Utils.buffer, 0, 4) != 4) { throw new IOException("Unexpected EOF, segment index section too small"); } long index = ByteArray.getUInt(Utils.buffer, 0); // Jump to the current segment source.setPosition(index); // Read the segment Segment segment = new Segment(source, i); segments.add(segment); } } /** * Parses the given byte array to create a SubTable. * * The content of the byte array gets copied, changes of it after this method call do not affect this SubTable. * * @param raw The raw Conversion Table * @param offset Start offset, must be the start of the SubTable * @return The endoffset of the SubTable inside the byte array * @throws IndexOutOfBoundsException */ public int parse(byte[] raw, int offset) throws IndexOutOfBoundsException { tableOffset = offset; tableId = ByteArray.getUInt(raw, offset); offset += 4; int segmentCount = ByteArray.getUShort(raw, offset); offset += 2; segments.ensureCapacity(segmentCount); for (int i = 0; i < segmentCount; i++) { long index = ByteArray.getUInt(raw, (int)tableOffset + 6 + i * 4); Segment segment = new Segment(i); offset = segment.parse(raw, (int)index); segments.add(segment); } return offset; } /** * @return The offset of this subtable inside the source */ public long getTableOffset() { return tableOffset; } /** * @return The table id of this subtable */ public long getTableId() { return tableId; } /** * @return The number of segments */ public int size() { return segments.size(); } /** * Returns the segment at the given position. * * @param index Position of the segment * @return The segment at index */ public Segment getSegment(int index) { return segments.get(index); } /** * @return An iterator over the segments */ public Iterator<Segment> iterator() { return segments.iterator(); } /** * @return An Iterator over all Patches */ public PatchIterator patchIterator() { return new PatchIterator(); } /** * An Iterator over all Patches of a Subtable. * * This is more a C++ style Iterator, you don't call methods * to get the next element (java style), it is used more like a pointer. A call of the content retrieval * Methods doesn't advance the position, this has to be done manually. * * @author KenD00 */ public class PatchIterator { /** * Iterator over all segments */ private Iterator<Segment> segmentIterator = null; /** * The current selected segment */ private Segment currentSegment = null; /** * Index of the current segment entry */ private int currentSegmentIndex = 0; /** * If true, Patch0 gets returned, otherwise Patch1 */ private boolean currentSegmentFirstPatch = true; public PatchIterator() { segmentIterator = segments.iterator(); nextSegment(); } /** * @return True, if this PatchIterator is valid, that means a call of any other method does not produce an exception */ public boolean isValid() { return (currentSegment != null); } /** * Advances to the next Patch. If there isn't any other Patch, this PatchIterator becomes invalid. * * @throws java.lang.IllegalStateException This PatchIterator is invalid */ public void increment() { if (currentSegment == null) { throw new IllegalStateException(); } if (currentSegmentIndex < currentSegment.size()) { if (currentSegmentFirstPatch) { currentSegmentFirstPatch = false; return; } else { currentSegmentFirstPatch = true; currentSegmentIndex++; } } if (currentSegmentIndex == currentSegment.size()) { nextSegment(); } } /** * @return The address of the current Patch * * @throws java.lang.IllegalStateException This PatchIterator is invalid */ public long getAddress() { if (currentSegment == null) { throw new IllegalStateException(); } if (currentSegmentFirstPatch) { return currentSegment.getAddress0(currentSegmentIndex); } else { return currentSegment.getAddress1(currentSegmentIndex); } } /** * Copies the current Patch into the given array. * * @param dst The destination array * @param offset Offset into dst where to start writing * * @throws java.lang.IllegalStateException This PatchIterator is invalid */ public void getPatch(byte[] dst, int offset) { if (currentSegment == null) { throw new IllegalStateException(); } if (currentSegmentFirstPatch) { currentSegment.getPatch0(currentSegmentIndex, dst, offset); } else { currentSegment.getPatch1(currentSegmentIndex, dst, offset); } } /** * @return The length in bytes of the patch */ public int getPatchLength() { return Segment.PATCH_LENGTH; } /** * Increments to the next non empty Segment or null (== invalid) */ private void nextSegment() { // Reset patch identifiers currentSegmentIndex = 0; currentSegmentFirstPatch = true; // Navigate to the first non empty segment, or reset currentSegment to null as invalid flag while (segmentIterator.hasNext()) { currentSegment = segmentIterator.next(); if (currentSegment.size() > 0) { return; } } currentSegment = null; } } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/aacs/�����������������������������������������������������������������0000775�0001750�0001750�00000000000�11211611342�017366� 5����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/aacs/AACSKeys.java����������������������������������������������������0000664�0001750�0001750�00000005445�11211610402�021600� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.aacs; import dumphd.core.KeyData; import dumphd.util.MessagePrinter; /** * This is a wrapper class to access the platform dependent AACSKeys library. * * The library gets loaded when this class gets instantiated. Its methods map to library methods. * * @author KenD00 */ public class AACSKeys { /** * MessagePrinter passed to the library for textual output */ private MessagePrinter mp = null; /** * Constructs an AACSKeys object and loads the AACSKeys library. * * @param mp The MessagePrinter used for textual output by the library * @throws AACSException An error occurred loading the library */ public AACSKeys(MessagePrinter mp) throws AACSException { this.mp = mp; try { System.loadLibrary("aacskeys"); } catch (NullPointerException e1) { // This can never happen, but rethrow the exception anyway throw new AACSException(e1.getMessage(), e1); } catch (SecurityException e2) { throw new AACSException(e2.getMessage(), e2); } catch (UnsatisfiedLinkError e3) { // This exception will be most frequently raised, it does not only mean that the library has not been found, // there may also be another problem with the library! throw new AACSException(e3.getMessage(), e3); } } /** * Returns the version of the library. * * @return A human readable string of the library version */ public native String getVersionString(); /** * Retrieves the AACS keys for the given KeyData object. * * The keys are directly added to the object. * * @param sourcePath The path to the source. This string must contain the platform specific path to the source * @param kd The KeyData object the keys get retrieved for * @throws AACSException An error occurred retrieving the keys, however some keys may have been retrieved */ public native void getKeys(String sourcePath, KeyData kd) throws AACSException; } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/aacs/AACSDecrypter.java�����������������������������������������������0000664�0001750�0001750�00000237430�11211610360�022632� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.aacs; import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.concurrent.Semaphore; import java.util.zip.CRC32; import java.security.GeneralSecurityException; import java.security.MessageDigest; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import dumphd.bdplus.SubTable; import dumphd.core.DiscSet; import dumphd.core.KeyData; import dumphd.core.KeyDataFile; import dumphd.core.MultiplexedArf; import dumphd.util.*; /** * This class handles all the AACS-Decryption stuff for HD-DVDs / BluRays. * * @author KenD00 */ public class AACSDecrypter { public final static String idStringAACS = "AACS"; public final static String idStringTKF = "DVD_HD_V_TKF"; public final static int maxTKFVersion = 0; public final static long TKFFileSize = 2480; public final static HashMap<Integer, Integer> BD_APPLICATION_TYPES = new HashMap<Integer, Integer>(); public final static byte[] cbc_iv = { (byte)0x0B, (byte)0xA0, (byte)0xF8, (byte)0xDD, (byte)0xFE, (byte)0xA6, (byte)0x1F, (byte)0xB3, (byte)0xD8, (byte)0xDF, (byte)0x9F, (byte)0x56, (byte)0x6A, (byte)0x05, (byte)0x0F, (byte)0x78 }; public final static IvParameterSpec cbc_iv_spec = new IvParameterSpec(cbc_iv); public final static byte[] h0 = { (byte)0x2D, (byte)0xC2, (byte)0xDF, (byte)0x39, (byte)0x42, (byte)0x03, (byte)0x21, (byte)0xD0, (byte)0xCE, (byte)0xF1, (byte)0xFE, (byte)0x23, (byte)0x74, (byte)0x02, (byte)0x9D, (byte)0x95 }; public final static int decrypterThreads = 2; // queueLength must be divideable by 3 or the TS Aligned Units won't fit in!!! public final static int queueLength = 30; private MessagePrinter out = null; private KeyDataFile kdf = null; private byte[] packBuffer = new byte[decrypterThreads * queueLength * PESPack.PACK_LENGTH]; private PackDecrypter packDecrypter = null; private AACSKeys akl = null; private CRC32 crc32Calc = new CRC32(); private Cipher aes_128d = null; private Cipher aes_128cbcd = null; private MessageDigest sha1 = null; private DiscSet ds = null; static { // Initialize BD_APPLICATION_TYPES BD_APPLICATION_TYPES.put(DiscSet.BD_MV, 0x01); BD_APPLICATION_TYPES.put(DiscSet.BDR_MV, 0x03); BD_APPLICATION_TYPES.put(DiscSet.BDR_AV, 0x02); } /** * Checks if the given ByteSource is an AACS-Protected ARF. * Resets the ByteSource position after the check to 0. * * @param ds ByteSource to check * @return If true, the given ByteSource is an AACS-Protected ARF, false if not * @throws IOException An I/O error occurred */ public static boolean isArfEncrypted(ByteSource ds) throws IOException { // Better don't use Utils.buffer here, its highly possible that its already in use by the caller byte[] data = new byte[idStringAACS.length()]; Arrays.fill(data, (byte)0); ds.read(data, 0, idStringAACS.length()); ds.setPosition(0); if (new String(data, 0, idStringAACS.length(), "US-ASCII").equals(idStringAACS)) { return true; } else { return false; } } /** * Creates a new AACSDecrypter. * * @param mp MessagePrinter used for textual output * @param kdf Key Data File to use * @throws AACSException */ public AACSDecrypter(MessagePrinter mp, KeyDataFile kdf) throws AACSException { this.out = mp; this.kdf = kdf; // Create required crypo objects out.print("Initializing AACS... "); try { aes_128d = Cipher.getInstance("AES/ECB/NOPADDING"); aes_128cbcd = Cipher.getInstance("AES/CBC/NOPADDING"); sha1 = MessageDigest.getInstance("SHA-1"); packDecrypter = new PackDecrypter(decrypterThreads, queueLength); } catch (GeneralSecurityException e) { out.println("FAILED"); throw new AACSException(e.getMessage(), e); } out.println("OK"); // Try to load the aacskeys library out.print("Loading aacskeys library... "); try { akl = new AACSKeys(mp); out.println("OK"); out.println(akl.getVersionString()); } catch (AACSException e) { out.println("FAILED"); out.println(e.getMessage()); out.println("Direct key retrieval disabled, only keys from the database will be used"); } // TODO: Should this class hold the KEYDB and open it here? But when should it get closed? Should this class have a close-method? } /** * Initializes this AACSDecrypter to be ready to decrypt the given DiscSet. * * @param ds The DiscSet for which this AACSDecrypter should be prepared for */ public void init(DiscSet ds) throws AACSException { if (ds.keyData !=null) { this.ds = ds; } else throw new AACSException("DiscSet contains no key data"); } /** * Identifies the given DiscSet. Sets the DiscID and adds the keys found in the database, if any. * * @param ds The DiscSet to identify * @throws AACSException An error occurred */ public void identifyDisc(DiscSet ds) throws AACSException { ds.selected = false; out.print("Identifying disc... "); if (ds.aacsDir != null) { // AACS directory present, determine ID file by disc type File idFile = null; switch (ds.contentType) { case DiscSet.HD_STANDARD_V: idFile = new File(ds.aacsDir, "VTKF.AACS"); break; case DiscSet.HD_STANDARD_A: idFile = new File(ds.aacsDir, "ATKF.AACS"); break; case DiscSet.HD_ADVANCED_V: { LinkedList<String> idFileList = new LinkedList<String>(); Utils.scanForFilenames(ds.aacsDir, ds.aacsDir.getPath(), idFileList, false, new SimpleFilenameFilter("VTKF[0-9]{3}.AACS", false, false), true); if (!idFileList.isEmpty()) { Collections.sort(idFileList); idFile = new File(idFileList.getFirst()); } break; } case DiscSet.HD_ADVANCED_A: { LinkedList<String> idFileList = new LinkedList<String>(); Utils.scanForFilenames(ds.aacsDir, ds.aacsDir.getPath(), idFileList, false, new SimpleFilenameFilter("ATKF[0-9]{3}.AACS", false, false), true); if (!idFileList.isEmpty()) { Collections.sort(idFileList); idFile = new File(idFileList.getFirst()); } break; } case DiscSet.BD_MV: idFile = new File(ds.aacsDir, "Unit_Key_RO.inf"); break; case DiscSet.BDR_MV: case DiscSet.BDR_AV: idFile = new File(ds.aacsDir, "Unit_Key_RW.inf"); break; default: // Unknown disc type, throw an exception out.println("FAILED"); throw new AACSException("Unknown disc type: " + ds.contentType); } if (idFile.isFile()) { // ID file exists, calculate SHA-1 hash sha1.reset(); FileSource idSource = null; try { idSource = new FileSource(idFile, FileSource.R_MODE); long remaining = idSource.size(); while (remaining > 0) { if (remaining > (long)Utils.buffer.length) { if (idSource.read(Utils.buffer, 0, Utils.buffer.length) != Utils.buffer.length) { // Not all requested data could be read throw new IOException("Unexpected EOF"); } sha1.update(Utils.buffer, 0, Utils.buffer.length); remaining -= (long)Utils.buffer.length; } else { if (idSource.read(Utils.buffer, 0, (int)remaining) != (int)remaining) { // Not all requested data could be read throw new IOException("Unexpected EOF"); } sha1.update(Utils.buffer, 0, (int)remaining); remaining = 0; } } } catch (IOException e) { out.println("FAILED"); if (idSource != null) { try { idSource.close(); } catch (IOException e2) { // Ignore the exception } } throw new AACSException(e.getMessage(), e); } byte[] discId = sha1.digest(); out.println("OK"); out.println("DiscID : " + Utils.toHexString(discId, 0, 20)); out.println("Searching disc in key database..."); try { ds.keyData = kdf.getKeyData(discId, 0); if (ds.keyData != null) { out.println("Disc found in key database"); } } catch (IOException e) { out.println(e.getMessage()); ds.keyData = null; } if (ds.keyData == null) { ds.keyData = new KeyData(discId, 0); // If the aacskeys library is loaded, try to retrieve the keys from the disc if (akl != null) { out.println("Disc not found in key database"); out.println("Retrieving keys from source... "); try { akl.getKeys(ds.srcDir.getPath(), ds.keyData); out.println(); out.println("Keys successfully retrieved from source"); //TODO: Set title to non-null to allow editing of the title in the GUI, ugly, but the GUI otherwise does not know that there are no keys if (ds.keyData.getTitle() == null) { ds.keyData.setTitle("Movie Title"); } // Check if the present keyData has a date and add it if not if (ds.keyData.getDate() == null) { try { long lastModified = idFile.lastModified(); if (lastModified != 0L) { ds.keyData.setDate(new Date(lastModified)); } } catch (SecurityException e) { // Do nothing } } out.println("Saving keys in key database..."); KeyData result = null; try { if (ds.bdplus) { result = kdf.appendKeyData(ds.keyData, ds.isBluRay(), ds.isRecordable(), kdf.getSmartMode(ds.keyData) | KeyData.DATA_VIDBN); } else { result = kdf.appendKeyData(ds.keyData, ds.isBluRay(), ds.isRecordable()); } } catch (IOException e) { out.println(e.getMessage()); result = null; } if (result != null) { out.println("Keys saved in key database"); } else { out.println("Failed saving keys in key database"); } } catch (AACSException e) { out.println(); out.println(e.getMessage()); throw new AACSException("Failed retrieving keys from source"); } } else throw new AACSException("Disc not found in key database"); } else { // Check if we are dealing with BD+ and the VID is present if (ds.bdplus && ds.keyData.getVid() == null) { out.println("Disc is BD+ protected and the Volume ID is not present in the key database"); if (akl != null) { out.println("Retrieving keys from source..."); try { // FIXME?: aacskeys overwrites all present keys, is this OK? akl.getKeys(ds.srcDir.getPath(), ds.keyData); out.println(); out.println("Keys successfully retrieved from source"); //TODO: Set title to non-null to allow editing of the title in the GUI, ugly, but the GUI otherwise does not know that there are no keys if (ds.keyData.getTitle() == null) { ds.keyData.setTitle("Movie Title"); } // Check if the present keyData has a date and add it if not if (ds.keyData.getDate() == null) { try { long lastModified = idFile.lastModified(); if (lastModified != 0L) { ds.keyData.setDate(new Date(lastModified)); } } catch (SecurityException e) { // Do nothing } } out.println("Saving keys in key database..."); KeyData result = null; try { result = kdf.setKeyData(ds.keyData, ds.isBluRay(), ds.isRecordable(), true, kdf.getSmartMode(ds.keyData) | KeyData.DATA_VIDBN); } catch (IOException e) { out.println(e.getMessage()); result = null; } if (result != null) { out.println("Keys saved in key database"); } else { out.println("Failed saving keys in key database"); } } catch (AACSException e) { out.println(); out.println(e.getMessage()); out.println("Warning! Failed retrieving keys from source"); } } else { out.println("Warning! Direct key retrieval disabled, cannot retrieve Volume ID"); } } } // Key data successfully retrieved, everything is OK ds.selected = true; } else { out.println("FAILED"); throw new AACSException("DiscID file not found: " + idFile.getPath()); } } else { out.println("FAILED"); throw new AACSException("AACS directory not found"); } } /** * Initializes the given DiscSet. Decrypts the required keys, if possible. * * TODO: Skip key decryption if TK's / UK's are present from the database, or prevent them from being overwritten * * @param ds The DiscSet to initialize * @throws AACSException An error occurred */ public void initDisc(DiscSet ds) throws AACSException { out.print("Processing disc AACS data... "); if (ds.keyData != null) { // This is just to determine if a newline should be printed if VUK / PAK's are present boolean calculatedPak = false; if (ds.keyData.pakCount() < 1) { calculatedPak = true; // No VUK / PAK's present, check if we can calculate them out.print("\nNo Volume Unique Key / Protected Area Keys present, calculating them... "); byte[] mek = ds.keyData.getMek(); if (mek != null && ds.keyData.bnCount() > 0) { // Calculate all VUK / PAK's try { SecretKey mekKey = new SecretKeySpec(mek, "AES"); Iterator<Integer> it = ds.keyData.bnIdx().iterator(); while (it.hasNext()) { int index = it.next(); byte[] bn = ds.keyData.getBn(index); System.arraycopy(bn, 0, Utils.buffer, 0, bn.length); aes_128d.init(Cipher.DECRYPT_MODE, mekKey); aes_128d.doFinal(Utils.buffer, 0, mek.length, Utils.buffer, 0); // Do AES-G for (int i = 0; i < mek.length; i++) { Utils.buffer[i] = (byte)((byte)Utils.buffer[i] ^ (byte)bn[i]); } ds.keyData.setPak(index, Utils.buffer, 0); } out.println("OK"); } catch (GeneralSecurityException e) { out.println("FAILED"); out.println(e.getMessage()); } } else { out.println("FAILED"); out.println("Not enough data present to calculate a Volume Unique Key / Protected Area Keys"); } } if (ds.keyData.pakCount() > 0) { // VUK / PAK's found, decrypt Title / CPS Unit Keys if (!calculatedPak) { out.println("\nVolume Unique Key / Protected Area Keys present, decrypting Title Keys / CPS Unit Keys... "); } out.println("Searching Title Key / CPS Unit Key Files... "); File keyFile = null; LinkedList<String> keyFilenames = new LinkedList<String>(); switch (ds.contentType) { case DiscSet.HD_STANDARD_V: keyFile = new File(ds.aacsDir, "VTKF.AACS"); if (keyFile.isFile()) { keyFilenames.add(keyFile.getPath()); } break; case DiscSet.HD_STANDARD_A: keyFile = new File(ds.aacsDir, "ATKF.AACS"); if (keyFile.isFile()) { keyFilenames.add(keyFile.getPath()); } break; case DiscSet.HD_ADVANCED_V: Utils.scanForFilenames(ds.aacsDir, ds.aacsDir.getPath(), keyFilenames, false, new SimpleFilenameFilter("VTKF[0-9]{3}.AACS", false, false), false); if (!keyFilenames.isEmpty()) { Collections.sort(keyFilenames); } break; case DiscSet.HD_ADVANCED_A: Utils.scanForFilenames(ds.aacsDir, ds.aacsDir.getPath(), keyFilenames, false, new SimpleFilenameFilter("ATKF[0-9]{3}.AACS", false, false), false); if (!keyFilenames.isEmpty()) { Collections.sort(keyFilenames); } break; case DiscSet.BD_MV: keyFile = new File(ds.aacsDir, "Unit_Key_RO.inf"); if (keyFile.isFile()) { keyFilenames.add(keyFile.getPath()); } break; case DiscSet.BDR_MV: case DiscSet.BDR_AV: keyFile = new File(ds.aacsDir, "Unit_Key_RW.inf"); if (keyFile.isFile()) { keyFilenames.add(keyFile.getPath()); } break; default: // Unknown disc type, throw an exception out.println("FAILED"); throw new AACSException("Unknown disc type: " + ds.contentType); } if (!keyFilenames.isEmpty()) { FileSource keySource = null; // Actually the VUK is the same as the first PAK, this way this code works for BD recordables too, // but it won't with HD-DVD recordables byte[] vuk = ds.keyData.getVuk(); SecretKey vukKey = new SecretKeySpec(vuk, "AES"); switch (ds.contentType) { case DiscSet.HD_STANDARD_V: case DiscSet.HD_STANDARD_A: case DiscSet.HD_ADVANCED_V: case DiscSet.HD_ADVANCED_A: // Title Key Files found, decrypt them Iterator<String> it = keyFilenames.iterator(); while (it.hasNext()) { String fileName = it.next(); out.print("Decrypting " + fileName + "... "); try { keySource = new FileSource(new File(fileName), FileSource.R_MODE); if (keySource.size() >= TKFFileSize) { if (keySource.read(Utils.buffer, 0, (int)TKFFileSize) != (int)TKFFileSize) { throw new IOException("Unexpected EOF"); } if (new String(Utils.buffer, 0, idStringTKF.length(), "US-ASCII").equals(idStringTKF)) { if (ByteArray.getVarLong(Utils.buffer, idStringTKF.length(), 4) == TKFFileSize) { if (ByteArray.getUShort(Utils.buffer, 32) <= maxTKFVersion) { int pos = 128; for (int i = 1; i <= 64; i++) { if ((ByteArray.getUByte(Utils.buffer, pos) & 0x80) == 0x80) { // Title Key valid, decrypt it aes_128d.init(Cipher.DECRYPT_MODE, vukKey); aes_128d.doFinal(Utils.buffer, pos + 4, vuk.length, Utils.buffer, 0); // TODO: Set only if not already present? ds.keyData.setTuk(i, Utils.buffer, 0); } pos += 36; } out.println("OK"); } else throw new AACSException("Unsupported version"); } else throw new AACSException("Wrong file size stated in HD_VTKF_SIZE"); } else throw new AACSException("Wrong TKF_ID"); } else throw new AACSException("File too small to be a Title Key File"); } catch (IOException e) { out.println("FAILED"); out.println(e.getMessage()); } catch (AACSException e2) { out.println("FAILED"); out.println(e2.getMessage()); } catch (GeneralSecurityException e3) { out.println("FAILED"); out.println(e3.getMessage()); } finally { try { keySource.close(); } catch (IOException e2) { // Ignore the exception } } } if (ds.keyData.tukCount() == 0) { throw new AACSException("Could not decrypt any Title Key, no decrypted Title Keys present"); } else { out.println("Updated disc data:"); out.println(ds.keyData.toString()); } out.print("Searching Sequence Key Block File... "); if (new File(ds.aacsDir, "SKB.AACS").isFile()) { out.println("ERROR"); out.println("Sequence Key Block File found! Cannot process Sequence Keys, dumping may fail!"); } else { out.println("OK"); out.println("Sequence Key Block File not found (this is good)"); } break; case DiscSet.BD_MV: case DiscSet.BDR_MV: case DiscSet.BDR_AV: long keyBlockOffset = 0; boolean bdDirectoryWarning = false; boolean skbWarning = false; try { out.println("Decrypting " + keyFilenames.getFirst() + "... "); keySource = new FileSource(new File(keyFilenames.getFirst()), FileSource.R_MODE); if (keySource.read(Utils.buffer, 0, 20) != 20) { throw new IOException("Unexpected EOF"); } keyBlockOffset = ByteArray.getVarLong(Utils.buffer, 0, 4); // Parse Unit Key File Header if (ByteArray.getUByte(Utils.buffer, 16) == AACSDecrypter.BD_APPLICATION_TYPES.get(ds.contentType)) { int bdDirectories = ByteArray.getUByte(Utils.buffer, 17); if (bdDirectories >= 1) { if ((ds.contentType == DiscSet.BD_MV) && ((ByteArray.getUByte(Utils.buffer, 18) >>> 7) == 0x01)) { skbWarning = true; } // For non-BDR_AV Content Types limit bdDirectories to one and issue a warning if (ds.contentType != DiscSet.BDR_AV && bdDirectories > 1) { bdDirectories = 1; bdDirectoryWarning = true; } int tcs = 0; for (int i = 0; i < bdDirectories; i++) { if (keySource.read(Utils.buffer, 0, 6) != 6) { throw new IOException("Unexpected EOF"); } // BDR_MV does not have this head elements if (ds.contentType != DiscSet.BDR_MV) { int tmp = ByteArray.getUShort(Utils.buffer, 0); if (tmp != 0) { ds.keyData.setHeadUnit(i, 0, tmp); } tmp = ByteArray.getUShort(Utils.buffer, 2); if (tmp != 0) { ds.keyData.setHeadUnit(i, 1, tmp); } } tcs = ByteArray.getUShort(Utils.buffer, 4); for (int j = 1; j <= tcs; j++) { if (keySource.read(Utils.buffer, 0, 4) != 4) { throw new IOException("Unexpected EOF"); } if (ds.contentType != DiscSet.BD_MV) { // Only recordables have the Clip_ID# ds.keyData.setTcUnit(i, ByteArray.getUShort(Utils.buffer, 0), ByteArray.getUShort(Utils.buffer, 2)); } else { // Prerecorded ones use the Title# ds.keyData.setTcUnit(i, j, ByteArray.getUShort(Utils.buffer, 2)); } } } // Parse Unit Key Block keySource.setPosition(keyBlockOffset); if (keySource.read(Utils.buffer, 0, 16) != 16) { throw new IOException("Unexpected EOF"); } int cpsUnits = ByteArray.getUShort(Utils.buffer, 0); // Define some variables used by recordables here to avoid recreating them in every loop // Intermediate CPS Unit Key before XORing it with the AES-H value byte[] ukey = new byte[16]; // Buffer for storing the AES-H values byte[] hbuf = new byte[AACSDecrypter.h0.length]; for (int unit = 1; unit <= cpsUnits; unit++) { if (keySource.read(Utils.buffer, 0, 48) != 48) { throw new IOException("Unexpected EOF"); } aes_128d.init(Cipher.DECRYPT_MODE, vukKey); aes_128d.doFinal(Utils.buffer, 32, vuk.length, Utils.buffer, 0); if (ds.contentType != DiscSet.BD_MV) { // Copy the intermediate key to have Utils.buffer available again System.arraycopy(Utils.buffer, 0, ukey, 0, ukey.length); // Initialize the AES-H buffer with h0 System.arraycopy(AACSDecrypter.h0, 0, hbuf, 0, AACSDecrypter.h0.length); try { FileSource ur = new FileSource(new File(ds.aacsDir, String.format("CPSUnit%1$05d.cci", unit)), FileSource.R_MODE); // TODO: This simple approach works because the size of a CPS Unit Usage Rules file is always a multiple of 128 bits // If this wouldn't be the case we must pad according to the spec if necessary while (ur.read(Utils.buffer, 0, hbuf.length) != -1) { SecretKeySpec aeskey = new SecretKeySpec(Utils.buffer, 0, hbuf.length, "AES"); aes_128d.init(Cipher.DECRYPT_MODE, aeskey); // Use Utils.buffer to store the intermediate result aes_128d.doFinal(hbuf, 0, hbuf.length, Utils.buffer, 0); // Do AES-G directly into hbuf so that it is setup for the next step for (int i = 0; i < hbuf.length; i++) { hbuf[i] = (byte)((byte)Utils.buffer[i] ^ (byte)hbuf[i]); } } // Now hbuf contains the final AES-H value, XOR the key with it for (int i = 0; i < ukey.length; i++) { ukey[i] = (byte)((byte)ukey[i] ^ (byte)hbuf[i]); } ds.keyData.setTuk(unit, ukey, 0); } catch (IOException e1) { out.println("Failed decrypting CPS Unit Key: " + unit); out.println(e1.getMessage()); } catch (GeneralSecurityException e2) { out.println("Failed decrypting CPS Unit Key: " + unit); out.println(e2.getMessage()); } } else { ds.keyData.setTuk(unit, Utils.buffer, 0); } } out.println("Finished decrypting CPS Unit Keys"); } else throw new AACSException("Invalid number of BD Directories: " + bdDirectories); } else throw new AACSException("Wrong Application Type, expected: " + AACSDecrypter.BD_APPLICATION_TYPES.get(ds.contentType) + ", got: " + ByteArray.getUByte(Utils.buffer, 16)); } catch (IOException e1) { out.println("Failed decrypting CPS Unit Keys"); out.println(e1.getMessage()); } catch (AACSException e2) { out.println("Failed decrypting CPS Unit Keys"); out.println(e2.getMessage()); } catch (GeneralSecurityException e3) { out.println("Failed decrypting CPS Unit Keys"); out.println(e3.getMessage()); } finally { try { keySource.close(); } catch (IOException e2) { // Ignore the exception } } if (bdDirectoryWarning) { out.println("WARNING! More than one BD Directory specified, ignored the following ones"); } if (ds.keyData.tukCount() == 0) { throw new AACSException("Could not decrypt any CPS Unit Key, no decrypted CPS Unit Keys present"); } else { out.println("Updated disc data:"); out.println(ds.keyData.toString()); } if (skbWarning) { out.println("ERROR! Sequence Key Block found! Cannot process Sequence Keys, dumping may fail!"); } else { out.println("Sequence Key Block not found (this is good)"); } break; default: // Unknown disc type, throw an exception out.println("FAILED"); throw new AACSException("Unknown disc type: " + ds.contentType); } out.println("AACS data processed"); } else { // No Title Key / CPS Unit Key File found out.println("No Title Key / CPS Unit Key Files Found"); if (ds.keyData.tukCount() == 0) { throw new AACSException("Could not decrypt Title Keys / CPS Unit Keys, no decrypted Title Keys / CPS Unit Keys present"); } } } else { // No VUK / PAK's present, check if any Title Key / CPS Unit Key is present if (ds.keyData.tukCount() == 0) { out.println("FAILED"); throw new AACSException("No Volume Unique Key / Protected Area Keys present, no decrypted Title Keys / CPS Unit Keys present"); } else { out.println("OK"); } } } else { // No Key Data present out.println("FAILED"); throw new AACSException("No Key Data present"); } } /** * Decrypts an EVOB. * * @param input Input EVOB * @param output Output EVOB * @throws AACSException A cryptographic error occurred * @throws IOException An I/O error occurred */ public Collection<MultiplexedArf> decryptEvob(ByteSource input, ByteSource output) throws AACSException, IOException { if (ds == null) { throw new AACSException("AACSDecrypter not initialized"); } // PESPack used to parse the current present pack data PESPack pack = new PESPack(); // Actual position of the currently processed pack in the input ByteSource long position = 0; // The start position of the packBuffer in the input ByteSource long baseAddress = 0; // KEY_VF field from the CPI field int key_vf = 0; // Key pointer of the current parsed pack, can be a title key pointer or a segment key pointer int packKeyPtr = 0; // Currently used Key pointer, can be a title key pointer, a segment key pointer or a magic value int currentKeyPtr = 0; // Currently used Key, can be a title key, segment key, or null for no key byte[] npk = null; // True, if this is the first time that a read was done, false otherwise boolean firstRead = true; // Number of bytes read during the last read int read = 0; // Offset into the packBuffer to write the read result to, is also used during parsing section as pointer to the current pack int readOffset = 0; // Number of bytes to read, initial value is the complete packBuffer length, after the first read always the queueLength int readAmount = packBuffer.length; // Offset into the packGuard, used in the reading section int packGuardReadOffset = 0; // Offset into the packBuffer to start writing from int writeOffset = 0; // Number of bytes to write, is in writing section always queueLength * PESPack.PACK_LENGTH, except the first read returned less data, then its read int writeAmount = queueLength * PESPack.PACK_LENGTH; // Number of packs to write, is queueLength, except the first read returned less data, then its read / PESPack.PACK_LENGTH int packGuardCheckAmount = queueLength; // Offset into the packGuard, used in the writing section int packGuardWriteOffset = 0; // Each element in the packGuard represents one pack in the packBuffer, processing a pack unlocks it, writing it locks it again // Used to mark when a pack is ready to be written Semaphore[] packGuard = new Semaphore[packBuffer.length / PESPack.PACK_LENGTH]; for (int i = 0; i < packGuard.length; i++) { packGuard[i] = new Semaphore(0, false); } // References to current open ARFs, index is the second byte of the header (ID?) MultiplexedArf[] arfs = new MultiplexedArf[256]; for (int i = 0; i < arfs.length; i++) { arfs[i] = null; } // The current used ARF MultiplexedArf currentArf = null; // List of finished ARFs, this one is returned LinkedList<MultiplexedArf> arfList = new LinkedList<MultiplexedArf>(); packDecrypter.init(packBuffer, packGuard, null); packDecrypter.updateBaseAddress(baseAddress); while ((read = input.read(packBuffer, readOffset, readAmount)) != -1) { // *********************** // *** Reading section *** // *********************** // Parse the read in data, increment readOffset automatically for (int endOffset = readOffset + read; readOffset < endOffset; readOffset += PESPack.PACK_LENGTH) { try { pack.parse(packBuffer, readOffset, false); // TODO: To reduce if's, check first scramble state if (pack.isNavPck()) { // NAV_PCK, update current crypto-settings // TODO: Fixed offset, better use dynamic discovery? key_vf = packBuffer[readOffset + 60] & 0xC0; if (key_vf == 0x80) { // Title Key Pointer valid packKeyPtr = packBuffer[readOffset + 62] & 0xFF; if (packKeyPtr != currentKeyPtr) { // Key has changed if (packKeyPtr > 0 && packKeyPtr <= 64) { // Valid Title Key Pointer out.println(String.format("0x%1$010X Key change, new key: Title Key #%2$d", position, packKeyPtr)); currentKeyPtr = packKeyPtr; npk = ds.keyData.getTuk(currentKeyPtr); if (npk == null) { // Required Title Key NOT present! out.println(String.format("0x%1$010X ERROR! Required Title Key not present: #%2$d", position, currentKeyPtr)); } else { packDecrypter.updateKey(npk, false); } } else { // Invalid Title Key Pointer! out.println(String.format("0x%1$010X ERROR! Key change, new Title Key pointer invalid, ignoring #%2$d", position, packKeyPtr)); } } } else { if (key_vf == 0x40) { // Segment Key Pointer valid packKeyPtr = packBuffer[readOffset + 63] & 0xFF; if (packKeyPtr != currentKeyPtr) { // Key has changed if (packKeyPtr > 0 && packKeyPtr <= 192) { // Valid Sequence Key Pointer out.println(String.format("0x%1$010X Key change, new key: Sequence Key #%2$d", position, packKeyPtr)); currentKeyPtr = packKeyPtr; out.println("### !ERROR! Sequence Keys not supported, skipping decryption !ERROR! ###"); npk = null; } else { // Invalid Sequence Key Pointer out.println(String.format("0x%1$010X ERROR! Key change, new Sequence Key pointer invalid, ignoring #%2$d", position, packKeyPtr)); } } } else { if (key_vf == 0) { // No Key valid if (currentKeyPtr != 0) { out.println(String.format("0x%1$010X Warning! No Key valid, disabling decryption", position)); currentKeyPtr = 0; npk = null; } } else { // Reserved value! if (currentKeyPtr != Integer.MAX_VALUE) { out.println(String.format("0x%1$010X Warning! Reserved Key Validity Flag, disabling decryption", position)); currentKeyPtr = Integer.MAX_VALUE; npk = null; } } } } // Updating CPI-Field packDecrypter.updateCpi(readOffset); // TODO: Which values should be preserved? // Clearing CPI-Field, CPI starts at 60 // Clear Key Management Information ByteArray.setInt(packBuffer, readOffset + 60, 0); // Clear Usage Rule Management Information ByteArray.setShort(packBuffer, readOffset + 68, 0x7FFF); // Set all CCI fields to valid and allow everything ByteArray.setInt(packBuffer, readOffset + 70, 0xF000); // Mark this pack as ready to write packGuard[packGuardReadOffset].release(); } else { if (pack.isScrambled()) { // Scrambled pack, decrypt it // Decrypt only when a key is present if (npk != null) { // TODO: Not nice, should the thread set the scramble state? pack.setScrambled(false); packDecrypter.decryptPack(readOffset); } else { // Mark this pack as ready to write packGuard[packGuardReadOffset].release(); } } else { if (pack.isAdvPck()) { // ADV_PKT require special handling int payloadOffset = pack.getPacket(0).payloadOffset(); int payloadLength = pack.getPacket(0).getPayloadLength(); // The payload must be at least 3 bytes long, 2 bytes header, 1 byte 0x00 for INTERMEDIATE_PACKET and END_PACKET, // at least 1 char filename for START_PACKET if (payloadLength >= 3) { int packetType = ByteArray.getUByte(packBuffer, payloadOffset); int packetId = ByteArray.getUByte(packBuffer, payloadOffset + 1); //System.out.println("ARF found, position: " + position + ", payloadOffset: " + (payloadOffset - readOffset) + ", payloadLength: " + payloadLength + ", packetType: " + packetType + ", packetId: " + packetId); if (packetType == MultiplexedArf.INTERMEDIATE_PACKET) { // INTERMEDIATE_PACKET found currentArf = arfs[packetId]; if (currentArf != null) { // Add payload // Skip header plus 1 byte, make the payloadOffset relative to the pack start! currentArf.addData(position, payloadOffset + 3 - readOffset, payloadLength - 3); } else { // Error, ARF not open out.println(String.format("0x%1$010X Error! INTERMEDIATE_PACKET for not opened ARF ID? 0x%2$02X found, ignoring", position, packetId)); } } else { if (packetType == MultiplexedArf.START_PACKET) { // START_PACKET found if (arfs[packetId] == null) { // The payload must be at least 258 bytes long, 2 bytes header, 256 bytes filename field if (payloadLength >= 258) { // Locate end of filename int nameStringEndOffset = payloadOffset + 2; for (int maxNameStringOffset = payloadOffset + 258; nameStringEndOffset < maxNameStringOffset; nameStringEndOffset++) { if (packBuffer[nameStringEndOffset] == 0x00) { break; } } //System.out.println("Name end offset: " + (nameStringEndOffset - readOffset)); try { String arfName = new String(packBuffer, payloadOffset + 2, nameStringEndOffset - (payloadOffset + 2), "US-ASCII"); out.println(String.format("0x%1$010X Multiplexed ARF found, ID?: 0x%2$02X, filename: %3$s", position, packetId, arfName)); // Create new MultiplexedArf, non demux mode currentArf = new MultiplexedArf(arfName, output, false); // Skip header plus filename field, make the payloadOffset relative to the pack start! currentArf.addData(position, payloadOffset + 258 - readOffset, payloadLength - 258); arfs[packetId] = currentArf; } catch (UnsupportedEncodingException e) { // This is not allowed to happen! out.println(String.format("0x%1$010X ERROR! Could not decode filename for ARF ID? 0x%2$02X, ignoring", position, packetId)); } } else { // ARF too small for header to fit in out.println(String.format("0x%1$010X Warning! Broken START_PACKET packet found for ARF ID? 0x%2$02X, payload too small for filename field, ignoring", position, packetId)); } } else { // Error, ARF is open out.println(String.format("0x%1$010X ERROR! START_PACKET for already opened ARF ID? 0x%2$02X found, ignoring", position, packetId)); } } else { if (packetType == MultiplexedArf.END_PACKET) { // END_PACKET found currentArf = arfs[packetId]; if (currentArf != null) { // Add payload and close ARF // Skip header plus 1 byte, make the payloadOffset relative to the pack start! currentArf.addData(position, payloadOffset + 3 - readOffset, payloadLength - 3); currentArf.markComplete(); arfList.add(currentArf); arfs[packetId] = null; } else { // Error, ARF not open //TODO: Maybe this is allowed if the ARF is only one packet big? out.println(String.format("0x%1$010X ERROR! END_PACKET for not opened ARF ID? 0x%2$02X found, ignoring", position, packetId)); } } else { // Unknown packet type found out.println(String.format("0x%1$010X Warning! Unknown ARF packet type 0x%2$02X for ARF ID? 0x%3$02X found, ignoring", position, packetType, packetId)); } } } } else { // ARF too small for header data to fit in out.println(String.format("0x%1$010X Warning! Broken ARF packet found, payload too small, ignoring", position)); } // Mark this pack as ready to write //System.out.println("Releasing: " + packGuardReadOffset); packGuard[packGuardReadOffset].release(); } else { // An unencrypted pack, just copy it // Mark this pack as ready to write packGuard[packGuardReadOffset].release(); } } } } catch (PESParserException e) { out.println(String.format("0x%1$010X ERROR! PES parser exception: %2$s", position, e.getMessage())); // In case of an exception the pack must be marked as ready to write, or the write section will wait forever packGuard[packGuardReadOffset].release(); } packGuardReadOffset += 1; if (packGuardReadOffset == packGuard.length) { packGuardReadOffset = 0; } position += PESPack.PACK_LENGTH; } // End-for parsing read in data // If readOffset hit the buffer end, reset it to 0 // Note: If read != packBuffer.length / 2 && read != packBuffer.length we won't get on the end and there will be less space left than half of packBuffer // But this can only happen when we reached the EOF, so this was the last time this loop gets executed if (readOffset == packBuffer.length) { readOffset = 0; baseAddress += packBuffer.length; packDecrypter.updateBaseAddress(baseAddress); } // *********************** // *** Writing section *** // *********************** // If the first read completely filled the buffer set the new readAmount to read only queueLength packs // If the first read did not fill the buffer completely, then the file is smaller than the buffer, process the complete buffer at once if (firstRead) { firstRead = false; if (read == packBuffer.length) { readAmount = queueLength * PESPack.PACK_LENGTH; } else { // The first read did not fill the buffer completely, adjust values to process the whole buffer writeAmount = read; packGuardCheckAmount = read / PESPack.PACK_LENGTH; } } // Lock all packs that we want to write for (int endOffset = packGuardWriteOffset + packGuardCheckAmount; packGuardWriteOffset < endOffset; packGuardWriteOffset++) { try { //System.out.println("Aquiring forced: " + packGuardWriteOffset); packGuard[packGuardWriteOffset].acquire(); } catch (InterruptedException e) { // TODO: Do we get interrupted? } } if (packGuardWriteOffset == packGuard.length) { packGuardWriteOffset = 0; } // TODO: Check write result? output.write(packBuffer, writeOffset, writeAmount); writeOffset += writeAmount; if (writeOffset == packBuffer.length) { writeOffset = 0; } } // End-while reading input file // ******************************* // *** Buffer flushing section *** // ******************************* // Wait until the decrypter threads finish and write the remaining data, if any packDecrypter.waitForDecryption(); writeAmount = 0; // This can never overrun the buffer because the offsets are correctly set before we reach this section and there is never more than queueLength bytes left while (packGuard[packGuardWriteOffset].tryAcquire()) { writeAmount += PESPack.PACK_LENGTH; packGuardWriteOffset += 1; // If the last queueLength part of the buffer is flushed we could overrun, so break out in that case if (packGuardWriteOffset == packGuard.length) { break; } } // TODO: Check write result? output.write(packBuffer, writeOffset, writeAmount); // **************************** // *** ARF checking section *** // **************************** // Check for open ARFs and close them for (int i = 0; i < arfs.length; i++) { currentArf = arfs[i]; // If there is something != null then this ARF is open, because closed ones get removed from this array if (currentArf != null) { arfs[i] = null; out.println(String.format("Warning! Remaining open ARF with ID? 0x%1$02X found, enforcing close", i)); currentArf.markComplete(); arfList.add(currentArf); } } return arfList; } /** * Decrypts an ARF. If the input is not an encrypted ARF, it is just copied. * * @param input Input ARF * @param output Output ARF * @return Number of written bytes to the output and the crc32 of the output * @throws AACSException A cryptographic error occurred * @throws IOException An I/O error occurred */ public CopyResult decryptArf(ByteSource input, ByteSource output) throws AACSException, IOException { if (ds == null) { throw new AACSException("AACSDecrypter not initialized"); } // Buffer used for decryption byte[] data = Utils.buffer; // ID string of the file String fileId = null; // Type of AACS protection int protectionType = 0; // Title Key Pointer to use for decryption int titleKeyPtr = 0; // Resource File Size field of the AACS header long resourceFileSize = 0; // Read the AACS header if (input.read(data, 0, 11) == 11) { fileId = new String(data, 0, idStringAACS.length(), "US-ASCII"); // Check if the ACCS id is present if (fileId.equals(idStringAACS)) { protectionType = ByteArray.getUByte(data, 4); titleKeyPtr = ByteArray.getUByte(data, 6); resourceFileSize = ByteArray.getVarLong(data, 7, 4); //System.out.println("fileId: " + fileId + ", pType: " + protectionType + ", tkp: " + titleKeyPtr + ", rfs: " + resourceFileSize); // Check if at least the resource data fits in // Note: some protection types make the file bigger than resourceFileSize + 272! if (resourceFileSize + 272 <= input.size()) { // Read in the Resource Filename Field. Attention, in two cases this field is encrypted, it MUST be decrypted too! input.read(data, 0, 272); switch (protectionType) { // Ignoring the hash values both formats are handled the same case 0x01: // Encapsulation for Encryption case 0x11: // Encapsulation for Encryption and Hash // Read as much data fits into the buffer, be careful that bufferSize % 16 > 0 is NOT encrypted if (titleKeyPtr > 0 && titleKeyPtr <= 64) { long written = 0; byte[] tk = ds.keyData.getTuk(titleKeyPtr); if (tk != null) { // Title Key found SecretKey tkKey = new SecretKeySpec(tk, "AES"); try { aes_128cbcd.init(Cipher.DECRYPT_MODE, tkKey, cbc_iv_spec); // Decrypt the Resource Filename Field! aes_128cbcd.update(data, 0, 272, data, 0); crc32Calc.reset(); int readResult = 0; int writeResult = 0; while (written < resourceFileSize) { long writeAmount = resourceFileSize - written; if (writeAmount < data.length) { // All remaining data fits into the buffer readResult = input.read(data, 0, (int)writeAmount); if (readResult != (int)writeAmount) { throw new IOException("Could not read the requested number of bytes"); } // Check for unencrypted residual data int residual = ((int)writeAmount) % 16; if (residual > 0) { aes_128cbcd.doFinal(data, 0, (int)writeAmount - residual, data, 0); } else { aes_128cbcd.doFinal(data, 0, (int)writeAmount, data, 0); } writeResult = output.write(data, 0, (int)writeAmount); if (writeResult != (int)writeAmount) { throw new IOException("Could not write the requested number of bytes"); } written += (int)writeAmount; crc32Calc.update(data, 0, (int)writeAmount); } else { // More data than buffer space // The buffersize % 16 MUST be 0!!!! readResult = input.read(data, 0, data.length); if (readResult != data.length) { throw new IOException("Could not read the requested number of bytes"); } aes_128cbcd.update(data, 0, data.length, data, 0); writeResult = output.write(data, 0, data.length); if (writeResult != data.length) { throw new IOException("Could not write the requested number of bytes"); } written += data.length; crc32Calc.update(data, 0, data.length); } } } catch (GeneralSecurityException e) { throw new AACSException(e.getMessage(), e); } } else throw new AACSException("Title Key #" + titleKeyPtr + " not found"); return new CopyResult(written, crc32Calc.getValue()); } else throw new AACSException("Invalid Title Key Pointer #" + titleKeyPtr); // Ignoring the MAC, Hash and Filename check, these formats are handled the same case 0x02: // Encapsulation for MAC case 0x12: // Encapsulation for Hash case 0x21: // Encapsulation for Non-Protected Advanced Element // Just copying the file return Utils.copyBs(input, output, resourceFileSize); default: // Unknown protection type throw new AACSException("Unknown protection type"); } } else throw new AACSException("Physical filesize smaller than AACS filesize"); } // End-if ID-check } // End-if Header-check // This section MAY ONLY be reached if the input should be copied to output // Reset position because there were already parts of the input read input.setPosition(0); // TODO: Check if complete file got copied? return Utils.copyBs(input, output, input.size()); } /** * Decrypts a m2ts file. * * TODO: Do i need a way to prevent key brute forcing with a null key? BDAV code calls this with key == null and usually * does not want to search for a key * * @param input Input m2ts * @param output Output m2ts * @param key The CPS Unit key to use, if null it is tried do determine it by brute forcing * @param subTable A Conversion Table SubTable to use for BD+ removal, set to null if BD+ isn't present * @throws AACSException A cryptographic error occurred * @throws IOException An I/O error occurred */ public void decryptTs(ByteSource input, ByteSource output, byte[] key, SubTable subTable) throws AACSException, IOException { if (ds == null) { throw new AACSException("AACSDecrypter not initialized"); } // TSAlignedUnit to parse the current present aligned unit data TSAlignedUnit unit = new TSAlignedUnit(); // Actual position of the currently processed aligned unit in the input ByteSource long position = 0; // The start position of the packBuffer in the input ByteSource long baseAddress = 0; // Current state of the decryption, -1: has not been set yet, 0: decryption disabled, 1: decryption enabled int decryptionState = -1; // True, if this is the first time that a read was done, false otherwise boolean firstRead = true; // Number of bytes read during the last read int read = 0; // Offset into the packBuffer to write the read result to, is also used during parsing section as pointer to the current pack int readOffset = 0; // Number of bytes to read, initial value is the complete packBuffer length, after the first read always the queueLength int readAmount = packBuffer.length; // Offset into the packGuard, used in the reading section int packGuardReadOffset = 0; // Offset into the packBuffer to start writing from int writeOffset = 0; // Number of bytes to write, is in writing section always queueLength * PESPack.PACK_LENGTH, except the first read returned less data, then its read int writeAmount = queueLength * TSAlignedUnit.UNIT_LENGTH / 3; // Number of packs to write, is queueLength, except the first read returned less data, then its read / PESPack.PACK_LENGTH int packGuardCheckAmount = queueLength / 3; // Offset into the packGuard, used in the writing section int packGuardWriteOffset = 0; // Each element in the packGuard represents one pack in the packBuffer, processing a pack unlocks it, writing it locks it again // Used to mark when a pack is ready to be written Semaphore[] packGuard = new Semaphore[packBuffer.length / TSAlignedUnit.UNIT_LENGTH]; for (int i = 0; i < packGuard.length; i++) { packGuard[i] = new Semaphore(0, false); } packDecrypter.init(packBuffer, packGuard, subTable); packDecrypter.updateBaseAddress(baseAddress); // Search for the CPS Unit Key if necessary if (key == null) { out.print("Searching CPS Unit Key... "); int index = findTsKey(input); if (index < 0) { out.println("ERROR"); if (index == -2) { out.println("Failed to determine CPS Unit Key!"); } else { out.println("An error occured while trying to determine the CPS Unit Key!"); } } else if (index == 0) { out.println("Not encrypted"); } else { out.println(String.format("#%1$d", index)); key = ds.keyData.getTuk(index); } } // Set the CPS Unit Key if present if (key != null) { packDecrypter.updateKey(key, true); } while ((read = input.read(packBuffer, readOffset, readAmount)) != -1) { // *********************** // *** Reading section *** // *********************** // Parse the read in data, increment readOffset automatically for (int endOffset = readOffset + read; readOffset < endOffset; readOffset += TSAlignedUnit.UNIT_LENGTH) { try { unit.parse(packBuffer, readOffset); // Check if the aligned unit is encrypted if (unit.isEncrypted()) { // Aligned unit is encrypted if (decryptionState == 0 || decryptionState == -1) { // Aligned unit is encrypted if (decryptionState == 0) { // Aligned unit is encrypted but decryption has been disabled, this is not allowed. if (ds.contentType == DiscSet.BD_MV) { out.println(String.format("0x%1$010X Warning! Decryption is disabled, enabling decryption", position)); } else { out.println(String.format("0x%1$010X Info! Enabling decryption", position)); } } decryptionState = 1; // Check here if the required key is present just to show an error message if not if (key != null) { out.println(String.format("0x%1$010X Decryption enabled", position)); } else { out.println(String.format("0x%1$010X ERROR! Required CPS Unit Key / Sequence Key not present", position)); } } // Actually perform the decryption // TODO: If in case of a missing key no decryption is performed that can result into false alarams that the conversion table // is broken, put the pack into the packDecrypter and let it perfrom BD+ removal? if (key != null) { // Key is present, add a job to the packDecrypter packDecrypter.decryptAlignedUnit(readOffset, false); } else { // Key is not present, mark the unit as ready to write packGuard[packGuardReadOffset].release(); } } else { // Aligned unit is not encrypted if (decryptionState == -1) { // The aligned unit is not encrypted and the state has not been set yet, disable decryption decryptionState = 0; out.println(String.format("0x%1$010X Decryption disabled", position)); } else { if (decryptionState == 1) { // Aligned unit is not encrypted but decryption has been enabled, disable it again. This is not allowed to happen. decryptionState = 0; if (ds.contentType == DiscSet.BD_MV) { out.println(String.format("0x%1$010X Warning! Decryption is enabled, disabling decryption", position)); } else { out.println(String.format("0x%1$010X Info! Disabling decryption", position)); } } } // Check if BD+ should be removed // TODO: This check isn't really necessary because the packDecrypter checks that himself, but maybe its faster to not // put the pack into the packDecrypter? if (subTable != null) { packDecrypter.decryptAlignedUnit(readOffset, true); } else { // Just mark the aligned unit to be finished packGuard[packGuardReadOffset].release(); } } } catch (TSParserException e) { out.println(String.format("0x%1$010X ERROR! TS parser exception: %2$s", position, e.getMessage())); // In case of an exception the pack must be marked as ready to write, or the write section will wait forever packGuard[packGuardReadOffset].release(); } packGuardReadOffset += 1; if (packGuardReadOffset == packGuard.length) { packGuardReadOffset = 0; } position += TSAlignedUnit.UNIT_LENGTH; } // End-for parsing read in data // If readOffset hit the buffer end, reset it to 0 // Note: If read != packBuffer.length / 2 && read != packBuffer.length we won't get on the end and there will be less space left than half of packBuffer // But this can only happen when we reached the EOF, so this was the last time this loop gets executed if (readOffset == packBuffer.length) { readOffset = 0; baseAddress += packBuffer.length; packDecrypter.updateBaseAddress(baseAddress); } // *********************** // *** Writing section *** // *********************** // If the first read completely filled the buffer set the new readAmount to read only queueLength packs // If the first read did not fill the buffer completely, then the file is smaller than the buffer, process the complete buffer at once if (firstRead) { firstRead = false; if (read == packBuffer.length) { readAmount = queueLength * TSAlignedUnit.UNIT_LENGTH / 3; } else { // The first read did not fill the buffer completely, adjust values to process the whole buffer writeAmount = read; packGuardCheckAmount = read / TSAlignedUnit.UNIT_LENGTH; } } // Lock all packs that we want to write for (int endOffset = packGuardWriteOffset + packGuardCheckAmount; packGuardWriteOffset < endOffset; packGuardWriteOffset++) { try { //System.out.println("Aquiring forced: " + packGuardWriteOffset); packGuard[packGuardWriteOffset].acquire(); } catch (InterruptedException e) { // TODO: Do we get interrupted? } } if (packGuardWriteOffset == packGuard.length) { packGuardWriteOffset = 0; } // TODO: Check write result? output.write(packBuffer, writeOffset, writeAmount); writeOffset += writeAmount; if (writeOffset == packBuffer.length) { writeOffset = 0; } } // End-while reading input file // ******************************* // *** Buffer flushing section *** // ******************************* // Wait until the decrypter threads finish and write the remaining data, if any packDecrypter.waitForDecryption(); writeAmount = 0; // This can never overrun the buffer because the offsets are correctly set before we reach this section and there is never more than queueLength bytes left while (packGuard[packGuardWriteOffset].tryAcquire()) { writeAmount += TSAlignedUnit.UNIT_LENGTH; packGuardWriteOffset += 1; // If the last queueLength part of the buffer is flushed we could overrun, so break out in that case if (packGuardWriteOffset == packGuard.length) { break; } } // TODO: Check write result? output.write(packBuffer, writeOffset, writeAmount); } /** * Decrypts a Thumbnail Data File found on BDAV discs. * * @param input Input m2ts * @param output Output m2ts * @param key The CPS Unit key to use * @throws AACSException A cryptographic error occurred * @throws IOException An I/O error occurred */ public void decryptTdf(ByteSource input, ByteSource output, byte[] key) throws AACSException, IOException { // This is not really needed here, just for consistency if (ds == null) { throw new AACSException("AACSDecrypter not initialized"); } if (key == null) { throw new AACSException("Required CPS Unit Key not present"); } // The CPS Unit Key as KeySpec SecretKey cpsKey = new SecretKeySpec(key, "AES"); // The calculated Block Key byte[] bk = new byte[16]; // The Block Key as KeySpec SecretKeySpec bkKey = null; // This very simple approach can be used because the file is always a multiple of 2048 (says the spec...) // TODO: This can be done multi-threaded while (input.read(Utils.buffer, 0, 2048) != -1) { try { // Calculate the Block Key // Note: The key gets ENcrypted! aes_128d.init(Cipher.ENCRYPT_MODE, cpsKey); aes_128d.doFinal(Utils.buffer, 0, bk.length, bk, 0); for (int i = 0; i < bk.length; i++) { bk[i] = (byte)((byte)bk[i] ^ (byte)Utils.buffer[i]); } bkKey = new SecretKeySpec(bk, "AES"); // Decrypt the Block aes_128cbcd.init(Cipher.DECRYPT_MODE, bkKey, AACSDecrypter.cbc_iv_spec); aes_128cbcd.doFinal(Utils.buffer, 16, 2032, Utils.buffer, 16); } catch (GeneralSecurityException e) { throw new AACSException(e.getMessage(), e); } // Write the decrypted data // TODO: Check write result? output.write(Utils.buffer, 0, 2048); } } /** * Tries to find the CPS Unit Key which decrypts the given input. * * This is done using a brute force approach using every present CPS Unit Key to decrypt the first AU * and check if it seems to be decrypted. * * TODO: Contains duplicated code from PackDecrypter * * @param input TS to find the CPS Unit Key for. * @return The CPS Unit Key number which decrypts the input. 0 if the input is not encrypted, * -1 if an error occurred, -2 if no key found * @throws IOException An I/O error occurred */ private int findTsKey(ByteSource input) throws IOException { // Save the current position of the input file long currentOffset = input.getPosition(); // The currently used CPS Unit Key Number int currentIndex = 0; // The currently used CPS Unit Key byte[] currentKey = null; // Used to parse the Aligned Unit TSAlignedUnit unit = new TSAlignedUnit(); // The CPS Unit Key as SecretKey SecretKey npkKey = null; // The Block Key byte[] ck = new byte[16]; // The Block Key as SecretKey SecretKey ckKey = null; // Seek to position 0, read one Aligned Unit and copy the unencrypted part behind the read part // The encrypted AU will be decrypted behind its position to avoid rereading it in every run input.setPosition(0); if (input.read(Utils.buffer, 0, TSAlignedUnit.UNIT_LENGTH) != TSAlignedUnit.UNIT_LENGTH) { // Source too small to be AACS encrypted input.setPosition(currentOffset); return 0; } // Reset to previous position, input isn't touched anymore input.setPosition(currentOffset); // Check if the AU is encrypted try { unit.parse(Utils.buffer, 0); if (unit.isEncrypted()) { System.arraycopy(Utils.buffer, 0, Utils.buffer, TSAlignedUnit.UNIT_LENGTH, 16); } else { return 0; } } catch (TSParserException ex) { return -1; } try { // Try all CPS Unit Keys, abort if a suitable key was found Iterator<Integer> it = ds.keyData.tukIdx().iterator(); while (it.hasNext()) { currentIndex = it.next(); // Calculate the Block Key currentKey = ds.keyData.getTuk(currentIndex); npkKey = new SecretKeySpec(currentKey, "AES"); aes_128d.init(Cipher.ENCRYPT_MODE, npkKey); aes_128d.doFinal(Utils.buffer, 0, 16, ck); for (int i = 0; i < 16; i++) { ck[i] = (byte)((byte)ck[i] ^ (byte)Utils.buffer[i]); } ckKey = new SecretKeySpec(ck, "AES"); // Decrypt the AU, store the decrypted content behind the encrypted source aes_128cbcd.init(Cipher.DECRYPT_MODE, ckKey, AACSDecrypter.cbc_iv_spec); // Encrypted part starts at 16 aes_128cbcd.doFinal(Utils.buffer, 16, TSAlignedUnit.UNIT_LENGTH - 16, Utils.buffer, TSAlignedUnit.UNIT_LENGTH + 16); // Check if enough sync bytes are present. Accept and return in that case, otherwise test another key // Due to BD+ protection it might be possible that some sync bytes are corrupted, we don't apply BD+ here so // allow some damaged sync bytes // TODO: Can this be abused? Can BD+ damage all sync bytes by intent? int found = 0; // There are 32 TS Packets inside an Aligned Unit for (int i = 0; i < 32; i++) { if (Utils.buffer[TSAlignedUnit.UNIT_LENGTH + i * TSAlignedUnit.PACKET_LENGTH + 4] == 0x47) { found++; if (found > 24) { return currentIndex; } } } } } catch (GeneralSecurityException e) { return -1; } return -2; } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/aacs/PackDecrypter.java�����������������������������������������������0000664�0001750�0001750�00000056006�11211610414�022777� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.aacs; import java.security.GeneralSecurityException; import java.util.concurrent.Semaphore; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import dumphd.bdplus.SubTable; import dumphd.util.PESPack; import dumphd.util.TSAlignedUnit; import dumphd.util.TSParserException; /** * This class holds a specific number of threads which are used to decrypt packs of an EVOB / M2TS. This class gets used from the AACSDecrypter. * This class is not thread safe, it may be only used by one thread. * * Each thread has a working queue in which specific methods of this class put job elements. The workload is automatically balanced between * the threads. * * TODO: How to handle exceptions that occur in the decrypter threads? * TODO: How to handle InterruptedExceptions when adding job elements or waiting for decryption? * TODO: Do work balancing synchronized? * * @author KenD00 */ public final class PackDecrypter { /** * Array of the used PackDecrypterThreads */ private PackDecrypterThread[] pdts = null; /** * These are the Thread-Objects created from the PackDecrypterThreads-Objects */ private Thread[] threads = null; /** * Reference to the supplied packBuffer */ private byte[] packBuffer = null; /** * Reference to the supplied packGuard */ private Semaphore[] packGuard = null; /** * Creates a new PackDecrypter using the given number of decrypter threads which each having the given queue length. * The decrypter threads are also started. * * @param decrypterThreads Number of decrypter threads to create * @param queueLength Length in packs of the queue of each decrypter thread * @throws GeneralSecurityException An error occured while initializing cryptographic elements */ public PackDecrypter(int decrypterThreads, int queueLength) throws GeneralSecurityException { if (decrypterThreads > 0) { if (queueLength > 0) { pdts = new PackDecrypterThread[decrypterThreads]; threads = new Thread[decrypterThreads]; for (int i = 0; i < decrypterThreads; i++) { PackDecrypterThread pdt = new PackDecrypterThread(queueLength); Thread thread = new Thread(pdt); thread.setDaemon(true); thread.setPriority(Thread.NORM_PRIORITY + 1); thread.start(); pdts[i] = pdt; threads[i] = thread; } } else throw new IllegalArgumentException("queueLength must be greater than 0"); } else throw new IllegalArgumentException("decrypterThreads must be greater than 0"); } /** * Initializes the object for a new working session. * * Waits until all pending jobs are done, then updates the used packBuffer, packGuard and BD+ SubTable. * * TODO: Also reset the base address? All actions here don't put anything into the working queue, resetting the base address would. * * @param packBuffer The new packBuffer to use * @param packGuard The new packGuard to use */ public void init(byte[] packBuffer, Semaphore[] packGuard, SubTable subTable) { // TODO: What if we get interrupted during waiting? waitForDecryption(); this.packBuffer = packBuffer; this.packGuard = packGuard; // Set the PatchIterator for every thread, don't forget to set it to null if the given SubTable is null! for (int i = 0; i < pdts.length; i++) { if (subTable != null) { pdts[i].setPatchIterator(subTable.patchIterator()); } else { pdts[i].setPatchIterator(null); } } } /** * Updates the base address of the currently used packBuffer. * * The base address plus the offset inside the packBuffer is the absolute address of the pack in the source stream. * * @param baseAddress The new base address */ public void updateBaseAddress(long baseAddress) { try { for (int i = 0; i < pdts.length; i++) { PackDecrypterThread pdt = pdts[i]; WorkUnit wu = pdt.acquireWorkUnit(); wu.type = WorkUnit.UPDATE_BASE_ADDRESS; wu.baseAddress = baseAddress; pdt.releaseWorkUnit(); } } catch (InterruptedException e) { // TODO: What to do? } } /** * Updates the key used for decryption. * Adds a new job element to the queue of each decrypter thread with the new key. The key is set after all current jobs in the queue are processed. * If a queue is full, this method waites until a place gets free. The key is updated by each decrypter thread itself. * * @param npk The new key to use * @param encrypt If true, the key will be used in encrypt mode (for BluRay), otherwise in decrypt mode (for HD-DVD) */ public void updateKey(byte[] npk, boolean encrypt) { try { SecretKey npkKey = new SecretKeySpec(npk, "AES"); for (int i = 0; i < pdts.length; i++) { PackDecrypterThread pdt = pdts[i]; WorkUnit wu = pdt.acquireWorkUnit(); if (encrypt) { wu.type = WorkUnit.UPDATE_KEY_ENCRYPT; } else { wu.type = WorkUnit.UPDATE_KEY_DECRYPT; } wu.npkKey = npkKey; pdt.releaseWorkUnit(); } } catch (InterruptedException e) { // TODO: What to do? } } /** * Updates the used CPI field. * Adds a new job element to the queue of each decrypter thread with the new CPI field. The CPI field is set after all current jobs in the queue are processed. * If a queue is full, this method waites until a place gets free. The CPI field is updated by each decrypter thread itself. * * @param offset Offset into the packBuffer where the CPI field starts */ public void updateCpi(int offset) { try { for (int i = 0; i < pdts.length; i++) { PackDecrypterThread pdt = pdts[i]; WorkUnit wu = pdt.acquireWorkUnit(); wu.type = WorkUnit.UPDATE_CPI; // CPI starts at 60, the first 4 bytes (Key Management Information) are not used! System.arraycopy(packBuffer, offset + 64, wu.dtk_cpi, 4, 12); pdt.releaseWorkUnit(); } } catch (InterruptedException e) { // TODO: What to do? } } /** * Decrypts a pack. * Selects the decrypter thread with the lowest workload and adds a new job element to its queue with the given pack offset. * If all queues are full, this method waits until a place in the queue of the first decrypter thread gets free. * * @param offset Offset into the packBuffer where the pack starts */ public void decryptPack(int offset) { try { PackDecrypterThread pdt = getWorker(); WorkUnit wu = pdt.acquireWorkUnit(); wu.type = WorkUnit.DECRYPT_PACK; wu.packOffset = offset; pdt.releaseWorkUnit(); } catch (InterruptedException e) { // TODO: What to do? } } public void decryptAlignedUnit(int offset, boolean bdplusOnly) { try { PackDecrypterThread pdt = getWorker(); WorkUnit wu = pdt.acquireWorkUnit(); if (!bdplusOnly) { wu.type = WorkUnit.DECRYPT_ALIGNED_UNIT; } else { wu.type = WorkUnit.REMOVE_BDPLUS; } wu.packOffset = offset; pdt.releaseWorkUnit(); } catch (InterruptedException e) { // TODO: What to do? } } /** * This method waits until all decrypter threads have finished their jobs. */ public void waitForDecryption() { try { for (int i = 0; i < pdts.length; i++) { pdts[i].waitForIdle(); } } catch (InterruptedException e) { // TODO: Do we get interrupted? } } /** * Terminates all decrypter threads, regardless if they have pending jobs. After this method call this object cannot be used any more. */ public void terminate() { if (threads != null) { for (int i = 0; i < threads.length; i++) { threads[i].interrupt(); try { threads[i].join(); } catch (InterruptedException e) { // TODO: Do we get interrupted? } } } // Release all used resources pdts = null; threads = null; packBuffer = null; packGuard = null; } /** * @return The PackDecrypterThread with the lowest workload */ private PackDecrypterThread getWorker() { PackDecrypterThread pdt = pdts[0]; int minCapacity = 0; for (int i = 0; i < pdts.length; i++) { PackDecrypterThread currentPdt = pdts[i]; int currentCapacity = currentPdt.getFreeCapacity(); if (currentCapacity > minCapacity) { minCapacity = currentCapacity; pdt = currentPdt; } } return pdt; } /** * A PackDecrypterThread processes job elements (WorkUnit objects) put into its queue. * * @author KenD00 */ private final class PackDecrypterThread implements Runnable { /** * AES-128 cipher in ECB-Mode */ private Cipher aes_128 = null; /** * AES-128 cipher in CBC-Mode */ private Cipher aes_128cbcd = null; /** * Used to parse the decrypted Aligned Unit and set it unencrypted */ private TSAlignedUnit unit = new TSAlignedUnit(); /** * Base address of the current packBuffer */ private long baseAddress = 0; /** * DTK_CPI field used for creating of the Content Key */ private byte[] dtk_cpi = new byte[16]; /** * Byte array of the Content Key */ private byte[] ck = new byte[16]; /** * The Content Key */ private SecretKey ckKey = null; /** * The PatchIterator used to iterate over BD+ Patch Entries */ private SubTable.PatchIterator patchIt = null; /** * Working queue */ private WorkUnit queue[] = null; /** * The index in the queue where the next WorkUnit should be inserted */ private int putPointer = 0; /** * The index in the queue where the next WorkUnit should be taken from */ private int getPointer = 0; /** * Every put operation acquires a permit from this Semaphore. Used to implement waiting if the queue is full */ private Semaphore putGuard = null; /** * Every get operation acquires a permit from this Semaphore. Used to implement waiting if the queue is empty */ private Semaphore getGuard = new Semaphore(0, false); /** * Creates a new PackDecrypterThread with the given queue length. Initializes the Cipher objects. * * @param queueLength Length in elements of the working queue * @throws GeneralSecurityException An error occurred while initializing cryptographic elements */ public PackDecrypterThread(int queueLength) throws GeneralSecurityException { aes_128 = Cipher.getInstance("AES/ECB/NOPADDING"); aes_128cbcd = Cipher.getInstance("AES/CBC/NOPADDING"); queue = new WorkUnit[queueLength]; // Fill the queue with WorkUnits // Put and get don't create or delete WorkUnits, the present ones get reused to avoid new / delete overhead // The put and get guards prevent overwriting / reusing WorkUnits in a faulty way for (int i = 0; i < queue.length; i++) { queue[i] = new WorkUnit(); } putGuard = new Semaphore(queueLength, false); } /** * Locks a WorkUnit and returns it. * * If all WorkUnits are currently locked it waits until one gets released. * Every acquired WorkUnit must be released with a call of releaseWorkUnit()! * * @return A WorkUnit * @throws InterruptedException Interrupted during waiting for a WorkUnit to become available */ public WorkUnit acquireWorkUnit() throws InterruptedException { putGuard.acquire(); WorkUnit wu = queue[putPointer]; putPointer += 1; if (putPointer == queue.length) { putPointer = 0; } return wu; } /** * Releases an acquired WorkUnit. * * Every acquired WorkUnit must be released. Do not release more WorkUnits than acquired or data loss occurs! */ public void releaseWorkUnit() { getGuard.release(); } /** * @return The available number of free WorkUnits. */ public int getFreeCapacity() { return putGuard.availablePermits(); } /** * Waits until all WorkUnits are processed and this PackDecrypter becomes idle. * * @throws InterruptedException Iterrupted during waiting */ public void waitForIdle() throws InterruptedException { putGuard.acquire(queue.length); putGuard.release(queue.length); } /** * Sets the PatchIterator to be used. * * WARNING! This method must not be called if there are pending WorkUnits! * * TODO: Check if there are no pending WorkUnits? * * @param patchIt The PatchIterator to use to patch BD+ */ public void setPatchIterator(SubTable.PatchIterator patchIt) { this.patchIt = patchIt; } /* (non-Javadoc) * @see java.lang.Runnable#run() */ public void run() { WorkUnit wu = null; for (;;) { try { getGuard.acquire(); wu = queue[getPointer]; getPointer += 1; if (getPointer == queue.length) { getPointer = 0; } switch (wu.type) { case WorkUnit.UPDATE_BASE_ADDRESS: baseAddress = wu.baseAddress; break; case WorkUnit.UPDATE_KEY_DECRYPT: try { aes_128.init(Cipher.DECRYPT_MODE, wu.npkKey); } catch (GeneralSecurityException e) { // TODO: What to do? } break; case WorkUnit.UPDATE_KEY_ENCRYPT: try { aes_128.init(Cipher.ENCRYPT_MODE, wu.npkKey); } catch (GeneralSecurityException e) { // TODO: What to do? } break; case WorkUnit.UPDATE_CPI: // Exchange the arrays to avoid memcopy byte[] temp = dtk_cpi; dtk_cpi = wu.dtk_cpi; wu.dtk_cpi = temp; break; case WorkUnit.DECRYPT_PACK: try { // Udate Keyseed // Dtk starts at 84 System.arraycopy(packBuffer, wu.packOffset + 84, dtk_cpi, 0, 4); aes_128.doFinal(dtk_cpi, 0, 16, ck); // Do AES-G for (int i = 0; i < 16; i++) { ck[i] = (byte)((byte)ck[i] ^ (byte)dtk_cpi[i]); } ckKey = new SecretKeySpec(ck, "AES"); aes_128cbcd.init(Cipher.DECRYPT_MODE, ckKey, AACSDecrypter.cbc_iv_spec); // Encrypted part starts at 128 int cryptedOffset = wu.packOffset + 128; aes_128cbcd.doFinal(packBuffer, cryptedOffset, 1920, packBuffer, cryptedOffset); } catch (GeneralSecurityException e) { // TODO: What to do? } // Mark the pack as processed packGuard[wu.packOffset / PESPack.PACK_LENGTH].release(); break; case WorkUnit.DECRYPT_ALIGNED_UNIT: try { // Perform AACS decryption System.arraycopy(packBuffer, wu.packOffset, dtk_cpi, 0, 16); aes_128.doFinal(dtk_cpi, 0, 16, ck); for (int i = 0; i < 16; i++) { ck[i] = (byte)((byte)ck[i] ^ (byte)dtk_cpi[i]); } ckKey = new SecretKeySpec(ck, "AES"); aes_128cbcd.init(Cipher.DECRYPT_MODE, ckKey, AACSDecrypter.cbc_iv_spec); // Encrypted part starts at 16 int cryptedOffset = wu.packOffset + 16; aes_128cbcd.doFinal(packBuffer, cryptedOffset, 6128, packBuffer, cryptedOffset); unit.parse(packBuffer, wu.packOffset); unit.setEncrypted(false); } catch (GeneralSecurityException e1) { // TODO: What to do? } catch (TSParserException e2) { // TODO: What to do? } // Fall through to the BD+ removal case case WorkUnit.REMOVE_BDPLUS: // Perform BD+ removal, if necessary if (patchIt != null) { // Offset of the patch into current packBuffer long patchOffset = 0; // Check if the Patch Iterator is valid, if not, there are no more patches to apply while (patchIt.isValid()) { // If this test results into true than the patches aren't ordered ascending, this is an error if ((patchOffset = patchIt.getAddress() - baseAddress) < (long)wu.packOffset) { // TODO: Show an error in this situation? Cancel BD+ decryption? Currently the next "valid" patch is searched patchIt.increment(); continue; } // Check if the patch lies in the current Aligned Unit // Need to promote the end of the current Aligned Unit to long because the patchOffset can be more than maxint away if (patchOffset + (long)patchIt.getPatchLength() <= (long)(wu.packOffset + TSAlignedUnit.UNIT_LENGTH)) { // Patch lies in the current Aligned Unit, apply it and increment the iterator // Its safe to cast the offset to an int here because it is never bigger patchIt.getPatch(packBuffer, (int)patchOffset); patchIt.increment(); } else { // The patch lies not in the current Aligned Unit. This can have two reasons: // 1. The patch lies in another Aligned Unit. This is OK // 2. The patch exceeds the Aligned Unit (the difference is smaller than the patch length) // TODO: This is an error situation, currently it is just ignored. This will also raise // an error that the patches aren't ordered in the next loop break; } } } // Mark the aligned unit as processed packGuard[wu.packOffset / TSAlignedUnit.UNIT_LENGTH].release(); break; } putGuard.release(); } catch (InterruptedException e) { return; } } } } /** * An object of this class is a job element in the queue of a decrypter thread. * Depending of its type, specific attributes are valid. * * @author KenD00 */ private final static class WorkUnit { /** * This WorkUnit represents a pack to be decrypted */ public static final int DECRYPT_PACK = 0; /** * This WorkUnit represents an aligned unit to be decrypted */ public static final int DECRYPT_ALIGNED_UNIT = 1; /** * This WorkUnit represents an aligned unit from which only BD+ should be removed */ public static final int REMOVE_BDPLUS = 2; /** * This WorkUnit represents a Key, Cipher should be initialized in decrypt-mode */ public static final int UPDATE_KEY_DECRYPT = 3; /** * This WorkUnit represents a Key, Cipher should be initialized in encrypt-mode */ public static final int UPDATE_KEY_ENCRYPT = 4; /** * This WorkUnit represents a CPI field */ public static final int UPDATE_CPI = 5; /** * This WorkUnit represents a base address */ public static final int UPDATE_BASE_ADDRESS = 6; /** * The type of this WorkUnit. This field is always valid. */ public int type = 0; /** * Base address of the current packBuffer. This field is only valid if this WorkUnit is of the type UPDATE_BASE_ADDRESS */ public long baseAddress = 0; /** * Offset of a pack. This field is only valid if this WorkUnit is of the type DECRYPT_PACK or DECRYPT_ALIGNED_UNIT or REMOVE_BDPLUS */ public int packOffset = 0; /** * Reference to a key. This field is only valid if this WorkUnit is of the type UPDATE_KEY_DECRYPT or UPDATE_KEY_ENCRYPT */ public SecretKey npkKey = null; /** * Reference to a DTK_CPI field. This field is only valid if this WorkUnit is of the type UPDATE_CPI */ public byte[] dtk_cpi = new byte[16]; /** * Default constructor, does nothing */ public WorkUnit(){ // Nothing } } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/aacs/AACSException.java�����������������������������������������������0000664�0001750�0001750�00000002315�11211610372�022622� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.aacs; /** * Exception thrown when an error occurs during AACS processing / decrypting. * * @author KenD00 */ public class AACSException extends Exception { public AACSException() { super(); } public AACSException(String message) { super(message); } public AACSException(String message, Throwable cause) { super(message, cause); } public AACSException(Throwable cause) { super(cause); } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/util/�����������������������������������������������������������������0000775�0001750�0001750�00000000000�11211611342�017434� 5����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/util/StreamSource.java������������������������������������������������0000664�0001750�0001750�00000016652�11211610726�022732� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.util; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * Class to read from an InputStream or write to an OutputStream through the ByteSource interface. * The read and write methods simply map to the corresponding methods of the stream. * * This Class does not support seeking and windows, trying to seek or add / remove a window causes an exception. * * This class supports ByteSource.R_MODE and ByteSource.W_MODE. * * @author KenD00 */ public final class StreamSource implements ByteSource { /** * InputStream to read from, null if not in read mode */ private InputStream is = null; /** * OutputStream to write to, null if not in write mode */ private OutputStream os = null; /** * Dummy File object that gets returned if getFile() is called */ private File dummyFile = null; /** * If false, the close() method actually does nothing */ private boolean closeable = true; /** * Creates a new StreamSource. * * @param source The source stream, object type must be according to the mode * @param mode The mode of the object, must be either R_MODE (source must be an InputStream) or W_MODE (source must be an OutputStream) * @param closeable If true, the close method actually closes the underlying stream, otherwise not * (set to false e.g. when using stdin or stdout as source) * @throws IllegalArgumentException Invalid mode specified or the given source is not of the correct type for the given mode */ public StreamSource(Object source, int mode, boolean closeable) { this.closeable = closeable; switch (mode) { case R_MODE: if (source instanceof InputStream) { is = (InputStream)source; dummyFile = new File("InputStreamSource"); } else { throw new IllegalArgumentException("StreamSource: Read mode requested but given stream is not an InputStream"); } break; case W_MODE: if (source instanceof OutputStream) { os = (OutputStream)source; dummyFile = new File("OutputStreamSource"); } else { throw new IllegalArgumentException("StreamSource: Write mode requested but given stream is not an OutputStream"); } break; default: throw new IllegalArgumentException("StreamSource: Unsupported mode argument"); } } /** * @see dumphd.util.ByteSource#read(byte[], int, int) * @throws IllegalStateException This StreamSource has not been created in reading mode */ public int read(byte[] dst, int offset, int length) throws IOException { if (is != null) { int totalRead = 0; int read = 0; while ((read = is.read(dst, offset, length)) != -1) { totalRead += read; } return totalRead; } else { throw new IllegalStateException("StreamSource: This object has not been created in reading mode"); } } /** * @see dumphd.util.ByteSource#write(byte[], int, int) * @throws IllegalStateException This StreamSource has not been created in writing mode */ public int write(byte[] src, int offset, int length) throws IOException { if (os != null) { os.write(src, offset, length); return length; } else { throw new IllegalStateException("StreamSource: This object has not been created in writing mode"); } } /** * Returns always 0. * * @see dumphd.util.ByteSource#size() */ public long size() throws IOException { // TODO: What value should be returned? Or an exception? return 0; } /** * Returns always 0. * * @see dumphd.util.ByteSource#getPosition() */ public long getPosition() throws IOException { // TODO: What value should be returned? Or an exception? return 0; } /** * @see dumphd.util.ByteSource#setPosition(long) * @throws UnsupportedOperationException Always thrown since seeking is not supported */ public void setPosition(long position) throws IOException { throw new UnsupportedOperationException("StreamSource: Seeking is not supported"); } /** * Returns always 0. * * @see dumphd.util.ByteSource#windowCount() */ public int windowCount() { return 0; } /** * @see dumphd.util.ByteSource#addWindow(long) * @throws UnsupportedOperationException Always thrown since windows are not supported */ public void addWindow(long size) throws IOException { throw new UnsupportedOperationException("StreamSource: Windows are not supported"); } /** * @see dumphd.util.ByteSource#removeWindow() * @throws UnsupportedOperationException Always thrown since windows are not supported */ public void removeWindow() throws IOException { throw new UnsupportedOperationException("StreamSource: Windows are not supported"); } /* (non-Javadoc) * @see dumphd.util.ByteSource#getMode() */ public int getMode() { if (is == null) { return W_MODE; } else if (os == null) { return R_MODE; } else { // This case can never happen, its only here for completeness return RW_MODE; } } /** * Returns always the same dummy file which either has the filename InputStreamSource or OutputStreamSource, depending on the set mode. * * @see dumphd.util.ByteSource#getFile() */ public File getFile() { return dummyFile; } /** * Closes the underlying stream if the object has been created with closeable = true and clears the reference to it, * otherwise only the reference to the stream is cleared. * * @see dumphd.util.ByteSource#close() */ public void close() throws IOException { //TODO: This way of exception handling only works if there is only one stream, otherwise one exception is lost. // Anyway, is the lost exception a problem? IOException exception = null; if (closeable) { if (is != null) { try { is.close(); } catch (IOException e) { exception = e; } } if (os != null) { try { os.close(); } catch (IOException e) { exception = e; } } } is = null; os = null; if (exception != null) { throw exception; } } } ��������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/util/PESParserException.java������������������������������������������0000664�0001750�0001750�00000002344�11211610702�023764� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.util; /** * Exception thrown when a parser error occurs during parsing a PES pack. * * @author KenD00 */ public class PESParserException extends Exception { public PESParserException() { super(); } public PESParserException(String message) { super(message); } public PESParserException(String message, Throwable cause) { super(message, cause); } public PESParserException(Throwable cause) { super(cause); } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/util/FileSource.java��������������������������������������������������0000664�0001750�0001750�00000047542�11211610646�022361� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.util; import java.io.*; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.LinkedList; /** * Class for reading and writing from files with automatic buffering. The read and write methodes can be used to read / write various amounts of data * without caring about buffering. The read, write and seek methods are all buffered. * * This class supports ByteSource.R_MODE and ByteSource.RW_MODE. * * TODO: More checks if FileSource is closed? * * @author KenD00 */ public final class FileSource implements ByteSource { /** * The size of the used buffer */ public static final int bufferSize = 32 * 1024; /** * The File object this FileSource uses */ private File file = null; /** * The file to use */ private RandomAccessFile raf = null; /** * The FileChannel from the used file */ private FileChannel fc = null; /** * The ByteBuffer used by the FileChannel */ private ByteBuffer bb = null; /** * The mode of this FileSource */ private int mode = 0; /** * The list of the current windows */ private LinkedList<Window> windows = new LinkedList<Window>(); /** * Reference to the current active window */ private Window currentWindow = null; /** * "Physical" offset of the buffer, that is position 0 of the buffer corresponds to bufferOffset in the file */ private long bufferOffset = 0; /** * If true, the buffer's limit is currently also the window limit, if false, the buffer is just full when its limit is reached */ private boolean bufferLimited = false; /** * The maximum position that was reached currently in the buffer * It is always true that 0 <= maxPos <= bb.limit() <= bb.capacity() */ private int maxPos = 0; /** * If true, there is data in the buffer that has not been written to disk yet */ private boolean pendingWrite = false; /** * Creates a new FileSource from the given file in the specified mode. * * @param source The file to use * @param mode The mode to use * @throws FileNotFoundException If the file was not found */ public FileSource(File source, int mode) throws FileNotFoundException { this.file = source; this.mode = mode; switch (mode) { case R_MODE: raf = new RandomAccessFile(source, "r"); break; case RW_MODE: raf = new RandomAccessFile(source, "rw"); break; default: throw new IllegalArgumentException("Invalid mode argument"); } fc = raf.getChannel(); bb = ByteBuffer.allocateDirect(bufferSize); currentWindow = new Window(); windows.add(currentWindow); } /* (non-Javadoc) * @see dumphd.core.DataSource#read(byte[], int, int) */ public int read(byte[] dst, int offset, int length) throws IOException { // Counts the number of returned bytes int transferred = 0; while (true) { int currentLimit = bb.limit(); if (maxPos < currentLimit) { currentLimit = maxPos; } // The number of bytes remaining in the buffer for reading int remaining = currentLimit - bb.position(); //System.out.println(file.getName() + " FileSource read: " + length + ", remaining: " + remaining); if (length <= remaining) { // Logical read, enough data in buffer to serve request //System.out.println(file.getName() + " Doing logical read , transferred: " + transferred + ", bufferOffset: " + bufferOffset + ", bufferPos: " + bb.position() + ", limit: " + bb.limit()); bb.get(dst, offset, length); transferred += length; break; } else { // Mixed read, deliver rest of buffer, if any, fill it with a physical read if (remaining > 0) { //System.out.println(file.getName() + " Doing mixed read , transferred: " + transferred + ", bufferOffset: " + bufferOffset + ", bufferPos: " + bb.position() + ", limit: " + bb.limit()); bb.get(dst, offset, remaining); offset += remaining; length -= remaining; transferred += remaining; } // Physical part // If we are standing on the limit, we could have reached the window limit or the buffer is just full if (bb.position() == bb.limit()) { if (bufferLimited) { // Window limit reached, check if anything was read, return EOF if not if (transferred == 0) { transferred = -1; break; } } else { // Buffer full, clear it // If there is unwritten data in the buffer, write it out or it gets lost if (pendingWrite) { // Write the buffer, position and limit gets updated too writeBuffer(false); } else { // No data to write, clear the buffer bb.clear(); maxPos = 0; // Move buffer just behind the current buffer length bufferOffset += bufferSize; // Because the buffer position has moved, update the limit status updateLimit(); // Check if file pointer stands on the right position if (fc.position() != bufferOffset) { fc.position(bufferOffset); } } } } else { // Check if file pointer stands on the right position long readPos = bufferOffset + (long)bb.position(); if (fc.position() != readPos) { fc.position(readPos); } } // Now the buffer is ready to be filled (either it is partially filled, empty, or contains not written data from an incomplete write) // Mark current position to be able to return to it bb.mark(); // If we are at EOF, break out, return EOF if nothing was read, otherwise the amount of read data //System.out.println(String.format("### %4$s Reading from physical position: 0x%1$010X, bufferOffset: 0x%2$010X, bufferPosition: 0x%3$04X", fc.position(), bufferOffset, bb.position(), file.getName())); if (fc.read(bb) == -1) { if (transferred == 0) { transferred = -1; } // No need to update position, nothing got read //maxPos = bb.position(); break; } // After a read bufferOffset and file pointer are async! //System.out.println(file.getName() + " Read done, new position: " + bb.position()); maxPos = bb.position(); bb.reset(); //System.out.println(file.getName() + " Reset position to: " + bb.position()); } } return transferred; } /* (non-Javadoc) * @see dumphd.core.DataSource#write(byte[], int, int) */ public int write(byte[] src, int offset, int length) throws IOException { // Counts the number of returned bytes int transferred = 0; while (true) { int remaining = bb.remaining(); //System.out.println(file.getName() + " FileSource write: " + length + ", remaining: " + remaining); if (length <= remaining) { // Logical write, enough space left to write data bb.put(src, offset, length); transferred += length; pendingWrite = true; // Need to check if current pos is creater than maxPos because of a seek back there can be more valid data in the buffer than we wrote int newPos = bb.position(); if (newPos > maxPos) { maxPos = newPos; } //System.out.println(String.format("FULL write, bufferOffset: 0x%1$010X", bufferOffset)); break; } else { // Combined write, write as much space is left, perform a physical write if (remaining > 0) { // Logical write, write as much as possible bb.put(src, offset, remaining); offset += remaining; length -= remaining; transferred += remaining; pendingWrite = true; // Need to check if current pos is creater than maxPos because of a seek back there can be more valid data in the buffer than we wrote int newPos = bb.position(); if (newPos > maxPos) { maxPos = newPos; } //System.out.println(String.format("%1$s FULL write, bufferOffset: 0x%2$010X, position: %3$d", file.getName(), bufferOffset, bb.position())); } // Physical part // Check if we are at the limit or just end of buffer if (bufferLimited) { // We are at the window limit, if nothing was transferred signal EOF if (transferred == 0) { transferred = -1; } break; } else { // End of buffer reached, write it to storage device //System.out.println(file.getName() + " Writing physically!, position: " + bb.position() + ", maxPos: " + maxPos); writeBuffer(false); } } } return transferred; } /* (non-Javadoc) * @see dumphd.core.DataSource#getPosition() */ public long getPosition() throws IOException { return bufferOffset + (long)bb.position() - currentWindow.start; } /* (non-Javadoc) * @see dumphd.core.DataSource#setPosition(long) */ public void setPosition(long position) throws IOException { long tPosition = currentWindow.start + position; //System.out.println(String.format("%6$s Seek, original position: 0x%1$010X, windowed position: 0x%2$010X, bufferOffset: 0x%3$010X, bufferPosition: 0x%4$04X, bufferLimit: 0x%5$04X", position, tPosition, bufferOffset, bb.position(), bb.limit(), file.getName())); // Check if new position lies into current window range if (tPosition >= currentWindow.start && tPosition <= currentWindow.end) { // Position lies in current window range, now we know that the buffer limit is NOT the window limit long newBufferPosition = tPosition - bufferOffset; if (newBufferPosition >= 0 && newBufferPosition <= maxPos) { // New Position is inside filled buffer range, logical seek bb.position((int)newBufferPosition); //System.out.println(String.format("%3$s Logical seek, bufferOffset: 0x%1$010X, bufferPosition: 0x%2$04X", bufferOffset, bb.position(), file.getName())); } else { // New position is outside filled buffer range, physical seek if (pendingWrite) { // Requires the complete buffer to be written! Even behind a window may be data! //System.out.println(file.getName() + " Physical seek, write is pending, writing buffer"); writeBuffer(true); } else { // Clear the buffer! bb.clear(); maxPos = 0; } fc.position(tPosition); bufferOffset = tPosition; // Since the buffer position has moved, update the limit status updateLimit(); //System.out.println("Physical seek"); //System.out.println(String.format("%4$s Physically seeked, bufferOffset: 0x%1$010X, bufferPosition: 0x%2$04X, bufferLimit: 0x%3$04X", bufferOffset, bb.position(), bb.limit(), file.getName())); } } else throw new IOException("Seek exceeds window limits"); } /* (non-Javadoc) * @see dumphd.core.DataSource#addWindow(long) */ public void addWindow(long size) throws IOException { //System.out.println(file.getName() + " Adding Window, start: " + windowStart + ", end: " + windowEnd + ", bufferPos: " + bb.position()); long start = bufferOffset + (long)bb.position(); long end = start + size; // The new window must not exceed the current one, but may be bigger as the filesize (as the current window), otherwise writing would not work if (end <= currentWindow.end) { // There may be unwritten data behind the window, but this will get written if the buffer is flushed currentWindow = new Window(start, end); windows.add(currentWindow); // Because the window can limit the current buffer, update the limit status updateLimit(); //System.out.println(file.getName() + " Window added, start: " + windowStart + ", end: " + windowEnd + ", bufferPos: " + bb.position()); } else throw new IOException("New window exceeds current window limits"); } /* (non-Javadoc) * @see dumphd.core.DataSource#removeWindow() */ public void removeWindow() throws IOException { // Because the previous window is always bigger than the current we dont need to write out pending writes if (windows.size() > 1) { windows.removeLast(); // Set the previous window limits currentWindow = windows.getLast(); // Because the windwow was removed, the limit may be obsolete updateLimit(); } else throw new IOException("No window set"); } /* (non-Javadoc) * @see dumphd.core.DataSource#windowCount() */ public int windowCount() { return (windows.size() - 1); } /* (non-Javadoc) * @see dumphd.core.DataSource#size() */ public long size() throws IOException { long fileSize = fc.size(); if (fileSize < currentWindow.end) { return fileSize - currentWindow.start; } else { return currentWindow.end - currentWindow.start; } } /* (non-Javadoc) * @see dumphd.core.DataSource#getMode() */ public int getMode() { return mode; } /* (non-Javadoc) * @see dumphd.core.DataSource#getFile() */ public File getFile() { return file; } /* (non-Javadoc) * @see dumphd.core.DataSource#close() */ public void close() throws IOException { if (pendingWrite) { writeBuffer(true); } if (raf != null) { raf.close(); } // Release all used objects, this also prevents the usage of this object after it has been closed bb = null; fc = null; raf = null; currentWindow = null; windows = null; } /** * Writes the buffer physically to disk, it is not checked if it needs to get written, that must be done from outside. * * There are 2 modes of operation. * * Flush-mode: * Flushes the whole buffer contents to disk, does not honor set windows, writes everything from 0 up to maxPos. * After this method call the current buffer position is destroyed, the buffer MUST be relocated! Its position / maxPos is 0, the limit is the capacity. * * Shift-mode: * This must only be called when the position is at the buffer end, writes the buffer physically to disk, shifts the buffer so that it is empty. * Not the whole buffer may be written, thats honored during the shift. * * @param flush If true, flush-mode is used, shift-mode otherwise * @throws IOException In case of an I/O error */ private void writeBuffer(boolean flush) throws IOException { // Buffer writing starts always from bufferOffset, ensure that the file pointer is at that position if (fc.position() != bufferOffset) { fc.position(bufferOffset); } // Check if the whole buffer should be flushed or (partially) written and shifted if (flush) { // Flush the whole buffer, this destroys its current position! if (bb.position() != 0) { bb.position(0); } if (bb.limit() != maxPos) { bb.limit(maxPos); } while (bb.hasRemaining()) { fc.write(bb); } pendingWrite = false; bb.clear(); maxPos = 0; } else { // Write the buffer and shift its position // This part must only be called when the position is at the buffer end! //System.out.println(file.getName() + " Shifting write, position: " + bb.position() + ", bufferSize: " + bufferSize); if (bb.position() == bufferSize) { //Flip the buffer to write everything from its beginning to the current position bb.flip(); fc.write(bb); if (bb.hasRemaining()) { // The write was partially pendingWrite = true; } else { // The buffer was written fully pendingWrite = false; } bb.compact(); maxPos = bb.position(); // Advance the bufferPosition to the current file position bufferOffset = fc.position(); // Because the buffer was moved, update the limit updateLimit(); } else throw new IllegalStateException("FileSource: Buffer cannot be written when not completely filled, position: " + bb.position() + ", bufferSize: " + bufferSize); } } /** * Updates the current limit status. * This method MUST be called after every movement of the buffer or adding / removement of a window! */ private void updateLimit() { // Check if the limit lies in the current buffer range long checkLimit = currentWindow.end - bufferOffset; if (checkLimit <= (long)bufferSize) { // Limit lies is current buffer range, limit the buffer bb.limit((int)checkLimit); bufferLimited = true; } else { // Limit does not lie in current buffer range, reset its limit to the buffer capacity! bb.limit(bufferSize); bufferLimited = false; } } /** * Stores the logical start and end coordinates of a window. * * @author KenD00 */ private final static class Window { public long start = 0; public long end = Long.MAX_VALUE; /** * Creates a window with the start coordinate 0 and the end coordinate Long.MAX_VALUE. */ public Window() { // Nothing } /** * Creates a window with the given coordinates. * * @param start The start coordinate * @param end The end coordinate */ public Window(long start, long end) { this.start = start; this.end = end; } public String toString() { StringBuffer sb = new StringBuffer(32); sb.append("Window :: start: "); sb.append(start); sb.append(", end: "); sb.append(end); return sb.toString(); } } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/util/CopyResult.java��������������������������������������������������0000664�0001750�0001750�00000002145�11211610640�022412� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.util; /** * Class used for returning size and crc32 from a copied / decrypted file. * * @author KenD00 */ public class CopyResult { public long size = 0; public long crc32 = 0; public CopyResult() { // Nothing } public CopyResult(long length, long crc32) { this.size = length; this.crc32 = crc32; } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/util/ByteArray.java���������������������������������������������������0000664�0001750�0001750�00000013445�11211610626�022214� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.util; /** * Provides methods to get and set (multi)byte-values from byte-arrays. No checks are performed before an operation, may result in exceptions. * Multibyte-values are treated big-endian! * * @author KenD00 */ public class ByteArray { /** * @param source Source byte-array * @param index Starting index * @return Byte (unsigned) at index as int */ public static int getUByte(byte[] source, int index) { return (int)source[index] & 0xFF; } /** * @param source Source byte-array * @param index Starting index * @return Short (unsigned) starting at index as int, 2 bytes get read */ public static int getUShort(byte[] source, int index) { int output = ((int)source[index] & 0xFF) << 8; output |= (int)source[index+1] & 0xFF; return output; } /** * @param source Source byte-array * @param index Starting index * @return Int (unsigned) starting at index as long, 4 bytes get read */ public static long getUInt(byte[] source, int index) { long output = ((long)source[index] & 0xFF) << 24; output |= ((long)source[index+1] & 0xFF) << 16; output |= ((long)source[index+2] & 0xFF) << 8; output |= ((long)source[index+3] & 0xFF); return output; } /** * @param source Source byte-aray * @param index Starting index * @return Int (signed) starting at index as int, 4 bytes get read */ public static int getInt(byte[] source, int index) { int output = ((int)source[index] & 0xFF) << 24; output |= ((int)source[index+1] & 0xFF) << 16; output |= ((int)source[index+2] & 0xFF) << 8; output |= ((int)source[index+3] & 0xFF); return output; } /** * @param source Source byte-array * @param index Starting index * @return Long (signed) starting at index as long, 8 bytes get read */ public static long getLong(byte[] source, int index) { long output = ((long)source[index] & 0xFF) << 56; output |= ((long)source[index + 1] & 0xFF) << 48; output |= ((long)source[index + 2] & 0xFF) << 40; output |= ((long)source[index + 3] & 0xFF) << 32; output |= ((long)source[index + 4] & 0xFF) << 24; output |= ((long)source[index + 5] & 0xFF) << 16; output |= ((long)source[index + 6] & 0xFF) << 8; output |= (long)source[index + 7] & 0xFF; return output; } /** * @param source Source byte-array * @param index Starting index * @param length Number of bytes getting read (must be 1 - 8) * @return Long (signed) starting at index as long, length bytes get read */ public static long getVarLong(byte[] source, int index, int length) { long output = 0; for (int i = 0; i < length; i++) { output |= ((long)source[index + i] & 0xFF) << ((length - i - 1) * 8); } return output; } /** * @param source Byte-array to write to * @param index Starting index * @param data Data to write, one byte gets written */ public static void setByte(byte[] source, int index, int data) { source[index] = (byte)data; } /** * @param source Byte-array to write to * @param index Starting index * @param data Data to write, two bytes get written */ public static void setShort(byte[] source, int index, int data) { source[index+1] = (byte)data; data = data >>> 8; source[index] = (byte)data; } /** * @param source Byte-array to write to * @param index Starting index * @param data Data to write, four bytes get written */ public static void setInt(byte[] source, int index, int data) { source[index+3] = (byte)data; data = data >>> 8; source[index+2] = (byte)data; data = data >>> 8; source[index+1] = (byte)data; data = data >>> 8; source[index] = (byte)data; } /** * @param source Byte-array to write to * @param index Starting index * @param data Data to write, eight bytes get written */ public static void setLong(byte[] source, int index, long data) { source[index + 7] = (byte)data; data = data >>> 8; source[index + 6] = (byte)data; data = data >>> 8; source[index + 5] = (byte)data; data = data >>> 8; source[index + 4] = (byte)data; data = data >>> 8; source[index + 3] = (byte)data; data = data >>> 8; source[index + 2] = (byte)data; data = data >>> 8; source[index + 1] = (byte)data; data = data >>> 8; source[index] = (byte)data; } /** * @param source Byte-array to write to * @param index Starting index * @param data Data to write * @param length Number of bytes getting written (must be 1 - 8) */ public static void setVarLong(byte[] source, int index, long data, int length) { for (int i = 0; i < length; i++) { source[index + length - i - 1] = (byte)data; data = data >>> 8; } } }���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/util/PESPack.java�����������������������������������������������������0000664�0001750�0001750�00000107515�11211610674�021545� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.util; import java.util.Arrays; import java.util.Vector; /** * A class to provide easy access to the parts of a PES pack. This class works like a mask, no copy of the underlying data is made, * only some values are buffered for faster multiple access. Data which can be obtained through simple byte copy is not handled by this class * to avoid unnecessary memory copy. A fixed pack size of 2048 bytes is required for this class. * * This class also defines the names of given pack / packet id's and types. * * If getters or setters are called before a pack has been successfully parsed, exceptions may occur and the results are undefined. * * If getters or setters are called which are not supported by the pack / packet type or scan type, the result is undefined if not stated otherwise. * * @author KenD00 */ public final class PESPack { // Lookup-Tables for Stream IDs and different SubStream IDs // Although SubStream IDs are specific for their Stream ID there are some intersections public final static String[] S_ID_NAMES = new String[256]; public final static String[] PS1_ID_NAMES = new String[256]; public final static String[] PS2_ID_NAMES = new String[256]; public final static int PACK_LENGTH = 2048; /** * Static constructor for initialization of lookup-tables */ static { // Fill name tables with default names (stream-number) for (int i = 0; i < 256; i++) { String nameString = String.format("0x%1$02X" , i); S_ID_NAMES[i] = nameString; PS1_ID_NAMES[i] = nameString; PS2_ID_NAMES[i] = nameString; } // Known stream names S_ID_NAMES[0xBA] = "PACK_HEADER"; S_ID_NAMES[0xBB] = "SYSTEM_HEADER"; S_ID_NAMES[0xBD] = "PRIVATE_STREAM_1"; S_ID_NAMES[0xBE] = "PADDING_STREAM"; S_ID_NAMES[0xBF] = "PRIVATE_STREAM_2"; S_ID_NAMES[0xE0] = "MPEG-2_VIDEO_STREAM_FOR_MAIN"; S_ID_NAMES[0xE1] = "MPEG-2_VIDEO_STREAM_FOR_SUB"; S_ID_NAMES[0xE2] = "MPEG-4-AVC_VIDEO_STREAM_FOR_MAIN"; S_ID_NAMES[0xE3] = "MPEG-4-AVC_VIDEO_STREAM_FOR_SUB"; S_ID_NAMES[0xFD] = "VC-1_VIDEO_STREAM"; // Known private stream 1 substream names // Subpicture streams PS1_ID_NAMES[0x20] = "SUBPICTURE_STREAM_#01"; PS1_ID_NAMES[0x21] = "SUBPICTURE_STREAM_#02"; PS1_ID_NAMES[0x22] = "SUBPICTURE_STREAM_#03"; PS1_ID_NAMES[0x23] = "SUBPICTURE_STREAM_#04"; PS1_ID_NAMES[0x24] = "SUBPICTURE_STREAM_#05"; PS1_ID_NAMES[0x25] = "SUBPICTURE_STREAM_#06"; PS1_ID_NAMES[0x26] = "SUBPICTURE_STREAM_#07"; PS1_ID_NAMES[0x27] = "SUBPICTURE_STREAM_#08"; PS1_ID_NAMES[0x28] = "SUBPICTURE_STREAM_#09"; PS1_ID_NAMES[0x29] = "SUBPICTURE_STREAM_#10"; PS1_ID_NAMES[0x2A] = "SUBPICTURE_STREAM_#11"; PS1_ID_NAMES[0x2B] = "SUBPICTURE_STREAM_#12"; PS1_ID_NAMES[0x2C] = "SUBPICTURE_STREAM_#13"; PS1_ID_NAMES[0x2D] = "SUBPICTURE_STREAM_#14"; PS1_ID_NAMES[0x2E] = "SUBPICTURE_STREAM_#15"; PS1_ID_NAMES[0x2F] = "SUBPICTURE_STREAM_#16"; PS1_ID_NAMES[0x30] = "SUBPICTURE_STREAM_#17"; PS1_ID_NAMES[0x31] = "SUBPICTURE_STREAM_#18"; PS1_ID_NAMES[0x32] = "SUBPICTURE_STREAM_#19"; PS1_ID_NAMES[0x33] = "SUBPICTURE_STREAM_#20"; PS1_ID_NAMES[0x34] = "SUBPICTURE_STREAM_#21"; PS1_ID_NAMES[0x35] = "SUBPICTURE_STREAM_#22"; PS1_ID_NAMES[0x36] = "SUBPICTURE_STREAM_#23"; PS1_ID_NAMES[0x37] = "SUBPICTURE_STREAM_#24"; PS1_ID_NAMES[0x38] = "SUBPICTURE_STREAM_#25"; PS1_ID_NAMES[0x39] = "SUBPICTURE_STREAM_#26"; PS1_ID_NAMES[0x3A] = "SUBPICTURE_STREAM_#27"; PS1_ID_NAMES[0x3B] = "SUBPICTURE_STREAM_#28"; PS1_ID_NAMES[0x3C] = "SUBPICTURE_STREAM_#29"; PS1_ID_NAMES[0x3D] = "SUBPICTURE_STREAM_#30"; PS1_ID_NAMES[0x3E] = "SUBPICTURE_STREAM_#31"; PS1_ID_NAMES[0x3F] = "SUBPICTURE_STREAM_#32"; // Dolby Digital Plus audio streams for main PS1_ID_NAMES[0xC0] = "DD+_AUDIO_STREAM_FOR_MAIN_#01"; PS1_ID_NAMES[0xC1] = "DD+_AUDIO_STREAM_FOR_MAIN_#02"; PS1_ID_NAMES[0xC2] = "DD+_AUDIO_STREAM_FOR_MAIN_#03"; PS1_ID_NAMES[0xC3] = "DD+_AUDIO_STREAM_FOR_MAIN_#04"; PS1_ID_NAMES[0xC4] = "DD+_AUDIO_STREAM_FOR_MAIN_#05"; PS1_ID_NAMES[0xC5] = "DD+_AUDIO_STREAM_FOR_MAIN_#06"; PS1_ID_NAMES[0xC6] = "DD+_AUDIO_STREAM_FOR_MAIN_#07"; PS1_ID_NAMES[0xC7] = "DD+_AUDIO_STREAM_FOR_MAIN_#08"; // Dolby Digital Plus audio streams for sub PS1_ID_NAMES[0xC8] = "DD+_AUDIO_STREAM_FOR_SUB_#01"; PS1_ID_NAMES[0xC9] = "DD+_AUDIO_STREAM_FOR_SUB_#02"; PS1_ID_NAMES[0xCA] = "DD+_AUDIO_STREAM_FOR_SUB_#03"; PS1_ID_NAMES[0xCB] = "DD+_AUDIO_STREAM_FOR_SUB_#04"; PS1_ID_NAMES[0xCC] = "DD+_AUDIO_STREAM_FOR_SUB_#05"; PS1_ID_NAMES[0xCD] = "DD+_AUDIO_STREAM_FOR_SUB_#06"; PS1_ID_NAMES[0xCE] = "DD+_AUDIO_STREAM_FOR_SUB_#07"; PS1_ID_NAMES[0xCF] = "DD+_AUDIO_STREAM_FOR_SUB_#08"; // Known private stream 2 substream names PS2_ID_NAMES[0x00] = "PCI_PKT"; PS2_ID_NAMES[0x01] = "DSI_PKT"; PS2_ID_NAMES[0x04] = "GCI_PKT"; PS2_ID_NAMES[0x08] = "HL_PCK"; PS2_ID_NAMES[0x80] = "ADV_PCK"; } /** * The underlying byte array this object uses */ private byte[] pack = null; /** * The offset of the PES Pack inside the underlying byte array */ private int packOffset = 0; /** * Contains the packets of the pack */ private Vector<PESPacket> packets = new Vector<PESPacket>(8); /** * The actual number of packets in this pack */ private int packetCount = 0; /** * Constructs a new PESPack. */ public PESPack() { // Nothing } /** * Parses the given pack. If a exception occurs the state of this object is undefined. * In quick scan mode, not the complete header is parsed, only the parts for StreamID and SubstreamID. * * FIXME: I think in full scan mode sometimes the data in the header is shorter than the length field indicates and * because i just continue reading after parsing the header i read from the wrong location (results into wrong substream id) * * @param pack Byte array which contains the pack data to parse * @param offset Offset to the pack data in the supplied byte array * @param quickScan If true, quick scan mode is used, otherwise full scan mode */ public void parse(byte[] pack, int offset, boolean quickScan) throws PESParserException { // Current absolute position in the byte array int pos = 0; int endOffset = offset + PACK_LENGTH; // Check if pack is 2048 bytes if (endOffset <= pack.length) { // Check pack start_code and pack_identifier if (ByteArray.getVarLong(pack, offset, 4) == 0x000001BA) { // Overread pack header and stuffing bytes pos = offset + 14 + (pack[offset + 13] & 0x07); // Update the internal values this.pack = pack; this.packOffset = offset; this.packetCount = 0; // Parse the whole pack // Check if enough space is left for a packet header, if not, disregard the rest while (pos + 6 <= endOffset) { // Check start_code if (ByteArray.getVarLong(pack, pos, 3) == 0x000001) { if (packets.size() <= packetCount) { packets.add(new PESPacket()); //System.out.println("Packet added, size now: " + packets.size() + ", packetCount: " + packetCount); } //System.out.println("Getting packet: " + packetCount); // Increment packetCount after retrieving the packet because packetCount is one bigger than the index PESPacket currentPacket = packets.get(packetCount); packetCount += 1; currentPacket.packetOffset = pos; //System.out.println("Current Pos: " + pos); // Advance to stream_id pos += 3; currentPacket.stream_id = ByteArray.getUByte(pack, pos); currentPacket.stream_idOffset = pos; // Assume that only SYSTEM_STREAM, PADDING_STREAM, PRIVATE_STREAM_2 dont have an extension if (currentPacket.stream_id == 0xBB || currentPacket.stream_id == 0xBE || currentPacket.stream_id == 0xBF) { currentPacket.hasExtension = false; } else { currentPacket.hasExtension = true; } // Step behind stream_id pos += 1; // Check pes_packet_length currentPacket.length = ByteArray.getUShort(pack, pos); // Step behind the pes_packet_length pos += 2; // Check if packet does not exceed pack size if (pos + currentPacket.length <= endOffset) { //if (currentPacket.length != 0x07EC) System.out.println("PacketLength: " + currentPacket.length); if (currentPacket.hasExtension) { // Check if packet is big enough for the following extension if (pos + 3 <= endOffset) { // Check for quickScan // In quickScan mode the header gets overread, it is jumped directly to the substrem_id position // Its unimportant if there is really a substrem_id, it is later checked if there is a substream_id if (quickScan) { // Jump to pes_header_length pos += 2; int phl = ByteArray.getUByte(pack, pos); // Jump just behind the extension pos += 1; // Check if the packet is big enough for the following header data // It is not checked if there is also room for the substream_id, that is checked after decision if there is a substream_id if (pos + phl <= endOffset) { if (currentPacket.stream_id == 0xFD) { // For VC-1 streams, the stream_id_extension is just the byte before the first payload byte pos += phl - 1; } else { // For every other streams, the substream_id is the first byte of the payload pos += phl; } } else throw new PESParserException("Packet header size exceeds packet size at: 0x" + Integer.toHexString(pos - offset).toUpperCase()); } else { // TODO: Somehow full scan mode looks quite useless now // Parse the extension (without header data) // These values are not really needed to be buffered // All values are not needed to be buffered, they are just stored because they are used for the header data size calculation //currentPacket.psc = (pack[pos] >>> 4) & 0x03; //currentPacket.pp = (pack[pos] >>> 3) & 0x01; //currentPacket.dai = (pack[pos] >>> 2) & 0x01; //currentPacket.c = (pack[pos] >>> 1) & 0x01; //currentPacket.coo = pack[pos] & 0x01; // Step to next byte in extension pos += 1; currentPacket.ptsdts = (pack[pos] >>> 6) & 0x03; currentPacket.escr = (pack[pos] >>> 5) & 0x01; currentPacket.esr = (pack[pos] >>> 4) & 0x01; currentPacket.aci = (pack[pos] >>> 2) & 0x01; currentPacket.pescrc = (pack[pos] >>> 1) & 0x01; currentPacket.pesext1 = pack[pos] & 0x1; // Modify for easy size-calculation if (currentPacket.ptsdts > 0) { currentPacket.ptsdts -= 1; } // Step to next byte in extension pos += 1; int pesEndOffset = pos + 1 + ByteArray.getUByte(pack, pos); // Advance behind the parsed section pos += 1 + (currentPacket.ptsdts * 5) + (currentPacket.escr * 6) + (currentPacket.esr * 3) + (currentPacket.aci * 1) + (currentPacket.pescrc * 2); // Check if packet is big enough for the following header if (pesEndOffset <= endOffset) { // Check if pes_extension_1 is present and parse it if it is if (currentPacket.pesext1 == 1) { // Check if packet is big enough for pes_extension_1 // Use < to because we are reading from pos if (pos < pesEndOffset) { currentPacket.pesext1Offset = pos; // Parse (without header data) currentPacket.pespd = (pack[pos] >>> 7) & 0x01; currentPacket.phf = (pack[pos] >>> 6) & 0x01; currentPacket.ppsc = (pack[pos] >>> 5) & 0x01; currentPacket.pstdb = (pack[pos] >>> 4) & 0x01; currentPacket.pesext2 = pack[pos] & 0x01; // Advance behind the parsed section pos += 1 + (currentPacket.pespd * 16) + (currentPacket.phf * 1) + (currentPacket.ppsc * 2) + (currentPacket.pstdb * 2); // Check if pes_extension_2 is present and parse it if it is if (currentPacket.pesext2 == 1) { // Check if packet is big enough for pes_extension_2 // Use < to because we are reading from pos if (pos < pesEndOffset) { // Parse (without header data) currentPacket.pesext2Offset = pos; currentPacket.pefl = pack[pos] & 0x7F; // Check if packet is big enough for pes_extension_2 data // Use < because pos is still on the length-byte if (pos + currentPacket.pefl < pesEndOffset) { // If this is a VC-1 Packet, then jump to the pes_extension_2 data block, because there is the substream_id // Otherwise jump BEHIND that section and assume the the substream_id (if any) is the following byte if (currentPacket.stream_id == 0xFD) { pos += 1; } else { pos += 1 + currentPacket.pefl; } } else throw new PESParserException("Packet header too small for pes_extension_2 data at: 0x" + Integer.toHexString(pos - offset).toUpperCase()); } else throw new PESParserException("Packet header too small for pes_extension_2 at: 0x" + Integer.toHexString(pos - offset).toUpperCase()); } // End if pes_extension_2 decision } else throw new PESParserException("Packet header too small for pes_extension_1 at: 0x" + Integer.toHexString(pos - offset).toUpperCase()); } // End if pes_extension_1 decision } else throw new PESParserException("Packet header size exceeds packet size at: 0x" + Integer.toHexString(pos - offset).toUpperCase()); } // End if of scan mode decision } else throw new PESParserException("Packet too small for extension at: 0x" + Integer.toHexString(pos - offset).toUpperCase()); } // Set substream_id only for private stream 1, private stream 2, VC-1 video stream if (currentPacket.stream_id == 0xBD || currentPacket.stream_id == 0xBF || currentPacket.stream_id == 0xFD) { // Check if packet is big enough for substream_id // Use < to because we are reading from pos if (pos < endOffset) { // The current byte is the substream_id currentPacket.substream_id = ByteArray.getUByte(pack, pos); currentPacket.substream_idOffset = pos; // Advance to the first payload byte // Note: for private stream 1 and 2 this is actually the second real payload byte pos += 1; } else throw new PESParserException("Packet too small for substream_id at: 0x" + Integer.toHexString(pos - offset).toUpperCase()); } currentPacket.payloadOffset = pos; // If scrambled, jump to end end of the pack, otherwise jump to the start of the next packet (there may be no remaining packets) if (isScrambledImpl(currentPacket)) { pos = endOffset; } else { pos = currentPacket.packetOffset + 6 + currentPacket.length; } //System.out.println("New pos: " + pos); } else throw new PESParserException("Packet size exceeds pack size at: 0x" + Integer.toHexString(pos - offset).toUpperCase()); } else { // Fix for Elephants Dream and maybe others: NAV_PCK has no PCI_PKT, the section is all zero // No Packet Startcode found behind the last packet, check if we are in a 0 padded section for (int searchPointer = pos; searchPointer < endOffset; searchPointer++) { //System.out.println("Offset: " + searchPointer + " is: " + pack[searchPointer]); if (pack[searchPointer] != 0x00) { // Check if there is enough space for a Packet Header, otherwise throw an Exception //System.out.println("Advancement: " + (searchPointer - pos) + ", free space: " + (endOffset - pos)); if (searchPointer - pos >= 2 && endOffset - searchPointer >= 6) { // Jump to positions back to the start of a possible packet pos = searchPointer - 2; break; } else throw new PESParserException("Invalid packet start code at: 0x" + Integer.toHexString(pos - offset).toUpperCase()); } } } } // End while packet searching loop } else throw new PESParserException(String.format("Invalid pack start code and /or pack identifier: 0x%1$08X", ByteArray.getVarLong(pack, offset, 4))); } else throw new PESParserException("Pack data too small"); } public String toString() { StringBuffer idString = new StringBuffer(64); idString.append("PES-Pack :: "); // Go through all packets for (int i = 0; i < packetCount; i++) { PESPacket currentPacket = packets.get(i); idString.append("stream_id: "); idString.append(S_ID_NAMES[currentPacket.stream_id]); // For private stream 1 use the corresponding lut if (currentPacket.stream_id == 0xBD) { idString.append(", substream_id: "); idString.append(PS1_ID_NAMES[currentPacket.substream_id]); } else { // For private stream 2 use the corresponding lut if (currentPacket.stream_id == 0xBF) { idString.append(", substream_id: "); idString.append(PS2_ID_NAMES[currentPacket.substream_id]); } else { // For VC-1 stream the name is hardcoded if (currentPacket.stream_id == 0xFD) { if (currentPacket.substream_id == 0x55) { idString.append("_FOR_MAIN"); } else { if (currentPacket.substream_id == 0x56) { idString.append("_FOR_SUB"); } else { idString.append(", stream_id_extension: 0x"); idString.append(Integer.toHexString(currentPacket.substream_id).toUpperCase()); idString.append(" (INVALID)"); } } } } } // If this is not the last packet add a separator if (i + 1 < packetCount) { idString.append(" :: "); } } return idString.toString(); } /** * Returnes the scramble state of the pack. * This method is safe to call on every type of pack. * * @return If true, this pack is scrambled, otherwise not */ public boolean isScrambled() { return isScrambledImpl(packets.get(0)); } /** * This method actually determines the scramble state. * * @param packet This packet is checked for its scramble state * @return If true, the packet is scrambled, otherwise not */ private boolean isScrambledImpl(PESPacket packet) { // If packet has an extension, use the pes_scrambling_control bit if (packet.hasExtension) { return (((pack[packet.packetOffset + 6] >>> 4) & 0x03) != 0); } else { // Otherwise decide by pack type // Assume every pack unencrypted except a HL_PCK return isHlPckImpl(packet); } } /** * Sets the scrambled state of the pack. * This method is safe to call on a pack which has no scramble bit, nothing gets changed in that case. * * @param scrambled The new scrambled state */ public void setScrambled(boolean scrambled) { PESPacket tmp = packets.get(0); // Change scramble bit only if the first packet has an extension if (tmp.hasExtension) { pack[tmp.packetOffset + 6] = (byte)(pack[tmp.packetOffset + 6] & 0xCF); if (scrambled) { pack[tmp.packetOffset + 6] = (byte)(pack[tmp.packetOffset + 6] | 0x10); } } } /** * Returns true if this is a NV_PCK. * This method is safe to call on every type of pack. * * @return If true, this is a NV_PCK, otherwise not */ public boolean isNavPck() { // An NV_PCK contains a system header packet and a GCI, PCI, and DSI packet, the last each in a private stream 2 // Fix for Elephants Dream and maybe others: the NV_PCK contains only a system header packet and a GCI and DSI packet, the last two each in a private stream 2 if (packetCount == 3) { PESPacket gci = packets.get(1); PESPacket dsi = packets.get(2); if (packets.get(0).stream_id == 0xBB && gci.stream_id == 0xBF && gci.substream_id == 0x04 && dsi.stream_id == 0xBF && dsi.substream_id == 0x01) { return true; } } else { if (packetCount == 4) { PESPacket gci = packets.get(1); PESPacket pci = packets.get(2); PESPacket dsi = packets.get(3); if (packets.get(0).stream_id == 0xBB && gci.stream_id == 0xBF && gci.substream_id == 0x04 && pci.stream_id == 0xBF && pci.substream_id == 0x00 && dsi.stream_id == 0xBF && dsi.substream_id == 0x01) { return true; } } } return false; } /** * Returns true if this is an ADV_PCK. * This method is safe to call on every type of pack. * * @return If true, this is a ADV_PCK, otherwise not */ public boolean isAdvPck() { PESPacket tmp = packets.get(0); // An ADV_PCK is in a private stream 2 with substream_id 0x80 if (tmp.stream_id == 0xBF && tmp.substream_id == 0x80) { return true; } else { return false; } } /** * Returns true if this is a HL_PCK. * This method is safe to call on every type of pack. * * @return If true, this is a HL_PCK, otherwise not */ public boolean isHlPck() { return isHlPckImpl(packets.get(0)); } /** * This method actually determines if the given packet is a HL_PCK. * * @param packet The packet to check if it is a HL_PCK * @return If true, the packet is a HL_PCK, otherwise not */ private boolean isHlPckImpl(PESPacket packet) { // A HL_PCK is in a private stream 2 with substream_id 0x08 if (packet.stream_id == 0xBF && packet.substream_id == 0x08) { return true; } else { return false; } } /** * Returns the number of packets in this pack. * * @return The number of packets in this pack */ public int packetCount() { return packetCount; } /** * Returns the requested packet, counting starts at 0. * * @param index The packet to return * @return The packet, null if the packet index is invalid */ public PESPacket getPacket(int index) { if (index >= 0 && index < packetCount) { return packets.get(index); } else return null; } /** * Pads the pack starting after the given packet index, counting starts at 0. To pad the whole pack use index -1. * Padding after the last packet is allowed, but if the pack structure is correct it fills already the complete pack length. * * The physical pack structure will be correct after this method call, but the information about the pack in this class will be wrong, * the pack MUST be parsed again if methods of this class are used after this method call. * * @param index The index of the packet after which the padding should start * @return True, if the pack was padded, false if not (wrong packet index) */ public boolean pad(int index) { // Check if the index is valid if (index >= -1 && index < packetCount) { int paddingOffset = 0; if (index == -1) { // Pad the whole packet, start AT the offset of the first packet // TODO: What if the first packet does not start behind the pack header? Is this allowed? paddingOffset = packets.get(0).packetOffset; } else { // Pad after the given packet, start BEHIND the offset of that packet PESPacket padPacket = packets.get(0); paddingOffset = padPacket.packetOffset + 6 + padPacket.length; } int paddingEndOffset = packOffset + PESPack.PACK_LENGTH; int padAmount = paddingEndOffset - paddingOffset; if (padAmount < 6) { // Remaining space to small for a padding packet, pad with zero Arrays.fill(pack, paddingOffset, paddingEndOffset, (byte)0x00); } else { // Enough space remaining for a padding packet, insert it ByteArray.setInt(pack, paddingOffset, 0x000001BE); ByteArray.setShort(pack, paddingOffset + 4, padAmount - 6); Arrays.fill(pack, paddingOffset + 6, paddingEndOffset, (byte)0xFF); } return true; } else return false; } /** * This class represents a PES Packet and provides methods to access and modify some of its attributes. This class works like a mask, * no data is copied, it works directly on the underlying byte array. However, some values are buffered for faster access. * * The set methods usually only set that specific attribute they are intended for, they do set other dependend attributes if not noted explicit, e.g. * setting the StreamID of a packet that has no Header Extension to a StreamID that has a Header Extension does not add a Header Extension. * Therefore the methods which check for a specific attribute, e.g. the Header Extension, may return a value not expected for the present Packet type after * some attributes of it have been changed. * * TODO: Provide methods to access the Header Extension fields? * * @author KenD00 */ public final class PESPacket { /** * Start offset of this packet in the underlying pack byte array */ private int packetOffset = 0; /** * Offset of the stream_id */ private int stream_idOffset = 0; /** * Offset of the substream_id */ private int substream_idOffset = 0; /** * Stream_id of the packet */ private int stream_id = 0; /** * Substream_id of the packet */ private int substream_id = -1; /** * Length of this packet */ private int length = 0; /** * Offset to the payload of the packet. This may not be the start of the PES Packet payload * (e.g. for Private streams this is the second byte of the PES Packet Payload) */ private int payloadOffset = 0; /** * If true, this packet has a header extension, otherwise not */ private boolean hasExtension = false; // The values of the header extension (without header data for that section) //private int psc = 0; //private int pp = 0; //private int dai = 0; //private int c = 0; //private int coo = 0; private int ptsdts = 0; private int escr = 0; private int esr = 0; private int aci = 0; private int pescrc = 0; private int pesext1 = 0; /** * Offset of the pes_extension_1 header */ private int pesext1Offset = 0; // The values of the pes_extension_1 (without header data for that section) private int pespd = 0; private int phf = 0; private int ppsc = 0; private int pstdb = 0; private int pesext2 = 0; /** * Offset of the pes_extension_2 header */ private int pesext2Offset = 0; // The values of the pes_extension_2 (wihtout header data for that section) private int pefl = 0; /** * Private constructor to avoid instantiation from outside. */ private PESPacket() { // Nothing } /** * Returns the StreamID of the packet. * * @return The StreamID of the packet */ public int getStreamId() { return stream_id; } /** * Sets the StreamID of the packet. * * @param stream_id The new StreamID */ public void setStreamId(int stream_id) { this.stream_id = stream_id; ByteArray.setByte(pack, stream_idOffset, stream_id); } /** * Returns true if the packet has a header extension. * * @return If true, the packet has a header extension, otherwise not */ public boolean hasExtension() { return hasExtension; } /** * Returns true if the packet has a SubstreamID. * * @return If true, the packet has a SubstreamID, otherwise not */ public boolean hasSubstreamId() { return (substream_idOffset != 0); } /** * Returns the SubstreamID of the packet. * If this packet does not have a SubstreamID -1 is returned * * @return The SubstreamID of the packet, -1 if this packet does not have a SubstreamID */ public int getSubstreamId() { return substream_id; } /** * Sets the SubstreamID of the packet. * If this packet does not have a SubstreamID nothing is changed. * * @param substream_id The new SubstreamID * @return True, if the SubstreamID was changed, false otherwise */ public boolean setSubstreamId(int substream_id) { if (substream_idOffset != 0) { this.substream_id = substream_id; ByteArray.setByte(pack, substream_idOffset, substream_id); return true; } else return false; } /** * Returns the length of the packet. The length count starts just behind the PES Packet Header, it includes a possible present Header Extension! * @return Length of the packet excluding the PES Packet Header */ public int length() { return length; } /** * Offset to the payload. In case of Private Streams this is actually one byte behind the PES Packet Payload start because the first byte is the SubstreamID. * * @return Offset from the start of this packet to the start of the payload */ public int payloadOffset() { return payloadOffset; } /** * The length of the payload. * * @return The length of the payload */ public int getPayloadLength() { return packetOffset + 6 + length - payloadOffset; } /** * Sets the length of the payload of this packet. * The length must be grather or equal than 0 and the PES Pack must not exceed the PES Pack length with this new payload length. * These constraints are checked, the length is not modified if they are not met. This method destroys the structure of the PES Pack if the * new payload length does not extend the packet to the end of the pack, this must be fixed externally! * * @param payloadLength The new payload length * @return True, if the payload length was changed, false otherwise */ public boolean setPayloadLength(int payloadLength) { // Check if the new payloadLength is positive and the packet would not exceed the Pack if (payloadLength >= 0 && payloadOffset + payloadLength <= packOffset + PESPack.PACK_LENGTH) { // payloadOffset must not start just behind the Pack Header, so determine the difference and add it to the length length = payloadLength + payloadOffset - (packetOffset + 6); ByteArray.setShort(pack, packetOffset + 4, length); return true; } else return false; } } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/util/TSParserException.java�������������������������������������������0000664�0001750�0001750�00000002356�11211610742�023672� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.util; /** * Exception thrown when a parser error occurs during parsing a MPEG-2 Transport Stream. * * @author KenD00 */ public class TSParserException extends Exception { public TSParserException() { super(); } public TSParserException(String message) { super(message); } public TSParserException(String message, Throwable cause) { super(message, cause); } public TSParserException(Throwable cause) { super(cause); } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/util/ByteSource.java��������������������������������������������������0000664�0001750�0001750�00000013151�11211610632�022365� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.util; import java.io.File; import java.io.IOException; /** * A ByteSource is an abstraction layer to allow easy access to data from various sources and hide the required I/O operations. * All operations should be buffered to allow performant reads and writes of small amounts of data. * * This interface is intended for streaming access to the data (although it is highly recommended that an implementation uses faster * methods to physically access the data), thats why only relative read and write operations are supported. * * Seek operations are also supported but should be used with care, it may be possible that these operations are very costly. * * Windowing allows to reduce the view on the data to only a portion of it. * * * It is up to the implementation to support read only or read and write access. * * Implementations need not to be thread safe. * * General usage guidelines are: * - Never remove a window that you have not set yourself * - Never close a ByteSource that you have not created yourself * * @author KenD00 */ public interface ByteSource { /** * R_MODE, only read access allowed */ public final static int R_MODE = 0; /** * W_MODE, only write access allowed */ public final static int W_MODE = 1; /** * RW_MODE, read and write access allowed */ public final static int RW_MODE = 2; /** * Relative read operation. * Reads length bytes from the current position and stores them in dst starting at offset. * It is guaranteed that length bytes are read except EOF is reached. * If an I/O Error occurs, an exception is thrown. * * @param dst Destination to store the read bytes * @param offset Offset in dst to store the bytes * @param length Number of bytes to read * @return The number of read bytes, -1 if EOF is reached * @throws IOException If an I/O error occurred */ public int read(byte[] dst, int offset, int length) throws IOException; /** * Relative write operation. * Writes length bytes to the current position from src starting at offset. * It is guaranteed that length bytes get written, except EOF is reached and it is not allowed to grow this ByteSource. * If an I/O Error occurs, an exception is thrown. * * @param src Source to read from * @param offset Offset in src to start reading * @param length Number of bytes to write * @return The number of written bytes, -1 if EOF is reached and ByteSource cannot be grown * @throws IOException If an I/O error occurred */ public int write(byte[] src, int offset, int length) throws IOException; /** * Returns the current position. * * @return The current Position * @throws IOException If an I/O error occurred */ public long getPosition() throws IOException; /** * Sets the position to position. * * @param position The new position to set * @throws IOException If an I/O error occurred */ public void setPosition(long position) throws IOException; /** * Adds a Window starting at the current position with the given size. * After the window has been set, the current position will be position 0 and the ByteSource will be size big (at most, may be smaller if it is physically smaller * than the window). Multiple windows can be set, but a new window can only be smaller than the current window. * * @param size The size in bytes of the window * @throws IOException An I/O error occurred or the new window exceeds the current window bounds */ public void addWindow(long size) throws IOException; /** * Removes the last set window. * * @throws IOException An I/O error occurred or there are no more windows set */ public void removeWindow() throws IOException; /** * Returns the current number of set windows. * * @return The current number of set windows */ public int windowCount(); /** * Returns the size of this ByteSource. * If a window is set, returns the size of the last added window. * * @return The size in bytes of this ByteSource * @throws IOException If an I/O error occurred */ public long size() throws IOException ; /** * The mode of this ByteSource. * It may support read only, write only or read and write. * * @return The mode of this ByteSource */ public int getMode(); /** * Returns the file object of the underlying physical data source. * * @return The file object of the underlying physical data source. */ public File getFile(); /** * Closes the ByteSource, no further reads and writes are possible. * * @throws IOException If an I/O error occurred */ public void close() throws IOException; } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/util/MessagePrinter.java����������������������������������������������0000664�0001750�0001750�00000002556�11211610654�023244� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.util; /** * Interface to be used by classes which want to output text messages. * * By giving a specific implementation of this interface to a class its output can be easily directed to a specific destination. * * @author KenD00 */ public interface MessagePrinter { /** * Prints the message. * * @param msg The message to print */ public void print(String msg); /** * Prints the message followed by a newline. * * @param msg The message to print */ public void println(String msg); /** * Prints a newline. */ public void println(); } ��������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/util/PackSource.java��������������������������������������������������0000664�0001750�0001750�00000045113�11211610670�022345� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.util; import java.io.File; import java.io.IOException; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; /** * This is a wrapper class that presents a packetized stream as a contigous stream. * It uses a previously created list of records to find the packets and extract the payload data from the given ByteSource. * The given Collection of PackRecords is not modified and must contain at least one element, it must not * be modified after given to this class. * * This class is not thread safe, but it is safe that the underlying ByteSource is shared among other ByteSources, the read and write methods * check the physical position before operation and correct it if necessary. * * TODO: Check if PackSource is closed? * * @author KenD00 */ public final class PackSource implements ByteSource { /** * The underlying ByteSource used for the data access */ private ByteSource bs = null; /** * The mode this PackSource uses */ private int mode = 0; /** * The File this PackSource returns */ private File file = null; /** * The used PackReckords to find the data segements */ private Collection<PackRecord> packRecords = null; /** * Contains the maximum data position that got written */ private long maxWritten = 0; /** * Traverses through the PackRecords, is always at a position so that a call to next() returns follwoing PackRecord of the current active one */ private Iterator<PackRecord> recordIt = null; /** * The current active PackRecord */ private PackRecord currentRecord = null; /** * The current position in the payload of the current PackRecord, 0 is the start of the payload section */ private int payloadPosition = 0; /** * The maximum position in the payload of the current Packrecord (actually one greater than the maximum position that may be accessed) * This may be the payload size of the current PackRecord or smaller if the window limit lies in the current PackRecord */ private int recordLimit = 0; /** * If true, the window lies in the current PackRecord, otherwise not */ private boolean recordLimited = false; /** * The list of all windows */ private LinkedList<Window> windows = new LinkedList<Window>(); /** * The current active window */ private Window currentWindow = null; /** * The current position in the underlying ByteSource */ private long physicalPosition = 0; /** * Creates a new PackSource. * * @param bs The underlying ByteSource to access * @param mode The mode this PackSource should use * @param file The File this PackSource should return * @param packRecords The PackRecords to use * @throws IOException An error accessing bs occured */ public PackSource(ByteSource bs, int mode, File file, Collection<PackRecord> packRecords) throws IOException { this.bs = bs; this.mode = mode; this.file = file; this.packRecords = packRecords; // Traverse the complete packRecords to the last one to set the window bounds recordIt = packRecords.iterator(); // There must be always at least one entry in the packRecords collection while (recordIt.hasNext()) { currentRecord = recordIt.next(); } currentWindow = new Window(0, currentRecord.dataOffset + (long)currentRecord.payloadSize); windows.add(currentWindow); // Re-initialize the iterator and currentRecord recordIt = packRecords.iterator(); currentRecord = recordIt.next(); // Now set the limit recordLimit = currentRecord.payloadSize; // Set the physical postion physicalPosition = currentRecord.packetOffset + (long)currentRecord.payloadOffset; bs.setPosition(physicalPosition); } /* (non-Javadoc) * @see dumphd.util.ByteSource#read(byte[], int, int) */ public int read(byte[] dst, int offset, int length) throws IOException { if (mode == ByteSource.R_MODE || mode == ByteSource.RW_MODE) { return readWrite(dst, offset, length, false); } else throw new IOException("PackSource not opened for reading"); } /* (non-Javadoc) * @see dumphd.util.ByteSource#write(byte[], int, int) */ public int write(byte[] src, int offset, int length) throws IOException { if (mode == ByteSource.W_MODE || mode == ByteSource.RW_MODE) { return readWrite(src, offset, length, true); } else throw new IOException("PackSource not opened for writing"); } /* (non-Javadoc) * @see dumphd.util.ByteSource#getPosition() */ public long getPosition() throws IOException { return currentRecord.dataOffset + (long)payloadPosition - currentWindow.start; } /* (non-Javadoc) * @see dumphd.util.ByteSource#setPosition(long) */ public void setPosition(long position) throws IOException { // Translate from window coordinates to logical coordinates long tPosition = currentWindow.start + position; // Check if position lies into current window bounds //System.out.println(String.format("Seek, original position: 0x%1$010X, windowed position: 0x%2$010X, currentRecord.dataOffset: 0x%3$010X, currentRecord.dataEnd: 0x%4$010X, PackOffset: 0x%5$010X", position, tPosition, currentRecord.dataOffset, (currentRecord.dataOffset + (long)currentRecord.payloadSize), currentRecord.packetOffset)); if (tPosition >= currentWindow.start && tPosition <= currentWindow.end) { // Check if new position is inside of the current packet if (tPosition >= currentRecord.dataOffset && tPosition <= currentRecord.dataOffset + (long)currentRecord.payloadSize) { // New position inside current packet, seek logically to the new position payloadPosition = (int)(tPosition - currentRecord.dataOffset); //System.out.println("Logical seek, payloadPosition: " + payloadPosition); } else { // New position outside current packet, search the corresponding packet recordIt = packRecords.iterator(); currentRecord = recordIt.next(); while (tPosition > currentRecord.dataOffset + (long)currentRecord.payloadSize) { if (recordIt.hasNext()) { currentRecord = recordIt.next(); } else throw new IOException("Unexpected EOF"); } // Seek logically to new position, update the limit payloadPosition = (int)(tPosition - currentRecord.dataOffset); updateLimit(); //System.out.println("Physical seek, tPosition: " + tPosition + ", dataOffset: " + currentRecord.dataOffset + ", payloadPosition: " + payloadPosition); } // Physically set the position physicalPosition = currentRecord.packetOffset + (long)currentRecord.payloadOffset + (long)payloadPosition; //System.out.println(String.format("Seeking to physical position: 0x%1$010X", physicalPosition)); bs.setPosition(physicalPosition); //System.out.println(String.format("Physical position is: 0x%1$010X", bs.getPosition())); } else throw new IOException("Seek exceeds window limits"); } /* (non-Javadoc) * @see dumphd.util.ByteSource#windowCount() */ public int windowCount() { return (windows.size() - 1); } /* (non-Javadoc) * @see dumphd.util.ByteSource#addWindow(long) */ public void addWindow(long size) throws IOException { // Calculate current locigal position long start = currentRecord.dataOffset + (long)payloadPosition; long end = start + size; // Check if new window is inside the current window if (end <= currentWindow.end) { currentWindow = new Window(start, end); windows.add(currentWindow); updateLimit(); } else throw new IOException("New window exceeds current window limits"); } /* (non-Javadoc) * @see dumphd.util.ByteSource#removeWindow() */ public void removeWindow() throws IOException { if (windows.size() > 1) { windows.removeLast(); currentWindow = windows.getLast(); updateLimit(); } else throw new IOException("No window set"); } /* (non-Javadoc) * @see dumphd.util.ByteSource#size() */ public long size() throws IOException { return currentWindow.end - currentWindow.start; } /* (non-Javadoc) * @see dumphd.util.ByteSource#getMode() */ public int getMode() { return mode; } /* (non-Javadoc) * @see dumphd.util.ByteSource#getFile() */ public File getFile() { return file; } /* (non-Javadoc) * @see dumphd.util.ByteSource#close() */ public void close() throws IOException { // Nothing to close, release only all resources, this prevents usage of this object after it has been closed bs = null; file = null; currentRecord = null; recordIt = null; packRecords = null; currentWindow = null; windows = null; } /** * Returns the maximum position plus 1 this PackSource has written to. * * @return The maximum position plus 1 this PackSource has written to, 0 if it has written nothing */ public long maxWrittenPosition() { return maxWritten; } /** * Helper method that does actually the reading/writing. * * @param data The source/destination to store/retrieve the bytes * @param offset Offset into data * @param length Number of bytes to read/write * @param write If true, writing is used, reading otherwise * @return The number of read/written bytes, -1 if EOF is reached * @throws IOException If an I/O error occurred */ private int readWrite(byte[] data, int offset, int length, boolean write) throws IOException { // The number of read/written bytes int transferred = 0; // Check if physical position is there where it should be, it could get changed if the reader and writer use the same ByteSource if (bs.getPosition() != physicalPosition) { //System.out.println("Resetting physical position: " + physicalPosition); bs.setPosition(physicalPosition); } while (true) { // Determine the remaining bytes in the current record int remaining = recordLimit - payloadPosition; //System.out.println("Remaining: " + remaining); // Check if the current record has enough data remaining if (length <= remaining) { // There is enough data in the current record if (write) { // Write mode if (bs.write(data, offset, length) == length) { // Update payloadPosition here to calculate the new maxWritten position payloadPosition += length; long newMaxWritten = currentRecord.dataOffset + (long)payloadPosition; if (newMaxWritten > maxWritten) { maxWritten = newMaxWritten; } } else throw new IOException("Unexpected EOF"); } else { // Read mode if (bs.read(data, offset, length) == length) { payloadPosition += length; } else throw new IOException("Unexpected EOF"); } // The physicalPositon must be updated too! physicalPosition += length; transferred += length; //System.out.println("Completely transferred, payloadPosition: " + payloadPosition + ", physicalPosition: " + physicalPosition + ", transferred: " + transferred); break; } else { // There is not enough data/free space remaining if (remaining > 0) { // There is some data/free space remaining, read/write it if (write) { // Write mode if (bs.write(data, offset, remaining) == remaining) { // The position must be updated too, because we may hit the limit and then there is no new record retrieved (and the payloadPosition reset) payloadPosition += remaining; // Update the maximum written logical address long newMaxWritten = currentRecord.dataOffset + (long)payloadPosition; if (newMaxWritten > maxWritten) { maxWritten = newMaxWritten; } } else throw new IOException("Unexpected EOF"); } else { // Read mode if (bs.read(data, offset, remaining) == remaining) { // The position must be updated too, because we may hit the limit and then there is no new record retrieved (and the payloadPosition reset) payloadPosition += remaining; } else throw new IOException("Unexpected EOF"); } // The physicalPositon must be updated too! physicalPosition += remaining; offset += remaining; length -= remaining; transferred += remaining; //System.out.println("Partially transferred, payloadPosition: " + payloadPosition + ", physicalPosition: " + physicalPosition + ", offset: " + offset + ", length: " + length + ", transferred: " + transferred); } // If the record is limited, we hit the limit if (recordLimited) { // Limit reached, return EOF if nothing was read so far //System.out.println("Limit reached, transferred so far: " + transferred); if (transferred == 0) { transferred = -1; } break; } else { // We are not at the limit, read the next record if (recordIt.hasNext()) { // Retrive next record, reset the payloadPositon and update the limit currentRecord = recordIt.next(); payloadPosition = 0; updateLimit(); // Set the physical position physicalPosition = currentRecord.packetOffset + (long)currentRecord.payloadOffset; bs.setPosition(physicalPosition); } else throw new IOException("Unexpected EOF"); } } } return transferred; } /** * Checks if the window limit lies in the current record, limits the record accordingly. */ private void updateLimit() { long checkLimit = currentWindow.end - currentRecord.dataOffset; if (checkLimit <= (long)currentRecord.payloadSize) { recordLimited = true; recordLimit = (int)checkLimit; } else { recordLimited = false; recordLimit = currentRecord.payloadSize; } } /** * Stores information about a packet. * The payload must only include the bytes that belong to the desired data stream, it must not contain any headers that may be present * in the physical payload section of the PES Packet! * * @author KenD00 */ public final static class PackRecord { /** * The first byte of the paylod of this PackRecord corresponds to this byte of the data */ public long dataOffset = 0; /** * The physical offset where the packet starts * This should point to the start of the section that is required to be modified to fix up the packet structure * (e.g. if the new payload is smaller than the present one) */ public long packetOffset = 0; /** * Relative offset from the packet start to the start of the payload */ public int payloadOffset = 0; /** * Size of the payload */ public int payloadSize = 0; /** * Creates a PackRecord where all attributes are zero. */ public PackRecord() { // Nothing } /** * Creates a PackRecord with the given values. * * @param dataOffset The dataOffset * @param packetOffset The packetOffset * @param payloadOffset The payloadOffset * @param payloadSize The payloadSize */ public PackRecord(long dataOffset, long packetOffset, int payloadOffset, int payloadSize) { this.dataOffset = dataOffset; this.packetOffset = packetOffset; this.payloadOffset = payloadOffset; this.payloadSize = payloadSize; } public String toString() { return String.format("PackRecord :: dataOffset: 0x%1$010X, packetOffset: 0x%2$010X, payloadOffset: 0x%3$04X, payloadSize: 0x%4$04X", dataOffset, packetOffset, payloadOffset, payloadSize); } } /** * Stores the logical start and end coordinates of a window. * * @author KenD00 */ private final static class Window { public long start = 0; public long end = Long.MAX_VALUE; /** * Creates a window with the start coordinate 0 and the end coordinate Long.MAX_VALUE. */ public Window() { // Nothing } /** * Creates a window with the given coordinates. * * @param start The start coordinate * @param end The end coordinate */ public Window(long start, long end) { this.start = start; this.end = end; } public String toString() { StringBuffer sb = new StringBuffer(32); sb.append("Window :: start: "); sb.append(start); sb.append(", end: "); sb.append(end); return sb.toString(); } } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/util/SimpleFilenameFilter.java����������������������������������������0000664�0001750�0001750�00000004417�11211610720�024344� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.util; import java.io.File; import java.io.FilenameFilter; import java.util.regex.Pattern; import java.util.regex.Matcher; /** * Simple FilenameFilter, checks the given filename, the directory is ignored, against its pattern. * The output can be inverted. Directories are always accepted, only files will be filtered. * * @author KenD00 */ public class SimpleFilenameFilter implements FilenameFilter { private Pattern filenamePattern = null; private boolean invert = false; /** * Constructs a SimpleFilenameFilter * * @param pattern The regular expression used to check the given filenames * @param caseInsensitive If true, the check is performed case insensitive, otherwise not * @param invert If invert is true, this object accepts the given filenames if they don't match the pattern */ public SimpleFilenameFilter(String pattern, boolean caseInsensitive, boolean invert) { this.invert = invert; if (caseInsensitive) { filenamePattern = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); } else { filenamePattern = Pattern.compile(pattern); } } /* (non-Javadoc) * @see java.io.FilenameFilter#accept(java.io.File, java.lang.String) */ public boolean accept(File dir, String name) { if (new File(dir, name).isDirectory()) { return true; } else { Matcher matcher = filenamePattern.matcher(name); return (matcher.matches() ^ invert); } } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/util/Utils.java�������������������������������������������������������0000664�0001750�0001750�00000023204�11211610750�021402� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.util; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.util.Collection; import java.util.LinkedList; import java.util.zip.CRC32; /** * Utility class, contains static methods for various uses. * * @author KenD00 */ public final class Utils { /** * This buffer can be used by classes for temporary storage during operations. * It MUST NOT be used to store values for a later retrieval, this buffer can be used by everyone. * It MUST be ensured from outside that this buffer is used by only ONE entity at the same time. * The size of the buffer modulo 16 MUST be 0, otherwise the AACSDecrypter will fail */ public static final byte[] buffer = new byte[4 * 1024 * 1024]; /** * Lookup table to convert numbers 0 to 15 to hex digits */ public static final char[] hexLut = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; /** * The used MessagePrinter, by default output to console */ private static MessagePrinter out = new PrintStreamPrinter(System.out); /** * Used for CRC32 calculation during file copy */ private static CRC32 crc32Calc = new CRC32(); /** * @return The currently used MessagePrinter */ public static MessagePrinter getMessagePrinter() { return out; } /** * Sets a new MessagePrinter. By default this class outputs text to the console, set a new MessagePrinter to redirect output. * * @param mp The new MessagePrinter, if null an IllegalArgumentException is thrown */ public static void setMessagePrinter(MessagePrinter mp) { if (mp != null) { out = mp; } else { throw new IllegalArgumentException("The MessagePrinter must be non null"); } } /** * Converts the given byte values byte by byte to a hex string starting at offset using length bytes. * * @param src The byte array containing the values to get converted * @param offset Offset to start converting * @param length Number of bytes to convert * @return The contents of the byte array as hex string */ public static String toHexString(byte[] src, int offset, int length) { StringBuffer sb = new StringBuffer(20); int endOffset = offset + length; for (int i = offset; i < endOffset; i++) { sb.append(hexLut[(src[i] >>> 4) & 0xF]); sb.append(hexLut[src[i] & 0xF]); } return sb.toString(); } /** * Decodes the given hex string literally to bytes, that is using two digits and interpret them as byte value. * Half the length of the string number of bytes get produced. * * @param src The string to convert * @param dst The byte array to store the values in * @param offset Offset to start putting the values into the byte array */ public static void decodeHexString(String src, byte[] dst, int offset) { int halfLength = src.length() / 2; int digit0 = 0; int digit1 = 0; for (int i = 0; i < halfLength; i++) { int pos = i * 2; digit1 = Character.digit(src.charAt(pos), 16); digit0 = Character.digit(src.charAt(++pos), 16); dst[offset + i] = (byte)((digit1 << 4) + digit0); } } /** * Searches the given directory for files passing the given FilenameFilter and stores its names in the given collection. * The filenames will be stored relative to the given baseDir. * * If src contains the file BAR.JAVA and baseDir is FOO then the resulting filename in dst is FOO\BAR.JAVA * (assuming \ is the separator char). * * @param src Source to scan for files, must be a directory, that is not verified! * @param baseDir Directory the filenames will be relative to * @param dst Destination collection where the found filenames get stored * @param recursive If true, subdirectories are processed too * @param fnf FilenameFilter files have to pass * @param silent If true, no output messages will be sent to the registered MessagePrinter */ public static void scanForFilenames(File src, String baseDir, Collection<String> dst, boolean recursive, FilenameFilter fnf, boolean silent) { class DirElement { public String baseDir = null; public File dir = null; public DirElement(String baseDir, File dir) { this.baseDir = baseDir; this.dir = dir; } } LinkedList<DirElement> dirs = new LinkedList<DirElement>(); dirs.add(new DirElement(baseDir, src)); while (!dirs.isEmpty()) { DirElement dir = dirs.remove(); if (!silent) { out.println("Searching " + dir.dir + " for files..."); } File[] files = dir.dir.listFiles(fnf); if (files != null) { for (int i = 0; i < files.length; i++) { if (files[i].isDirectory()) { if (recursive) { dirs.add(new DirElement(dir.baseDir + File.separator + files[i].getName(), files[i])); } } else { if (!silent) { out.println(files[i].toString()); } //out.println("Debug: baseDir = " + dir.baseDir + ", filename = " + files[i].getName()); dst.add(dir.baseDir + File.separator + files[i].getName()); } } } } } /** * Copies length bytes from in to out starting at their current positions. * Returns the number of written bytes and the crc32 of the copied data. * If less than requested was written then EOF was reached either during reading, during writing or during both. * * @param in ByteSource to copy from, starts at its current position * @param out ByteSource to copy to, starts at its current position * @param length Number of bytes to copy * @return Number of written bytes and their crc32 value * @throws IOException If an I/O error occurs */ public static CopyResult copyBs(ByteSource in, ByteSource out, long length) throws IOException { int readResult = 0; int writeResult = 0; int writeAmount = 0; CopyResult returnValue = new CopyResult(); crc32Calc.reset(); while (length > 0) { if (length <= (long)buffer.length) { writeAmount = (int)length; } else { writeAmount = buffer.length; } // Complete requested data to copy fits into the buffer readResult = in.read(buffer, 0, writeAmount); // Check if EOF was at reading start, abort at once if that is the case if (readResult == -1) { break; } else { // If less than requested was copied, update the value of copied bytes to write the correct amount if (readResult != writeAmount) { writeAmount = readResult; // Set remaining length to current read data so that the loop aborts after this run length = (long)readResult; } } writeResult = out.write(buffer, 0, writeAmount); // Check if EOF was at writing start, abort at once if that is the case if (writeResult == -1) { break; } else { // If less than requested was written, update the value of written bytes to return the correct result if (writeResult != writeAmount) { writeAmount = writeResult; // Set remaining length to current written data so that the loop aborts after this run length = (long)writeResult; } } crc32Calc.update(buffer, 0, writeAmount); returnValue.size += (long)writeAmount; length -= (long)writeAmount; } returnValue.crc32 = crc32Calc.getValue(); return returnValue; } /** * Returns the Number of a Blu-Ray Clip file. * * @param filename The filename of the Clip. Has to be only the name without path, e.g. 00000.m2ts * @return The Number of the Clip or a negative value if an error occurred (-1: Filename doesn't end with m2ts, -2: Filename is not a number) */ static public int getClipNumber(String filename) { // The number of the clip int clipNr = 0; // Offset of the extension dot int extOffset = filename.lastIndexOf('.'); try { clipNr = Integer.parseInt(filename.substring(0, extOffset)); } catch (IndexOutOfBoundsException ei) { // This shouldn't happen because the filename should end with .M2TS clipNr = -1; } catch (NumberFormatException en) { // This shouldn't happen because all streamfiles should be numbers (BD Spec) clipNr = 2; } return clipNr; } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/util/PrintStreamPrinter.java������������������������������������������0000664�0001750�0001750�00000003355�11211610712�024121� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.util; import java.io.PrintStream; /** * Implementation of MessagePrinter which prints all messages to the given PrintStream. * * @author KenD00 */ public final class PrintStreamPrinter implements MessagePrinter { /** * The PrintStream used for output */ private PrintStream out = null; /** * Creates a new PrintStreamPrinter which uses the given PrintStream for output. * * @param stream The PrintStream used for output */ public PrintStreamPrinter(PrintStream stream) { this.out = stream; } /* (non-Javadoc) * @see core.MessagePrinter#print(java.lang.String) */ public void print(String msg) { out.print(msg); } /* (non-Javadoc) * @see core.MessagePrinter#println(java.lang.String) */ public void println(String msg) { out.println(msg); } /* (non-Javadoc) * @see core.MessagePrinter#println() */ public void println() { out.println(); } } �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/util/PackScanner.java�������������������������������������������������0000664�0001750�0001750�00000032455�11211610662�022504� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.util; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintWriter; import java.util.Date; import java.util.Iterator; import java.util.Map; import java.util.TreeMap; /** * Scans EVO files, identifies pack types and counts them. * * @author KenD00 */ public class PackScanner { public final static int SCAN_MODE = 0; public final static int EDIT_MODE = 1; private MessagePrinter out = null; private int mode = SCAN_MODE; private boolean quickScan = false; private boolean blanking = false; private boolean reverting = false; private boolean navFix = false; private boolean zeroCPI = false; private boolean logging = false; private PrintWriter logger = null; /** * Creates a new PackScanner in SCAN_MODE with all options disabled using the given MessagePrinter for textual output. */ public PackScanner(MessagePrinter mp) { this.out = mp; } /** * Scans the given file and patches / reverts it, depending of current mode. * * @param inFileName Input filename * @return True, if scanning was successful (or unexpected EOF, programming mistake, who cares?), false if an error occurred */ public boolean scan(String inFileName) { File inFileAbsolute = new File(inFileName).getAbsoluteFile(); FileSource inFile = null; out.println("File : " + inFileAbsolute.getPath()); try { if (mode == EDIT_MODE) { out.println("Mode : EDIT_MODE"); inFile = new FileSource(inFileAbsolute, FileSource.RW_MODE); } else { out.println("Mode : SCAN_MODE"); inFile = new FileSource(inFileAbsolute, FileSource.R_MODE); } } catch (FileNotFoundException e) { out.println("Could not open input file: " + e.getMessage()); //e.printStackTrace(); return false; } boolean noOptions = true; out.println("Options:"); if (quickScan) { noOptions = false; out.println(" QuickScan"); } if (blanking) { noOptions = false; out.println(" Blanking"); } if (reverting) { noOptions = false; out.println(" Reverting"); } if (navFix) { noOptions = false; out.println(" Nav chain bugfix"); } if (zeroCPI) { noOptions = false; out.println(" Zero CPI field"); } if (logging) { noOptions = false; out.println(" Logging"); try { logger = new PrintWriter(new BufferedWriter(new FileWriter(new File(inFileName).getName() + ".log"))); } catch (IOException e) { out.println("Could not open log file: " + e.getMessage()); //e.printStackTrace(); try { inFile.close(); } catch (IOException e2) { out.println("Error closing input file: " + e2.getMessage()); //e2.printStackTrace(); } return false; } } if (noOptions) { out.println(" NONE"); } out.println(); long offset = 0; byte[] packData = new byte[PESPack.PACK_LENGTH]; PESPack pack = new PESPack(); boolean returnResult = true; String msg = null; boolean writeBackPack = false; TreeMap<String, Counter> packTable = new TreeMap<String, Counter>(); out.println("Starting scan: " + new Date()); try { while (inFile.read(packData, 0, packData.length) != -1) { try { //out.println("Pack retrieved"); pack.parse(packData, 0, quickScan); // Check for NAV_PCK and apply operations if necessary if (pack.isNavPck()) { if (navFix) { //out.println("NavFixxing"); packData[0x516] = 127; writeBackPack = true; } if (zeroCPI) { //out.println("ZeroCPIing"); for (int i = 0x3C; i < 0x4C; i++) { packData[i] = 0; } writeBackPack = true; } } else { // Apply blanking or reverting if necessary PESPack.PESPacket firstPacket = pack.getPacket(0); if (blanking) { // TODO: change for parameterized blanking? // Blanks the first DD+ Audio Stream for Sub and the VC-1 Stream for Sub if (firstPacket.getStreamId() == 0xBD && firstPacket.getSubstreamId() == 0xC8) { firstPacket.setStreamId(0x00); writeBackPack = true; } else { if (firstPacket.getStreamId() == 0xFD && firstPacket.getSubstreamId() == 0x56) { firstPacket.setStreamId(0xFF); writeBackPack = true; } } } if (reverting) { // TODO: change for parameterized reverting? if (firstPacket.getStreamId() == 0x00) { firstPacket.setStreamId(0xBD); writeBackPack = true; } else { if (firstPacket.getStreamId() == 0xFF) { firstPacket.setStreamId(0xFD); writeBackPack = true; } } } } String idString = pack.toString(); msg = String.format("0x%1$016X: %2$s" , offset, idString); logMessage(msg); Counter counter = packTable.get(idString); if (counter != null) { counter.inc(); } else { packTable.put(idString, new Counter(1)); } if (writeBackPack) { inFile.setPosition(inFile.getPosition() - packData.length); inFile.write(packData, 0, packData.length); writeBackPack = false; } // TODO: Does this work when called from a GUI? // Available is 0 until enter has been pressed, so let enter be the abort constraint if (System.in.available() != 0) { break; } } catch (PESParserException e) { msg = String.format("0x%1$016X: Invalid pack found: %2$s" , offset, e.getMessage()); logMessage(msg); out.println(msg); } offset += packData.length; } } catch (IOException e) { returnResult = false; out.println("Error reading file: " + e.getMessage()); //e.printStackTrace(); try { inFile.close(); } catch (IOException e2) { out.println("Error closing input file: " + e2.getMessage()); //e2.printStackTrace(); } } out.println("Scan finished: " + new Date()); if (logging) { logger.close(); } try { inFile.close(); } catch (IOException e) { returnResult = false; out.println("Error closing input file: " + e.getMessage()); //e.printStackTrace(); } Iterator<Map.Entry<String, Counter>> it = packTable.entrySet().iterator(); out.println(); out.println("Pack statistics"); out.println("---------------"); while (it.hasNext()) { Map.Entry<String, Counter> entry = it.next(); out.println(entry.getKey() + " :: " + entry.getValue().getCount()); } return returnResult; } public int getMode() { return mode; } //public void setMode(int mode) { // this.mode = mode; //} public boolean isQuickScan() { return quickScan; } public void setQuickScan(boolean quickScan) { this.quickScan = quickScan; } public boolean isBlanking() { return blanking; } public void setBlanking(boolean blanking) { this.blanking = blanking; if (blanking) { this.mode = EDIT_MODE; reverting = false; } } public boolean isReverting() { return reverting; } public void setReverting(boolean reverting) { this.reverting = reverting; if (reverting) { this.mode = EDIT_MODE; blanking = false; } } public boolean isNavFix() { return navFix; } public void setNavFix(boolean navFix) { this.navFix = navFix; if (navFix) { this.mode = EDIT_MODE; } } public boolean isZeroCPI() { return zeroCPI; } public void setZeroCPI(boolean zeroCPI) { this.zeroCPI = zeroCPI; if (zeroCPI) { this.mode = EDIT_MODE; } } public boolean isLogging() { return logging; } public void setLogging(boolean logging) { this.logging = logging; } private void logMessage(String msg) { if (logging) { logger.println(msg); } } /** * Simple cmdline scanner to start the PackScanner. * * @param args If one arg, filename only. If two args, first is parameters, second is filename */ public static void main(String[] args) { System.out.println("PackScanner 0.82 by KenD00"); System.out.println(); if (args.length == 2) { if (args[0].startsWith("-")) { PackScanner packScanner = new PackScanner(new PrintStreamPrinter(System.out)); for (int i = 0; i < args[0].length(); i++) { switch (args[0].charAt(i)) { case 'q': case 'Q': packScanner.setQuickScan(true); break; case 'b': case 'B': packScanner.setBlanking(true); break; case 'r': case 'R': packScanner.setReverting(true); break; case 'n': case 'N': packScanner.setNavFix(true); break; case 'z': case 'Z': packScanner.setZeroCPI(true); break; case 'l': case 'L': packScanner.setLogging(true); break; } } if (packScanner.scan(args[1])) { System.exit(0); } else { System.exit(2); } } } else { if (args.length == 1) { if (new PackScanner(new PrintStreamPrinter(System.out)).scan(args[0])) { System.exit(0); } else { System.exit(2); } } } System.out.println("Usage: PackScanner [-options] input"); System.out.println(); System.out.println("Options:"); System.out.println(" -q quickscan, does not fully parse the PES header"); System.out.println(" -b blanks the VC-1 Stream for Sub and the first DD+ Stream for Sub"); System.out.println(" -r reverts the blanked streams"); System.out.println(" -n enables Nav chain bugfix"); System.out.println(" -z zeros the complete CPI field"); System.out.println(" -l logging enabled, writes a log file with offsets to every pack"); System.out.println(); System.out.println("Example: scan C:\\MY_MOVIE.EVO, write log file and blank"); System.out.println("PackScanner -lb C:\\MY_MOVIE.EVO"); System.out.println(); System.out.println("Pressing ENTER during the scan aborts it"); System.exit(1); } private final class Counter { private long value = 0; public Counter(long initValue) { value = initValue; } public void reset() { value = 0; } public void inc() { value += 1; } public void dec() { value -= 1; } public long getCount() { return value; } } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/source/src/dumphd/util/TSAlignedUnit.java�����������������������������������������������0000664�0001750�0001750�00000005203�11211610734�022755� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������/* * Copyright (C) 2007-2009 KenD00 * * This file is part of DumpHD. * * DumpHD is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package dumphd.util; /** * Represents an Aligned Unit found on BluRays. Contains 32 MPEG Transport Packets, each with a 4 byte TP_extra_header. * This class is used as a mask over the byte array containing the data, the data gets not copied. * * @author KenD00 */ public class TSAlignedUnit { /** * Length of an TS Aligned Unit in bytes */ public final static int UNIT_LENGTH = 6144; /** * Length of an TS Pack in bytes */ public final static int PACKET_LENGTH = 192; /** * The underlying byte array this object uses */ private byte[] unit = null; /** * The offset of the Aligned Unit inside the underlying byte array */ private int unitOffset = 0; public TSAlignedUnit() { // Nothing } public void parse(byte[] unit, int offset) throws TSParserException { if (offset + TSAlignedUnit.UNIT_LENGTH <= unit.length) { this.unit = unit; this.unitOffset = offset; } else throw new TSParserException("Aligned Unit data too small"); } /** * Returns the encryption state of this Aligned Unit. * * @return If true, this Aligned Unit is encrypted, otherwise not */ public boolean isEncrypted() { return (ByteArray.getUByte(unit, unitOffset) >>> 6 != 0); } /** * Sets the encryption state of this Aligned Unit. * The encryption state must not be changed if the Aligned Unit is encrypted, it must be decrypted first. * * @param encrypted If true, the Aligned Unit is marked as encrypted, otherwise as unencrypted */ public void setEncrypted(boolean encrypted) { for (int i = unitOffset; i < unitOffset + TSAlignedUnit.UNIT_LENGTH; i += TSAlignedUnit.PACKET_LENGTH) { unit[i] = (byte)(unit[i] & 0x3F); if (encrypted) { unit[i] = (byte)(unit[i] | 0xC0); } } } } ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/PackScanner.cmd�������������������������������������������������������������������������0000664�0001750�0001750�00000000067�11132225672�016000� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������@java -server -cp DumpHD.jar dumphd.util.PackScanner %*�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/DumpHD.jar������������������������������������������������������������������������������0000664�0001750�0001750�00000610364�11211612210�014733� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������PK���6:������������ ��META-INF/���PK�����������PK���6:���������������META-INF/MANIFEST.MFMLK-. K-*ϳR03r.JM,IMu (h)f&W+x%ir&f:$[)d%�.\\�PK5e���g���PK �����+:���������������bdvm/PK �����.:���������������bdvm/vm/PK���R?:���������������bdvm/vm/BDVMException.class}NPQJ "+F]j\h` -'rE)EsJѤ̙/O�vcno{U~}Kŭvө~~ ҳEג˓:N 35Z-%߷;8 L m$fH`0gڎ<һͮ$LsTWOȚcEFNgϰԤH{E'KJ|\\yQ#h-%@HH4ܾגvxWVX/! q:&0H~'ˑ8jfC0Yr(J*~"+6^`iLH UAedἕu–i5(l!4}XqWGEERKaPKP�� ��PK���R?:���������������bdvm/vm/BDVMInterface.class]J@Ԙho;$7핱 E%^jǰ%nd }^�>8k@eovΜ}pAaT<lOcJNUiL>K.]v3sVp';:&hO øk&5Y7ߜ"#صǫƴ*j&qDr֬m~ a'΅wD n!|{=/GߝcR{8PKˮ7���9��PK �����+:���������������dumphd/PK �����0:������������ ���dumphd/aacs/PK���L:���������������dumphd/aacs/AACSDecrypter.classżxU8|νo 2UJB PBD0 dBҜI(׊TfuQlĎe-u]®m]Ϲ;%!A|3s˹{?~�-3A +j+GSJB wEpUpDMny+BV1[ vx)u`]`MSnO.5+ZXtF҅KFPaa( !`$jRpM$|fgqߏUX`S J]ZZ6*MiS1#l %uƦa4Ą5";hHUbI!gזFB՘5#/GpU`mx[Bu G+c/Q^0LMPjn hzvL='O !++h_c@ЈYE < MUU1+MG1;Y"'w2&2s+"cFO  |I=30bʂ)cF+.5HKS jXJPZ:8 /h^^623+1yEhEI;]*juƉC)!"%ȃq07I ǥt >{`2)%C2t\nAO{f03FOY0 0ͣ\<dY0NU68xv<!G7!t m-' ׄWk#a;ihAjj-"[=µHEh$zSThGM8Ψ9\?x^4>Ұt mJ:j/= MմHpmԂ ` א|~ TA ˉjAV(>uU\<XP RpRQC씒܂)ż9_!8ZKYPMD\茦` Kp[Ɯ[J:tK鴒1S1d v`H8 , tazϥ,q {[6ҫ- øt9˥-nn`M.H\+Sք$ÇR̈́[w 6: \mV`̂%#N)1w=p΄;d$-CVЁ vR8%1 @Ԗ({a ѩ;nd2Jtt 6,;Y!."IVP<{j~0oM}߄Gwz. ֔8Y<NZ8F~sI),4lj>!j[!1o eZd᥮$ G,J&y9x~1ŐҢ7&װI( G@$D!Rpɮ լ ( "PU@y0 &](pQo%݅ݶcׄwv\&wh<�,|D"F;3vk 뢁zT޷-ʧ 5�3 >/HGC5J\ ;,)%,<K 2m 1릴*}k Em M_ ?/<Y|ҟȱig^nB+PdW*g8; %ˤI29?XmHpmCM#׆~It\hb2wYcαE D.zH :8A@ LLɳB_Q G"X7>ĸsNc\fXb8fb-?ZиV9=LJ"Vӏ=8ghxwO {!.bp$^967eߞ.O)tl|/]0oxʅ4.F#gKaSkyJʺuJv$O s,à :m=6bq5Ǵw8㙟F8 '3yeZy2q\;Kvʡ8g6%Dh$̬W mL7 TS1UlCPB *N iP80u4,QY44B(IX8WQ\QS <d  EF2 YRSȱ(. 0C-\dXFG~Fhzj,1s 4MuBrR ⊎)), bmPghw&iHZIUMuњXNK;#=dNyöDp7vυdD&ג0rQ%z XHZ0c܉#29(Wr Ѧ UM55q;LD/L R'- 7shԯ γ|4dPZxI.RS Pp+KY୚`qN}e*4r ׳c*Gch)7Zژ`G z 7M4+jyo&>piAp-7np7Z񹒽[- 7S-6ᕄrRcZ oHqŋ;du'vvȉnKD(%?ߴ )J茍.C.ҹk}J˰t!93 GE F[6 g9;E4I_}ޅ- Fh_ A(o^n&}d \E&O~Iغ*_Y7XPek.0J۟ TҶoCZ$TK2KJWVD٠ nQ̐Mw3C;fjq| k~_ܢجd2r6\9?#{Ƕ3BDB:OT4y6oXf 䦷,|5Ԭ&li8ڲZN¹=(;9]Ra^f¿#zy]q%Y6?5񟤛tX~$3HRN;!86b{-FrӋk Q;Iu&?~<ĝ:/?͟6j- 9=VQ߰t|+;V]q`8\;ZBI;TY?-\#vn[&[o7Vߥ:KWsi. N`\:Jʭ>.;@ KP S'UR="0e~ ciTu]&'š+ Kdr2P++O>Kdǭ@J'>u pSDB}LAbk+TPKD_#]1-1@PP&SJUXc;o}%s7;:0R&q%ŶiK#@㉋K@(Bڪ]@1hTgiI.1H1Whm<[lQ'X"O&G3&SRxgvM @22yw|:XV,|GIDbbKB$Su6a5+ypL"Jf(;uy \b z'#m706w zn3O4G<}mpm m_J&3 O|0guX^__9yb%fbFoڻY-i`)*c .Cg fnj\d= 5` +*X,NBTGjm\cSށK,Fy"I:l (K:, Z:rK$_$P_E~ԓcetΖ낆pEMz vN p\S((*څ.* bshy::}F"E ϝ7062@}](yklTUܑ/#8_xi#m>ブ<oֆq`p:};:Ny'gt8TZMt 1#o=0DG3s3ҹ:tT/y"2i"F]dJeem &X|iqЬ#:H;*C䄉r~m9%'Cj͖xnVs%0'X_dYhyUUQVvqwV+ 0O,+ԷJ*8jԚVZuySה6MNo̠YŎɎ,??`㮿)((xgL5v[Ի#3SW՗#޷U&CR&&TYe(isf8v4Es"ϟZ—`q�_)(bbmܨG#Cܧ|Cĥdy !-}✱s3#JK;.F-|qn': PAe%(-D_#XSԷ:U+{ a5lG<U 2ɄՅVs=#OXJIOsVjfMHij߰?"%vLHJN;$^ċ%R6V`/#< u%fhWwz$,;6-%^E \jcaA߿ovN]@teAYu'g"6s~qQmuxƱ ;9Ӗ2y^kkK0 h"\c,  Ŕi5c|9!x1)ZS(߾7r-17qV7|.??9oFB5!:,Ysp"<ĿĿ0XϹ>f_Yk&(M!CTPɇkvŀlw NВ�7D8$'R]6+mƃ[-[۝#3!%g}i &R$R([:uES/2kj)9Qoձ`ZhRƎ0r,tK`NRjN(8rT 6??0p̀(!+STvӬ{'_|!iW_Sj @ #+Cu҂1N5rΎ:ۗ2\p /N>j1m&~ܴn7ӒY|N`j n d6¬:nvح%#pwɈڰoLػ#L|s;#b2Hv:eG#9I[$77er: :O[]@I�̥@d:n C,9WÍ#E EI8r,Y[`qEII;RXnqsT27Cu U7$t@,UZG2嘘wV#&:쫷F}S*h%I</R ! #tUޕF~Nk *_^yl"U)$)hzS0R $EBZlDV|ĔPʣ%uǛ)$E8TB{-9KkpR-pǰ&o2ܡcVdcN\?WF"s&Fg̍wZy&;4H8Z9cFnbZcfQHrǔ x@/Nr)rBaivM*,YNj#rq1%9 5v'2TD|%)MP|!<SKg W%9՟t&ڽH0K.; Y+l9Nj*Zb߃\cɵL~O @?oN%F3?ε+;l~\\bpEFe;ͦS4:uq |X샔)&/i1E\LޚqU4̆Q @[j.Bl;#ܥĠ9g-)<4啉wj`iIA ɐJzzWSH''Z:yRuQc<''ӳvsX$P}]& u :V4`uN|mFðt}\ڀd+i}6 1̥-Sqq7ۉ-M&}w"O(&+LVGwO�&͡29G(<n6>LǒFFLs&%%!_A|܏|)!G-sm'k-"Nܵn{2Ql$]?5I/h>1 Kӝ>Iڶt VtpY?Mǭc)I 嬥YKGK>|ƒ!%_vieq6h/[BWH8C[́z$;BE^MzSa8]&/kW6ili⺺PdJM0Uxl }yiϳ:T3e'BTH׻P zճGlUJnt귧S7$ T? P8|~;UÝ0RBk 'Rr`W N|{\eқRZyJ {eHi{Wio}oѷ}{[;Ż- NuW }w)}N vACPL˓=ɓ} i2CWOj\G\\<Mu5A 34ETT6&uG\ȧ=�+$X M(),D8X~ RY#.b$-;1e\ݰt0{ =R^X\g3ԸtmNJ+DWE2&DJEM=#fZ3U ΢z>D9J~$ygY82t7wuЌu\;b\_ IT~5um9<a8?=D3cn3~ ͸g7lA{޻!zUZnA9,'SOvU<` ϕ 3im!sy p\2b.W^U/đa6ϥs $юO3:=aҙtf9MChqx:app\ `#\we4pZ߆aM3,xd�U ݈P˛x{JJЄ[L~u;τs»DM*q>ax� 'x쁸pf(ѹD6RI$ d8ݕRхG^O,&qOTMP{_:�'g}ϫ;we<6]椆vk{H.ȟ\L4܅.m7`&+w7U{<wap10aQϦjBe2؋]89҂ZpMN (_% -x݋'siʓjД=/ްNʥZ H ]8ކ g\T#h }w<Nupղ5Zh|-i"f7Z kΡwg^w)ۯIon\؂iqL<]QBi;=[*-_Nc}ۋ!"XYIʗ{hyx { 6~շYHR&݂m< i.g 7ʲ]xE ^xdF|fn# 28 9Vo[[pK Cܞo(1^~�2Ykjئ%@bSy3ERcSYbgM,s_1ֆc0Sp.vu>`3L3@KD[2Q[[`~¯`(~#e>&rwV8G+҂Z%>5il^^#oȍ�=%P9P;!-w6iP=AvIód#v1^^ p2 ,` e6r+ nq|(|/?O|A ?/N |/<"B8,bUXR q MwG@K<yLoW||(v݈=d6 ~28P3q<sd/r+<^(:+_|!qO8YK;jHS|Lm&Nٴs*,"X5BV<Uیev\Ӵt)\XaX7NOݏg'`>YZYdcVJU= yI|c%, <<@Fqς&<RHz4Q M;^Jpg#*P{ y;>pԽIB> 3L= M8rD>gg=hʙz੕ZɭԦ: y߅T  D{pM|<&8#38r<?ccj%hCUB=@!D1,xbn� z)枽wS+Ok'!Zd_ wr3]~f7m"RIIWyH~oMvJܿiuKܞ[i<W3s1~.t }Z?4R6^w4Ém5 O2EJyzy{PMbIP]l1q ع%pm9ƟEZB? ڸݍ?\6 "}Q3@ bJN+ge' ;^@7uj'O?뀌Mn=y"+`toͩx>Ntw5ˑ)&˛)OrvyM~79tg@nwdl,eiN3';#%HQ[U5EOƴ Υ&o`wTy-/f{vAt.}HhFx(OkCyKA_w򝭻ڈr'DNܫőh.{OSbfF~j3tD\1u7/̷K3}$71ޱ1)S' ƄUL$aU[hLLQdhǺe^̳ɓfDŽmjwTi1&[s,'oV[in/ Ń0�g4[n&So^/U&8Ƶ|ܞA^lm+<{Bw[Kxi2K3<b0)F[R@j$y^w#tQE-b&{X=iZJiḤ'qm^Xg&m >M4Ku.H]Zlw8Y=LQNa{i7]h˜q#yȣGh{~T<{?6l[%OA2@{deІ$J(2V| Q~~,{,gvBJ} I꺛?ǻ2>l?G [[ɼf#Ԉ,O Z )> a'[7ԗaġΌ)fkkOSbuj-qދ[q:cLM؎DRisNZD0STYĨT{Q\4k3uVdt]YeǕ[WvBul $I%6:q87ή9_kQ ΨjiKF;E4ۇbU<aءs쓙Z7,h'jIȹ>B%2©W>"2mѮ!Gw2Ao5uM&³ESkEfk+]BW{p]O}Zwu= d߳WAIܕ ‹ \^ z0<\ExWRZ8za?>([)L8[1la38 g8S<A-o]x?= Q~|I-S‹Onϊ�QE &Kb "ૢ _:g[*n6BŻ;S|o3_AxP"_0~-o#P|ש}<Bt^P)PKB&4еƈ m& V(іP[U K[!R&]Dmծ&**j[LC¯=*kω뢗}%&^O#C@=O "G$rB1Bb~&כE,N"_R  M(4R(2(1$1Ә%fbQ&!qQ'ĸP ĩx_ <o(7 {2ATXaf1Zs7ljs8<EDrd6MLs8˼Wm/ ̿A;,.v q+~{\ 5S\Z"r.v׸.׻n+][ō&ͮVɍwC_,6-<]DmdqD^*tR[)rMuTD뾜Rzqf}x}xнG<~Ft!vݟG߉}c)Ozg<‧H<'^<Oz2zN~lNdN|jq&|AQvgp2*l[ WsyP)·40l( \%GÙn]\_a"ټQ\π.a'.9LwPsK\F3啰L\N=jHѺ,xB+-*zףO+!$m-(E/ **Y$Մ%*xAab0q-X\%p{MLm*xz3̛Fq-Ӽ&F2_t3[*eFtN+L7BwՖJ%^QJT4 n{p G_ <8O[V(e[-6 �Qp! S8Gzb(ۇt:Yqap\3"N,3ӄEg*4XaS[8[$|`TqFslwO0V�$c&3&u! MeVI[)+?�B:M|~0Bbjk% -j�D*bU$6Xwicrެ*7�i?B6(IH9E!A=<wpΚIj"@]_t1/Nvuc yzq8+?=Jx^y oLrUG jd^v3d}2)w%їQ0y:£bG~#gbɺ#X7{)E<lE<ܬ֗K]o>e�_r[ލxnNX6&?7s}"}-xϽVw@U8i>VN;s7riSK<n'yd3%Sgӱ&k/}.U? *?禅l 5;ߨv._?61>&lSm7$\\paϓ)c@l�Ǘ"B?p*Ek->rӖR/əwtGxӍ0r�r_},nODpl2w-ǼNH^_c覦ΙvQ[ƕ^`&Ilku=@qrlXK-<Zގڊ˾^V쭰= _z'}ey}ݩ,O3d*zF3덾޾>ZrЦM_FM]}Mt8 {Ŏ2Ky5T- &ߵZ\hC6P[$~;-2O)bŒf=lk;N9NIG"-(nі 먡*D r+fkY2=\@ο'V[;lCU.5z2:)4s./×:/ӗd 4wُUi=ˊёD,ݗG`>>.5[Y^zIje^F3tEdN)͐L=lSLdI飸%6/;.洑u~)v?H6R#GKz\6\Ȕ혚<mw2C<ϡ8H^_| ķ#~#0\a8QIPJ*u)M+pL2 B D\ ,R:Mv-{.xJqRrr$QD]'@<9K8<SK$WNT|ZN |Kğ,fg9Gd"_Ir(Ry(eb<]DoUr^.e];d/KA%#$>grN%A+MyLr+/99N^&'+T^ΔU^K=FY*7-^*풷G4/ ;-S6kCiN۵%~,wi=N|T{Q^ioǵӺK>?'䛴]JA~$oˏ{@}LB~?-_{?;s__yH?". LM>*L8e RqeLRZ1GrglFfk~Inzk*q6ȸC;θGbІ;a+ZkG}j#U\=ŅxXST:J<9^Ɵ|}9J7*d<#S C'`ODS 6ȉ F 9$`]Nh eG`0ոKR)f`q,R&ll2 Wir:SE[p3d1.,HRfQnPΦȴ<KΡ^LT.{9J=dyTIO^v#ORoy.R>"K}_B=LNpjc+PUlg%r!Q�zV>{W.McE=^! Дr므5),&7ݢ \;IaoF�[CHC9?#}읓z3MC4$_Laq7^<chP-c`.k2Oy{9,WzSlC9_khQkz 5r2,$8l<am"P~V^'z.1(6~NN=é_o o8^nS[dN[BQ WL"^"+l3%K7 W].Suc7Dom`[ٸɗ^iUbn[rllC|iB75˺JfXFӑ-$E(I '4VWo%?u d`)W&핵endĀ�tXN[V<G؂€$4HzOSZ!dkEצAOmO 90EsP i m I;.am<)^*M-kk+VGTZv1ZOТ8Y[38G[ڙxv6hI�.Ɲ%O.7+ozTծŸՊtzmEv(n6Ѥ)?{KIӉ9fvdlq# ~q<θ]rR><yJGg l^#/ u|C6;v%g 3 (/cc#2q*3U?2I%os!Jq>g1 K4NN ai}A^!@JcTRx^?a\^q3ޠv{:ȫeouXRy.gHO)|cN>^O^˴)dJg|Iuɔx$_):!a5y>V8>EG:|r̵7:I>ߦN0q}xLoHo= u4W㤇KYhMկ+?E[g> W<` D_FrW17wK*J*fx=HN!>X(i=;kq4EA#I#)n'08dYrEv&߶#K$ٮX_2R|Vrș!Eb93F|BuH=>93ctdחTZoA!9S}iNi?9퐓fVmrS'B<Rh;e9LPBdYxSC{zkB_yi/PU koX-''h.abcߨOPk_AEܮ/!+xD} oi{1KiG8@7pq t gixt<SlY~|Do}m?~¯\M!zD_}Lj "O+j}P/Y.vA}xD%gtC/%T|"O?觋H]_*SrWH?er^#GR9_or(K&D_%CjY<G?Wϓ+ 5Ezby~LmwWzeᗧrOl=1E%lqi2[\zD%Cn7}dzSzRDש)ʫUdA.ETt<ŷU4V^b./S1&3US*#e4U1R&ʐ|1LpDp6'֩e?dˀdh|%3zMdCI%ؗ}@w)ΎN}2wO'6w";ƪ)eR:Qhm~yd$gg B2s" )/\wn$p<'JrVOOB[[?=ą~oS- }-0\wXrr)(ҷB^8Mw~?\?`[Qx)Q>$bh/ދ2Q-a|F]wO]Y6̹4>w2+#t <~{BYFźMφ֪TZC(yJ1|Sm 1[cְ۝~yMg-{vh/=ߓ@\y!Goz1mKz~'^E/KᏒ%}-w[?&m uJQ=fl/l"<^\6;}+xmçm_gc?=iW=6DQ58` sp %}De诂WS @}?I`OT3X 4_*+X+G[? 0 яg4CF gbFjxqL/V]+Jo&%!=:bob|�VH5JX4=d:[~8KA.3U$!=Co�N-R0BYXM3sB[h꘦ p'FSrsf>vHiTy2L: VB8>k}/+? ..?PKq"6I6>��w��PK���Q:���������������dumphd/aacs/AACSException.class}N@ƿB"( c=bHĤᾔj%'>e- *{ؙ7;KTdJ`2 4 Oko:/9-ǖ!1je[~!^g`*c3 ݲ͇`7.M|%ѥ,doߵavD4<1=. uCGJ-C@@ J \üڅF4D%QFã]EF1ΰJI EB#lQ(nSthRhjc2JʓP"v "=Y&eq>2씊B\LdZ`,uRk, Xk�PKp����PK���U:���������������dumphd/aacs/AACSKeys.class}S[SPB+Un ҖKD1/L3>HI茾{|Vrɷ|_XºTJN`mll_F` AojzYܗNHUjQR] ej(:[uDŽeL`nwd-t*g$ 䜐KDW0E3%qm%koZ7PV!B狒P B> YbQQ '꒸q.,ft&LLj7 srS;Qd$dJ &qN]aDJ}U Do\VܮWRwWvs3a>oFcr0a_ޜD/['y|(E" /3�Tg92-]ЌJZL{&Bq4yսIaA$Ob+C#8clw`D/ƚ=D*KН奧 >`> ;ďcVu;>1)~w X@g&0",>]d25yx,|Y0ϱbXw-3ZU$p28 /#9SΤp⴫'cX PKr����PK���Z:������������3���dumphd/aacs/PackDecrypter$PackDecrypterThread.classW{xS'mOr/7ai!R .-ˠLzC$$PDэm:Et^Nр9t۳.{6Ŋ?r{{/Q�xKBXd!]gC-z{29Ú?j2ޫz3ܱ͈T*FS)#iTn]\pJTwlvP2:I.E# Sf0#jϙІX8ivą|(W:N$,#%ZuogLVϟx74޽#L-89>aFjFŻ",SC;d>;6°\Z-$oٰmяwƴs-i3hV5ˆNc5ybj:өx޲T3]i`^^RӋ<Q5F9&0;pHU+ո#ndrf:Uq*{V'k"f/H1w?L!kH'r  QEZZ j(WE0cC"V' j l Q!s4LɲZLէ5LTYkVi*Vf>4<cWlv,Za&fx01 56ԇ[«VEЌU&.@Iesz*TO,gκ1+Ƌ ] #2Ul$„2=\NVDq=٪[]d!t\IHe5R.{.qS36kH|++/ɛ17WDJN1Sۙ4+DᤕvjU]A5 O'XF.4FhH3 Aq""BYE[)i&#-c'mI$Es,\9g';NI{|2 cĺ/c \ Hޭj\Ä)Htt*ъe>: _ėm8wjVmCUmr"g_Vp=Q'֗q\,/ΰŵ (۝͸Y-̐j3'Y>i+= #>2Db6&Wq;;mc4{.ܭ[d8{mN[ޭ"pjjDz[tK1(R'|G]uc%Lg43;wh>H ⡑.{~x ٌnIPc 0AthL9l,GGzO$5< ~Dp]6?Cre=`)n9M:RfUO f/*GO1*,ik KehG{c{=b7VvErIXoH٬^4F3gӆ7 v1<8<TdېǺ yېG .qnC6QnC6B6)U;s?w%�56%;X=C8-Hʧ&(m:8X>%A1�L!:K>uY \e7y<f�khEMph[<}&v*qwl&3& XO6wN(vөOa`-RTYXB+걂֠a 5#J1OqN8vP'vQc1o<a `>Uz< (؅v"py9!^U|Caj(b1FڿGveCNGۻ*D ډR.[wW߉sXh}U1$9ek HsFd]Ͳyk˛FQ[ăaecϱgcjpP@hcDnxKi"vRWŕ|K\S&]wwsSဗe&}ne%71\NQiCa ;a Kò/c v+ÀBt=;m  ~es \կ`}?} !Qo3jz+f�{UF.8k׿]H<a|U(7]6gW. R~ܿI+n=oJDM)zw/B&wWNTZp~49^$k9)XSO5[&)Tx 臷5i@<`^.VsOc\gn6z*wx ֕2wO?v/8'-TSN%?ܳ�'P'?{͵ Zz(J\>^Tvq9])wz1B%0Alt;c~;I\KOqw|{9~p>nYx^ ^;70HoxzЯh)oooY?NM}eLTQzoD_>C* *% )Qf)L (>\eҬWZ]dJe2[Lc\nQ+ +3ܫq9M)3{;;N)a?Q./e+J� ꅾ&8ߓ;sA.&"*t^HND1(7o!"sICTx5ynk5{R tje %PK7T�( ����PK���Z:������������(���dumphd/aacs/PackDecrypter$WorkUnit.classSn@=ImH J[r}!%BLj5@2gۺIv,"$�> 1kR> ^v̜9kGx° N:NYGP}08CZ{~T8!Ca[rU}/xo(b4 fUfҐ9Fƛatle8e^lv8'21j3/=ɔ0,M"bݰ~`ԣ x86DEe@ KxnUr\ ꤐ)! 3p%*y`/b[-SwD1lqKe$w[\]?m+X=pat=QΚ}Zw%K;?'NC{nµ*m_ AOVٙa7vY1|SNM4),O‹S8C^䑮FF67hdI2t$[Fvy)1 1 _1\#b!4Ò9#֕c<֏bZT.)<SPKt =����PK���Z:���������������dumphd/aacs/PackDecrypter.classWkp=WZimy;&6_qH0El&H\ kic+% G-}Q@i(Z ?@httt*(ow0Lwswwo^yÂB8Y$c;=+XMyK؞#V咅sN]mm8JPF[orDVz*vRa7XHcl&Qm+SŭY37-F^ʤ XXRL^ n'DTa.vl3˃r4BcehRGzBB6FZmjV::V)D/yt^B' "|dCB #U^[d̩51Ћu6ov֬6r51:~k|o2hLcEikLYff r-Eh[Ll1_蚲I 3fkc2apBS5xNeL%N~<&S* (4gfi 3)Cen>PUؓQ?bC0.7 ̕0##%(S\ŋS~P7[r63Uؙ=d[{O^#qb˙ɘ,n$s%Ij:p=-hado -K\1p2i[7nk-Ŕm]^]!D:Kja"cf?hI rL<l`PT58u ;bਤ޶?bUڀ`Nn+*(X\vYAzeAqEŜX>g%评m82ηp l&{TɑvO\$;2hrts O+|(tIS.=AGc46pћT:kp盶m%9".E)80DṣW/Djf_3uVA3nӇj2!l0X9.x5z S+!twžLzNo?mSUfS&eLm)}ҫɦ2)O{t&Y-k$(y)+}!6 YT>ט]vyo"mųE;aL9GĢd1Ma4m_ !Uqvx.z&Z3-y<yeXzg<͌F@g[}4.U,?-*?m'j?޶ߠS>y AӸ_;a4."@CpÁl}a-gKG8?WmO}aƶ:~i=C@~\hAVD A;vdDڈ l4ǿq#ucw>vFt1i aHk-abB;p%qE؉(لY.PVvcDa]uzߪͣ;"V8MZ_t ?^qM5ig_M1@ [)r+,A>*47i8PNKsZ7P?VPH!9jje0zEmbKka):KRyh8kQ"6!ңhÝXtu fbf!j%ffaPkΒ ssťœs3k,ƭN6{93pP'yԆ`{:m/UHӡ%}ζ|`$q11=A?Nr֓ӎ�W�@n:Q۝ CNKt2(AGa;{hѩ eg ί@wLCsk#iow7w{DTt j(Znk`,>*U2@pџ!s_ԧ \$GPY!a ;}agVm%+T8~~`a$7ZT@ Z"w2+9U?8{e� \gp!WVQR<Sj1&x.^φʹX?2ԟ(kg yk:G F2[Z; 0+xKsEvhW\U{E**-Jy'DZ)]HM_Ar7!=>uBxp*+czSE 4q@v18X4Mĵ{$E]ïoyʼ Reʏ,X#*PgS˽]S0~a{w$Ln7.5|uu @S)6W.2PxPѬѡF@jDTuk /?:t x3O;}nkrHV_C ~y.sI :� )?O))Sg8)֦FެS+'?PK>NW ��)��PK �����1:���������������dumphd/bdplus/PK���`:���������������dumphd/bdplus/BDVMLoader.classUmWG~W�(V%*Z JKPf$.lPQ9-Ƕ?Ls{<gpo4t4Vjl6٩E7+v!)ph\7U+ +y߫A @଱ el8Pr6[$M8N $Cɼ_u$ЕmЉ} dzge;3.5|tz˘ kN]t.]Jy׬x8ܦg]XZp LdQ4H)gO:NጆӛV@g?}R !#U鸀G+p [n7nid4 G*f) cDg.wӳHP&q$O&/2>|%p)9R8^u32H9 WyvmVU9uaʮnn0e3&-3[MwV :& OzݬLt,o%e;#Z࿖1%78זìN $tZd$m5'|AH6j/5Zkv<m dxQ-ji;{d Y5j# 9cos`w>:mဘc[׾ U MU\kX ~/x,͒XĒ=Qݏ:nުBn4mW֒:,yҷVp]jwjcH,Kp5Vz;S""PGBWV\bcD L୓l 3T˗^kHϛ0R'V=5FwI>gګ&�zo컞wfL<__a6|8j:V!8e6Q|@*#~E{;naIdqh∊އD/c;i1<18_* do%3G\53r?|d_|_ڝv7W{. a7o" o99SJtwÙ>LͼO@;j"f Qc" `Bh(>CRv=_%4G)i^IMb9-GjY0.bY,e~A?Ml`o|'3DyXr1^4OiT2QKrƮ[V؄ Ex/1�e 25<N2)*=k$QOjQEOxPIq%)Rz%Ia?"w~.?_PKzI�� ��PK���j:������������#���dumphd/bdplus/ConversionTable.class}V]WWݗ$ QA|jXTjQX"X~ FC& X'_|]l]}oB]2{9{}O�05 te Ll6Sؘ_4ײWٜ/tX4b9#?K0Ӟ@'W]Z,zV.vqzC)k.oxE3x;p8"tg%5Vu|p/n9:xh UyϜ3~[ވ/3tج1kdD3A:װCyCAlGt=)ySG#[V޼T\526rScy-r+IGxmJat3SvICXR,xj]eٱx҈kG(՞&ruD%]ltfF7a?5^;c-<3G 827 IJWBm# em'4ODK>pUEScƑ >'r}}Nv:A*r&S#+eEb]u(i[p u FeJ:,dd2R,ݖ5yb\GY3JHmH2I\4rE3e#5hHiZ$aP5TUayhkh"*w)UbDK-Zut}K؜M^@p\v'RS]#YQɸK䋜J1Yd2~׺mj(M,踁XT%;2\~oN[KM|E2\deK۲T&ᚷ!AQTìʷ,{cx#j{\xOI~.$OTtl>aLS~+Mj}-\#;h^Ŷhpt3bStva؊~M^][F{8{d,q; >~C73*)0x<+Z;W0y81ݷS_U:3\zOW0uOtu>|DSHp )vQOes8doq9n_(k!>9<k 4+ Qޛ*Ҫ8x=': |A]%aP_y,##YEM{AAhw=|q(vzLS(ר �?Ge/2@<z6 z+ ~ݧH((Wy<NPI?  r+W*5T]>8{%]Ľ L\:Z+VpN<O*Kv(*NH] ^f%oZB"؅̷tBݓuDPK6j��I ��PK���o:���������������dumphd/bdplus/Segment.classSWǿ7 V iZ Zh- h *,ɒC6n>ԙ>t';S;ZuhO=fANqɳ~y`n+1d˥\&)V"g YҮiV&&͠NN <>rId6l-[+3Z ]CvI3 ٸÉEK' cPhlu6ϭk Ly@W{>FFÏ*"hawuԂaZyo0x]v؆z{MEELyyA74nŒf؝ ع<Ÿc|Sj]/$Vm=eʹ> 't14 qF"9!WGQ*^nOVjч} X*ʋ8=J&!b.+%l=퍹W #f-kBl0c90`Jjjف0> UMi4UӉHqpPʄ0^"τc ڶG㔭JnrԔ4ӢoGܝTp2Q.f,yaLଂu5Zl}Y94N(2Ľ:8dJԮS@[i $lj]-RPF XtV4~*^qc/egi:Y+*hAZyҹRٲoFSt 5wN>bs4kp&5;㉒aSg,۳`&cE\Sb5pY̮sXt>Uq[\W]dƟ vZF>^&ѧwI|UKo6o|"hF|?Ds�hͮ8{(o:pO@bԾA#KQ~@3 G~Z4; xSxަ�P){y(Q ~ @~ŞM?ba G]_C7OU\k$RܾCwHnυ aƄ&EBL 1<%\,n c R{$I>$s0qZ= 1H<."{J9És.mOUs_`HѪJk8#FOnP&Z5 ](qEHt%*͛HPMAIkj8HMTqkuӠ%cIW:sl|^:0ܚ㪶+5\מrq`C.7!}K^-. ̯!llx~0`[$Eb%m`k8vS]YB%4+.U{xl 50)� Xvs>^`$8߻;DR}MÂ;=7xHn"ݙOx9ix^u[ث$#?oWyciQum^yj{JV%wF=\ސ/У[PK �� ��PK���r:������������*���dumphd/bdplus/SubTable$PatchIterator.classuT[oUoisoRi▦&!4 MqqB! Sgv(#xx҇:"@+Psyϙ|3̹{�"0+zd%JZ/ivv5isSC(hF>q1sgmkܰ~ )UBbh]fL0e:F% }ۖ-& kgFפ ,hYM˖% ^խc7 ݞaK3x9 TÍ.fpGciaUOU/nU!A1a3D'SFZl[{b\u-5M퍔nS~DW51Qrƚ'*€7jDRKknJ RŬVHk.dGg}(n %K}킶kՍks,x^+nd ?Nˏi(xhk:\*jorfʟl.gR򔊳XNԌD'Vc>NM&əF_te.K *qZ)MڟU\"s5޵2G׮Y=ō`xM+x im$YլE_*,_*wk5iܜ/hũWFqMO.$)$B?yݐ'm"Y!v,A$}qwqOяqxGec[21 J%ڦэ;,_8y zN t�)l-}gӽI[Iݺ'c҇Ub}(TtDow0B[|JyZ^UTkw突?P}?b?!/p~3tPo[;g16oA܄4$6on"P?ٟ/&fC0^WHPVM:1LMwTso#uzR?T/%0z'W"9T %\&<tIt^C:3ΈZ6wON ԕrī|(8/0|鲬E<[ wO@pxY', �փ(^΁:DM ~Y`PKh6V��a��PK���r:���������������dumphd/bdplus/SubTable.classUSWnɆ(* (XQE+H+ pI6d5$qweӇ֗:3ک ec܄#T'w9|܏� @K8[Ȥ"өBhGq}:kP7HVDFIGah:m%TM lcf9@sL39eY|̴^9ӝeZϧ/VO }ftФ2O~ѨACCj˫& **Zshd6 > l9|qvڰd'n]TIfYf\#/ZIs%csݫHtbUѹ֛$ Z1c8cypBR9A[$a nEYN-TSh(9c@m7R3vqZ6;{VfPa܉j06:q2s3TqB;d-L6FC/(k"r6a.ޣbc<*(r@ N l2r6 נ^Г3OuIJ\9 Q)Ф1kUF0`R}%9b zujSrA}ù23͈k5ROj,]ٖAU#X '3E)e .@CѓGBy=ݲIt=kyEp茦訔]a.<Ʌ ;ߢ eDVb 9ֲz'j%p 趺g_Tt Kwt'Z*M'ac48(IfCr.GXgO:wyjOO& 6t@ ;cD4bgh.gXYݶ M7 +4\MKV\[sS%l{-ֵ?CS-{AB<@h#IZ 7EB_>K|J*,oxVWxo %+F2)h[/]lzcJ+9N 8a҄!Cӗq J0u}*CS-\H"ܱ^m.h秒`"9PC iqD.3>yoL{\O'd+~ٲ2$/AQLS|"PtIyI@.B D)WeSNw$^vdLt0=#o^mVUfì5+K>EdSTɟ"/n\aF3 [I~ZqXRҳ6>U*E dt4?(0MR+}Y`B%fK|zG !!FpTWnޮyOeu]7ķ|l1>_爫R<s0[R? y: +Jqsn"~nVDU)#e[ �?Vԕ�}ta`N[keKϯ@o;PKXdm�� ��PK �����2:������������ ���dumphd/core/PK���v:���������������dumphd/core/ACAException.class}N@ƿ_Qň1$hbxp/e5ВRD3Avfwͷ E!|o:zRk4o{c+dCv_[<,r#i:=G DH-tFw(ҺcÎZ|.#Mjٲk幖ݯU:l (#9}t+ Yf13VKc+)0$/o9Sה6k.F 4BPF]-EA1ΰNIq"OCYb.K{NJ UB>!7WRHi,IE^p Kv6-`J�,CR36*BJuEfcPKv$o����PK���y:���������������dumphd/core/ACAPacker.class{`TU9羙y3<&3a(2 %D&B("PdH&H 3 Z7Q lkwUu{}o&}s߹/>}{`4=�BT65TDC#&M. VE!\Q1wIF=\Y W#dno6 zEcƅڦNrԩ h,G6?iκp]h~sC(6'nj ׎8&le`cS4PеDŽI*ijHKw%aN( VJyPc(ʝ`EljO1yrQWG~P2E%SxKd٨\E5,bVÄp}qYE*C#waC?^e@CyJg; !.Ȇ<94;eOY(n+7H̞d taZytƚ:Se:JP2.MU$Y5m\9Ku"K3Kg0UJK4]1TX4uei:RY?HyfH4-E׆F* Ѱ|*g)H (UdG7 YK,Ỳ^>ƚ0tn݌@[Sl�j{8 ,t@=xKKH+4E+B҈]T١cA:hv�V(ha<$YQÇ01NF5Hzyp*Ԁ3/,P'g^28gu8s> eϫ W7(e#<xAٰeEŲۅ.b% ۙue<YheS6I-v:WrBhـjwu5:;"ؒd{a$wXPV=X:MXh7͝%ŀRZ>&+).1>s*|U&Wq^*\K+#> 6VjcavDne۶?\3xԠGmpߤwNW�3*{f2T>RwpZDy㻱%{V/`\=)\ƪC+ 5H)!K&MdYπ=p?&g3]u%kB9bClsxfs:<O+>I9crb3/8nt?dt52<c~4?ihnUU,Ԩټ>.[4hh9uxK%:P/ëx%|Ε x X,2o)[-B"ft0&^;hN]ŃRC1du(*- jdϊ18VGMUIP:N?>f=Fa)n뭦r`cSl?tRNid957u ko㌿Ïl ]+\tJh'91ꂵ\/ S+P1C#,fp1_Vϖ(9.k6V}3(y+c{qhX#MbԢ`^9O'Kcfg{eHdCF%P3Q{"Qei$V x>O أ)@ɉɜ1yYgw\H{} 쇇I=oVRęf\f∇8s0յ)(50: U2؝P0ZQ#$iD0JL2<th(44 Śj 8 8pth!d`7m܅Ӧ!/!ln4B'Dpw9MKx Jp29pzr3p*Nc}Ma?k`!U ´ys1=3p BwXRKsenbDCuU!^5plUFb!# *jF%6)@ ġGfdrIdHpe Gb^:yѺLJ;3k0\xIZzU<[s85 7sGj+Z8SdbGBΐt f;>p u&{h f'%EBu_EwsBǞԑsbIc#jJ-9I[=W9kq ZZؙdOSYtyˆ <C"ggBzPLn]2E^kxhm0X\_Z#9Ax"X<1=IQ/Y0ZCN1sK 8^;f.ͱPzߒJI@RqŵSl[71(ȴ>`RI"M + *ڎ[Mú!Y[ 1aGVjm5X [:ΰZ) z +4Vr3 N0VvVwtRf+5js4֑SHA$Wk?X"{y3NL?`^YstX!Y3=b2͵f4qRJ;%+/\"[~aK2d/a(,ݱ;wNXotā2%Nio^:\pgfȟ-=쨛nlN&3 F|OGt|Sc\=+j͏TX2cEIw +KXtG x{! ֱ~)m"?rY#GcOnluI/SW1m- +nj_ ~+MM0:"h ?"g B"M!~p?;UPZ5dA@Uխ GeABV8D.@d'bM(NQwr.S6qp|^KQOvih$*S8 J'4ޭ%9g1ny JtKm0u+̏+B:˚ʤ )WщJL0KN}&:I^}"ncKS:̠zH Ԃt+BY.@ bmJSUU(@ACe&Lc7g/X,'uqPe&;6>LdvD N#Q)1܎>ƠF&>YfAҔ\-diAy'8X<VZDk0]/d:dIr<^EShtfz*cYi@=Dtx$kFye8K]S Li9S)ÒX00.R&:m]$sV:q2lԐG3Ͱz+pPpĤ$g#d!L&&"<!j0_mzNLr0{yՖݹ%RN? ͮ/d,7yz ËM$T}O ĩaZ٢!6U9#:I.bqGܸT8J�̝O}")G[.O-|lp?X+]oNRK^Nh %\G>uP'fS,TTk:5(+$`Zrl &̋ky?k^J<䫑خUSxK֜/7hr䥞WLe ֙;kDH>\*C*~#˥Ø Wb쇍:1H5}$ٕ g9:.%3qUǬlb6F{yo':1e.4 *%KEpL@*_bw$kI꣎u19NTԆ% ˢ$,3IhzIo %Ogw{l=4~n#z ٛ<FCU]HrgY 5ʐ7򼳦O2ER6L *BFdQ^vfr u'!o6\Z[Ջ?z^Um85 ύf;ٲ5NctGeǠ{e&+Yr<Vkwْky;YyztѠ.*%}y~Ne":=BՎra'*%yXo;#ZaPcS>~c#fwY-_vR6Ps) 3\ ��t 'R.x/S s:f[3{Xs~<0�| ߻=п| AC@v r[a"Y1JVؒ* {R‘TQ +"-yLI4Q$:J8j&WRMT3MI53TMϤbUT3Kդ&Q5iI5s۹eY(ݗBu.Ip?-8-a9\ B4Tϡœa@-C=VC<WHQ`a.E<7f�, '抝v1zb9}3Nrky6?`5h00o\5d8'jS~s ,NX 7o0?X(e=C8$+,I^I4%v1#_'qF�sqֲ8΃1p>.T"{I4IJL)L|j|pa&oϊq,!X; )΂TgA2YEMa5Aw7*Hg 8o\0'_/@dCh3j3صqnUg5T)Eh^-o7pjd.y7Hqo|E=b�rѾ^Vw%z;w򭰻xVx4GƾZ/㩲r-㙲r[Ƴe;2^H,,yZ-i%gtv>{.| ^bZR".o1p42{(u9|v*8X'a:@75mxn`A7 +yRsשmottx@e kAozf{/+#K{'MulI|unWEߨN^O(_ d큕Te)Hp4a=w=Gύy=&o}+ `0 p _Zoe_m wp'G5vܝ03nLp p?V^q<x<x5- |xOxx^*xxEjxZz^g5�xo<xSQ­fWť!WA�)rp^b Uzb|( 4(xlO+S6nK0s)> }X٢\ē=9tr?B:NUyDѥ; :Wg7ZÓ K8Y`Sʨ; \#7<;AՒa^U7Q4i$LymnIDz-0Cawts$�aVey>G,9Wa/zmevP*ܘj&g™spM83W#M:,oTM8^KQ=.Bm dyG@׵ )>%^J>ՊV"\#f;UAyVx|8P3UqF \`iAtck0&Ytg$�@/az1;q5v8>9vnsRI8scYܸ@G *Լ^fK Mn U24A2Svr:/؁d �p' 3O0dCH1 U 'bǼ`#;-w?ȮzKfϏ^Yx$YOľXa=XLW -8l|s/11RA#qGR "O*BQ,4:g%XLWLg8c9G.7p!}K<~f-!x [\mJ9^Il f5W6H: Q0C&}`YYN?D~z)p= 0X@OVuU[! b^ p! G4 pѝ r5} 80݃tg\x\.5faqX婌;g$LHܻ (q)HUiH7}E-/c7rn7[p-eԌZ/2.ɼN¾TOf+ߊ$cvge}i{bP J|E!9hf8En6qxC+n3)ͨZ6<sCD|,֊L ̄pY< 3G+,b�fG>o+X{u]&*錹>HA7#t[推yu >f%W.v/k 3q;>BA:?r&hw*Hq~~qB5dk!vN&Ə3t˨90mnL&6d^ L=?h vD^ԉ.1/d?q>U^&v=te3`P M+$1Tր'ւ W@B66B3\ GP¬w!Kt3$ D72΁uCƹp;|f^̌¿2/qT�587`!^q3&7߂_B>k6۸ޅ,4nWpF >"Cix?y{i(>B1 4|O$g"|hLjWn|:=7ѳo3h!_ +txHLŏE)G,Oʼn_f\_ Kq5~BG`}"'IzSV6q%j/B[BWe8 =}ũVϫ |C@JT M<QN:%,dpy9𺙰 |_�Ny*xS&ձjOj8̭^F/x^yϥd|e4zגk1d)f09aG/[&G `0 ɘ=d<]ȑ#Leox_W޼/W!Y,pdb;X)K0ZdSR?Zd'EZ7M1{oZA0Y)&?^qqfyTq %n;ɩtI-ex W1nmdhS3^vFzyz%HlI6؈f"^%J wW)d*Y0a,M30{NuI^29HT~R3% .3a-:@)ȡ63sFٽvf:mw{ sTfi3 02a6y`y!LYp2u6Q?v³t8J4~Q6R.fQ>P%hg g;c99Ki. 4ki^!m=4ob|ft#_S TJTFnOi i2@RZD'2dZNgPKtz V::I0E jW)JP}Nk:~ӄF:CtNőt(ub])*+gzq>mhw6n/q&,�`_&/K2X~>ix&;<Mix&^e?,yYe2!G<03cl=bi d+D!\lSap.>!=~m, ׍T.pL ~3;['&XoW`9(Nܻ/X2ja_ Ex 3/)Q K[xWlnOLFPsfkNoo޹}vBUK={veOֱNkeTs/ۍG\hD1)wv3ii+~Z~쫿.>@6G�l]yJ{2UZj~ V'X?fc?xl2b+[8jP.YA`Х6ќ1jS n $A϶,wcEZf>jrNR~N"$�6˘}pJ5:|vCv%O셵/3g7|+9=hpOo3v5F-Ppth%wrgj<sRRe*܇˚LLN:`_YvK,Fdwc^PV.aChD@Ҍ8}}p.LTMV{ZE-sBKjk}Nn':ԕAWG-UIM|ڲE=lh#ܕlC&ې*ԭ쉵qtdŨIpv3$q..AWb[W˒eJ9ESwь-Ip;WXjJIޝ)J c1}0+q覹±BY ^Xqly~IpBpb*PPqp $16ex!ֱgS~u///{@xn FW-%1_|#b6DˁC{L+[j_p.o82tS+8iRsC{|ȣ0 br4D-s`:5x yo]^!# Ca).p)}Up5}gѷx}[~'|~'W|S D62<A>S_ADO,LҩRd0̤sc3^tM 7= HO1|N Vd"Gd\!" F)b(cbqD('"ʭgBq 6b8JlGdGS bxUocbQ2'J\m8V<@.j%8m(תb-"h1qJ,N'je"**"UCZ/ړ$=BXj_h+m"fF`d.VFf[86Yb)NlNjm!q^ŶZi;Me;WmLc[/ڶsmy|B"Kbqqsqq:C\aOW}*@q=O\c%z{`-6KjI1lgљt ":[qD}nc/RYй4p)hx4<\a\J3j8.8, %Ǜ9ﮢ<t188ojBK,4 S|vlR>r9\8O^e{ԺNz}MZ%Wgj xܳg{{`]),Étt V9ŏsjy|vXoΧx!gi;`ܮ=iоPl̤-ɮf˜(;D@0sG0 Y[]UKY5N%6' ~C'Nw93-# ێִ+ SsEzwZO8AbO>U:Hadv],0 ;g0 [` 2աkr7m+i:?y<旲pp{yh>ܴyluϖnIa.r̯;̯ڒ HvpZ{X+moIaGcUPkU#Z`zjLp_ky!E=n$!~hN x97d V�+a#lbl ӖNoeǺiv $..^+x&0Et 68NJq`\)b/\,+CI<7Gax ng.)xQ¿ eYxu&ocx'h>N$xpJP;84z8]@8hx{D mp\@OГ\Aa5NcJg;ӵN7ͦ䂽C^2-%}"G $Ot?PKwث)��O��PK���|:���������������dumphd/core/DiscSet.classW[pWi#{NR KvDRđ%QmˉרPZZ˒]q -Q{Ii..JovH20afx7^L;[[Nf|?ο_]DL1^XF<ephvB_%<<a GRx"J&r cJvKUR0! ^0%4g`c]ErĈhO^5ђlb$q7LJ,OHh)TʎQv RV!eZaJ|,u`{֬] õbdqTYoH•׹  B-mV.SCc+۽lY[1E3˺SHxl#fqڰzjP MXCpS:dOl%*(-̲Xľudz!c[STXU ' ̢Qq@բI]kDqA/ ,w g9(A/Upj$'WD *(<tDn $VpvJ6llunҰ|Õ^)|?˧F2ڙ  # n.aݫ`g ++sUXG'1 1 |q%ɇC^,M7E2־`/(o)V֤"ܙ1!fX<W1of_ŴJS#aGs (IYNMV˨  wX`}~(S]H{dT)e2*U`ŋRq*ڳV`ℌ̔a;&{t<ٴ.HjuQ6p.෹Oid<+XSUqHgmiMl3\st,2^3ѴLvP+2^c%CmdT k'mTJӂ\oP%+xo 9`:'y,BnUsP6N:rޞ=✓K(B .`[,xWlyԙ1-+cOrד_,UF$8͆bW&zI o S'C]hF^PLKR+[kmk|A v\}K}}xvNz|_ϮGhuf%.zh;&F5;|z@/k-/hUC'b }L0>H/{ĉD/"1(2k(ڸaFX"ep__q?{Ѯekg!%YAwerqŭ )h_" \K6ێ}J#LoBCWvyNIU>Y k lfװgkack@|z �}b^l}0zH<Gxl1C+xb=& S#gS?G?FkL=MS_\%yW%X躄%,eSq~[xt~%َ={Z:!]Xr;n<{0*Q*1܏yOPO#i<x<[i!^e?eox_x&;oI[b;K|Agn$ ^6c| مd#`|UHSNP\{Fr,,C.h*rAn@!p;8?&aq߹7�PKcWh����PK���:���������������dumphd/core/DumpHD.class|`SGޓ\c@Tэ Ŕ`l SB-@`ˎ$&ȑ/=қ/bCHO..K%KB̾'YeR۝]O?�ci:ƺ5#ÁT]ο?Z3rުu(9,/,Im p0f2BJQ}(ƀ.E!-_9sAEɼrLmԈ RcmΞ4B >6`Ȳ@$_OFaٳ>Tgkqmk"H﯎zǠfdaaQU,ڪ u=J9(,+V_^3TbEVA5HuE AȲDPJ( a</̔vԋ-1j$H6% FKW"gn4D4-: Flx  a >&gtB ЍKy&d@&Fpdqi ݡƘ:О7 Lz"3Vb>҄^ЛKM0ҁx8Ӌ+bt(5hG=p&E&jU4W`(PX*^_jQZ_nWjѵA#J; M/p9u [H>Vyߪ&(:C5! ZK3W2 m�uQ c 1ǀKLbETe]X+Hz`H1`%)ܙU3FT^ ^Pͥ.I4`5’ҙ۱ G}C5n~֓7֨zN@*աۥ5!3S/;a]nt7%6&6=%!4Wl&'[ZaӢÉ=mjj#0fٱKYf:=ń߳]XTDTN :Xr5Fb'Gpgv%0l\: 2p\yB1|c uhF 7k}j|@upu5dEߧLW5kk|z 1hWBjsI > < O_�p9 ? Dh -Z^OBa;BDu/bUA+b@J%8`Ea'3ؘ*Ҟ5b يUzD+qKOD gtZSQfؓuɜB/@uZRrjmյ؀;hT%k_QLɱ˄O`QR#h *S\-&==:.zLx�$ây&o+šM^vb嫩DT\rc&<OdVNU7�?/&<B#0Sn2iX_Oj7jAFf5Dx5y +\DE^'G*OBQF=d<56%Us4j҂k%x|Ѧ\6KwhIB(ޅ(,.(ZX5 m+vYx>`?$.鼲}L[wo>bfL.= ?&|_kj:1XjDrs -)Q\|g~`9FS23%~$f6XUSC1^jfWT0PA"n Hah0h @\ib p$9tZ|kk`V{F jg{y=/3̝u7$ fC[s)'{ѱ7jb_G,Wm[,p I�,$^p`;#juQTj!?oF,1rb.iIbKO&Dʍ#Ьp$95vhmq18NDZ^p#& <a]fvBs6N2H, +)L]hy)8MǩdmsC(vjn7f!8#HbU6p:9)%'&"ՓaR?²1 + `S4s’Y%E gXNl7CF&ݍM<)vSPDP^&.Rx?RNV%$kRr/"\~J5)p48]Ez@?ͱRSB6 ZzZ5HҨYp$j{Z ky’PyǶ|ѵoug<W5)2ߊv1Y%ÓI%L[7Fվx]3܋C3 !jb#;:'Ynh&lqUDNikIOēH:F]H(E%!!5>&_a&_|T*(OtQh}~cH$[[M<O&pU1Qc^0Lt&ѥqt<}YBǣ_sg[BJ71?R(>T&^I-6iÒ `R5;<!\%rKu/R8^!4cqfO‹IdXcmFǫHm|XI6$-Oxj{fXXX^\kEڳJf@'ee" f41A%s 4 b$ [aH<tc0{!=O#MFxS`՚ rK|voC3`TRW4AM"B'%!Pz˶z pVq]4uňBC$X?̽$űGL|n W+pv�:VH;FvKld-ƽ0AǧvKR2go:}6LHgi`!O~gB̶󒥖yė966"Z= |r}V@ߠ&fw^0)"'v ,E:R7R p$h5=N?~@)PMrU G&~*d?bM)Wj\i93pb28$ۙf +/w;+{&[ܯw@olkxz8xR>',klcڲ 1sS1ڑG"")S7a 'wE-(\a2eV hutjφa%<&OM\e3J(ViSo?X, ToRu aMb=&Jhͬi5v]RZ -0D8,9oq%%ң!^kֲ5Tyl @YZ&)z*~ԭdW}.(/Liנ _J/d:_ 4 N%YH(CL1DlM1 V<u`&r-%Tx&M1ó$vYGb[O&18adՁc; $emKBիA>5k봤kI#=/UdmRE[=@L!d[lg]}Ay^/_pE]`zo&BD)fY2X37ԯBee|h_X Rp,`@ҦҶ ëb(7<1""{ӶAW_1a ai/2E%뎮1R%LɡbuEn@0űbPs^Ls A8(N Q tQӶh6ێ j2Jb[kXq5}$$9a"$Ɇ(WS QO/0x5,MLa5 SEbIA_.y6SFvE+46 d%4.G"T7LqC%NWao6l*Hrd'u<[Mq mtSlFWq`)gZzQ'%z)ZyazD˽!(tB#5ќȿ"&ҙM q mؐmm*Tto;JlͯqSmڒ|ôHQ`{ٳŦWƼ�_l+ Zq5>Iv)ilG7 TY2r^&>,j' Ȯ~tq  }<3x(3q)eo/kJHkimG] VXFl%i'|rl47K2n5hیXFkѐ_0B|XIREwcڙ )_GDK9xqAF-=b?:o ڙ g?ۧ륊!厹wU@c'z )rΨ<ITʸIjVoIˠ_و( #~y:9vvkm9iQ1ژݰ脤x-0Mx-k4Pdwg=>^7K 6%Cm_m4fŮ4CvV~MI,WSOt(\Ue_K j"k 5{Ǧ]g p!(=Z^ =B8 ѾbGv AŹ!"dܶlh!ېd�t Y4`X" k{)EeEn}^PegVv)wd 2/Q"iMu9.N QBRŢSf27]<pD凅2% >aHrf=R.yi ɻe/RUIɧϧ`GL$t:>]WԆJ 9tyv nTPb50섫. kn|F2W1j|^(umCL<a dj9߳㎁CF 4X7xzevrE|W೯-"+l-K, gWShe_B1R_@Ȑ i e.wکhwɐfUgBMڐSd 5W3egH Ifwb4 s&5:2A?Ĕbٮ9uxe*g{2IY^s!gSeOQ{Δgϋ\ԓ7">Z 9aRF`Ƶ{˓IwHaTJ\nWB8uSv~諫 pK#ɁVNCKN+~f<H>ٸ` 9i(M NPmH6_6削7f|Rz|%e~pMmZ@4XO®6?`ާ!>vy(kHHJx7ɷ:\_gJiȤ� ̚al2a %Fa/$85Fm>zQ PW 9~X1JԾu/�]R~/1IE9զ\2Hr:Iv¡nZSIZ8E:ߩkY,Rc _ݠ25NKmJax yƾ&<J?m:_r[>)<0$ -@D[mRD;h �'&ww»SST92RC={?{Ao}S9h} i6P* zNi9W.HV;[aN#wBA. 3V߁I!;\0x ˈ0a4d80"p4P%%Y~-߹ ڧ 7 '88`*Y\ĹshPA!#b(.32\  vbKc[VXEl $5|S:UV{ TmV32ln DM=pBU+T?o-9@"8r7A4ðD^Hh_2c;p}0ZkK㲸֟Գv@lex Υ)) +?L2:&a…PVDeQz :w 8Hoi8} 7 vwT*0&VdWU4eT"`t= TzZ 7m;!j~Gp jӞeZ7�%rF(ޮK"s; ;H޹ vC-V0KTP^0Zm|zogU#3#{Bx9jh7Ale}J#|f|ĸW'涁;/&hm;wرc_q Ue"?q3-?qiŽw-b:cH3mtD6,܄zvTS&zWvh&>LtǗz4`I+Џhhv- 2lzf[}"N ?@on"<o^> ;pQD_y͵/J7mYǾ bKx59SۃCIpٚlm ܅a܅i8}q3[U?m_qW\5 1~Rʤ$PYĹ@߿!'H# q^7+6u泯u'-ne+.2AURUWyz؍:uol'ĔUoy d]s),pwn*\ .ZHz2\ Vy0]Wg+ LRf(7À9fbbxk3$Qgvr*6hWc_)"v(0q #ϊ\t؞P?/NJ$5 j Τtv!XóNaFF^aDnh p3˼ ҳL:pgC0ɝf3x\IǃLAfڏ;(\5ʓ;ITrX.n3wS 4#V=Ue/=xz3H<݉cb<IֱZsvXkWRF t!p(Ǡ͕gJ3:%HOFx/RaL-W-O߇7?&|oa&{",<xSz,|k#q|M9nu_c |/6|~/(lICb" 1(GX.qVkcxSū&n#_aw-#a_9�!8DfP9q-cl<y"S<Bތ8Fűe'_ =ZnX Z1NJpҎZ5iXmxv9q j ˵/p5+\聋1 8q9pc9p4౎-ws\kWZmt܃;α C=؇;k78FMN&nvf|<9 8)iY9Mx<y<u^9/Νxq^7nwz:^+x>�և N7s&'m}ީ_w]xiS[~|@ C!|腏>a }0O#iHQy_0|#l\U0O?]Đ+?qu᧮!+_׮y gTsRK]eTzsL ) ]ⵔi43p}dxB)d:EGVt~]&[YZ2[āS5x6St 3ǛÀ-x zI?ex:v%0"qq&T8%]k:ψҭg0J4sux75A7##)Nc iqZ [ @6͟`ux Z^`3 =i6W HaR4O uMyɇ( 7trX�uxbwIwElF?@w/q�F/D0Yo`($.[D*“I(Swvg -: WIU] ;w)m̥wN( y<b%jxUFۅ?E&EAZ` M#'B0 K'L R`Mj#6HfUyB �)jC*2͂j78<�!8$ 6 $N-҈W:v'"a)U[/Vp(X>5xx>GI}vY+񵙒U|ZSד4ZαLxW[N,1m2jm5o7;NNcO NJc t 6جf9ٳ m}o텏-X;Tgf7(ö]&کNC" ^ K¥G]Dk8*P`tvUbۯEFQy fvW2),p('E<Aj@e"O0uXVfg7%n>%TڨXV5IE{S[mPBOފ+Qn[=Z$FTY6Zh^-}AYmOc1={,U:y 0VYc u-j Քݓ/$~p})V-vckEG#\-:R+Cs Ox칝[MoeqL3ob!BXl!_[Tef6͛"/i6)Rl`qupY-lt͌�d5,2D Ym@%"<[bVR'wRR]ج( XfhBzf"Nƴ}1c&�i-]Ł{;_UrBZ;^*HzVz'FT_fY4'em^VWVuw.lU˔VL+5S"ݵ8er<L uǭ̶VMfo#ooǾ[ĥv˘o}m~O83"׶/QJm^ot;73^kW')x~-OB'+{~R,f^T)l0fMgmKYGoYz9f״^s̤ktXsNkNCkNZu`{ɥu\GfBblJ@gYv?rbxlq!/-wi)f4iJd8fIdf/t]xS?cLlUa_÷!|T܌cϋr̗`r5VhAVTk;f ~Q~:p<;(},v} Dy~nlԗ???c1MJ8`kkkuZ Ezu]^It}9=w?g*U;=====C`h1V Oύ;=PK<XSOO~z<74 3 vBA3bnu捰S a�b @ b\ b89$raȃ3D>\,Feb$.Fn1c1^M1  _)Q!f`Q,H!hQS)ȭsXQDFD9,b>'vQS<{Xe1>.DC,OűX?!Z1TpNZ1Yԉi"$zQ%ְX/bh :I,nY<"NOH'^[Hoĩ2M!{3I3{a%1'ˁrV!r*Wȓ<y8K-ΕOT~ ._5!ibCz+J-_\WkGk:Q;Gܤh۴Sq&/+vi?]<p=Î0cx1A<8RQ(9J_)2cxαA8I8S8GHqxqxqxqa #Sw OK!vz7N9\|';'Gg8,5␳AwR:ϓNvv^)SOnν2t~!{;+8CҧdC=GxW<}җ&C?Uϐ} d"»% !GxOއW#>hr-pc1yFaTRǨ�o%?M{D3cWxu9xWi| '_)9Wʗ]dk,r͔3]erk,qVY:[s]&#2Ant$7]{剮Ir9ߵ_V\Nr{r2=_r_vNq-#?ʨrѽ]nrɓrSt.yLSF<}d3Dn <3fD\yg⩔xrIyN!6Y﹑pn&{>yp^"yWbj:!#)fhr9QhN'r9`kv< ln M{)^NWL&}Hyy1P{0=a~ nwP;$ |C%+|P܃A Bp~u<}b'~뀁CVPs?bu1}h!w]D%&7<A/GT9k-:W<-$T s 4%J0j= (iCaxL<Z>!uA32䈽beQ/4L@X(RGTx[<C:.E]ZBKwz`I#ݹ< @JT9ȯ,XI(^:+XIÍvK\<qb\R~+9sE5='PytdSIso<b%G)Vr}2Q/ǘ/5Jjx״ZZi5b˱y@-V,(ډ>�tx{~H2:oIڭiTӽCDHҬWs,+ ^e'ETAnw;goD|+r@/$t1ׂe\QNE3_'G8M`4Cx fi|"wP!8Ds˘T7maHg!Rp%bW0@$uFrY`<V [*v1I{LHv% ]l,C SX<1f)xKgNT]/^8w!1 ?)a#ީjvE)ދWZHQbFu{oDȳ9Vy[ûՋE+ Eb6'lUev;ǾW92'~_u7cwT} &w l$dJA !l X�fϻ6!`y+L^ U:(o-F8KfH[*y\#=p~{P[>Cؗ91-DZI<J>Ei#Qg;R_,pan/UR^<J; H'K%ռ褎jcwP] cp'Htu ;? g@ "U|e*E< (O4hs;l 4.w#s</A| daK}sPb/tKH3GIPҔo(W6)0r;ۏ52Ck`ރL>a0U*&6;չ)5Cϼ!߇{ w٦>ޥfSb: o|˄82.>oN9-ҝijA,Z9pqq^+j7srÉ}vm;Q?% c|A46#m$-|AOa1a⃰O=h8s4uӤm @ B)൧~81`Zef /r,а\^)k}gEL9PU:2<R"W܃Og-rKo2SX MXg8VACRK^ot1L5~,S'<v\BghMlz)' ScbX(VٞѱXanP+̋*bEdUPz_D :1˙е?uFr1Z3+6dvG38)Ve?"ςL� jQ**|IӷGVW#rm%sץz~i^G^~<~1)V`)UAtOA" pn:-TʻҠ}n_ˀZ& юl;R>I i}R j`I[!p6eqj` iyJCHxZ /kcm>NIԎ4�{jq6s8VSp6jP+"\c67ih<Ki%Misf 1;>UbViKSm~jjD- iPmjD-$z1[k 8NZXmmr7iD[Xo"5bܠV؁JnJEl0- 3fr?(ny"e\+OW]3_ SNg5tO=Dhc(0&CEO#|S83 p@}Af zʓ+"O$TPK@c3��lj��PK���:������������(���dumphd/core/KeyData$UnitMappingSet.classR]@=vcQ]V]6Y0Yq,iZ&aKAgGwBaUCns/�^"r`1Lr6[qkR˥,#3s^Lsik6g%EeR_$G HN +%Ar8, utLĖH3Y/~A*L3+”}dއZcAa]2{Ѹcpa3l&XS>id<J>lQ&l4?0cc7ZT*o)ʹE!IR}[xZePtYF'v2vC(E0D،V؊a 7/qηFw;>gb|R$< Oh)G;:\JŸ3*:>!`6 !PK(d��!��PK���:���������������dumphd/core/KeyData.classX |gqtZe>l>ɒ-Bdɒ%9TGqpҭtwLi1!UBJZC $$8I6u!%4hy@H[N8Tiwfv|ͧ^}�M :LFIvl_8vI(> 7Nǎjiw_P׍C^zT:O3ղ|ŏ^A`MI= pOVHh_ tk$/+44,ćzJ`CqH45݄t4=JL'63Nb[&nDh=Ŵ8GiS98mΡg90:3IXn1 -vC؟LVgZeb)~Y4t%c}T}6 Y; G9ۄ)W{ wF\O"y@�9RH>'t T h*T hUliW ^,h/vApux6 h:ԭb 63j=Z8&ҽ FߒB<Jk18ȋj3x1}!|E@&6%cz$#dTљUIsȑu?׉Z0iH/5:t !YYkUzsO0]H4ō=jXMYذrZmR&&D[lRXF%5˽G9akረX) gS`3}&KSgo_+o|OT~n1?Sa50D}I)l<nU14.؄ Gz<&*k\IR_^*`ݩlJ݈_q+6t"LrʖZ(5GT:'Tmch6l3۟+cqCl0"16PY+b\\1!㳄v|=% l/d?|w_b>߷m:mjz~*[ |é)19OcDFS9 7Ex,izmɥCr x9cF/4_?"L<D(7ϟr\tQHN5ƿp_ʭ̑#Zҋp_b=uOO7|UBjq^z k|O-΢Q!a~pw҉ڜ.E(hȦ}{ܵ|+d˃b,_v7MnϘ|OčZzI+*ñI$caU6~B4}J8dlrӡ6_"nF_֔y/U<縔gXܸ;ddڦ=ԍxk{�9 ~f b_$TmVŰ˄u^ߒ.I;+ܩ h߶IxT%igDR:^DS_1J<FÕFA^$JOhi>U*Jj,z%Njh֎tlwLv=qxVFXRtZKnZ_TJq�d8H?BijxFZU psƵްdd_k{ڴtqS*mb6R$Ju$79Ai7pjz{ f ۠ {] :ho:,6v+->vuTj P<%{éƻp ꦶ%*'-3׸R]Ř[a<߂{/6ʂ2^d/Bj 2 u#a7u{|aM9o,&^'g=S,|b[vhfhy=g\E(^aC<9kg gP>\us1oN00?q; \iDHBCOaգkws O8qKgBUg0:\c£F9W&[ö[rk+1 f+GU2@ q \d[j8jan%jE5wI5kuVsI0%m\ٯlչ_\®~ \ۘUq j v&ɥggX! In2-w]N7܉w 7MxYjMo7<lYNΛN;Yƀw9w_EG[LG[LG[h%f^b8a+䎶~E˲LC.ـŲe3t/$\/"O#[=fXmß '֑bܴR.;IםPJ -9S!h7nZ&P\,slέ9lѺ!*8*R%S56zK5ʣ$7a#" ^gP@մ⣆lICuiꮪ̺wY˪{(.[̪n wGi^ҵBE :#8'Ep.{+#l|jOn"zP4ֈps:,sQuZniW8s$Œէ.?mw`k (xNnA{M%"I)pp:)= ^p=+Q:et($=n]"p٘Hȼ5![2y4ZsBnŽ&lyNȻDA'-8D+[-I52k&ehғҘג܏Q/-n.pW`�lDZ/SU,hD;O]YtDEnO E8~j5#PvqD9r \/wb}mt{.\CA:Q<n/bFdi3F4\b^D4Lj t2x̡[NGmﲑV<b.RԳSAEU_SUڪkWdq axy<6ZUj_론 I)wKH|oixo0pWK|Qy~S֒3Gyz#S9g2Nw1nnUP!ǞP%j4YS1:Z$[S:ZdN*s_b`Y1[Ly[y) UN .67I<=ZzA_~^-y @c ӫ'N)N,(.<x𨒏/(R"W)"jմ]YC-J] eŔrw0jYb${,ϐB|S=vn/r6hQl4Pt n<pmt]›9-܀dˋ(|I'/7 ɍ.bwm+sV9*Ą\@.)Ub 䕇sqWp*x *UMپCVEڔq4h[)9T8Ju+y-6]k=,,3)n#7+a1U=$`(*}|W|ȷ;/6%W�cұ͕-A @8qV[L = yn+m(RQAҁ:70< Zd? PKEsh⤭Η3'갋"$S௕$lrb "qprSTQ]-2}oV{9=yvS+wPKyP��!��PK���:���������������dumphd/core/KeyDataFile.classz |T9 Y'0!N a $  !I2d3AEEEiVA5�..uZwZD{0 ~}==s9��#=:0 ͭ [3¾@_ ! _a/pv90B̊Ye5es~V] +Ca_0<�|᰿-MmU $!2Pn "<&o@/.,/.VYQ:jn͒<kWW."\#T.>%kF!奆G8"C*C:`r`E7 3rD$SJN++_fT!ۻFׅlɚC$??:-bU߱/k$[kh) 64OB2`hYc=ba ~/7 TC!Km4J}W"n €.Ii,6&SIo<m*0y%M2Mu(!DPJ# y=惃rg�!1J]ɒYkr ot8%Mʼn3^@/3^. 6g$$nI7x|”=weT}%:\_ϴ-y 'D!Kz2-̲UpʾөG1*ffL0S@9(, U Ȇ# <+` Hh0HB  SuXЧcYT7VΖR}t<AΓL6Ñ:E›#̄aYL96$_Sy7hS;hoE^'5 J*#JeB+pncPb+#jgJXê A[-m|Ĺc/E/\ӧx{VjI|,x ^.~Fn-6ڟzu A^k W+D,U~er<4l^;-[T*Ip-lm- Eq^DrFc| r&[%.gHMܬHWZ#lֵʂC۸pa4\2[fS&[BfF[=0- 3tjID5 xvRn9ÿ*.*H[!򑯵lPiL)I+)HU ␪sa o<J p�Lx~Ó]eJS[|WDEe\UOFRA 5ˁgN\^f)L8t^ "SF7(rNK ~Qܐ'ogg?_hL2*E۶#iI٬ J~Nb^I xL$@[ކw+V(^H5U'=.>o[dv>& )|F݃#- WԲPn|J@h$MFdy:k%΢R#R:w|'CB?o$tJS CD:|YJQ*1 ,VHj@U9 <UY)S)vnq1eAs%bo}oB+_vY0[`jminKm;8P hm5QÅ*5X5Ч'ɼ7ڢRrzJwAnJ%\DjqE*QPl&QumVF۫D8aqQ4~xXL"Nfd%& N8 'S.l$a7X?ZU hil}t 1-iYس6rUJ_ю>G^=jdm 1uĢgmf Y]XU467(gC5uX-p.֨9Sz|k I"~vk;9<ĽY\H/.xQF(["l{cz:OH.1~<xBySxBNG}{%)VDϕȈy؀s{Ʀ@MԱ!פBN$6a-< zxsBJ#W.2Nr[-<.F{j, ޅyt<|mf56dAne>dË^ q/4*Yr}w ՎKiEl6�_rx99f5lh0q=^A.F?ԞhM ֑^#6?x=5^;AHvKF&hSoi4TN[ތJ!H+aF,LtZڛ 6oE:RAg TCe_Twbx*q:-Bemm-mr>˘^31R&wuܝ~*6l.z{ok 4’#PMr1J)o|:TH&O ->M% fʙCUE׊{#hjiYN0E~ALʊp$Ogd+b6tťi*X76d3BS7dB3ڦB~ߢ&.PBJ_^uMvO?~+d;9ap"F \) /%ʪWVn:7P.o"z(zpHv'?~'~O>T3[j0Dr4Rj錚meSvDw ұS$<Ӧ-Ȼqd%( 4qlEv&XzNsuW1-¿7c-X_O Y&;?ˑj0to] bH,OP]j LX|T1ɾDcCB10 B'cUSfĄKX/y`c5WJ/bc&tcnDTc]jv!#%OL# 3v`gȊ"ZVBMbG٬sx*(RS˦}*$JJ_Hl`jWjd= lKm9 -w=\5YLEj45zgM̝b)WQ1>@Si'JilkiC?an 6xi x[|s^uH>KTX>b09yW <YN(rl@rÁ~-mUC<H׶HOvO)~+* ue*"wL_Yahيժ괪dƵ~5u,#'qZC*&(-s>*l' jkWSW:jOQD~"?#?ƾVF$ku/ Kei+2yOK_Peh-/Su/SF#̚sK `X sZGT 98@ԷѴ:I7JAT:EơZ'Vee(* lS5o(jh[I϶[tv ݶl/&;o\&LxIԷ;]FXC7/Zv:*BSI0�s 6ӗN_ vIp_p'wrܷKNoWw&}qG}՛_;wx{@ @ރTz"ݼG髖S _è1){a쯡8mB69R3(!f!2`5r1  e }9NkU}@TrveC|̖3Z>jV>E Җ쁟I$Ed LAMVA-AZx/脵O^G쎩_JTi#yMȟ7﶑1pL{a:s~8 sAX;9.U~\ GJYy#cd:/ɲ+_*ºNpfx֕t u5EMFVdqeG)�EOrp³ )}=&1}p )jq`# ].ݭpe-0H{|?X=pG4m?]n[źK]<Xw= NxZL =91 [ C369֥n $/n Fh I#;IïX)Q*v5K.f a^[0_{sPlt>Dre脯/Ҿʁ'#"l9嶴綥#ODj5Y1A,P?^hd+)^IդC-Ж%j!ገ'ttiK$>gY!ED 띎,O٩jÙJ5b:7` -8 ކ.U >gPSWT+ÿPJʣ8Dǹh_ā3Q`L a2a %шa6n k0�|�s8 |Nsa>~t?P:ꍠ#HGӡ+|qlg qvNfr6TvNc[mrvv gr,hi!*-&BI fe@_cӾ'- ҚӁB0P8G 856�clrt͡jl dg`U#($HITCq:<a8O; zQa{bp$Exp8 , ӟ=X3+p~\J-p 9T*5TvF֜/ 3/vibYIK~LRFt #pE Ҕ3L'eK z=BNNNNx2zlDN?NY�I3+^)o!H)S )68aI ?3_@ҵq^6c̘$3&ɌjF/.,R":ט:vBq{$e"n58V-k:q^'Χ*Vlۋ}XGB]&Qgϭ߃W$ uY6[{TfNj1vAF(}Pa>U1U|VYUgU�/$Dց-qm/2~Nn|n"F~k8ƚH$^ w(u)8 Q8m7Ft^\(E}x =+S25!xf'^[#ΥMl;8"A [( [iA[j]b3Gr#MNܶZQ+i3 `c^'16!f.51`Nĝ"w9m}}E.zTbezoyÔZ׸\a&\lTv'"=[vݩR;M=ՓFN%ϻ^VԟtPU@Gmw@tdz=W͎aも=w;څCWܩv ߊ=TI7;-p-;Pc{ ܩ)b/~?]BaAy'~Qܷ-tB1T39يۉrK:L ;@~)]lLGD OIC5eȈ!cv#ZN09Q{ G=ꋢb@TNҙM!Shgo"PqtB.9A EСP,"1PdfN&^djv)"dDvWNFDF7|D<]t aErolpƉv*Y'dCD+qtۍ {CٖS:KҞ'Γ]ZQ没'?e#s72.}Rғ=E'mi5S==_/J{J=O߷gRwM)Lǝs&^$ k,J)wb{GĈq< N6D uBݣOCx6f.{lTPGih˾\;ZWwᢶovft*xJJ΃b\\Qh؁㙹`OcjztIP|7eKl^m<XPۅٱ=[RKQ׎ɞvRBEδ`ug+H$3 6K B 9NXi[tGzmmfې8"Gv]hkIOAwϠSQ-RQ'n6oqO\ _/#qʺoid>5*64jAXcg4P<HϪptl)#3Ob2#rX0Е-sE(NaW_1R ڹ?v1Q=Vъ c՟_/_d|]:ftS8?%3@=~< -L2;\ R6w %g}ᷬ<k,e}6>f^!؇Fi8Mb6KX)NfSM٬ 鸈,`jVk|-_%x5;o`u-2|fς$kC\|y[gk|]߲5.ciJa?g*6]&kױW|<=Ķ}l;{`/;{a߳GNvg'T$ɞ|;D+;=3%̿a/k+Z{M+bkZ%{S6w{fm;P}uOsgMg_L-}kf*[V͎mgq88m#ma{$Kub>n>b<^=u|koN5|}+w<p|nzq׫$}dOC|~>/ׯgWM|~g*-^^kBO1%(~QhΗyy0o5Vq_e5/3~/7W/;'*j&ot-&1(3ñhw;=0qq1?;7;#]c2?ß3f/g7_1O寚#kh9mVw̟wzh?1Og&z̿4_�ߚ/`)LMsz4sf8hPKvҝu2˴,mZ-׹Mwޮywk'B;h'Zkm6N0DjG$ 21F;]L*D6CTib6[,֪Rmi&&čByŃy[לYלp⡱ AǦ y+hW\\V*R\#* @ I,6RU8zA.`B2k:v@he E]BPl=AP&jإ$-se`.'(flq g v]IjTuXA(PY6o8dW6 `V`9Q̮&{<{c fh]KP(d+QK5^fly('+a@Lj L|]OcWZac yޘncfڨ^ʱ)H|s%ܯ e[ʖr8Gau:lDiqbQ4*@FaY,U9`{@Lg=GltxbDљJFZlH<qaHw֏db{cH& MXӾ)~PM1 gw0Y?忶2 [0gaH8V�L7XDb^'>54tdQmuϋ<,47մl+ܚJ:odشR'Dygo>zgcKϡ�m0д&kAph n-YZ< $OI0Sj`v zrh֮v_mE_φNu.u#lvЦZS~FbPeT`˟6Ho8WF"֨+ϔ5T+[}"툌۴ʋ:6?gm>zkR}FrrVr-KFw+6~;0F P=@.rnX= u^k\z%D{XO"b.L9Vxrtʱ\96\2Lә PK !��C��PK���:������������ ���dumphd/core/MultiplexedArf.classWy|TId  fe *!ĀUd`2g&HVjkA.غX7ܭ[PhnjZ~$,T~1|;=ԗpCShko5CVZPB*s7-yU+WYM:U0$G@wXޟ0fi颲vYrQ܌GA7C͋}~Kax-̓q20"#-XP'xL!lZ]cQ/ET-o/ᑤZ_Kt(hMɫQyӪ{ΛF6uBV ȌUa% `P@b2V Cݙ "+̬6Z責zgIkd(82T0\z @N20:F\0Zz3 қe ;腳 Gtx#%�:N%C7P4r9%3ݘ !ۆq[)ʒ efSFPEm3WԊ`7C>&VdBűC(ꘕf} ii%eeY:*2y(GhU;2GgA-{G48[arBoڈJ6Y_0B3F<L~>Y0/z1⧝%:4 eQxaf?hz{ɱqoF|M163$<7 Dӳ˄%&$:DM,s ܽuzwE~g 7VMT(,<T)D q@ɍXh*Ľ v�VD Co2ϜO}ң竤'4aKǷ(yv47[!7;"V~ nG{Kq 0#AZ2'bEɸW긂J ^5pЀ6^k:\̵Bd Töێ$CDbnďt恵 G,:q3;(kL/$"B:,ȅ Nϟ6+93 M{k{|aO0HXJ_KˀGǍ=U  [3K[L3ij {/+쯔mA[q;Us0fRSCd qG\Qc.IzLDc^lc^~}p,o`;vpBD̊eBvb] 2ɘ jA)tʊ!qZM:R78Mn<t<J7C!OXTLz׋' <i Q;+@K5<Dg <'{W;d%^2dD}օ߳2k?rlo.wl=h*t?)sP(x¶1s=S-<␎?+LDj0L-a1ox#ft2f0oC2JъF ]b\sPV?4Љ >*OFseDžZfB##" 4գvc>}@^nOO ;$'Scu`umH&^ݗNYR-`L^7"d=e٩C(p|ո]t3f&ڲ@ p!O]|V$p998)n89nlZ[v24Xlv+wND~'qQ8dwcrځ)؁w#;3d2Os 4<.Ӎr:WNOm~9M |b.RLEJ`z23q*p> ,(:\,; v &-Ò^))mAvmD(od;c1n,BuCv#cTo+F]cj}7'64Uj'6C ]A/,'KvǾ s>2b8}6nlgFS8&.؈ ܁"DA߸㵘Μ/1TgtNZh&a-6l%�:yl#�6$61]%dMs3E`t/6MAi�+yUՔ~-rX/<(.Э1@UQ@lNo;q҈+$$t#B')r0 ~P'!ߍz Vf,`T-q '8AP2Ĝ/sX8u"qmި]iċʨqWLwByaHt&ʙ e7m¡pCO{htƝT?¯Eo{jv%:}N4rBW.?T'ޔxxF~H Lñ/zmU p社҅WrR_ ғp@z[U(WESյC1n/Qm͛6}k(cWYWbx(9N5Hxm+,x}ۅ* _?O=J^ )G%zlOۘkxWmSqP-JXkiUۢݡev�VeNmcLϷdr͌ gi{@ ]O`x ,DE^QNFC*l\7UkH/Q~BxO}kYHnÇ#ՅVx:O[B/'xJiS)Jf)VZJ*ժ_hkv]hW45V۠ҵ-jvݣ<Zڣj:I{YeiTvXjo*ODҎHRs<(U U#Gf'뤶p^#3 m%ϙj+dpTI+br<C[d-ZJ+|t$XO+]I칠iTR, Gec8(M c:\G2ys3L& q3GpNTUѫ9Fj9fHa))RI>W#XcJA�PKC �� ��PK �����3:������������ ���dumphd/gui/PK���:���������������dumphd/gui/FileTableModel.classVkSW~6 ,E%rj&V-A⥩\ZBAEd !y~_St#?ڱ9, ĩ3N'{9g_->t*.f'3@.edT MGm1ۖW<Ry.V2gcf>0옂 7Y> MM n`ef:P[2:ij6XSꦘPWPmfMG?U#QAV*PQA5h@jѠJ6i`}5 %EQ%̨n5iPr2o U1<ZÀ4c%xmS.as)Tt�)Pӆ}Lu͡ *3Cl2-雦FJ:2l_FϓHC(.DHOL^2-ڤgS 곔YTYHJ^fdLQ"'TYy{egQjiK[5vఆ*nSN_i^ͼA/TKLk8.˛Ȧ1FL%\V䌑x xOCE>. / V~ N%ךav];1"];#5lQ2LU.Im \1LhX�T2SƢOW'8Ш:'5$QEżS$/P SjYHִ3cZ3iY{ g!ili=?(`LTMRs)4*1;Pְ{<8Tzh,-2x@^gWBJ..jsIe\lsJtLm^Z WWtgv=Iz\.[9+XI96^{Z<5,yJyߕlm2a7I.>'./|zZk%q-mO#\Ŧ#l(Ghz!Zmm"2x XZѪ9(،-zaHOc}N@tU &Ѻ_nrb$PPѧBQyf;0_<@f4,JK+u"n}d bf)h:XĚ%㰓A*|PZi2}xe5�9  nwWa[YÇC>thn%|hEeyey+G$DSl>@+9X')s[L p jLqp"(4tӉpM AN>)];CO0UZ%|v[V0}i 'mۜZ¹{q(Y?<1hssA/m$`G)fdj54;f0m暥ՄOX8Iy[z"9ʕu'BFq.xs_pnn uBeMchY;Xs 2N:]zEiBe:\I&Iy mWvtqo]Y&W[&wzshW -� eocTPKI[63��} ��PK���:���������������dumphd/gui/MainFrame$1.class}R]k@=mvc쮵n?엚u$RZR,}MSIAQ|J%rsϜ̟_<EaKg4tt9.Ԕá&{.T)3ތ9<,>V_Isba2|"q0sF<Snp3:}KtũeLXDegv?avR8;Ny1唰ۏkydkqc6u'TpYCiwHX[&xd,/p5S}ڵ�s ^*K.'96d-]܀Ymސ-}G0寎sb-z\兼-5'֮k|_F/oa-l;;gW<M ~rSPK����PK���:���������������dumphd/gui/MainFrame$2.class}S]OA=RYj)ح}0&B!EH <CdwJIF?e3ERdw� xY 1LqxT r# Cp/e(kO5_Xd èeZuk iJ%K`z10#aCab -^-}ގC<:4K0caܠp f+8銀a䆑):3, <b!}_iDS/K* 0{ A_5z3٧\u(\ގ{mǧ5y eA- yqW,R}En[:KEMdFf0f<@9 slke&0oS^#GqaJqLZ*_2dԦUً{/6i֌upCQa9yJ\;\-E4)\aZ#`Ţ6zCzt6S]|+?Y c%$&-~S,Xb3.͗2Dy@<g(场4Tlԋֶ%34˨YFA/PK| vU����PK���:���������������dumphd/gui/MainFrame$3$1.classuTRA=~.Q `"Va>l0v7(~_UVy)KR{6 ,[5;3=�0E rmZ6*5ig]&#ɜC1_튱XVަ56J/#b8j׹EX<+-U HPsi'ҩ1 NY[65:88G)]_[[C([+ռa0?C2GTnf*-T>Ip/Reh)</90t`ҡqI u\%FC6<]O!!M%qYsӴ¨ 4avyu0W"XrjIg1j>K˘r]QU%ߕv2U, ۗ+ۨ.c)YJw`74\*u1Asj^6řgPiWk>A],a`WdQݖ`(DQH\~~:|n>)jc}^Jg6-ǣuQ"3<㮉2ÙƩ!֩T2m[{p.mZ "@Gaz":F7i;s 27HdޢUKxv裑ZH8~ P e^k 'pn4I0&&FJw-}Yb/X41Pگ7 ;=?('W@'CAGn6! )!c L~7KdG75%WDT4b\Py,PGӨl'QXPK&`M4�� ��PK���:���������������dumphd/gui/MainFrame$3$2.classuR[kA&Iͺ6ilco^֚v"D -( O6cf6-D "& ;sΙϧ�h0xwt'B>}91?y;nl ϰ_Ћ=LGmQCOz#ꬠCE -7EB%lU;ϒQO/y NrScNQbXQnlHj|EXeL y"]ٝNu l2,IN;}u4p2-܂MM3à*m w8aΓ$Vs4=aݓ.s5$tŁvT*=ξt +aзQ  ‘3lBcd)Eyhs?V*ŠM_IK$= ]Yz=,1ʦ=*;$Y$+,i2*) El|bkgJ3fo("Ο$~ JZAIUԨF]g2 M.ѝ̐< PK?����PK���:���������������dumphd/gui/MainFrame$3.classUksZE~�'56MMU6%/mVSX"(KrIg3Q_q|R:}yvϳ/ͷ� DNۭf=h[ua9h̼(CrGy[8fmG8�~^ޗ_2}K9%#]ߴ Qq/Ҋc7K т0<a`8!q 0 J#7ڭt+fKTI® ҿ`Ta8w<A[ҽܖ3LfK}\1K H3q 4e%s-2ymղe'E?d ^DK}8.eQ7l [5{,(֞rԋ21א~YT)b!q49K5Om_jXTdAs[oLJA%}ryu0ҵg) ,P帎Ó~E eZ79kGZ습oqtL[ EJ&ufQs%PDIs3&ǖ8ʨ<$hE\Ň> rl#2XH a6{j`=7J]]*lZ^JQC{L?yݵ1>@!:B(6V.}NS|.Zhڮ)<eҽZqL[y$s]MU t3.C` ϓ.PO ,͗/3Ptzܥs1~EpBc~{(k9 S8/x8n.mLw0 W7u#,obu!mB< DO#) ~+n7??pB/x;v>'Y 5|J#Ĵp <JL0Q'w8q0hv]H.x�F4 .Q6c1PKJ����PK���:���������������dumphd/gui/MainFrame$4.classTwUni-mj@ƴRL? Lf$FwX{D ,X߸AI%)3'}{{>CZjz67I]ّ#ݺyzWY;: e+=drO86 {]^be۵ëSۄdѫ/ 8F)9M.ٮl7ʯȪ%ϒζmIabM3,#[ʿMU#Ε\6G^5ppbW0I8&-KA@&4g vlN5Qņ_w.p< _S&b#62A)ˎZ 'RP-ύk9UXVi\18Lp1gA枃j9PiXk\R=󜾙CD+~Ew~pI{жd'5B99\nZ|)ɪr2>=+U#PaE톄0e|$f4Gtn#a1-WXe.-7 l 'ҵ \3O*"<X&dLUm0}k++iFf6όr({ f1.{mRqfKĜfrqPaëSc_\YnI<� `GW#=xBїغkg="+v4eK8c30q2+.f8uKyBמk}X2xϭ^N?ƏIK6_Y*< Qr0NMny p- :v#XQO.>x||&b,9Svk顿AEIтRIQJ/PK0�����PK���:���������������dumphd/gui/MainFrame$5$1.classuSmOA~-' AĶ ׊ $EM4|ޭv CE?8{mP>3;;!fݸwqvS;IkjX]֡t"ㄙs1JQ 6U7S^0_Er+ \6a`,LQ$d+0|Z2x'Z]I(5}GtD�#F|l+Ry؍0T, \'%MaB8 CN49naa:o6 5Jz td_ٰ}TX--vnq3Ydx{UО~8܋1OőnЭ2t&*XTr~:kN AOAŇȖREd̗J`+PXzidxQ`[OyBm;L~M8W4x8>ĖɶFb:gP_sKo0Oq7OpUr^>}NVa''1 ~@r G_PK5jp����PK���:���������������dumphd/gui/MainFrame$5$2.classuRjA=I:11j5IAR[P*(?ٲUD_B/PMj9s;??b棓arHm9RLSG cd*39FT<*P}6Mwa#mt֚%jʻq \pࢄ%*jR zڨ|WkُދD,ے3\R) a;&V\%4[ aAJSo=SpNXv.p(93ۀsWqj_n,g=Dq@e8hʹ 2*y'#'ݑ2_*O ɞ3cT4U)6xt%^ZN9^U^`q;[_!:߱\\quFe/cWИ<h|utև>OcXt J"69ywaTgGPK��?��PK���:���������������dumphd/gui/MainFrame$5.class}SmOP~6(D_1(QtH4M4$~[kMہ O~R&F?xni067=}=O�+kHZNlls[/!Ca:m{ 5 #V<nh>BsvJWcen$i}D9e~Z2 yt1b !]V# ە;nSDӑ|ó+|[c2!'F*q:#"_x~W3Fb svY1YTQJu0\!U*UVt1 iJM3p]#x־r50,*گҮhKc *˳X$hz!V>z5}huk]С;(Qb{ǵ-ovQԟz=ߒ[ӓ% OƦk9^`mv;UB~T`A U^\N 1wi^Y̞c^Y ßyE'5AX10f ۹c1 \ީ|qeT]<Bo1 Yz<},}UNh -xYLc5V tn2V9B~: 5ȸTԆmn:2vb[<1�PK7*,o����PK���:���������������dumphd/gui/MainFrame$6.class}TNQ]- mGKIբ\ox)1AElg)~$coR9AZd2{Y{ۚ�7l!Fw{ͺxNMxz(ڲd!A}ᴄpJWY%L8PܗrV\~ՋeHUM/*,2uw˞bw6!es6,ǀ$2xQ/7:n$d+Z"3!f*,q9tTς-넩bkkRa(aqLȌ9QЉ<-yVB))\Da+%\&R= NS v~(અi P$$*jYWth,xwl<מb2ma0CQ׻03;t6agM;j? cS yMM[M; :+=-KX15m'I Zk #l Q$#Yq{@,c1H7]~מTi5[y3'Mef[{/`0|$9YzF`6;1fxƑX :>!M1N_XL<3I"m9X4Y\D 7;&[ ,?K;8CP6iPK`3z��o��PK���:������������,���dumphd/gui/MainFrame$LafActionListener.classUmoE~hܦiԱC/iyI)5nRRή@)wkκ;+/AwT=CY{;;;;3;}-4U( VѬYze9n;ko9W2f`ap"^NU]fba&.'d j?pv Y\;p&1da!u-14!I 3D`Ұo5+r]03\7guWpߴ?p7CMAxk| m 1 v^Y"*66! FbNu%sUq7sUix /3E`vƱքub/p*^ˆ c4ϥJ1VGC*Rw#v` _kHZMoRXg85qo|IH]bH]ٚYiYAOR7V_IoW? 2{器l 4\û jPlTI2֟oL L4p N5ű.>8=,DPp}[>;*z4urfh)^|y Qstapw3%Ej -4lH8'<AKnyLD񑆏Q\Q CjO8~t@Xuc^ŧ S m00g:dl8JU1v7H;f ;l dȎcݖg ک%]u£a|Eh?FtN Z-J9L} -GJQ qdMtj i\d3l8[L+hxe IIzҕOq5iVg#ƝI#!Lwn8iDBiB(?b\ SϘQ~W\W~MwQ}O`*qyލKJw㤝FRGT0UTiu'{o4z1I%[J` ʿPK;VQ����PK���:������������1���dumphd/gui/MainFrame$MainFrameWindowAdapter.classRJ#A=Չ8oWw7,لQM%/.si%.a? z 0 >LSsx}`-BvN<=F(;j z۞<Uh!K(s^s!証L(6Ӕי}of+锟l= C #LFWΑ ȣ"7lP_;_=BD?&,Vc̆m1ca0,lTWJiSk/ful>aK5Y˕}2﬘.%DyFA8|p.8c ac F +li0z6uqK<$g^_à9̳3R_s\vt/"I'~$PK{y��2��PK���:������������;���dumphd/gui/MainFrame$SourceTypeCheckBoxActionListener.classT[OQҥ\j)r(bQ51.#,t }ggQtζvwf73χ� "Can/gͥ ǜlZLah/ErYثSIs z"/:4Њi9+d>X\#֖gM?޲FΤ+NǸw XeG ZA0Qk@EG!5&N^X[ŇbN0Nl+*:R/5YhL䁪m|˂((D#xgѦ9g. TCS;7  Ҽm[*@]`8nF(@H=1eRWcl[nta$Tש*)@}&#x`B r\1U ̾@e]( sqAj2\c 7N?7qotvqTiPzRϲW.Ti?wW΁Ꮌ5ԆQud#>Y$O)$\Wu8: Z,79*u ׈~4C&Kh/-;ΔB5d[O~@D5wqQ{,ꈶKҶ[4R"1+5ke7?ĮFRDJRlUE@Z!5)Z ZogTr)M5D^I"ɕ#2$4LgajX<bteHFxf+]œ,m=(?PK8K����PK���:������������3���dumphd/gui/MainFrame$TextAreaMessagePrinter$1.classTn@=kq5iC ri Z �U7ql"_J BUG!f@%;;s;Gxl@cxay]ɽ{ N]<Hf"qJ ?^e ?3Pfp r֏RMa:3f0yҐx;S�!O"e^g"qPb U 整da -,Ơډxz"9H }$R牲ffۦ$&n -yW5ߝ6B53pVd4%]UO{! ˅� M|42 T0O|TT`jܮaJ–bWRd'i*q+5*NkmYlGM }QEXdDS:WST맰_|m9VHR\믡6zNHesP93dƃشXҾLtؤD=k$"=�PK�����PK���:������������3���dumphd/gui/MainFrame$TextAreaMessagePrinter$2.classSn@=:1CcLvh1&NT UJ�Ѫ{rƑo!T<G!@ʋ-|̹ۜ;?~Ů a;'qP&b"cy=O4#& U&k`w‹yL,18smFa)Ki%Yl,uPݏY5 48*MMhC%_員Lc_D'" ~Vu ^._Irea7L\ghp縉[ 5;(Bv3qae9}lvi)K߇$?Ujx, #Յ<&H]1JٟQFM 8O|yjd@Qy qphqͩH1Jd?i*IŗEMV.ٶz:,B/ZO/XPpj8!o;8_=GL}f>cpe@ø'=UA`Umxd9Y|~PK;W����PK���:������������3���dumphd/gui/MainFrame$TextAreaMessagePrinter$3.classSn@=k:uM⚴\%uKੀU+%HJ\9ȗBPĂ@K,wv̜~3xd@ck@G|(Cq<o8}: d""i `\Wc'M*%a!M49xvøOq[�}2/<d<ex^+jG QO,h@^IaqJ zUA@+):#<UT <snИMbs'|ewLTpmR݂3-J:z[->j +� u4|<GjV``&/Շ_MCۓ~8ɧ-gCX! ց" y jvPi]fjh�Q#COKIzBgbqB} v#w ͉]\U#qW&(MCul{qe7S8t/(n(k?!L tlNhmIC5B= PK9`���;��PK���:������������1���dumphd/gui/MainFrame$TextAreaMessagePrinter.classTmOP= J8^D0/ kb$oelK~DBƘ1G]]l99޾(U( b]g"2hyY;Όōm#o[t"0x03[k<ahPda'&iǔ03e`!0΃`8CE*4"P/74$okZ6r16r+%؛ H: CHeJ'e8ݟ<`ɡ SB)+IhhF, ì1Ё*:  .wn9A t]0tv9K뮎Ѓ^RN4z:\)u P'M4\W0ҵ*og+'y)ei%ϗ#2 FT1Z_`Lw 4LaѮF`Rjt# )wB\2,Mao&rms[,嗇n?.>:VBӯ,fh4N#Z-{='4RpɨFOQD-i  �ͫ.ZS{h}9Ay&6P)/R^Uy卋U^ x+NA(?ЪG6G@$VdGĠ[hP>Ev#\JzG WK"g dF2|/Brp#G2F&Uz1JO?.BvS 3fN_PKd  ����PK���:���������������dumphd/gui/MainFrame.class; xTy3eY0QYj` a 0 dd2f&@p X"@DAa ؂Uk]TؿJUs{3L?wsϹg �P(RA r457 Vx *쀳ɭ~s.BQUzY+jf"dVtԄb*!/mqkBpU% Dejj˫Jj˫:Bv!}1 Ҡ'A7Rr#Q,^*咷 VJϹR)fr*?16Sq!G o!D&[BoA;$tHټ5%.CHil%ed%F 95^OhG}っ3yS]pֻ]V@3[B!!;&v$G A]D,mt7@%�GfW'|ԷKr̬ι`MAsy 5PzwA1h`6Aqux9N1<sS3b"[3%+f 6V4G<^ְ3J* jW2Uo7wYϒVx|ds KRYM˷@Sb&Bx;׿$v%0 Ѧ4x=>OhDnbRm2,2K_s`zN'$s󾮷�<Y# jiwj sɬ,v<nڦ<My`:TB S_C6^}/~[Ce: AܻRFjrR{: s0 soǽFaa$WP=c`,u(4(~3(to̮aL&u pYf2[õ0oԡ~Ki0ya @RZMk0nQD V-7np+YA%튐XH{]S>,gc vnޝ:wm*eUoKXH?$-g"Hv x|.yƄ/N: =ie`S.1Ϛp8|\jns} C]zTft*ݾNwWa"eZGi8aP@Jrұ 2 yMR)I0oS y?(<c\ Ed$1C@[nysN0(-H X<6ݜa9l*<0,nT8Wm"K%|jW,1'oi0T>ЯFu!{Nuf;F\g8?Hk[| &BNN.t<~و6շtxWi}c3Sޮwޒćelg=~1%ؤRn.<Kk> 8BfXߐu{5�ZRO pQ?xC+d2ess ÛV7;׶pJ+%Kp)T!PL>507ޜx1]K[sc֠܋ IU8[ipd}Eq8ƟK90QccYC*BlFB6Uh; k=!J$"$;V."D|ώ)WcZIAM13rZO3'K*Rn$ǶcUӝco2:q�$nh Iu{V7M~'M % &Nb5Q5w,a0xTp%3[|YuUmIŊsV/)-)$v q8ҵ1iI[F8y˼-43 LH9T昦q,.R,-cPq"„R$Q(/bqUq*IRSϢбgrUD1Kmoh)L,> ge` *}RTͣH|ۨU:.Ys - ( ާJr9RX5Ώ9Q8 .Oj${DVEL'go"},;U|4fBǕQYMeIEE4$g&iәO WFZ1n8̲J H f MJ*Zj(K 'cC:?^.Or$YgF)&Ge!B :^hs{\FA8}u -obp[zvfA57⳷m枃qrm:GfޱxEnx;x;ީ,2z:W$e%p2}B*D!4wuwN>:bg%QO>8cKtXG3:Žp .p/k;§81RpjvǞY8Z<yal'5졝f3s:~)Ei@G%e^u|_$kbĩk&=.^&Jn|~!/$k& [*?${.SD;51mI< "76v<|m6PNZg<nMş&긦!z ?c{/&"ݎQ 4*;]M dp:K:Վy?Bm x꜎Ə4hp)e f't;~b P%Id-f񟌖K0(�KlOZ6}y˔;vMӰ K4l\0Iˋ @*!I /[Pݬ fф5cINЖ=2R=RFN^E !It,-\]Fo�;KĻ֘j]VJf)')l lܞg+3窘2ISM;Iُ*q4tD!rR޼7z4Y4&j<&1KCeǤLLH{0F\jwF 9T:{Z%_8L#bO#gUJ{\f|]B J;1GX]cI,Op?52M<px9 -{B/H |22ǽ隘@\,hᓝ1I\ɉqZ:K"_rXRrU@SI{0k]* ,T]L]QC3|28\]_Ԧ$S4BI\ỦDEǖ NR vXմ۞Ɂ$rsQ*SBQHzE^H6َSR]ԱB7b;M庸#պXT4\A*4<E=##_1).]#qMtXQ^$@>8ktFx SI<ٵ:ӦEq™L,xB9śFdFg(ZtlivuՒ<AlxXV1vD{)`y(zWe B<-\| ߀H5f|l$Lt=b]?QXDs.-ER 6]lVv]k 8;tIR!ci>#w3 u\}L]Z|u[YO=Pl8sEU0RkϏ[Z}~~t4:tN!Bѐ1R/Pc.ŨXPL: &f5o쉝IR 6 `#p_?ۧD5{EJS(긋&QTReQ,尋?D*g(O"f:n}ɬMq.KnH1.WF)#NH�T ~`&@w{{*yO zς^ eAsUd;ls n/d;v m.6Fv$h(+B8xs|L$s|2\."(&T; 5d%43FP? (}ʏJ+Eڂ<@Pe B dCu>ɷE[:~,UX'?jpo ?Z~hkpoA c%VaEV,arSN6(eno;cpp_vu"e>Uu̶vxHq(p}2 Or(axvQah_"k^(J逗֫EVD{|ʳa^Q#ա]3u(N zǍZH ϨI _K61N/<|aO ?(g'ϻ02 sGEvf?vO/9v" "P$FZNAR%HjPj5;0.SkTcp ؓ{Zi>.3TMc0:N`?/aġ>Fe/ա38œg:ptbI[qL;#S"'pRQ|¶N"tIMv,AbݡqwY8-Ӊ K߁uY8i+ڱF 0w1݄[D�KڱFqsr*Е % @rnd뮎lnP+&Cw Ɩ_W:w˯7ɯoM)-A~[+ҍm[;wo5_'5F~Mvʯ@k"@ZHڍjZ7s)kׁ-uc] 7aH5F-LLk ^?;[7avSH^+۩:;RK4@ ;ECx{];S4)aDw(* 3Th*hLMF,hem$Fg7>`t:wS M $MleHzmc!cW7ߣ~|Ei4ӌO4)J;>G1hef+xՍhE=78ziV1~Ik5 _ + ;yC6 _SY9 o$SY#K&o 6a3ܔ;}P;[`gϐܑ0"_ʈkNW:)8Y9uaP[ac&�GZ*;0%rב<0 4Ru?<{>&JM)ײl]-RJ(ˢĭ/%l)Y2PJa|b6vshf.<SMYZ-gu]Mo �ht,Jf6 +Vpvy#5�YAp W!lOa|w܉9pR~,܃p/.ݸ�<7v؋w a<(v32<C'=x /U ',ȄE8% 1 ~$JbKX/q-[;s ΋8 ċx ZP*>E͢`-02u4a4JiitF̰lL˽eً,1c9}-rY>ƁOq`(e^L˔ZġJ3S6pe *b8\~ RNh},Pc| N8:'[ZJsWbX7Tf[ A,q,28zg[?ƹXnW؆am$V&Q[ lpmخZĶٶrx^= 'e;nkvWFc'n]kNx:7c[ܬ-j5nUvu%ޮ:ܡnw#|Tݏ$rW)M|Z=QG- a'16_J_ծZ<w|W#syv >?i?%9EÏR2o)<u~^J .'K>c.RoA]{HTf"jdC_[dш},KdC ^ћ=;yNCT% ;~ Rŋ5tqʶR-K4ah[@K"Г Dk=q)d.z>ETO*Ωpf(S_*OÙaqyTs -<'.\JX M(=SڪIu&&D"/Z1ҤQ`Ұ2#Q\ މl5șH\'a1ze2Y0KNS1=!_t 7D~䒽 hb� QDHb(%(y7 ʘ8L&Ls491 5 8ݹS��>7 \8L% *|~҆F=' S"KvC/cbrXk�|t r\\eM\Dz )n9Rʰh1Jj۠(iiMi51sȯ;eq8 K-OJp<˱h2vU1~prDaW[E(]gPHLCIb(RL#.PAa)E<F;,'zB 0 D.Fa?Q18TNjqX&&2(+, !8HH#`4N_>dqr@&troɫ'I&f` 4!dbW K(PqzL1qѰӼauiim0kd) [^4vցaqG 4jS1ق?|$Ć3*\@O,O~#/: oQ\%>f@:u 9C7,(HJ{jiJQNQ k|J[D ld `)=:(SN*u|Tm*QDtJHYm2aT5^M!Άca*(f }td,sbi<!i50agnlx`j<ع>, 9#mlspFʝ0:W*MB  ľ Ƙ7\jpr3_MCx4TceBqTn8TbT_keK:pCS=@c̚mmvj&,5$&:轤?0bl`NrPZqYÍR|IqcK.Tb 2xSCY~:f'N_:gEPX-vlxZE񳰸. UDgtm@TFa|Soq&q,gl6AJľl2T؝KŹ|[.o#.w;;;\cًL8Izf? xʹcB b3ۇȨ}l5;Qføl~ӻ0KͥךK5^k.3ab"3,O'"vREr7>b]*{T#x#pxnmT- !œtdB)*.xW~$Ǧﷻ]w&yCCc)!GB(.(⩣ &1(G᮫$&u<CuEiJDr,)J0*'AKDҞey"ʌ$(`׉]a 3-?G/sR<dw"gcP^/&AYRRROT+IV*q1(%AY2.5.[68$nYu ,ӒL,I'TD?J qJ>?vA0~ 6۝fl_7NhCi*&P{_R&e'PK xi��uB��PK���:���������������dumphd/gui/ManagedJob.class;o>#Nv&Fє܂L}ļ$vFIJDļt}F<F5 t)kML!FҢT̜TF~z l @@h+$8�PK{3y������PK���:���������������dumphd/gui/Manager$1.classmRnAPQ޴PQ)mx)Uj4meeً1^j6�>Jdv|_~�FAo.wiH#|EC!k0ʶ!;֮0} }Hil𻖗_hj_0L fvu-R`8Ρ!#4G XA[A%'fi;k�+* ysNk8OW/z8CVN3L4B+^-sX'ٷd'FT鸀r cNpatOA8'CMpyXr5zBGT7t3P1ya 1FT"D`{kMUS$DZ IRq FP,qVcێ\5K)-5N>XR%XxԯMw6lH)ܺmxdѦСz%z+D/~/ħB~,Ida<'D uϘ̞| w䟩{WPPR 9>;ÔH }K;Z4XC 1 =WJwƽ" PKTizY>����PK���:���������������dumphd/gui/Manager$2.classmJ@ihc=OjRoDA *oӥ]M74>(x gcS37�U,ajDVҌd+S5f/ x\5+'Kv di{!iY'Z?n;R.ha!7D } dMP͐*iC&8uZ..x 9Ӻ.8U*%D( &uE: ݋-Ju61Y3'-2|<Rn%5=cx_J< áR"xH^0CC+<Et֊Y^yU~F}ZѷoQȢXa ev˗`` S1wL si8/$}PKF����PK���:���������������dumphd/gui/Manager$3.classSnP=7 Y&-Y bJ."x(Tƾ$.]yI,(P�|beHq<3Μ>;Mla[#/\1NCs"spjC+}|8}j{ p0a%TH9nNDh#۵ 3.!$3v<K0qy8 (*o, ٶ*lW%$d3`NAätoє%sЏ̐$kwzGb,pݗ#iebpjʏ\4A 'C iAR9M`a2MT_wIep,czJF;"ߔl%Oko5/{/Ñgqc I3 r\W;*l Jc[R6j(֍3T3T3q3h i�E'j0kq:TU72xAS?&(wH(QT_p _W3,?i_{fWF6GK_a1PK)����PK���:���������������dumphd/gui/Manager.classW |Ov7,`pb&B*A-9&`APK'C2 졶WVjŃkZZڻm}X{3{f;}{>4)2@4ԟ2C ^U;=z([ };`RKǚ=WvtQt(nٽz,ex(͞UpPlLiޝc^έgl8 6tmZ^䰴FCma%͸լ`vOb3qo{b3 $kۄZH<a.Vꦵ: sIL@mSP23->GW55Tf�ƺ,4hJWQ[ k)b6OTg6_C3p(6-+5g$6}1ZGX0ݜoF2JD:.{Ζeءfg*8%eƢF )2p&C~4IXAM)ὂbeXNg!Ê*hߎJ4�q!W7g\tj5'Y4Dz+8)0thԯ`io(9LP9Tَ5*V+'rNW;اke *ǀM3 B6Ma TW)%lаiǤP<een &8*P8qœl@zsi5bKv;2CԺ7؝cŮԔmǭ?2ЫuНu p L+AZ;1a0 #C+-y~B\[6 mdوj0PΛ4 `' d058^%'N1 YN}oS5 ӧG IU,JؗҰG6@_ 1100p$S W}6pN;Y4PJpI#:!$d/S2,kqQ_!v%#ZF!&Bd Or>]+ǔjn)oWa'V|TZlܠ& 'Fn0&[ǭCHS&X q#Ͷ3m`` LkO|ʊnM;0u,KGʼTYa?>ͺ RH&iF4۹3ST*< xTN Ar&Z߫\K0 +L(crt8;#8Nrp 1u eވ1d�=.MIx_Pyk|{4|Q<XƬ |IACkd`+H<3N1V=1i|]R<egyȌbM>\o(X/G")w`xb{ܱ"Ƿc�Q(O<+GBtjӛg{l6̞(l_&k.g䲵$eۦNϑ6׈lJZTvF؏KR^x Q2]#%b hok$aKfֈq{X.a*ܒi٭o&MIChQ=vz-orBٿ).`$˰.EB:^"d8D&{dK)26˺/~MB-|(J \W\O*ZkEkJVF*{G~/~kX xV=00E3{rq\T[JLEhBtTje[Cǰ(ƒ4ޖG2% )Cs8ȻJJkA+5B5φs΢tjp=;OE'<$w<qp>:J?4uwv-8H#ߋ G@4vȍٍ]AXiܵݓކZor+}ѕX,q\yH1ԇ>fdyf0;8mFt'..ٛ*ƵHnpK!dwIdx!Y-~\™kp)g^JvgTg߫."Cg/$wyy=|tw?*$qYsN8Hxk|`myWj9^ wx) u%=jk�abq-:qp؂[p;n#;ܝWm>ęm79dr7bkT j¹@p&'hF+?LMLP)j٥c.b19GqVn>4>v#֗AXٰ seh~R~cn L&wҿ`={iɧBbzT6�̭l8\MQfqf(31oPKw>=|(߇9rH ףL0Z}T(r5BJ'ϵ@OB 닙,Se6+\ŋ[ ]A/טƯsZ=Y"_pqo;笌~d< .al~d\4=2!2y^xEXKrf.<ʔ vUͧap ])^uyΘ(,kc"jfߏ2.b}PK} ����PK���:������������+���dumphd/gui/SourceTab$ButtonListener$1.classTmwE~&vtHci�[)$!)U $mP:t7g3'<z<sVG;4M8'v;Ͻϝ{'P1i[qJó7F{r;3:K^]+_8CܓvU{mr4 C@Y;6àP,jNСWJm3~F/n&C|!p0F80 X8)0q+kLGKV*$2qn3afOP"b bt<^YW)~C{U>ptCVw&M~kL2S S}D+0 CqT=zӹRiE\s{.vw]'~BBk6=SV`+J/reT Be7 fVmÐ$Egj`%Mz/0u05 4"zK6)K_2,5- Y2Rg`J̠Q)cT4Z gېQu JwNF%vqm驣j |2VZ(l:G8>$/v&{ fUt:7;#~9Ҝ5 -J ҥ -/˲u:b,'+N\jP0JQu~\k.ҶSoG&KF(OP0e7X*e3cF|Ҟ iϑ#c?Gi' D}S$d8_Vjr &IH\Ǖ}^գcT߰{FxIL!CcE1L`\}{df?RWŔomd،c< Ә'xHx[OKȬ8 4Fgh8PK��/��PK���:������������+���dumphd/gui/SourceTab$ButtonListener$2.classViWU~.Lh6mHIKť@+eаXx\``I'u߷=x<nM?7 ?x<; dJIλ<Ϗ?hŻ JNS xt&GG1>rˌIG*(eE59.`|F)2gVORB"7q=G[7glȴ:tSw.0 #<PmE9jT(~娨TT51C)a!d^ƍqn=#,3KRV*cLXa_Ţ A_Q\Deso jEAPq*Nd6bzM bfD r5P�)!QtG,ZjFR G YȔ|}2|f >œ34AŃ8Ϡ$AׅvMm(e|YFӮ!+Fe[KQ]E86c`pYX-k[&@O `PA.֪rΘXv툊GZ,iEã"brLw *s}L2vb<wTL:j7Tmo /c"B`:f{1K(%]T纶y@S<mr- \O7D#w0# mc^ڶeW` 7,3̋1pt&xvVrZ ~ L)LЂU 㲔iX<%Ad@Hj Od4}|!EB)kj™7V(i^q9ՈE$ʵ{&O&5:E}瓗˫VB%m:CϚ5T7Ekڀ;װr>@=# Аb?TB@~oZE&o:Nh+\\C׎aZ1P(κkW#[#O:|uY ncnB[A$z&7m!R_AkQiF~iiy,ȟcR<no!t }N)gdCc_\Mvؗx}kloE8[şlU 6]i2ՋxXߗ]F_q9,\ rzե5NR;ަdGy| PKR=�� ��PK���:������������+���dumphd/gui/SourceTab$ButtonListener$3.classUrG=m /1Ę `m $$$ v,0DV< BBU>TA#URJmQуj{On\0D8SUkǁJn>"+0R r҄tҫ9ʍ, {�ғ5UWѦs9Bayv= @S_U X؛E 2%ls@A{nUQCa໲!mmeڐ%$pnɦ# ;kڭ[xpz�|xW X8&-$g8.fێzs8k vUlBE6Z$;0(giX8߅ \EQe$y;zI֗.J77/_n:tK*gp[pY\#XOZaya&W@י|g٘n|nLB# HP`Klة (}딼ݗṸE@nqi;^ma;(Z(l年].V  `-^D2tngPεNHϮjD>evﬕ"p_auq{7Y{׈#4v 5y>dٶIIQ6 &jVߎt-u?EmUG[S#?RU6OS䨧ʋɕ%#'Ŋǻ`.PFپ#0ɻ2ſ=Q2`dYk|6 _bAK,Mo0ƒ`hpǑ6J#p+}8Tlħt_</:/7q{߱1zDI%Ɛf (9oqţH;5=~~d0+IM⮢<MRPK(νx��I��PK���:������������+���dumphd/gui/SourceTab$ButtonListener$4.classU[WULHO@6ڔK;RC-)=&8sp-_u-胏>ԥkgI0kٗo={~\ö.Zrh/-*黡R%]of[t[6Rz�+ʲzTW)B!0m͐cl{+^00O ~^ 02ڠ?g^X-J ɐypքo{Sحetl]LX(1 Å#sų qaY2uDB2pa0=xa?܋ ko'4F \$b;aev}Q%G2mHM acDk{z˸pRQ`ŷ]%}sU:1*Q*X_g8ߎ/e5|` eCM169QNԥi[q|iځU*ۋqS9f0KlGB d!L7@j,RԄU[og03IJ93,c95 Pi۷8ֲ>-<gծiUF\O=iX㸯31OP.{%4LJrs3m=h_0ܜ}oD"]JFm7EH&"@ʔSȶ$ Z{"Jx`Bn\S TڂC&9UkK9d;J' zDZW`O:IIn-TzuwQP̋Zs&S6oK]ѮP\8rʤOTe*U`މL9Z|5 |ZRc۝Z]"At$'}_E6',Ґ7 I:q:k) F! 7P#?ӻx u%oq {WNV\KOS|B+6Ǩ:|O~]Xmr}IǢGO β_1~:?9 ;FM6ApGc?z5|Jn$-zPKzD��V��PK���:������������)���dumphd/gui/SourceTab$ButtonListener.classVipE:de0 (͹A""$ $#D$CfglBoVoҠƣ<,O'?;kI6Lզ_~O� Oh_ڊvi<l}]'f|OAdQ;}a d|؏!uoMd(Բ:ű|5ƥc1FTt1܄4(P(| sPP c#6q[01v, C7M|3l^%Eai$7 rNq&RPpz9hiLr/T&p=kTq.Ry &#p>.m Hlj+v) V" fn_T*  *7Mdz۸0jQ J9aV2hMORΌL![H=p1.!OZ,{a-Q@ng%L`4};,3w4 ױG 7#2LסSư1NHDLzqp\|uX?Cs16!Ќ 6w5\!+M;^_E E]WsC}ݍ*61l)NF\u=RMTFaP L9d ݠ}v;(U[Gec:2$]N Qt]RL爛2lE;-cА?SP>o\=WуnI!aUm e>w_i߲Ĝ=*2? F <Sv.`)71 {0@%9(3O]Gk j" 3S+";LՍ7 R3o CG7$,HC;eXd3RH +IZp碡zS$ߧ\Rq3Üv;=U*ne*992NA4wpQ9>|KƋ;4܉^P9U6]ODec~= fXg[ýC84 IF6bjX!N1,5EKBYV0c qK2,ׇqU<IT&/ں7 {,R?}}:2|1b^&9ْ~î7`l1Ќ^U>7`t U1KLP{U&"̰fy-׼ZE&?*KӯD>BsiUOÕUA<yЪ� 1NsMB,J eORQV/ң88B{GQ3UjQ_֎$_1} lLG+ Bcر4t̯i(εw�ȕEiQi(cxZOeyh;i}X D7Hw[r JC vPJߎS={TI 3w`P<vkhfO_'gcԤ?615O ߰߳/ 'ٷJ؏l U39aٜSxZV[g,ey 9<OCȣFfe\[gNbuVqct)7%;- BY\U'Jb,QUZL9$ot~g7iUOM-@דe#7PKa(����PK���:���������������dumphd/gui/SourceTab.classX |Uf2(a[Z^%Ӧ )$Mi'IMBhSi|M$RDWeWuPZIi-+]eE>@tY]@io{{9sνywG�,#. ©ѱaH*닧&Bfp؅ A/zwu߽^ Hx, ƒhtZPkdFG CPֳQ[;Yق`,dF5}/x[ۆ>P,_b>(#k4 >~M%CY8! 'LH"g&9Yva p$\m1U`~E%p[*Ufrdlp,2%P<ȑE-:‘w@n|Nvͽ&"+=6]XQ�%M40u.\K/503xGLEw%֚ GHnP|G4VphoQ}I'U7:,rk!b~F9G=~"-^g%"b#lD "TF5b&{̈́yOEtMI5zAxWq ~N}!SڇN2gr<z0~ Upv 3S愃o  F;Y"arl.S^o`fko^<v PB4i/j|\ q ,i46-i m^y&w%a&7c0I#j$"#ffTf$TdPx˅YeޙL>ئs|a0=c_Q?[mć ܉ m >l}t.3R֟Aa¢-'O%= Wkl38D$D_};>a-*'`rrsD܏ \Y_ڟ%EX{~1^"!ơ:ѿփ⋺_&"y@92:Cu;b>a7E4FH-YZCH<eѲ&3p`8lOfΙj6s7+ÔҪYn<ή]8V ;\xp_Td=ayȲ&m(G5SX'O81ShG޲`ّD)+Ym3;:(4Q>X`Nn9߳f9KyoA0gPgO50_?Էݺ;Fvt#xхd-'IM[-<[nӞ%?̞mΓuO82 ?be]"O$~Wyew@qJ-h,n">,~ ^#NLX5~f?IæόG'aWNOX;_x&jYvK %tؐC7~o&itﺄ{+j'M6hBhTo4,S~1~9m } .ozU9Z\R7sbZfGjhi^&r$~:ːi‹kwm0,fo-9G[3mH`ˬV4lmSY̖9?yɂdy%)gT2]Ose^_k6ڹJ8*r>>+ HOinX�K!ĔoeDQ_[- nYJ-"@grYake`QI|RVJz.#ڐKXX/ v u:i:UH Z~RCcv+CQ3HpTmFjy}qw$p^H| W3cR][l'o"ƼYC5ɄY4ohצ.FSvU:j.CB=!d;uJSͦ s91aפֿK?wkvNQե Xw02 *s/Ԍ3I|f$K؞E/ߌ\w* y籧p-÷LO1GɀVpS_9:E%8rqT7Ǟq g卧q\7>c:ٟK'Ab|X-#V˧!b|X-!V_Fg&]bu<R,|\AmwkDZ14k1>rM2m*kS]{6nVy۬]vu1< Sؤ П#T,@*&-rR{noY`,J6V܁(aw(>q.ἮL.۔x7NF9!#=S?#g~Fϸ~ʜϤ~66*S1x @5Xu%Ac0Lcvb5bhAWc7Rn33hX=Xh y5[P%ik?M?C]fX,]Zdu誽|꿛3Q6C.uBnVU-SC>HE>_jڢmI> [e gx??jC/_kH_qfL[q7i3ƽGlƸxt=a4ʟ'u<;?nvו]v&U֩Ixڦx^vTL ~̟vxϰU:^j<Q|B<Ӷ2WOڦ_쾡]1~J.uA'1!aޗ/3m<J`8| w0&q&f_!<ٯIOE&7ߧ7e!F>-WYYd%|yفoKߑ /Gb<^G=8~ O%y?X_u>4�m@;VpWlݲ_ʤeHT2PDhTÔ$^հ j<4LH8 &-]v;Sm:|GdnZ9`MhVW/8-V)Ӆ~j?jic?:VrkeNcz{ K8HQ!˞Mii56O%rV|UgX^~Ms/Y ~ELoh7Y7b~xP|H*pF>!UtN<1r➙\u=m 2yYq^r{Q6.iqa5p-U{M&vS)i7QռAR0Mf`HfAfYEj>Yr.#UwɶeZpSc;y}0eLyb;LNY͜3! p1҄MORY咼ٙM'QimCʚeW[Qx+ 4MKLkyW_,/rUOT;Z˥:J.u:KT%.7UHWpeVn;U|Hxp;OuHPIYWB8R,e1= iNۣg}NۯqwqժrUѴ*qe 22I^|pfLx{I2xnr#fww>-,n"M/ʳPMёRdhpuHXğ'2Trby"KY$yLSd&dK eLB7,&( (PKOy����PK���R?:���������������dumphd/gui/SwingWorker$1.classuN@PV߂b"Dhb&X\&$.|�xB&̝o=7:U$v4xrj/rWn$ٷg^ݡCvB#!RC K4Ǯp' ,n!mÂi JY 2MWhͨzw-m܏aRPQT :ְ"ϐ_i k~pAoQgcNIP! >t|0΅Aóء)4LS^$A3 <EDè|b5,I;d)2(t+1_9v*ϴ&Ok|XMZ8l#/PKd9g��E��PK���R?:���������������dumphd/gui/SwingWorker$2.classTRP]-BZ-z #3뫧=&~>m2ڎ&s}v^Y  sհvN|"W4}~-{MU}_8Ql&<^whԒaL`*1w Kz2lô7F7=c k M!`4aLYSu$34C.nYw[T3c$_! t\U 3q!^Qg躁0Ga~ajѣ(-qOBJRn+AE!aE,k(1, k`Ezqt"*PtFҕuT2;#0y|'6?tI}\J:n?%Ƽ vMi {Q˧4^x6]hF0M5^UETV=')t,-ar]N�{̐ Yu1ݣHH->s̷ vmQX޷2X=0N ޹H<YaM, :=wioⵆ{<GtOр,T:}PKb��e��PK���R?:������������&���dumphd/gui/SwingWorker$ThreadVar.classPMKQ=wԙ4S0A4Ѷp#BS:61*(~Tt8b{||8EŀFxhB<8fJ;o IMĽ]D}!p:9&X;l(M$nEH4R ٶɫpڗ~W]eKfr>vA;Hr,\>B /!$wHrE@PZ6*__OԉwҜ5k6^;zA+ Y:c2rK 6l+WQ@1ֲUT_2�2Ub Lo %5lGq!|_PKb��n��PK���R?:���������������dumphd/gui/SwingWorker.classTmSU~.Id)MPCMA/PŢ@ketIVXfw ~Of,qdƏ:ӿ_p.I6ܓ=<9~P0 0Zk ;MPδv7}VHGz[;{F!0\[twmC=w˺-0Yy ;[ˍdg.*^mԌ^ mѴaouh hvwMG هk_LY@<ᙪ q&iή2 fZaCWR|EC 42 2 L* hb L?)O5dظjݐ K^=9Tp +h){ Ӓ oHjӶ %T]??Qjfb1ʗ)w%*.= `#мE,˸Ʋz8SVp?ʂJ }Q?jZԋR5l4e=#4ONZkf"Kjg"ۮPælTTV Ǚ)_o<Ei9Ae ##Mj%2oDmݲ {;+kO%7S2�D=˛C+1e\OZA^XW0^ eF\).Uϛgά0GߨUhI̼I[]dD�% he$O2vWߠOJ£DUnoE5G*<Z"P?ʹ?Oh,~ǿ!8Dx3 fk.nG*a[& ۤ+s[S/}0\wϰX]<J%o*-̝!]kᣧ/ ᾡPTgg<~9<}Fvv'*%CwM]C=zӰm3ա<.}G]=jKG(>~)qL)Qu?zg[ws9=P VfPPK.Hs��3��PK���:������������'���dumphd/gui/ValidatedInputDialog$1.classSMo@}ӄu MK m4HpB BBmVmkEu3AK(&'d7o?}� s8_'b -N2,qʄ+q*1ҁ<T'j@}k1J~L3;=By7"KaE =8@( a89١8%a"L,YE&\>,Ӱ̍zwjMV]Tqef(aW " eMB/za9[n+G-5ppPMZ&_28dl6%C?Eʶ2\qRRufTI;cftֿ!%d$vc2&]Tq4Ƿ9z{Y6}C< U_DeW~ťϸv[,թmms|̼رd+ PK'<����PK���:������������4���dumphd/gui/ValidatedInputDialog$ButtonListener.classU[sDvD딴)-m! sQ M Źẖ7RY20ϝ7??;3041iGZٳo 0Yi^mz[fb7][Nud]0=_U`H][ܮa zRbWؾ~Efz#S 7 ]|Hy޴Ma$U;SbH䝊y zU1IxZ* L[5en%RVG„ a'`xp.* Ӆ,oZe\Q0pt= *A^~(;:t.DwM ۦ[Wq/(yb1B0/ܢM,j.bi/Q.]АA*ϟpϦoZ+b_/\;'kd ׽=R)eSX29ideBHKoN+R x]k N [Cd}&)?_T5 oaC%so)xHE,a mĦnuGlK2qhXO w$UQUe_*k8>m sgqpnuWŇȣ Ar[h JnyU$u\ñeϞ쑓dJZThX hبS' 2;DSCs*oDl;O;$Wr jiX6N'e ڊM<IVӈK&?8b I,bٱ_eb@BZbiŸHpiu<uX…f[xum7pYȇoCLDpHmQz'mGA"2,|M)~~翥=G,$ _��Gi^a ' n(X T%*;۝O-x `$ PKa2��6��PK���:������������%���dumphd/gui/ValidatedInputDialog.classWyxTWl3yBH(@ʒ%ʖ�% B'K4t/3љy̛$ZZ VԶ.Uk 6 j<7af4OcysY~{3g�Ŀ<LCY̠۫nH4ao15A@^}X ڎF( J@;eX%fe.1Fmɡue[c1+' u1uӜ 36HٔtnF,"9 ƌAce fĴ7 WU 4[ACA>fb7* Q*<2(l>LUQR ῂ0Ëx@�^,뼨"CH)F,P1᪋fZW8dfhOX>28YV@1{Ȍ O.SU9r@}Įu-Z##FmO؈Ae9aUFA)dȠ=Dne,8(F$ZܯYkҗj6sH剂-h`+$bSpV8jr(I�۵CuScc!+ntDn5VLJv6+Tt3O%^70="õN=ރKEKTGTߗ 4Y~}f-dnZ:UcAك[Up:Ա6=B2u*AhtᨽɊs36 kv[Wm9z!'X Q'=NUl�P6*Zp-q9bJHgc3Dvwt$A:Sj2ZZRd1*^WS^:vԺmwwgcskFA \ps=4Ľ4DfZvAQNح6u*ysؼ]=d`w[ߟ2уf>7В;7xe2!4B<&{fS mEf6 ,@OՔT K�=}p;k< ۋw"{0<] 2Q;G|'^*Lm$puZҌaMm 2ŋt ާ8F7%ȗD^C`UViR[q)QS*>t R{,WBj�;R,B^=e T|8qtΖEb P03  'T|)u;Hi-(9>AATƇvzN:yGuўJҪ(/O]ݵ2b<ʜ9gjYIM.m/>dKu?[*P7 t17\(S}6c\]Fag&|*$kMiƷ{ +~/38*y0Hz @ZP'/ ))h©m+%J`e=q_OiKKESN� 9[Z.,EkJCz<ns4Oϧ|: ǯ6&Gzɑkr>N0f 5*g&̪?+S32ɦee_^|Kwm AjV`=gab-XJuu4I+\~O&h4IKưKiZaeѼZ䰪a6o*Zj +ORkȠo"z %UO{I<Tx%bIm$JozQ_{oF򼉬hyw<i5ՊN}=$Zy,H$Gᱷ^ީ)Ӕ1ܔnM9@sĒ$Kϡj!^|$5E%qO8tJ5J}R)rszUiT;\Z9}ZA% Sx3xIIO?LӚam8Ո1^ǣ}w o/Jߞ;6ݒQ̌RIfJKO)"}ZiIAMIL$q%N1=>BǒC/_B9BpFm؁y\6X;A[I__{p3nW/"؉aAzx7q>[cb�EN c!&uDX۱OtгqCH b#.CHzp8{+TMQkIZ<ZQ PR\E*vʃ NyQGx�!l|!q >Os>_9|_ėjĝ2W*(|}z<<4u|5md2WlNo&]~H&I,C4LܟgO4pLW<?MOOri*|2/~ߺ** IŜlUR8O"3ED@`qD \gɬLcmjD-"%B&XN8Gh?_PK ����PK �����4:������������ ���dumphd/util/PK���:���������������dumphd/util/ByteArray.classuV]sU~6lC)K )FlBh(3 ϖvw`d4Bf@A?8\9?^+mbyyyL~_<C 4;ˋCjN3]}yZt*C/Ϟ| r֠^qtt€aDXjS좧!5QsZY/Ll ^`{i@Ⱦ31NFm+3% Fz+4tɅjM06' 8V[ usr[8NT+i`$DU5lw^tRuBz =YZrYINʂON8 5Qh~ur1ma�8n؁.;*d؉?$) XHa;[ez7d[+#BF֥F.8ԇ%D.V{CmNs#X7뤧s,n|`4Gs-0ea- l3iSt!eEO3#V1I'ieWC [D5K4fZIBҳAN)PVRJRPUJ=ڦTf\`'fҶڔ6VZ$9UIN{4&0xKxfUU :J#Ə[Ȥ[Ueӆм\bhi#g=ǔ.#K%'ހ11 Bqzc&`>==E?QYdoLm\ؤdˌ=)T5*0kJ-fmHBJvBQ {1RH)RHҕ dɈj"2$xU:!s\׉1u$¨bBF.Q+.wTζVGeSF,+jlfD =jF-%nɧ|nfKF~/q|/[-~Z]U\A֛Ty^69۴>#[wyO=wx?E; ?⋙t"Ϳ?@R�PK]\9��k��PK���:���������������dumphd/util/ByteSource.classeN@B(vc@LJ$L`T:!t ꣹|(i-&3wΜ̙/�׸TPr1Czf]G;\P#lk6^z#Rz,\E}ٲ'*(gD1͙$7emT,Jdo>.RP-]aZCr<t%gк\>ЄnԈLkF!m;N1fy_T-JƱЗ1^ L;ݹ q?,"7S2M ^E)'$RhX'ڜBaHB{ )b{젴DvW-"z]ZqPK*Y��[��PK���:���������������dumphd/util/CopyResult.classmNPR.VFKMpqCtA&jؗrBDʅ1qP9璙oz�E# 6gswlő^0؋h[OT8A[gA>!iK_Flx`,J0P@B@* @:Dxo<&Vc{C;NZ>h ]j=Q}#[⌔vYK\]q4;oX_|IXX;21~ְ^f|`dcciN0u%GxL}o%7PK@E����PK���:������������#���dumphd/util/FileSource$Window.class}S]o`=_ NnZ :K4#D/f ^.RܥIM�1^m1SE9<m_xKzp=o[9]sm9!k<4tp{]#p9R6!vr,&jEzfa2H$@8)#X 1|r̻~t'Aװ[kzڌx;ֈPlWs婍Pk9ۄZnt lxktcI?FBHJy&áFU=Jv? 6}/2A*Vd, !4#$Sab o8hd$%6 >0E!|8Ia_i�'>,~{~+$5RY 8=3hcaT+|^w}@>4GaJ#2Hg{\8ϑRpY'(+SMҮduگ'$}F(7hݗ rXA<UCgE=\5PK67����PK���:���������������dumphd/util/FileSource.classX xTf7ln6QEMh)MzٽI6Bm!ڨP[ $*hZjjkkjys>B[/=g933g'O?0p){;ó{R쥑I,/ #כQ31{źV(E R ߺv+ըP$KXjn! %zH\@!/a+L 2cxPJ&W{HֈQ%ibV4Xbںu ؠM㤻+M[Q{m͑d-IPfX7Uoz +Fø=i&nb5G")+̱5 ]5+tbH5IEbTBi7V&F\S4ɹ*㩥X8p-a|tTʪՅ1hH(FH Ha`JEhx1Oap1a.b1.0^&`H Ldm` DZlX0\)% W‡G<xyCҜeyK*@.Z]�FݕYajV1:DGOA�IF3 baVїQrĸ}ħJ9LVdm^\0zX+jf4)HIhU6V ^a- hW0D3!bAY:H[{Yuʹ%LDt:#s$"?T▔p٭z'7KX&MV^XE,4Ȁ&6A$<Q-MFOx2V5p 6QCs[ lŵ E\#Gxr(j u.X&p>'҉|.75ɒf2ag |VLuw C4vo5`҅,/ue;-?j:Rt*0cI.NH}3lRLVXQpuI AB ܭ ^1W/("l+8 I Dn;*Adv^jJ_6Qc3*;r68l pXemXN& +QAh:;VȤq)_MMRV\nwNmt$/dAoһ7 'X|lfScnJ |G "c Mad`v //cJvL %5gB@s#/ g4|!o Vs=閁CnSsUyou L1E07Da+Q?+,ֳڀu $묀9e|ȘRVtS1Z ZT+f 2< . d5{d*~:n ›5'Ň? =ў$BA0"Y md!J,ɤE: <y``<{ʅ,.K&ZfgK]~'o\WceͦC8kT~uT/aG J31Sy0'xscc:ycQp,DIٗ@♠ULn6,9} 4cY[Xq"<X%j8ҔĊ4(wB|xq{=fjzFXhrԠu̬X0:Ft`"8㓈bb؋8A70Y FT@KDQiIrq7Pr"ّpt +)U~ī=(3pud&cc=jw^A _W/~輦?T%~.lr&9h'y܇VM�>ə8_;vjpf܏ ΁\j|yEt\lʵE&b>wP=&~m3~\̸mϞ.&Kq#V&2f^Z["[p+W|mԾ{ytϻq?U܋Gq~Ļ789pbWxD<|Sf5f䦚 ۰SxCiHSCW䆡iA)2]gyX6D6I.8;_UL-q?.曺#{3|39yRpHtK >{`8,KьsB{X;;>skϙϰ+ga%.*~9G+Ux{IxMbH F)F8{E)JRG#5i$HW::ކ6P0 }q%T| ۮBe /slUn6T8kEj<eBNM*5igF=eqi^.n]0F.L Nž�Od+x܀>5 Sh?p''d�LUvU\MCJ]-*m1=HU~'kG�%Ƣvώf$Z̓Ph(7%,XM7TEg� r}`JhNT fUXL-9[3"5m4%ݿz nfP+vQ]֠Xk֊#9?9 (RB5gX,s;J1{H6dM-_ڐ!ľ,g/_'I͔be_3_Tg73 &OY]Ն1j 2 p~ixSĆe|ͷX+3ԝp;;?f9{qW,0⧆F',Щ)}:l~jyoQE8ȈC%UI^C=Dy#&M(W1MmAJ^C9QL[:Z3{=ĝɺ0Dʓt/R䗈x( 8u[ͭL}HٺԷ-a,95-qԸxQ˥>Jx}�oêaoeocw2n"jVoK݉j?�;{]݇ARpaVG:N?Ti N9>(g6oT|ƻqON$8)VMZkQ9A@8Y~ ѪCOe]"EU=b8 -7Ob.sLqL,&;/\'1]B:IN淋; PKh ��?��PK���:������������ ���dumphd/util/MessagePrinter.class;o>#nv&F܂Ғ}Ԁ̼"vFIJDļt}F<#B2(ncd`�KS2sRQ�T�@+Y@Vi #'db�PK-������PK���:������������%���dumphd/util/PackScanner$Counter.classuK@ǿsw\Z)}/>9ZZ*g ٝٙ|gnxeB/?:>yF| DdL&&jP?{(`(!Գ0] <n^sV6a~4kgC=i1UL8kut`Nh|ue'i:" _kJaYYDvF3J۞d;/3n?O&T! ugrZHSX"̔ lUIxPuUp 6{{Uu[g]a[ds6Uà3Xtqi?sCAfWٳpZ{j邙_4˹*XMhxc#0_aӷ!Lc+1O {s� *6 }݈_<[PKȷ&��H��PK���:���������������dumphd/util/PackScanner.classY xTյ9ə HPKBЄ@ I20 3*">UPkTVeFjڇ\{[k뭽֖jVҵϙ$^{^;AHs<=]ݝ=ao2w6o9S}nԥ댶0!zAÕK.!P-Qn1d fYXd"ȁ^;>q%F(0Gu</`-;x*hl4awo\DP@2Ƣ֤ABayo`Ϫ+} _H(**PM a!cs0SRڔ!Hs5d![&jȱziȵz%^|7YC՛#^jW[}@z8J;!,SJ]2UYXR?h0c^i-E^sI]gY$8k�#5 }>Q<`7*AUlVK"k4,lR$&갘M6BE*HQ�%nj8`Mfb+0ޡQ(IC_Jv(P0"Q8nc'nBP6y^IjKT\(2z* <?|p Tq-Z ce6‹=~Oͦ6; 5sYѳ`KِKS_ I^fhLXo2,. z[[{:ڽT7tUheϣb_U"bcU=Fiхmְ%F .5kRer z(vD7_ 7)p(7Eo Z4xDq[9p؏%ĭCX].L̝vT fQk@P *`,m}1 $%F?؍  ۛs+mG/Fx#16-|5`;~E=*b!yVn|Gv!vMdMUmn<icwݖ7sszFÿ[fh[osnImi}NFw|�W2jtlj=޵B@Cb6K by xy%*5;mSӦWv3&x WrLnVC13O8~`07Eb\O9iwvvh𺆟B_4Q,D 3 i޹^B֛l^{uBxKïk^-J9)pSHE8D]βଭф4`Jזf}YSPpO,`jED[Nè>*kb Do nU4ݬ7]\ 1WTdϸ~$d6ޓ3)$"1IJϗ"[u: HqAf"H"cnzso&ˤ4RJr3iB#2DF$穔\-2)rʉ])Ir4ʣ|>Z< cFJwqL*s4)#-664..&h4VL<iT.\OͰlJ\d?9yktHJ;`b{f!w(+:#V1!,aad}ꪆy=+VI4G2f c]녫i 61RlpOпXaFcs,3g:~ص'NąOf7'E_sXzeYI0l+x>\tR x rv5f0u<?"=;dw=@lQ'=뙶,TBq&.|j C[Ix'ڽKU\ #ohU,OZՋ25Zb[;sDt 2rvkcDɚzi-?[=iYTjZ2AI)sf譛ņiv2eDCtkOB\z|%pb1T?v\}Y٩'~,IS\D!Q;h!ΏUTU -KEj޵zimըmh]C7itJ}#)&op>fYiCs6c7|ŮUKOTߚ.jrvftx{F| |uÝΡwu/e"ZhͿ麥ƃ:1\/\X>A%~ NhI2ݪVn¯~:烮Wl_,!mn6xKXWf<u3tw_[toY av&2_z%nRӴ4*pPЃy*Rh _*dY [aEr#<5 %2a=;u+F<cXV }P`3P4C)TRi0ksgL8t)<8#al/w0]Y6ۜhmm~-m._'teUF1L>"3^L?g)?傯3O4̓ Ma�Syi89&a&&cC2g GdIDZ L#@esy?D0o 5}cpQsނ~TO`aDPK%\mF &i�M͓MAl'lqT-+m&G'^Qb{8!#'ň4{+F춈6 fyP?jƟߏmp}sOy8eZ%";p+=uJOze;raQ0;-W K~Rm˕eʃ.-.!>G'1}ͩ:Y8ǝjOb,6"LK_̯tAs*yō*t=13aS#rXq:"x6G`Frd;t\0!qzF9M{xSG.X,9l W-vF.9mMq9,H;mNG!ⲗ9S"Y"ә}(8\\? 3{1MS9zo+X[܏V|Ћp]#$qca 4!=;-.Ҟ/{+3vB8#d%NV2W|tJ[nŝD'}WZplЖ+N>fÔEx :+;hA=qߑ%L$<p0c?g2\V[ ]A,y>!?*?!?'))@"NvsqR\̉{ X,}9<hVm*܌؅f܏<5x\g�xW67ю_X'E?ll`!C:p#LBwZډ^loc=iB _t ;u>nK2z%;TG1#M^ۙxL>i.> <xFZJYi;nAi^ԇ�^^8&W1~ }~,'8.d! ]xS^_k܎_V|ޑooG;{>^~  H6wL|n'ml-8myŝg{&Q _mmRm;&s%AUmTllMC[c,M^:>4i?4 HR%͆JGwh͠i.fc[bZ&.y4#.j6.w ^`}Bkl2#u`XXXEcmh!p؏٣�rtZ>UZ\>&FJMU p.ϰD3(FRpĽbcIP-iڦ-(ŧ $2S๢1'QxK5WO+TG U1t'1n"nǟT~4`jV; *QdӃ4!ADK/Cg\ _zh.k9QlBٹrjxn1-O dP )4Ɓ@  {(EbK6eൠsHl'{(1   ̪L�)ZXNucXy,XsbmJuP)c]Ś-Nbuf'pI͠ sf8lQ)!Xbsjgb|xY ؒbPU)!I&Cd#,aR`$%*keTVU:<B 7K9;*iI䜸3*t(Bx�]Op'BeGe|к^✚)Oz><,c"wp mR-o3-_kG7DQ-M}(>袽#E>>F0ݸLsy{۹Vѭ}f6Sʎۯdu܊6t{\IFtgsWswso;ֹ?QF>=?L6wOs Z_Bjxݙs2<IWZg ɃC\N[5 u. S?5U8A1]t;4xFŴ-\0J6.SwP)ZKwvRMW^am:ny<c(%kLӘ#EI'1$d<yaDw{ NyE򨀞3PKi��$��PK���:������������'���dumphd/util/PackSource$PackRecord.classT[OQ-]Z"HKEzobbҦc JnK[ |'ó�qveb|993_폟_㶊.TQ˴<<՗VTDbGd,a72+F#k+z 61߮mrD$H |A5mӻGPUBS3P0Z shHF#z輆8bi[[D[\9QvZ)3mML?@PRXz<⹦ sPЃ$ '4\*>Ov 'b*^{IFJ ?dSv솆Y\a;j+uVM/u298tU̱ah{FpekQ,ȨX8m4dwK ΐ=@8kVmýofht(W /‹a;ňFɷq*xSПbPrays c|^#/oy-ߡc>`v1q].zq)hMT5IOJߔtKHT4s}:&h:}TXnPHjB>(>22>A~>1~0xZQG݌qe4*1NXC|<);}naPKW����PK���:������������#���dumphd/util/PackSource$Window.class}Sop=vr'E7-W>%&3 t['R{_ıDK_Q_2g]_{+�.!@X>v~<Բ`WB1zϰ{#p8R4.!rò-&jI fA2@8)#HO Baqm:v'A5 rSu"6\Dz8$ đâR3 9jU,*2.`M0Cύz(OT%B^^%TQ-XB`&,7FܶCVA^mY9ʖ`^_W8rF$99{;^ }b{2H!>4B<?׈$� 8=3hc~T+|Nw<}@>z4G~J#2yHgԻ\8/{TqQN&(sM*uʫ'О8}F(7hݓrXF<CgE=\5~PK6����PK���:���������������dumphd/util/PackSource.classWkp~޽dB & 1Q0$H\.BuwcwZ{[QQTjUh wLg:vZ{Ti8ϪEsooIuw9}9~+�+ @q8l\gvFcBvVq ok\uHx7iEh²W(8.nUp E))Nk HcۜRYH,H]H zCfb8F9'ske2wn +Lzcz;pA3\+ bV8aw)~u Qst0bE!qBt(eQڬ I(C'Tےs[٨N؆ݥP%kJсx(`f Η3#w:o`W XQi=XaFj]j1o\Lt/A~C _FtB"]fDtb`.JPVkiKWyi+^'60"uDH T={p X5P2Xu*p*N5ЋP)Ґ1/Z``$HoF/RNLkVBM[T7܂Ē| xn30mA]Yԭo; !CJdLӈ*JG -b`W]W1jjŮ7 #, f,$d+1B$Z&,&8omverӞ-^f]$gN|=;0UH*V?56/a�I&b hǖRHD#Fa oK 0:#b8Bk2+ m(3z xP'XBz2qZ`>yew.AJGW|RkY;,W\5( R~4m ˕B{Zgd8lvSI 8PIOYKdFjًEO LkM85s'̑5>\x,)Iww#qIگ$"6{DzmJD -n)mA", [#so` H^;lNJ3P$4qx3D¥7y!L{Cx\Mb-O&2y<_?\%LMNϟ?ZZ_1jPKbk^r%|sBɣWpEj2DmX{q2Bf5٢f=3]+ o ͧV5uq_ԹeS{Mo918YK.7mqk6VFԉȳ.,$$M N3C_E&q-7 \(O[co} i -/|{9Xlr>$$В$SK>.{T=/=PᡭsBU\`PCXD܇afW8q1oSUrji7%|ij/+ϨQ,[ܟç=Ijrz S,M;Uo'M;U;ѩq2ũv-\ה|guYyzcھ}%djWH?I4;Gפ|!]45 R}ԣK4PPSw2\aܹ&#Rp6J cښ]y)Տvs 5xr9xvsNx~ '0N__x |x]C_t?D6>}׻*\\/x:]<)<6)Ŀ 7O 1='NGu.wg<W_u;jOODF1o�~G=w;?DsV1ӈ 2 Cm3p\^M>a% Sv[se)賅cal$&:?`#K;,x/K::qH]ђ$g.Ix:|˘f؝^|y9u'-(nO&\`NW&xڊ S9j@A91GPYM{phw_< 8U}\',xWq6qJAA Ph6U=}e)dxhm┊>"j{cQ,VZ$eI SefQ({+~[IY,0!jț7-EglEӳMu,O{}KLpYsAL+s?h}uy>nʟd<(sK),`$gé0PbM-+~/DjvM_f<ZpD-s'RjZT7>V=J)*TTN5^uڠKݠQ{}fePAկj@ݩBjڮAuH gUX,L+�}i?o.t5/sxO2HP=<gR<L?KH>3b+ fOs>KCb Oc2u4s%gkܚ;sp9\(J.TݨQv,RwUݥa(w$ %ɾ_6,\Ŝ7KK)7y[?PKUV ����PK���:������������#���dumphd/util/PESPack$PESPacket.classwƿ.=F<-?c4Œ5mYv䧌 9r 5Ӳƌf-l8 HB,Xs;e- prf\vMuU{O K|z9,&fy'j 4;HEt6==υd%yza!:XU[zvw G( ࠵ΆVUb٫3r &.~)(KTͭA> 4Ar9HP_ U8el l,.PJ%ȩ ױL԰抿Pb!غAϔE1P*l\y&_@}e@*JI$~8z@=KtQF3vH8) ]*%:D2H}qt@bmK%z8Ć8zHbcMIl%cqMJeP^^'Oe^`AM&%;LH?ڐkv KQk-%ǜ6/nwz'@_ ժ"vVzPc fՋhwўmઙr*z]jI|BW` %Q$M<)}1^8jc}ß=jyIJZCXY3ju`Em{h[sh3m;l4jK|xٝp2eO5&v8b;X<,e.V7fX au3nͰV7꺬꺬꺬꺬꺬Ku;f՜ԯ!lW vi, ӾluR$Խ6۵оS{=ݵ_گѾ[گӾGo~K^V>Tzvi= SC%.n_AeٖVѰ22#ʌ*[1e*_RfB#dT2;p<&Z X+Q`8- vDD<UL 8!Ve,XPqHZ9,y'mE%UoU R) mI*8Mn6*/*_:jt} *jmF޴XiVJR) G}YEJgm 5\"ōlRZZ )$_ _+FLl}сsjmo?mK+RCo'[Tʸy3Ӈ,Sw=Sh<2;kW^ӆ9](k]DT6))o x.8 SH*/jlM o6'cDN7Ìlg cؙAdU|G3뀗y ߵ1,0{ֆM cY[c7snA@xg?w6jvĘn)1;3f2e<kgL1ٙ&s17A2ַ3jJeiEvWGVdDh?"{Mk+Dd?"M+]C3;3a2+9b2ɚeL5U Sj. hE&4Q]iSky7yq5j/6˜d̐L3v51.\fEcd_;" ĚPK H��S��PK���:���������������dumphd/util/PESPack.classY |յo|>`򅐄LBHH `H" $I&ɐlL+.χkVTHMQܗZZWof2bs9ܳ{!� ! x[ʫ\MklP \k]V]i  β%ռpeE?Gum_=WUO`iatHUI钆 kIZ.@P;Y r)e~+`OO0ֵͥ\7nw|B) s*0u|pfc C J;y;mNSGdeײbn Q1mgt*:l&t&Y5du/(<ώq`xBBg{1gk?5Ff9+NamG u `mXY$2Lf ggʥ!/E%e5stk+è)lxrgmIMyCuʆ)êJʜKYa:!|a^aC|Yt %ΥfcqTX!Ԗ3:.)j>4oJYC !UҚڏ+`KKcش" cjp MӰ$XCe t bfh8'iIS 4AboިAboAbo\hAbobM1H O {cB7&Abo\ 7b 7.AbolT޸,qy { >rJV9O)]ή\5 AX1 ?aa_? AX878 tO^;N>vzv]`{N`UjI{yRV' CU4T.�Jjń0YնqjZy<$i{cބ"ZΠ\e#;,xLnw:b }N!O5˽V,8Á1v&ZZXNϊزV.p֋^rCe1,0/e^-"\|KuWYjf3{q?zJoⷬ߻#;% oqݲ^x0=k@#ײk{ۺn6%kn^}C||E#T9Nqjj7z8*/O:>ǟy)oBD8,)E uO$H [?r*a`*{]Gi?AzSot|+WtdC~yB?#HnHG)Jۈ_'ۂ.oJ3y3Z<.Fep{ Y7quvYE'BhN: ~7бȳ.T'Dp4\'%H_KTވYDFP휕yir *hSQ tD;&SzdDf PlNcSvN"L")G(Ti;M` tBE:Md)izviVV6GBi|ΊԬPڶ@(\ҶE:9Ҷ%:UDamKuZ(ͤm5:J(^"i:]5ԡH֤; +mkiuGR#jmcFYrh(p`Uimῳ8Z7% u ̡gk/N]QbPk?ɗ>JGhNn_sqCvu934a|UQy|E^mǭEp |w?Џiwg ixX(=8Nu6wX$nw{ĬőGֈ;Daa4qiMu[zg;;{/fhS  [$blSfNw)~!>CWˋ$c֬ FtO<8vCy2+C#nKsJ_GLi' zEHeA!)Ean2KkK*ev+dK2 CXJ,Y|}Z^D63uQkU`tvz*5KANkmngYć;I1;P.w`mw{ .:ezFDٲ}l^\^~ӛms}VmAԄ P <#HPA.i ~B &@w.7[|LAs]&^ЃZ:۹iuQmaoa $,SS7IRI9j1 aX?v0\NIɔdr+p!7+8F02Fz:˨>CVc5*1cXwIoL6F 2 M1>6Mc |j2ό9&p˜'sdg1X DZFgqHrhTLc*9fc1ר$<9rl/|B9rbcc,ǩFɱht]3N94|re8X+9:95qQMbLfx 1&q 53獭&q hd/ƭ&q jiMu&qj͸O ٸzs" gspf" SiŘbJW٨40p1c=•(V,mu7Ѓxa, X7PwYGX?cD9ɆjJ nkLK8:zZ*Oj܆5PrkD7MwCzB'qz/q1} ~Ŏ.%k DutWaRJ.SjI�{i$Wh`)쯇<A9 ~ xa3;KeŞڋs,{7g?zZ/܋sh<8%ϒyɵ|l1Sꠛ4 p[Z+5F rݫMx%a鎡Eۦb5'W(.:rT+on< ؇7x8t/F~ˡ^+xfYsx6TkڇTk�؇2Me=H xz҇zje}n$xcRܽLMem]m:IM 79э=9A@ h~%R\/ْtwѰ}d죑z8MúѺF3mʓ&'$NNƙUN&' fJ4na*7J64ZŘﰚ@ap㌸F6#ƶ v_X4/bI1+4ea) jSpVw3:S vk=< /gId(C@pփ##2 :(GsZr?7WۉpeP4|Bkc Z{i6ؓℋn6!Pe]CVZl$k6$dwLKГqn!+_֕jJ, nǏO$QD&tٺQ]#Ci�oC>w'A[=|?=w^zC=sr+7 O|x/MnOWBͷһ|O*>C>&#LSBsWU:]:7q'QHf;B:J*VHS4+ 4TIaP(AM4Bi5Di4Z(RK#4AyTޥl;ʱ$\liTdIŖh iOs,\g4XN ,S:D-Ju.UU\咭u^SwSVۦYtm# Ӓ)nX'fƩ- aksVd8z]@jw0d7|dYD9Qk1-8_V{dU)(<9r gc3+̛[� +|sK+r}s-s{𺸮ͻj)!<B13 iL$}܃yK*cLGM^ cE71kI-ZCR JFq<*0[J?=CUUHa7\;A.$s&3 ZL-a6l|.ƍv ^#iu8J5q>by_!9bǟ1z0>I89`QNv6~fs1#A= 7bf8rD,f|n)9*([RPE,SsEcQ,\`̺i]]9oWuCqn;P!`<=& %띞}p9WN:%tdq暴m8#ñh�tz 0#\e{aϡ\E9dC?r?y*zLd+iF&Egq lݸaRm.!iIs]fw)ee;94V:Bv�4:t{x Y~V<υXOsA7EIJΓ cGl:,3!iv~yFIݮ~$>YapEP~l!BEp"6c2VG ;}g}'WV]_ڑ}ݡYh IuM(4V/I- ϣ"Nj3B͸{㰆gBryњ*t%)ntTXQ,~MSx8o̒*(?nMSvn-sӔn$E&8m)=4<�D#7 {ŋntDkDhKԃ9IC}\ܟS4ZaΨ@p6/CQa2L*c1_E<T&FɂKE2yVuJAD]l ckñS\JX{\Y4<*/rD94= ~|wtaFO,{R:CH^b$+c$ PK}E��)��PK���:������������$���dumphd/util/PESParserException.class}]KAɏ-+-MEBHKÛ0$I]ُou%tGE笋{1wp |OQhvgL\eg"#i[e*.*;o" $4awH6uxݡr AjR7H۵9r[8rZ +�N:CzP5Ԕ@'=^li !YXq͇bq0)#n }w Ȝ6.U@)"9|Bf (E䐚^`vN.|Xafô�*Cr36*"LN'PKts����PK���:������������$���dumphd/util/PrintStreamPrinter.classJPi11uuۂE/DQ\ (OC=&\ W™L: VܹlUHճ@zyS:^zڸ@/a[y,iDEHqDiۄh0DN-a!&\)MIii0\[wꤙ.eH(^T uszl?q7iٚIL"8loz鮧8EU2:)sU'hs~NwLT8r+!,�JYVFkF0 l/YCO?d g~L`V 6/.6t )5~KhE=PK<��#��PK���:������������&���dumphd/util/SimpleFilenameFilter.classSNA[X) EAKU7(cBRФ7lG:nvKO`Lc"hPeA%3̜sowϯLcYGaҮ7ɚUfM<5=3D6jٴml 0ʆu6aUp{t0X\-\ə @+|*1W2hSRt\PE=D0:9F0ʠۍz(3:%Tql3Iv}C/6+l1$ J~B6oU%6+z~ƛ>[g>J$Mm& 4<1<eitX&JU(`WH.5puU5q3RnQi $X;J"bNlB �T*Ͼ4R*tc(Hvm_2 _N%:MF.GLjH SC`G' Ӟ5o=Lm8-c;!.GC\!3%hJk/ *?~d1a!I`(<Qo%(Vh|Kp8B"(,L!~OT7/әda+k~A>3amϙ�<Fp]]|vI,EFhXv]?7PK.\�� ��PK���:���������������dumphd/util/StreamSource.classU[lTU]g3 }�e`JTVX:-(3ӹb⟉_RQM@~01Qc41F?|/Cqs;m`kr.͔g Se:F͌bZ'P{^u|ch򼞶4}z/[!1J)dBٲ1yh9*[01g.6rYɵ<L̒MCqJ V@ckj1Sm>3GŚ(jP'mm "$gk#"gXmb%VQ`22HDB"eb"MhfB$idmAlytПY-w-yi`f>6n=]$f(0Q/ge+5.DI&R"oZ -p`+&PEC ޱC@5EҟEu]-xr`%B*IyXyfR/UKi-7 l)YSiRT! X9jiA�N@to "ﮗ!$8z؋8(iu3@OuZƨ)a*)i%seJ->43%&ssJ:WZS|Oii%IcI E1aGx#\`C"b+WtVduk,R2W%: yˣdJc5iJ+[iP>; P'$KF>c^3˲ȑ2j;̒ZOJ,Q:�CQ1/I*rfֶί:,( & }AeU_}T`tDln;vyO}jd] j] Nz{}vq(?}g ` ு}_ISĥ<6M6):_ylΡ äDW a4TrvxÒ(zIS.A%qiэ#\(pǐr[;8I;NC͞JU=LDy)e+h11,Ӻ6>)w9plhv]g|r;!~&^GDsoQ,oP#IW[tJL+1L-/q%f8m#aH+ BdNRUW*%'Rq,n}L݋\;l,maVhB2%qG:z–ajM*jVB `, Vbrcb"vzg-º#=Wy|ڼ`~#E.g?cU[[Cbqsh3}8gi`I7Y8̒bsaU?O8,ےOu.xw(;2"EkHگVwp:R[{w93�$["|;8R{؁/x%~Ei -w?Pҏ? zO- TĹə 3u?dje3,?5t*( �PK 2@"��= ��PK���:���������������dumphd/util/TSAlignedUnit.classuSMOQ=ߟR `ARUAʇU`ZH>ڡ f:5bq`N Fdzs(ebf޽{y!XۨS5SRܤt:SKu`2tA~2z亹ȵb:@BJQ#`OVW e\*EwE.pM^k 8)Jy) LEn"sM2tgtYF]Y1/TLqt(75"7y,c i>2gN3 U/ JWZF0gGb)մ^06+RZ!n X\S2i|3KH2xJ Ȭ�Q49K۫=* W*W{jcI!�}d0ցN8M  N9Q<oS� }OajWAHR(1l#.RGE+_шbhs ~D(1,(DW> M%_n7 HC8oя$ΆAokwKd$G{|PzNǮ}NEE,AYZ(e =1y&$zTFBwl廷ڐkۭw4̑HER>yK%hycLYEAD]3={`p/kPK,xB��y��PK���:������������#���dumphd/util/TSParserException.class}]KAkLKʠ5& @~E'tW[] ]Q9b^ygs?>\!$PyW\5Ov k]e"g"Cie*..[gzQR eިc-ՕöÈ;Pqce@tmek6wImd8^9S@25#J!fvW _ZgH^ ޴<k+~rnyL@4L!ECU5UA5ON"8q@G nRe@2�ޘ^ 5O('䦮ji9M"`=PD镋 vagt܇Lm3XKnEܛɩYsPKwSr����PK���:������������#���dumphd/util/Utils$1DirElement.class}QN1=&!!�M@eҪMM R�@LEm.J]({ P\Ƿwv t4&>}//iJ /sEcc[dڞJ)-m>hOom%DJ^$($"hFþNd(iz2Ӝ?@γ&a׍DJ;Gmb\NU1pi5'|6IJD1CE˻ vƐ5&PY댽>[yK`|*яOIl?R|A%MJ7Ys7I)zo(+\Te`:[DX+#^,<ռhɳ-Siju ^3PK!����PK���:���������������dumphd/util/Utils.classW xTl/3y!0A,J" ,! Z̼Ifs ZZQ*6Z%A'`(jXjmݻWkVmk5dOs{O<`bUaP0 Zd0TKBI}w<䍴kޢ ,ͩ@@+0l[VJ7cT<NlHx[sHRW(ŋސOV;rzEɩ􅂑`r9u5*0~݈3+k0"&iP@Vd\T ~{"5*-w&i2ԔQeO3> & P" 5BS06Rf=dm 5zA.m0`$[ *4B7EOŜД}vN&$FnQY{U o*ޒ 6K,Q0{C^<TEq8H7őh8 lX�f)s8$wT1^ֵXdtޖT]SS6r _tggҚFPL7#~Ez3 *ĆUj7fR(Չ8oD^R×iI2D3 $u16 jE 4b,!ЄMW%3m8b~7%>fTfx wK5e5ȣOP jq[侓R(eۄY $oX'JPV42$Oi6w&/k}%MUgrϴ"B b'تk aP>g`|eHfC ^B:{C m.B1-+ʚ &%ʪL䑔 z<W07kj e_Ѱl8yCR_x7JHs9M8)`jaz˘U&z'5 Z 7f7)ǥC_N*ŎM1jd�Ʒ˺mަvބɵmPJ_P04 քcӚpn ?NiSGQo CiL ǒt`2AƖ'sJ# D}q$TdhBVE:%j) yp&J̌P:TӳV9~-g-9^MzBWoOTV5sTHl?sf]Up%byRS#qgT./T<;l߀SL'(6"5sz׈m/hd(LB^˄ {z8箣xUkx3kĎj~#DmFQßY<w1#@L>зp4%yD榌 8:coGjޣ76QL>EjNdQ TCߠٍEtbBO\S bp*.PHA8Ʋ;8YAbw\L3mɾiƠs)=Bg~QLEE&nbn5$  P(~kP&{y$G||`6{<&پc/n% ) U/5~*ViUګyn^Wg_x 0�1Ӆ90>Pk~Z),嬌2)=r.8TXbf<$OsB։.7g; Z< c\7zwᔇ}ԑ/A=} �ʄ&eȵ2Tdm<5Uݨ'uay].S܎t_$}+};͊0}i~+4i\Ч&M׃*e2ji5.l*Fp6֢:s:m9YF çpT9 X EiԛCˍ.e6,X갘[]ӡq~Ν24oòaVoiQiz�)A 2-|J0Gx!"(\6Vpm-2ol ldj: ~>ot8q B 'UUT*mҭW!ApaAtҍgO#Z0bTEs'.|P_N_Oi\{  Mi^Ss]v\].J=8cwbuT+S{S-|HBH'C׀;Zsd_o\=;d!5kQPA։ nǹ717BNwVn[};el]2=bҔ a;eu� -]\3ӏp<A&x#mu0s' YGw B̀E*=(W;+mTfJx?嶟I(<*ȡAC; Gɒ/S;17'ڱڽ+E!YŻ&Lvrr&aU5Oꗔ2;RL)*ȥBs?^qXvy+żU{{/.̸BuDCi#QS+1'Sp*!'EDL<.(q}CTO2OZa?mx;.Ëx{J&ZoȬw2af#eߔl=,f+"D1Ibk/]e1?kY$ o7mp,3X/H;SAZWK^I$>�6KfM~_ަ;w)}RCyFħw+S]a"i|2*&>&jv'%FJ=ŢrVP-ЛPK5~{ ��y��PK �����+:������������ ���resources/PK �����+:���������������resources/icons/PK���1*:���������������resources/icons/bd_logo.png>PNG  ��� IHDR���n���&���%P��� pHYs����+�� OiCCPPhotoshop ICC profile��xڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $p�d!s#�~<<+"�x �M0B\t8K�@zB�@F&S��`cb�P-�`'�{�[!� eD�h;�VE�X0�fK9�-�0IWfH�� � �0Q)�{�`##x��FW<+*��x<$9E[-qWW.(I+6aa@.y24��x6_-"bbϫp@��t~,/;m%h^ uf@�Wp~<<EJB[aW}g_Wl~<$2]GLϒ bG "IbX*QqD2"B)%d,>5�j>{-]cK'Xt��o(hw?G%�fIq��^D$.Tʳ?��D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;�2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ<FFi\$mmƣ&&!&KMMRM);L;L֙͢5=12כ߷`ZxZ,eIZYnZ9YXUZ]F%ֻNNgðɶۮm}agbgŮ}}= Z~sr:V:ޚΜ?}/gX3)iSGggs󈋉K.>.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz�%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9<qy +V<*mOW~&zMk^ʂk U }]OX/Yߵa>(xoʿܔĹdff-[n ڴ VE/(ۻC<e;?TTTT6ݵan{4[>ɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG4<YyJTiӓgό}~.`ۢ{cjotE;;\tWW:_mt<Oǻ\kz{f7y՞9=ݽzo~r'˻w'O_@AC݇?[jwGCˆ 8>99?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-���gAMA��|Q��� cHRM��z%��������u0��`��:��o_F�� YIDATx{p?gGP'T4bQK֪ )u#ZucԩPhMD k"H$vO Қ~N=sϊM<@_@l>Kuy:p ���#Z:_v XPP !nvsrGZ"`G.pј6U݂;H'(�J+0_]'~�'m N_|SGx.0 6`'3^tAڐ iu}N^df �nR=u:nVW uRg$p:@xd̨K~["[5#FQ:y�a: )~;-@$W4%* ;@kQՕ&;_K $nІuLʫ^CN2x<  xD&F]gPJ�'^ (J� "i ~ޭe!jc4pۈ42uv$<J V3dJEO2a5)Hg!|~ˁc2˓�/NN4ik=t他d 1*m�'sJ'D90(7SBiF ؁6,KB zB6J&n$y2ZNP׹=cJ-vn@нM> FȵFĔ'Qϩ\KiJ[!�hB'1OO nـwXrƿI ӘCN`v/bA/遆= a vpX_[Qk3= x 4Qpu,&`x&6u'SXIV�<cV:u֧ 76%::B.ZKl1ZkK `_p V:i &lEt1Ϩ ]c]`1뀞Xvޱ#;wofwM_ꐚG*n{rmL g~W$uZ 3OG%@]:Y ^սt\ޖ#q7 dd�]-/Zɨ6q-Q]@t n�6W9PK I? ^ݟ#T>k:@N!bDrpqy[h$ŪPq9@anyt=b X_-3`_/TlxUW"sER^uTs@2 EʫGGXO}pS8@a}l`%CHe:@KI݋VV@Tǧ+i>?jŒ~טCMSRg|Othl9 n8JWūIҫsZ4Y*N}0<jKZzF^ٗa@`"ՃV`h1j_1Z \|(T@ʽG3_@uunt{^?r"PgѣTvuC %× 撙cயD !?U7U#cߤj<,ۻ翆؟:KnFyNJ0t 0H3N<0 W|HRxZ]F),>=Ρa%;̬J KA -~M;.4 ZZS<"`Pge@othQ`Nנ<M@. )EQXdD,v:DԠCiu̘@<1p10Y]0Am 0 3曥:%d*'б�<ؠi lDAXTEX--)eX$Trn4)}`=TAeM&(hU )M:Y�(Qo�ant|,^em36-7>qטS,T`ac1lu ϟ 6܃p_-Vs,e r'-qϯA7+"臥e0JlIo!?K]I2w DehȽ-ABY`8oFk`r2?gP]g㡼{d$nN 6`2>q^'E@e-(%͞dSElDG,J`?<ڐďB�rL ZDFդo_+,lk:qyp2H6QYOP�"p >"d+W$0(7lRtiT5&{ljSar�|EY����IENDB`PKx`<C��>��PK���1*:������������$���resources/icons/bd_logo_disabled.pngPNG  ��� IHDR���n���&���%P��� pHYs����+�� OiCCPPhotoshop ICC profile��xڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $p�d!s#�~<<+"�x �M0B\t8K�@zB�@F&S��`cb�P-�`'�{�[!� eD�h;�VE�X0�fK9�-�0IWfH�� � �0Q)�{�`##x��FW<+*��x<$9E[-qWW.(I+6aa@.y24��x6_-"bbϫp@��t~,/;m%h^ uf@�Wp~<<EJB[aW}g_Wl~<$2]GLϒ bG "IbX*QqD2"B)%d,>5�j>{-]cK'Xt��o(hw?G%�fIq��^D$.Tʳ?��D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;�2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ<FFi\$mmƣ&&!&KMMRM);L;L֙͢5=12כ߷`ZxZ,eIZYnZ9YXUZ]F%ֻNNgðɶۮm}agbgŮ}}= Z~sr:V:ޚΜ?}/gX3)iSGggs󈋉K.>.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz�%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9<qy +V<*mOW~&zMk^ʂk U }]OX/Yߵa>(xoʿܔĹdff-[n ڴ VE/(ۻC<e;?TTTT6ݵan{4[>ɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG4<YyJTiӓgό}~.`ۢ{cjotE;;\tWW:_mt<Oǻ\kz{f7y՞9=ݽzo~r'˻w'O_@AC݇?[jwGCˆ 8>99?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-���gAMA��|Q��� cHRM��z%��������u0��`��:��o_F��!IDATxylTUS@ *"AD7ZTQuCuAT ,b$PP%F@c*@o2ä훶tIn̼ޜw{=s(2d!Hf@F v%Hjjj2Os yK@O|Nu@:o*TX D8瀿O5�n@mE )ygۃ1a!RMPuu@+~f�w˜vV𸀫m@з }lj}P-^5Z\"w`tYl# Hkˏb2a ?e#lZ�\.WZ0�mm*cwwy� (`~('O8+Of48#CVM$i+2ͥ]ujҹ*C\I+y%9>g @\, Ȗj�݁OŔe",А"';֚tVE),mc|^Ć\`T?4*'K6p>w{Nm}d6p8 ~UIG-K+=x8 �f/NQ8�2daż]~d|V$+ɼK)L x‡޷nzfRGu4j{h S|�[9TU/'xXD5B)3#r \MX MJ=Jx8R<.GPW05ReC2+Xn]Y W~ *HcSp6V"W{+(+vǵhYYc#V[8R]ulu5= |/Cn?u6`{0EqA_&:1 ź"4ڋ{>*l56$ɲ[$BָÕ@!$ 0UNg�8R_hXҘS0OhotLc˜Ɠ= 3œ &ܙXOlLUZu&S6y:vKt ߏ\ȪuG`YfaWׅ%x]hǺ>^h%݉CX9,E>H5"jyЁ(]XF^>𖼯qX2yeund5oRRy4=EFe{/2ӵxva}.+%^}Z(_&bUXq+W$@Ori$6 NJհU A_vK /˴NĚ:ޝViA.Md {SZ7YX\,g~s3Odih߸KXd*ڠ9"e1 +HIҵoc$:SP|{I}m3sd"[KSņ[$55({Z�`s(U?Γ9\"6Rۻ T<m4ImdʖiO:fI'c>G䴼T2?sGke_ѹkRҦsZK>ki$n(VE$[ixZ0~@|!(Û$.9!tTW_f0CYoz )t(j<mx';IeJi픹.s</p�MehPC=H^&zF ZhO((</D 2!P;B\'핦N�qsǹY����IENDB`PK; ����PK���1*:���������������resources/icons/bd_tab.png PNG  ��� IHDR���������A��� pHYs���g���g(&�� OiCCPPhotoshop ICC profile��xڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $p�d!s#�~<<+"�x �M0B\t8K�@zB�@F&S��`cb�P-�`'�{�[!� eD�h;�VE�X0�fK9�-�0IWfH�� � �0Q)�{�`##x��FW<+*��x<$9E[-qWW.(I+6aa@.y24��x6_-"bbϫp@��t~,/;m%h^ uf@�Wp~<<EJB[aW}g_Wl~<$2]GLϒ bG "IbX*QqD2"B)%d,>5�j>{-]cK'Xt��o(hw?G%�fIq��^D$.Tʳ?��D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;�2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ<FFi\$mmƣ&&!&KMMRM);L;L֙͢5=12כ߷`ZxZ,eIZYnZ9YXUZ]F%ֻNNgðɶۮm}agbgŮ}}= Z~sr:V:ޚΜ?}/gX3)iSGggs󈋉K.>.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz�%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9<qy +V<*mOW~&zMk^ʂk U }]OX/Yߵa>(xoʿܔĹdff-[n ڴ VE/(ۻC<e;?TTTT6ݵan{4[>ɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG4<YyJTiӓgό}~.`ۢ{cjotE;;\tWW:_mt<Oǻ\kz{f7y՞9=ݽzo~r'˻w'O_@AC݇?[jwGCˆ 8>99?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-���gAMA��|Q��� cHRM��z%��������u0��`��:��o_F��IDATxϋE׬ebnZ!"/2:$` vKQᡣm~@MTR*=duҝ.|f~~ 9g:,lU݉ ra<X18S]zX=l)L?A 8P>i 20S[rum~w~QL�Ѓs ch� r;jc[ȽrUq9 ~a5VaC_:.ࣜb |O#C9Ņ8 TKo㝜@RӦJJt7LCx0S2w^rs 9?!)·CyrFbP/p c?8~yW6,+||Gn!0ĖGp0b# oc3^rc_c$v[LPppv=pO@+x3T`x o^ҧn\XNR׋N�YcT(����IENDB`PK,,2 �� ��PK���1*:���������������resources/icons/empty_tab.png PNG  ��� IHDR���������A��� pHYs���g���g(&�� OiCCPPhotoshop ICC profile��xڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $p�d!s#�~<<+"�x �M0B\t8K�@zB�@F&S��`cb�P-�`'�{�[!� eD�h;�VE�X0�fK9�-�0IWfH�� � �0Q)�{�`##x��FW<+*��x<$9E[-qWW.(I+6aa@.y24��x6_-"bbϫp@��t~,/;m%h^ uf@�Wp~<<EJB[aW}g_Wl~<$2]GLϒ bG "IbX*QqD2"B)%d,>5�j>{-]cK'Xt��o(hw?G%�fIq��^D$.Tʳ?��D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;�2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ<FFi\$mmƣ&&!&KMMRM);L;L֙͢5=12כ߷`ZxZ,eIZYnZ9YXUZ]F%ֻNNgðɶۮm}agbgŮ}}= Z~sr:V:ޚΜ?}/gX3)iSGggs󈋉K.>.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz�%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9<qy +V<*mOW~&zMk^ʂk U }]OX/Yߵa>(xoʿܔĹdff-[n ڴ VE/(ۻC<e;?TTTT6ݵan{4[>ɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG4<YyJTiӓgό}~.`ۢ{cjotE;;\tWW:_mt<Oǻ\kz{f7y՞9=ݽzo~r'˻w'O_@AC݇?[jwGCˆ 8>99?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-���gAMA��|Q��� cHRM��z%��������u0��`��:��o_F��6IDATxԕ?JQC GFHi{*`Zx*{�ۀ ڸEMp,--GR`eom\;l{%_ ,/vq{wI[ l#UG ` ,5pH6o ;-L V l`xRO"(} Qh;IgR*i.$Xi@o|_�5F\1p$ݍa&8INnkj7\g3p(ԛ +rI>*%ef{R㪠 Hop?�@"y����IENDB`PKmd �� ��PK���1*:���������������resources/icons/hdac_logo.pngcPNG  ��� IHDR���n���&���%P��� pHYs��C��CH#�� OiCCPPhotoshop ICC profile��xڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $p�d!s#�~<<+"�x �M0B\t8K�@zB�@F&S��`cb�P-�`'�{�[!� eD�h;�VE�X0�fK9�-�0IWfH�� � �0Q)�{�`##x��FW<+*��x<$9E[-qWW.(I+6aa@.y24��x6_-"bbϫp@��t~,/;m%h^ uf@�Wp~<<EJB[aW}g_Wl~<$2]GLϒ bG "IbX*QqD2"B)%d,>5�j>{-]cK'Xt��o(hw?G%�fIq��^D$.Tʳ?��D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;�2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ<FFi\$mmƣ&&!&KMMRM);L;L֙͢5=12כ߷`ZxZ,eIZYnZ9YXUZ]F%ֻNNgðɶۮm}agbgŮ}}= Z~sr:V:ޚΜ?}/gX3)iSGggs󈋉K.>.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz�%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9<qy +V<*mOW~&zMk^ʂk U }]OX/Yߵa>(xoʿܔĹdff-[n ڴ VE/(ۻC<e;?TTTT6ݵan{4[>ɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG4<YyJTiӓgό}~.`ۢ{cjotE;;\tWW:_mt<Oǻ\kz{f7y՞9=ݽzo~r'˻w'O_@AC݇?[jwGCˆ 8>99?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-���gAMA��|Q��� cHRM��z%��������u0��`��:��o_F�� IDATx{le?e=СD&ܺ!AE0 ADy#F%$ Q#;((bu a]w9- o=0HKy~&(%̆ %݇lXa<<< !iTf !),"?<�3G~xTYX^`PdQbh*v_> t鬲.+K5c!VDѫ|XN"|:PD$@$R1^ o(+=/1L@8yp:gp-$9G'�t& ƄoyB9MxOV-.1pEs p I7'b0#idq-@c m>,FII8|ܷQ#.E9IcM87 !r`f] Gx_ুC]89xvu'b&]]ztJ{h"_~*%�?+3g숡xy|6B2?c~ZtӆT y]e"|ڀ:3 beH{o|5Kz>|&KtAnt`hf ;[돗fVdi&%.v]3=:}HR<npC­XP*,<z}"(O6 \gNᇮ FQ3{$xGL!+I`Bw\n,n0c{ _޺)9o<|<47zhzwy/뵂{bFZOqO翦w"n1&e[<x_ui ~ʃ; |9&\xk3{ES%L8cͺp[a '6̿!8h ae#[3�DW /;bMx|녵1#/.[<#x z5 K-S "? h֔Ӥ:w C<Td#/nt}ѪbƊWfW_k<[cn`qJi]D=,Lfb #6'T>z^*˺CuUܜU(ig:fWWM$=Fy8'Nz=;y%=rX8b9n taӣdj%`Ww.7:jG'&g3uuRS:[4YY?>:Z?==]_W[[˱344<Y355Y[S39NO.سgKz'I<ulmnzPhAWsgw.7ֶ`laA �2"u�YVz1{ɤ>3X9225IYIY̲,?(`ifb6I1֡55I2ð?01WHe' Shgzoh;u#oiYxL]fx 62{F0Qc ,Դ$ e1[2Gxx x,cd$c.茵aW;9>ؘɜJ_" >4oՁ%&`}&s1[J0d0^$H! Jhc/v0trJqr㣒n+T䴶ލtI) =<l1G!]nmm51J|Wۘ5V(M.tINK%\V] wo^XRcv ;kjY9'9QK."7^z�x;?٦ƱAЭ0r&xn҃;_L)/6J7tES�[1;㐲U0؈m; ÿ)dڝ` /�1kgw.my,ji9CE;baEv^/l0inWskc"RK1[*8j PGofvl6iȳefv{2{%sד4D uM[͑kɿ2؝˭)vV[IvZt ZY$̚&f[Wl)%~ohnnͦff ,Ap,R;R#fiA}`FJpչ"H20 WO�&1|;; ~ ݩԾ}}͸$|drƟԬr;dG &9!UM iApb I7n++"b| mT\TOjR-Rtx%|rDOQh%zOgyR|qq7𑴙o>ëD7F̗OW waĪ^ϰ\$U+tU!Vx=/X!ǪTiH^Uy>YJS.A|+Cy<_>�o�_v����IENDB`PKMI?����PK���1*:������������&���resources/icons/hdac_logo_disabled.pngPNG  ��� IHDR���n���&���%P��� pHYs��C��CH#�� OiCCPPhotoshop ICC profile��xڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $p�d!s#�~<<+"�x �M0B\t8K�@zB�@F&S��`cb�P-�`'�{�[!� eD�h;�VE�X0�fK9�-�0IWfH�� � �0Q)�{�`##x��FW<+*��x<$9E[-qWW.(I+6aa@.y24��x6_-"bbϫp@��t~,/;m%h^ uf@�Wp~<<EJB[aW}g_Wl~<$2]GLϒ bG "IbX*QqD2"B)%d,>5�j>{-]cK'Xt��o(hw?G%�fIq��^D$.Tʳ?��D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;�2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ<FFi\$mmƣ&&!&KMMRM);L;L֙͢5=12כ߷`ZxZ,eIZYnZ9YXUZ]F%ֻNNgðɶۮm}agbgŮ}}= Z~sr:V:ޚΜ?}/gX3)iSGggs󈋉K.>.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz�%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9<qy +V<*mOW~&zMk^ʂk U }]OX/Yߵa>(xoʿܔĹdff-[n ڴ VE/(ۻC<e;?TTTT6ݵan{4[>ɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG4<YyJTiӓgό}~.`ۢ{cjotE;;\tWW:_mt<Oǻ\kz{f7y՞9=ݽzo~r'˻w'O_@AC݇?[jwGCˆ 8>99?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-���gAMA��|Q��� cHRM��z%��������u0��`��:��o_F��9IDATx{UU?p\.7$2ӈ r"G5 5eG4Vɲg10^yuAgfglͬ9笽o=:… 1 R!´@//K-Xi 081<"g뀿+AvD`[`SfVZ>#c̯Jo}͎1hv�ΡZIOъriySR`XS}:;r[" C5t4a +3$ 4ི0i0Ɵ2`Ogp;c~ pdlÁ>Ʀ[M� |=; pm`-ہ{|1�n/Pu>.0XeP1CAEcuX׿ S�] l7A`'I|:xa<Xfz1wgy~Тr\RߗP`^cc=^5C&n<<x;y[ �I̢2M4�jHF'ayO9Fs.!?9ʫγ}LSEԥ &,U]\pt:| n5? mYq_ ߖGnJa>[~ȧ@`f@צځ(quV tړF2=x<[ߜ3R\_nv |-L~ xWF$ ·?Y{N ouSpWͽ�nW4l+ ^de &y[P|*gHfhEkNi4mR!>EuՆYhӡ5|c ozdM hcHU~F�x<eLL~x#_Q&26D9]Ր<kypX+ꌤ{X s~w�+x<clO9~\GhWڠlN5SPtxosp[M%I"X'2(m9%ݯ5};yۣ5m�&یQI dK0++Busl cV36n�lgrmi۟,S`xRGtZnH?3Ưb5QUGHPR5* b6b;<.󺴰=\+M"kGpu:eI5>I[Nۤ&x99PGR|ޤ}aP#5C^۫ q=�W#QD8 їy� B Yޙ <JRwٝag:Q?(BM,UmMu+H{YKgSodJʈ( ˛mqZ4"[}R'\B5K#] iFP^ |d)�5^g.>C:nS9C4#tiF<E�;Ir/HU]8 I81#]-l [Eo)j4]EԺJ<2Cgt):U$9% !S-5i&$Ґw\BNuxO6aw:*-S%gj)N$YubY7֒܈?';1D�BH.4:/0;qO(i{?ps*E:)~ 'f!EMyX2VD8R_6t$ %}@텷AfκArcQb2Q:<V?{>"k\t5km ˘>4pP�EyRz 8rh}V41ʩ5m)OgIj^Oی,|bmE)˧%W&EmBt$rI=*ABdH3qS>J.Ϧ6XG*5 � r7����IENDB`PK9|#����PK���1*:���������������resources/icons/hdac_tab.png @PNG  ��� IHDR���������A��� pHYs������"x*�� OiCCPPhotoshop ICC profile��xڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $p�d!s#�~<<+"�x �M0B\t8K�@zB�@F&S��`cb�P-�`'�{�[!� eD�h;�VE�X0�fK9�-�0IWfH�� � �0Q)�{�`##x��FW<+*��x<$9E[-qWW.(I+6aa@.y24��x6_-"bbϫp@��t~,/;m%h^ uf@�Wp~<<EJB[aW}g_Wl~<$2]GLϒ bG "IbX*QqD2"B)%d,>5�j>{-]cK'Xt��o(hw?G%�fIq��^D$.Tʳ?��D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;�2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ<FFi\$mmƣ&&!&KMMRM);L;L֙͢5=12כ߷`ZxZ,eIZYnZ9YXUZ]F%ֻNNgðɶۮm}agbgŮ}}= Z~sr:V:ޚΜ?}/gX3)iSGggs󈋉K.>.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz�%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9<qy +V<*mOW~&zMk^ʂk U }]OX/Yߵa>(xoʿܔĹdff-[n ڴ VE/(ۻC<e;?TTTT6ݵan{4[>ɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG4<YyJTiӓgό}~.`ۢ{cjotE;;\tWW:_mt<Oǻ\kz{f7y՞9=ݽzo~r'˻w'O_@AC݇?[jwGCˆ 8>99?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-���gAMA��|Q��� cHRM��z%��������u0��`��:��o_F��IDATxԕ?HWQ?W_?] YEݒ" pjڝ݂(!ZKB$1r..~C9?9TGID,&Qٿ <F'"dW8@0p97b1tW|Ou�^0D;cHgws`I9ݬJy[ȓV+s ,D$F֧K  Dh3[ 8iZEDGPDMm7D9<,_UMR}MltZ6%+y I  <�"uS/~ϵ\E'0L9Ƌ&5/to,}!q<πA4Mkl>R^"pVu=,ʛ6}qR~c�3.����IENDB`PKnzU �� ��PK���1*:���������������resources/icons/hdsc_logo.pngPNG  ��� IHDR���n���&���%P��� pHYs����+�� OiCCPPhotoshop ICC profile��xڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $p�d!s#�~<<+"�x �M0B\t8K�@zB�@F&S��`cb�P-�`'�{�[!� eD�h;�VE�X0�fK9�-�0IWfH�� � �0Q)�{�`##x��FW<+*��x<$9E[-qWW.(I+6aa@.y24��x6_-"bbϫp@��t~,/;m%h^ uf@�Wp~<<EJB[aW}g_Wl~<$2]GLϒ bG "IbX*QqD2"B)%d,>5�j>{-]cK'Xt��o(hw?G%�fIq��^D$.Tʳ?��D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;�2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ<FFi\$mmƣ&&!&KMMRM);L;L֙͢5=12כ߷`ZxZ,eIZYnZ9YXUZ]F%ֻNNgðɶۮm}agbgŮ}}= Z~sr:V:ޚΜ?}/gX3)iSGggs󈋉K.>.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz�%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9<qy +V<*mOW~&zMk^ʂk U }]OX/Yߵa>(xoʿܔĹdff-[n ڴ VE/(ۻC<e;?TTTT6ݵan{4[>ɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG4<YyJTiӓgό}~.`ۢ{cjotE;;\tWW:_mt<Oǻ\kz{f7y՞9=ݽzo~r'˻w'O_@AC݇?[jwGCˆ 8>99?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-���gAMA��|Q��� cHRM��z%��������u0��`��:��o_F��IDATxyVgAD)nhH(Kq %ZmڨikҦ5Fkb쒴&F!Vmj!.E1m4A2 8pgr}} ܼl{*�8.U? P�uz` _l�N{s7K%dֽxNU?�JUD` gNsn* Y5Xs pw= yAnfY`a4G#<爟<xhځW"0�lUM9j&5ފˀ9.r.pHi3)rV c ri>髁3# 3 8'hU}&7 yXUr󢪾EBY7DOWՍO}±NU#yظ2v{czF5L, th?}݁�:1z36Y%ڮ})7h /_wdjqp ON6gUƌ7#8959<],< hMt# yY{BX0FD"i*yTDd�ߋ^Zi U}nd [TQf\ȁ__qSmZ_ YLZn*rv�bšpMή^;$cnv)u42p,R~_Dӗ$BjVJ${:z]ܑ8`(b'sv ~н�8'Ac8c3wXy"v8'|_n"[r [-v~l `8�R2/O;Q0iM1T16"N;]97e;6Y7:_MSf =]]cD!.<1N5_�nBbw5V�O)쪒GppZ:YXLuHY ,W3C^|<c|vcơk[8}ۤOX8EU&Kѧ+N}׭2p2 ŕvxxCU߫ѲI,]/'�Uu[)9bFFpWɻXUX1\ia۪&G# '"jb ͊]G٢}4Uu1Ŏ695?ںYX³13ߴEҋhf Q24?jOE0.0PUB{CcX2mpUΒӭm4Y3Y{y`{6ζTzfБu{7fKyk>ڳ]yI3p>)�MMﶙ@mցYb-,ۀ>47-5hƚVB_JtVbW PbԂ'?r^cvePZuv ls |VDH^[Qxs}5>6]eg]�3nրUuO$*,xbkk3P(6Se;"ss-}9}&=5! F;svZDۈ3ߴL&|$#í6Z|_6܉V[w,]ljȸ~4IQՅN[@5C3=JnURv{4|m3zL)1?șVSh[溽Hg_}hͪHtZ|&۩]#;LGӈ ќsL Xs4T ϺM% ǦT+Miql<O)E [^N<::58u $9{V'5"Q"kS+O\JyL0vvUJ)pPEHrI*4DKŸuXXzRBAET4I:�@f����IENDB`PK �����PK���1*:������������&���resources/icons/hdsc_logo_disabled.png PNG  ��� IHDR���n���&���%P��� pHYs����+�� OiCCPPhotoshop ICC profile��xڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $p�d!s#�~<<+"�x �M0B\t8K�@zB�@F&S��`cb�P-�`'�{�[!� eD�h;�VE�X0�fK9�-�0IWfH�� � �0Q)�{�`##x��FW<+*��x<$9E[-qWW.(I+6aa@.y24��x6_-"bbϫp@��t~,/;m%h^ uf@�Wp~<<EJB[aW}g_Wl~<$2]GLϒ bG "IbX*QqD2"B)%d,>5�j>{-]cK'Xt��o(hw?G%�fIq��^D$.Tʳ?��D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;�2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ<FFi\$mmƣ&&!&KMMRM);L;L֙͢5=12כ߷`ZxZ,eIZYnZ9YXUZ]F%ֻNNgðɶۮm}agbgŮ}}= Z~sr:V:ޚΜ?}/gX3)iSGggs󈋉K.>.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz�%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9<qy +V<*mOW~&zMk^ʂk U }]OX/Yߵa>(xoʿܔĹdff-[n ڴ VE/(ۻC<e;?TTTT6ݵan{4[>ɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG4<YyJTiӓgό}~.`ۢ{cjotE;;\tWW:_mt<Oǻ\kz{f7y՞9=ݽzo~r'˻w'O_@AC݇?[jwGCˆ 8>99?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-���gAMA��|Q��� cHRM��z%��������u0��`��:��o_F��;IDATxm]E޶[ڲ("RjYDQ* 1(h4D|R"!!(R X !-AC5eK)vٖ۷ˇdx9޻P'{yf?37Yh80m:i�}:i)`)�h=m匫?{wOG3:XƄ'4j >f`oS$`Ϝq?+@?ف[3&<R7X8#gm^˅9< ,F,nN< �:N\3L eh`Ny,bxy~pwg3x<śc.~ \Yr6xz@`|Ƹ^_'E/k!;k2upwYA>4S<&p1+VY@2 #oqm09?�0҈ѓM*Q`A +1/yɚ] X$;ch}p?-z4C)~ <H,e_)Eoz^!21qPYt@nѢHkkd;E(F)&iyo9j%Fs6С8)9^p�+q,kUP8KIOwj2q"cUi&w7~5,ϑXKg 'D`f@fځ@(cqUՇV t-CF]6x<on`9%%ק^nc㪟BMn[n0z8PuFq3u[R,1HhR6Lpb@]d Ztq4mf a[Fa'*;+`ip3+zqO.5ꙁSq[5܇qMFF$&k2@gdREZBt/F"̐|F8 Ygi : :(*J&9kJ�-9KOT~�u�;\>%E~`n n' ' d<'Q=V~Y@XhM7cddS0++CU9I0;Jα]nQ|5InYCEm*JЁ{(JKܐ_ݎET8BEyO dZ\hNV^h-6hóHd岴)FzJzNMzem|AyFۨ&g<~`Fx'=|ި}c0Țʉ!U7 fuV!ZNz� RN4il})Bz;t߫40BE.R5ښ,01I{UK;_RڟFz ;=bO /'[v yB0v+-o-f&`_kij }?oe7aJz@5K#.�X@o'>Tpˀzn~R&&JSԮ�1iP=b"jAN s%ΣC14svM=9jŞw倽f='+)zYEI=OupZw ]^y=;$c5O)$̫nPb$Ũ)Np(&ז̾rCįu2Uxd.b.oZUru'GxwP`k,٭fZy y�ER$J(I.9@R7 )3l�<"GţKJwKh}頣OQ՞< NjfJ5~^'.e#G*@cqu#F%-\Oݘ|̜ƧR@�W?L5kOdqF,$WD%&G,S\Ffo6 (Iwc8)mqYӚf^K$'b��&bZ`y.1&N2yQdlbT4ɒE3|�6p٥����IENDB`PK o%�� ��PK���1*:���������������resources/icons/hdsc_tab.png TPNG  ��� IHDR���������A��� pHYs������"x*�� OiCCPPhotoshop ICC profile��xڝSgTS=BKKoR RB&*! J!QEEȠQ, !{kּ> H3Q5 B.@ $p�d!s#�~<<+"�x �M0B\t8K�@zB�@F&S��`cb�P-�`'�{�[!� eD�h;�VE�X0�fK9�-�0IWfH�� � �0Q)�{�`##x��FW<+*��x<$9E[-qWW.(I+6aa@.y24��x6_-"bbϫp@��t~,/;m%h^ uf@�Wp~<<EJB[aW}g_Wl~<$2]GLϒ bG "IbX*QqD2"B)%d,>5�j>{-]cK'Xt��o(hw?G%�fIq��^D$.Tʳ?��D*A, `6B$BB dr`)B(Ͱ*`/@4Qhp.U=pa( Aa!ڈbX#!H$ ɈQ"K5H1RT UH=r9\F;�2G1Q= C7F dt1r=6Ыhڏ>C03l0.B8, c˱" VcϱwE 6wB aAHXLXNH $4 7 Q'"K&b21XH,#/{C7$C2'ITFnR#,4H#dk9, +ȅ3![ b@qS(RjJ4e2AURݨT5ZBRQ4u9̓IKhhitݕNWGw Ljg(gwLӋT071oUX**| J&*/Tު UUT^S}FU3S ԖUPSSg;goT?~YYLOCQ_ cx,!k u5&|v*=9C3J3WRf?qtN (~))4L1e\kXHQG6EYAJ'\'GgSSݧ M=:.kDwn^Loy}/TmG X $ <5qo</QC]@Caaᄑ<FFi\$mmƣ&&!&KMMRM);L;L֙͢5=12כ߷`ZxZ,eIZYnZ9YXUZ]F%ֻNNgðɶۮm}agbgŮ}}= Z~sr:V:ޚΜ?}/gX3)iSGggs󈋉K.>.ȽJtq]zۯ6iܟ4)Y3sCQ? 0k߬~OCOg#/c/Wװwa>>r><72Y_7ȷOo_C#dz�%gA[z|!?:eAAA!h쐭!ΑiP~aa~ 'W?pX15wCsDDDޛg1O9-J5*>.j<74?.fYXXIlK9.*6nl {/]py.,:@LN8A*%w% yg"/6шC\*NH*Mz쑼5y$3,幄'L Lݛ:v m2=:1qB!Mggfvˬen/kY- BTZ(*geWf͉9+̳ې7ᒶKW-X潬j9<qy +V<*mOW~&zMk^ʂk U }]OX/Yߵa>(xoʿܔĹdff-[n ڴ VE/(ۻC<e;?TTTT6ݵan{4[>ɾUUMfeI?m]Nmq#׹=TR+Gw- 6 U#pDy  :v{vg/jBFS[b[O>zG4<YyJTiӓgό}~.`ۢ{cjotE;;\tWW:_mt<Oǻ\kz{f7y՞9=ݽzo~r'˻w'O_@AC݇?[jwGCˆ 8>99?rCd&ˮ/~јѡ򗓿m|x31^VwwO| (hSЧc3-���gAMA��|Q��� cHRM��z%��������u0��`��:��o_F��IDATxԕ=hUAg0 ( b'D!b'X NJ2 AH $X(H pl¸<ӽ…=sٽ!NmtzH» 6Ypn$߿l�<.�!FZk4<+#p1",?ln|mucN/A~N%?J+wF7jzz+FR^AYfZL58a'C%e¯KYH})�vnM/s%F9Ly,{]IWOr"kLݵ[fM/ | OI|`�ր,J}RokrKզP-t&KAt>{e%Y0&0˶-]iIz׆Sq�Sb ����IENDB`PKo`. �� ��PK����6:����������� ����������������META-INF/��PK����6:5e���g����������������=���META-INF/MANIFEST.MFPK � �����+:����������������������������bdvm/PK � �����.:���������������������������bdvm/vm/PK����R?:P�� ���������������-��bdvm/vm/BDVMException.classPK����R?:ˮ7���9�����������������bdvm/vm/BDVMInterface.classPK � �����+:�������������������������@��dumphd/PK � �����0:������������ �������������e��dumphd/aacs/PK����L:q"6I6>��w�����������������dumphd/aacs/AACSDecrypter.classPK����Q:p�����������������C��dumphd/aacs/AACSException.classPK����U:r�����������������D��dumphd/aacs/AACSKeys.classPK����Z:7T�( ����3�������������G��dumphd/aacs/PackDecrypter$PackDecrypterThread.classPK����Z:t =����(�������������!R��dumphd/aacs/PackDecrypter$WorkUnit.classPK����Z:>NW ��)���������������T��dumphd/aacs/PackDecrypter.classPK � �����1:�������������������������^��dumphd/bdplus/PK����`:zI�� ���������������!_��dumphd/bdplus/BDVMLoader.classPK����j:6j��I ��#�������������Pd��dumphd/bdplus/ConversionTable.classPK����o: �� ��������������� j��dumphd/bdplus/Segment.classPK����r:h6V��a��*�������������Hp��dumphd/bdplus/SubTable$PatchIterator.classPK����r:Xdm�� ���������������'u��dumphd/bdplus/SubTable.classPK � �����2:������������ �������������t{��dumphd/core/PK����v:v$o�����������������{��dumphd/core/ACAException.classPK����y:wث)��O���������������Y}��dumphd/core/ACAPacker.classPK����|:cWh�����������������h��dumphd/core/DiscSet.classPK����:@c3��lj�����������������dumphd/core/DumpHD.classPK����:(d��!��(���������������dumphd/core/KeyData$UnitMappingSet.classPK����:yP��!���������������*��dumphd/core/KeyData.classPK����: !��C���������������m��dumphd/core/KeyDataFile.classPK����:C �� �� �������������M�dumphd/core/MultiplexedArf.classPK � �����3:������������ �������������u"�dumphd/gui/PK����:I[63��} ���������������"�dumphd/gui/FileTableModel.classPK����:�����������������)�dumphd/gui/MainFrame$1.classPK����:| vU�����������������+�dumphd/gui/MainFrame$2.classPK����:&`M4�� ���������������-�dumphd/gui/MainFrame$3$1.classPK����:?�����������������-1�dumphd/gui/MainFrame$3$2.classPK����:J�����������������3�dumphd/gui/MainFrame$3.classPK����:0������������������7�dumphd/gui/MainFrame$4.classPK����:5jp�����������������;�dumphd/gui/MainFrame$5$1.classPK����:��?���������������/>�dumphd/gui/MainFrame$5$2.classPK����:7*,o�����������������O@�dumphd/gui/MainFrame$5.classPK����:`3z��o���������������C�dumphd/gui/MainFrame$6.classPK����:;VQ����,�������������E�dumphd/gui/MainFrame$LafActionListener.classPK����:{y��2��1�������������J�dumphd/gui/MainFrame$MainFrameWindowAdapter.classPK����:8K����;�������������L�dumphd/gui/MainFrame$SourceTypeCheckBoxActionListener.classPK����:�����3�������������O�dumphd/gui/MainFrame$TextAreaMessagePrinter$1.classPK����:;W����3�������������Q�dumphd/gui/MainFrame$TextAreaMessagePrinter$2.classPK����:9`���;��3�������������>T�dumphd/gui/MainFrame$TextAreaMessagePrinter$3.classPK����:d  ����1�������������V�dumphd/gui/MainFrame$TextAreaMessagePrinter.classPK����: xi��uB���������������Z�dumphd/gui/MainFrame.classPK����:{3y�������������������y�dumphd/gui/ManagedJob.classPK����:TizY>�����������������z�dumphd/gui/Manager$1.classPK����:F�����������������}�dumphd/gui/Manager$2.classPK����:)�����������������~�dumphd/gui/Manager$3.classPK����:} �����������������R�dumphd/gui/Manager.classPK����:��/��+��������������dumphd/gui/SourceTab$ButtonListener$1.classPK����:R=�� ��+��������������dumphd/gui/SourceTab$ButtonListener$2.classPK����:(νx��I��+�������������?�dumphd/gui/SourceTab$ButtonListener$3.classPK����:zD��V��+��������������dumphd/gui/SourceTab$ButtonListener$4.classPK����:a(����)�������������{�dumphd/gui/SourceTab$ButtonListener.classPK����:Oy�����������������{�dumphd/gui/SourceTab.classPK����R?:d9g��E����������������dumphd/gui/SwingWorker$1.classPK����R?:b��e���������������3�dumphd/gui/SwingWorker$2.classPK����R?:b��n��&��������������dumphd/gui/SwingWorker$ThreadVar.classPK����R?:.Hs��3����������������dumphd/gui/SwingWorker.classPK����:'<����'�������������ν�dumphd/gui/ValidatedInputDialog$1.classPK����:a2��6��4��������������dumphd/gui/ValidatedInputDialog$ButtonListener.classPK����: ����%�������������C�dumphd/gui/ValidatedInputDialog.classPK � �����4:������������ �������������a�dumphd/util/PK����:]\9��k����������������dumphd/util/ByteArray.classPK����:*Y��[��������������� �dumphd/util/ByteSource.classPK����:@E������������������dumphd/util/CopyResult.classPK����:67����#�������������?�dumphd/util/FileSource$Window.classPK����:h ��?����������������dumphd/util/FileSource.classPK����:-������ ��������������dumphd/util/MessagePrinter.classPK����:ȷ&��H��%��������������dumphd/util/PackScanner$Counter.classPK����:i��$���������������"�dumphd/util/PackScanner.classPK����:W����'�������������t�dumphd/util/PackSource$PackRecord.classPK����:6����#�������������j�dumphd/util/PackSource$Window.classPK����:UV ������������������dumphd/util/PackSource.classPK����: H��S��#������������� �dumphd/util/PESPack$PESPacket.classPK����:}E��)���������������.�dumphd/util/PESPack.classPK����:ts����$�������������(�dumphd/util/PESParserException.classPK����:<��#��$�������������*�dumphd/util/PrintStreamPrinter.classPK����:.\�� ��&�������������n,�dumphd/util/SimpleFilenameFilter.classPK����: 2@"��= ���������������m/�dumphd/util/StreamSource.classPK����:,xB��y���������������5�dumphd/util/TSAlignedUnit.classPK����:wSr����#�������������8�dumphd/util/TSParserException.classPK����:!����#�������������:�dumphd/util/Utils$1DirElement.classPK����:5~{ ��y���������������<�dumphd/util/Utils.classPK � �����+:������������ �������������vH�resources/PK � �����+:�������������������������H�resources/icons/PK����1*:x`<C��>���������������H�resources/icons/bd_logo.pngPK����1*:; ����$�������������X]�resources/icons/bd_logo_disabled.pngPK����1*:,,2 �� ���������������o�resources/icons/bd_tab.pngPK����1*:md �� ���������������}�resources/icons/empty_tab.pngPK����1*:MI?�����������������p�resources/icons/hdac_logo.pngPK����1*:9|#����&�������������\�resources/icons/hdac_logo_disabled.pngPK����1*:nzU �� ���������������ӱ�resources/icons/hdac_tab.pngPK����1*: �������������������resources/icons/hdsc_logo.pngPK����1*: o%�� ��&�������������,�resources/icons/hdsc_logo_disabled.pngPK����1*:o`. �� ����������������resources/icons/hdsc_tab.pngPK����e�e�?���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/packscanner.sh��������������������������������������������������������������������������0000664�0001750�0001750�00000000106�11133460656�015745� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/bash java -server -cp DumpHD.jar dumphd.util.PackScanner "$@" ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/DumpHD.cmd������������������������������������������������������������������������������0000664�0001750�0001750�00000000040�11132225672�014720� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������@java -server -jar DumpHD.jar %*������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/ACAPacker.cmd���������������������������������������������������������������������������0000664�0001750�0001750�00000000065�11132225672�015320� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������@java -server -cp DumpHD.jar dumphd.core.ACAPacker %*���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/dumphd.sh�������������������������������������������������������������������������������0000664�0001750�0001750�00000000105�11133460656�014735� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/bash java -server -Djava.library.path=. -jar DumpHD.jar "$@" �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������dumphd-0.61/Readme.txt������������������������������������������������������������������������������0000664�0001750�0001750�00000017671�11211605310�015057� 0����������������������������������������������������������������������������������������������������ustar �marillat������������������������marillat���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������DumpHD 0.61 ----------- DumpHD is a HD-DVD / Blu-Ray decrypter based on the idea of BackupHDDVD by muslix64. The goal of this program is to make a perfect backup without any traces of AACS. This programm is written in Java and requires the JRE 1.6 or later. It should run on every platform that supports Java. It is HIGHLY recommended to use the Server VM, the Client VM suffers a SEVERE performance drop, almost by factor 1.8. Key features so far: - Dual-Core supported decryption of EVO / M2TS files (for harddisk to harddisk speed records ;o)) - Support for every pack type of an EVO (including in-place decryption of ADV_PCKs, excluding Sequence Key Sections) - Decryption of every ARF protection type - Multiple files (currently CLI only) or complete disc mode - Usage of a key database to get the decryption keys or direct retrieval of the keys off the source disc - Supports HD-DVDs for Standard / Advanced Content (but not both on the same disc), Blu-Ray ROM BDMV - Experimental Blu-Ray Recordable support (with multiple CPS Units, BDMV, BDAV with Aux Directories and Thumbnails) - Automatic BD+ removal using the BDVM Debugger or manually by supplying a correct Conversion Table (currently CLI only) - Streaming output of EVO / M2TS files to stdout - Very much console output for free ;o) - GUI Homepage: http://forum.doom9.org/showthread.php?t=123111 Important information --------------------- Starting from version 0.4 a new key database format is used which is incompatible with the original BackupHDDVD format. This change was necessary to support future enhancements. For details of the new format see the included KEYDB.cfg. DumpHD can still read the old format but will write only in the new format. If old format entries are edited using DumpHD these entries will be converted to the new format. The direct key retrieval feature could not be integrated as an integral part of DumpHD because it requires low level hardware access which cannot be realized using Java. To use this feature, the library version of aacskeys is required. The library is platform dependend, it is required that the one corresponding to the platform where DumpHD is executed on is present in the directory of DumpHD or in the system search path for shared libraries. The filename must be corresponding to the platform, e.g. under windows it must have the name aacskeys.dll. If the library is not found during the start of DumpHD the direct key retrieval feature gets disabled for that session. The retrieved keys get automatically added at the end of the key database. The library is NOT included, you can download it here: http://forum.doom9.org/showthread.php?t=123311 Since version 0.6 DumpHD can remove BD+ automatically, this is done by using the BDVM Debugger. To enable that feature copy all files and subdirectories from the BDVM Debugger into the directory of DumpHD. The BDVM Debugger is NOT included, at least version 0.1.5 is required, you can download it here: http://forum.doom9.org/showthread.php?t=140571 Usage instructions ------------------ To use the program from the command line, use the following syntax: DumpHD [options] source [destination] Options: -h : This help screen --convtable:<file> : Use this Conversion Table to remove BD+ --infile:<file> : Process only the specified file. It must be given relative to source. This parameter can be specified multiple times, the files will be processed in the given order. source : Source to dump, must be the root directory of the disc destination : directory where the dump is written If destination is omitted streaming mode is used. In streaming mode only EVO / M2TS files are processed, they are decrypted to stdout (EVOs without decrypting ADV_PCKs) in alphabetical order or as specified by --infile:<file> switches. Pipe the output to another program which can read from stdin. Textual output is written to stderr. If started without arguments, the GUI is loaded There is a batch file (DumpHD.cmd) / shell script (dumphd.sh) included for windows / linux for a more convenient start of the program. Known issues ------------ Symptom: Under JRE 1.x, Java complains that there is no 'server' JVM. Workaround: Install the full JDK and copy the directory <JDK-dir>\jre\bin\server into the bin directory of the JRE (mentioned in the error message, usually located somewhere on the C: drive). Symptom: A disc has been loaded and cannot be dumped because it was not found in the key database and direct key retrieval failed. Using DiscID override the disc has been successfully identified but still cannot be selected to be dumped. Workaround: Create a dummy entry in the key database with the detected DiscID of the disc so that it is identified when loaded. Then use the DiscID override feature to switch the DiscID. Support utilities ----------------- Two additional support utilities are included: PackScanner: Scans EVO files for pack types and has the ability to "blank" VC-1 / DD+ Secondary Streams for a "IME-Fix". ACAPacker: A tool for packing, depacking and repacking (ACA -> ACA) ACA files, with or without AACS processing, including a list function to show the contents of an ACA file. For usage instructions run the programs without parameters. Use the provided batch files / shell scripts for a more convenient start of the programs. KenD00 Version history --------------- 2009-06-03: 0.61 - Updated Key Database to version 1.4, comments in entry lines are now supported - They key database now doesn't append wrong entry data to the title if correct entries follow the broken ones 2008-12-14: 0.6 - Automatic BD+ removal using the BDVM Debugger - For BD+ titles the Volume ID gets stored into the KEYDB additionally 2008-12-05: 0.51 - Added support for multiple CPS Units on BD-ROM BDMV 2008-11-08: 0.5 - Experimental BD+ support (in CLI mode only) 2008-10-01: 0.46 - Fixed streaming of complete Blu-Ray discs - Fixed line terminators in linux shell scripts - Added missing java.library.path to the acapacker shell script 2008-09-20: 0.45 - Experimental Blu-Ray Recordable support (with multiple CPS Units, BDMV, BDAV with Aux Directories and Thumbnails) - Updated Key Database to version 1.3, new entry types: M (Media Key), B (Binding Nonce), P (Protected Area Key) - DumpHD can now calculate the VUK / PAK if only the Media Key and Volume ID / Binding Nonce is given - Replaced single file mode with multiple files mode (works only in CLI mode) - ACAPacker now works with direct key retrieval too 2007-09-09: 0.4 - Change of the key entry format to version DumpHD 1.2 - Key database access completely rewritten, now reports every encountered error during processing - Support for aacskeys library to retrieve keys directly off the source disc - Implemented DiscID override (change the detected DiscID of the disc contents to use another keyset) - Implemented Title editing (changes the Title of the entry in the key database and may update the keys) - The CERTIFICATE folder of BluRay discs gets copied - For HD-DVD Advanced Content the Title Key File with the lowest number is used to calculate the DiscID - Streaming output of EVO / M2TS files to stdout 2007-04-18: Private release 0.33 2007-04-05: 0.32 - BluRay decryption fixed 2007-04-05: 0.31 - BluRay VUK processing fixed 2007-04-01: 0.3 - Complete rewritten disc detection and aacs handling routines - Lots of internal changes - Experimental BluRay support - Initial GUI version 2007-03-09: 0.21 - Small fix in EVOB decryption engine to avoid crash - Support for HD-DVDs for Standard Content added 2007-03-07: 0.2 - Initial public release �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������