Compare commits
No commits in common. "master" and "original_c_version" have entirely different histories.
master
...
original_c
26
.gitignore
vendored
26
.gitignore
vendored
|
@ -1,3 +1,25 @@
|
|||
*.o
|
||||
*.exe
|
||||
*.so
|
||||
*.dylib
|
||||
*.dSYM
|
||||
*.a
|
||||
/build
|
||||
|
||||
# CMake
|
||||
/cmake-build-debug
|
||||
/cmake-build-release
|
||||
CMakeLists.txt.user
|
||||
CMakeCache.txt
|
||||
CMakeFiles
|
||||
CMakeScripts
|
||||
Testing
|
||||
Makefile
|
||||
cmake_install.cmake
|
||||
install_manifest.txt
|
||||
compile_commands.json
|
||||
CTestTestfile.cmake
|
||||
_deps
|
||||
|
||||
# IDE stuff
|
||||
/.idea
|
||||
/target
|
||||
Cargo.lock
|
||||
|
|
34
CMakeLists.txt
Normal file
34
CMakeLists.txt
Normal file
|
@ -0,0 +1,34 @@
|
|||
cmake_minimum_required(VERSION 3.16)
|
||||
project(pso_gc_tools C)
|
||||
|
||||
set(CMAKE_C_STANDARD 99)
|
||||
|
||||
include_directories(/usr/local/include)
|
||||
|
||||
#find_package(Iconv REQUIRED)
|
||||
|
||||
find_library(SYLVERANT_LIBRARY sylverant REQUIRED)
|
||||
|
||||
# decrypt_packets
|
||||
add_executable(decrypt_packets decrypt_packets.c utils.c)
|
||||
target_link_libraries(decrypt_packets ${SYLVERANT_LIBRARY})
|
||||
|
||||
# gen_qst_header
|
||||
add_executable(gen_qst_header gen_qst_header.c quests.c utils.c)
|
||||
target_link_libraries(gen_qst_header ${SYLVERANT_LIBRARY})
|
||||
#add_executable(gen_qst_header gen_qst_header.c textconv.c quests.c utils.c)
|
||||
#target_link_libraries(gen_qst_header ${SYLVERANT_LIBRARY} ${ICONV_LIBRARIES})
|
||||
#target_compile_definitions(gen_qst_header PRIVATE ICONV_CONST=${ICONV_CONST})
|
||||
#target_include_directories(gen_qst_header PRIVATE ${ICONV_INCLUDE_DIR})
|
||||
|
||||
# bindat_to_gcdl
|
||||
add_executable(bindat_to_gcdl bindat_to_gcdl.c quests.c fuzziqer_prs.c utils.c)
|
||||
target_link_libraries(bindat_to_gcdl ${SYLVERANT_LIBRARY})
|
||||
|
||||
# gci_extract
|
||||
add_executable(gci_extract gci_extract.c quests.c fuzziqer_prs.c utils.c)
|
||||
target_link_libraries(gci_extract ${SYLVERANT_LIBRARY})
|
||||
|
||||
# quest_info
|
||||
add_executable(quest_info quest_info.c quests.c fuzziqer_prs.c utils.c)
|
||||
target_link_libraries(quest_info ${SYLVERANT_LIBRARY})
|
|
@ -1,9 +0,0 @@
|
|||
[workspace]
|
||||
|
||||
members = [
|
||||
"psoutils",
|
||||
"psogc_quest_tool",
|
||||
"gci_quest_extract",
|
||||
"decrypt_packets"
|
||||
]
|
||||
|
619
LICENSE
Normal file
619
LICENSE
Normal file
|
@ -0,0 +1,619 @@
|
|||
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.
|
23
README.md
23
README.md
|
@ -1,19 +1,12 @@
|
|||
# PSO Episode I & II Gamecube Tools
|
||||
# PSO Ep I & II Gamecube Tools
|
||||
|
||||
A small collection of tools, mainly intended to assist with my own efforts in making it easier to prepare and distribute
|
||||
downloadable (or "offline play") quests.
|
||||
|
||||
Please note that I am **only** interested in the Gamecube version of PSO. I do not own 'nor play any of the other
|
||||
versions (Dreamcast, Xbox or Blue Burst). Because of this, the tools found in this repository are laser-focused on the
|
||||
Gamecube version of PSO only, and that will not change.
|
||||
A small collection of tools, intended to assist with my own efforts in investigating how download/offline quests need
|
||||
to be prepared in order to work correctly and tools to automate that process.
|
||||
|
||||
## Tools
|
||||
|
||||
* [decrypt_packets](decrypt_packets/README.md): Tool for decrypting and displaying raw packets captured as a `.pcapng` file from a PSO Gamecube client/server session.
|
||||
* [gci_quest_extract](gci_quest_extract/README.md): A very specific tool for extracting PSO Gamecube quests **only** out of pre-decrypted `.gci` files.
|
||||
* [psogc_quest_tool](psogc_quest_tool/README.md): Conversion and info tool for PSO Gamecube quest `.bin`/`.dat` and/or `.qst` files.
|
||||
* [psoutils](psoutils/README.md): Library that all of these tools use that contains useful PSO Gamecube things (quest file formats, encryption, compression, text, etc).
|
||||
|
||||
(This is more or less my first project of non-trivial size in Rust. I am still learning the language and ecosystem,
|
||||
and this repository probably includes quite a number of mistakes or poor quality code because I simply don't know any
|
||||
better at this time. Feel free to point out any mistakes or suggest improvements!)
|
||||
* [bindat_to_gcdl](bindat_to_gcdl.md): Turns a set of .bin/.dat files into a Gamecube-compatible offline/download quest .qst file.
|
||||
* [decrypt_packets](decrypt_packets.md): Decrypts server/client packet capture.
|
||||
* [gci_extract](gci_extract.md): Extracts quest .bin/.dat files **only** from specially prepared Gamecube memory card dumps in .gci format. This is a highly specific tool that is **not** usable on any arbitrary .gci file!
|
||||
* [gen_qst_header](gen_qst_header.md): Generates nicer .qst header files than what [qst_tool](https://github.com/Sylverant/pso_tools/tree/master/qst_tool) does. Can be then fed into qst_tool.
|
||||
* [quest_info](quest_info.md): Displays basic information about quest files (supports both .bin/.dat and .qst formats).
|
||||
|
|
248
bindat_to_gcdl.c
Normal file
248
bindat_to_gcdl.c
Normal file
|
@ -0,0 +1,248 @@
|
|||
/*
|
||||
* PSO EP1&2 (Gamecube) Quest .bin/.dat File to Download/Offline .qst File Converter
|
||||
*
|
||||
* This tool will take PRS-compressed quest .bin/.dat files and process them into a working .qst file that can be
|
||||
* served up by a PSO server as a "download quest" which will be playable offline from a Gamecube memory card.
|
||||
*
|
||||
* This tool performs basically the same process that Qedit's save file type "Download Quest file(GC)" does.
|
||||
*
|
||||
* Note that .qst files created in this way cannot be used as "online" quests.
|
||||
*
|
||||
* Gered King, March 2021
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <malloc.h>
|
||||
|
||||
#include <sylverant/encryption.h>
|
||||
#include <sylverant/prs.h>
|
||||
#include "fuzziqer_prs.h"
|
||||
|
||||
#include "defs.h"
|
||||
|
||||
#include "quests.h"
|
||||
#include "utils.h"
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int returncode, validation_result;
|
||||
uint8_t *compressed_bin = NULL;
|
||||
uint8_t *compressed_dat = NULL;
|
||||
uint8_t *decompressed_bin = NULL;
|
||||
uint8_t *decompressed_dat = NULL;
|
||||
uint8_t *final_bin = NULL;
|
||||
uint8_t *final_dat = NULL;
|
||||
|
||||
if (argc != 4) {
|
||||
printf("Usage: bindat_to_gcdl quest.bin quest.dat output.qst\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int result;
|
||||
const char *bin_filename = argv[1];
|
||||
const char *dat_filename = argv[2];
|
||||
const char *output_qst_filename = argv[3];
|
||||
|
||||
|
||||
/** validate lengths of the given quest .bin and .dat files, to make sure they fit into the packet structs **/
|
||||
|
||||
const char *bin_base_filename = path_to_filename(bin_filename);
|
||||
if (strlen(bin_base_filename) > QUEST_FILENAME_MAX_LENGTH) {
|
||||
printf("Bin filename is too long to fit in a QST file header. Maximum length is 16 including file extension.\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
const char *dat_base_filename = path_to_filename(dat_filename);
|
||||
if (strlen(dat_base_filename) > QUEST_FILENAME_MAX_LENGTH) {
|
||||
printf("Dat filename is too long to fit in a QST file header. Maximum length is 16 including file extension.\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
|
||||
/** read in given quest .bin and .dat files **/
|
||||
|
||||
uint32_t compressed_bin_size, compressed_dat_size;
|
||||
|
||||
printf("Reading quest .bin file %s ...\n", bin_filename);
|
||||
returncode = read_file(bin_filename, &compressed_bin, &compressed_bin_size);
|
||||
if (returncode) {
|
||||
printf("Error code %d (%s) reading bin file: %s\n", returncode, get_error_message(returncode), bin_filename);
|
||||
goto error;
|
||||
}
|
||||
|
||||
printf("Reading quest .dat file %s ...\n", dat_filename);
|
||||
returncode = read_file(dat_filename, &compressed_dat, &compressed_dat_size);
|
||||
if (returncode) {
|
||||
printf("Error code %d (%s) reading dat file: %s\n", returncode, get_error_message(returncode), dat_filename);
|
||||
goto error;
|
||||
}
|
||||
|
||||
|
||||
/** prs decompress the .bin file, parse out it's header and validate it **/
|
||||
printf("Decompressing and validating .bin file ...\n");
|
||||
|
||||
size_t decompressed_bin_size;
|
||||
result = fuzziqer_prs_decompress_buf(compressed_bin, &decompressed_bin, compressed_bin_size);
|
||||
if (result < 0) {
|
||||
printf("Error code %d decompressing .dat data.\n", result);
|
||||
goto error;
|
||||
}
|
||||
decompressed_bin_size = result;
|
||||
|
||||
QUEST_BIN_HEADER *bin_header = (QUEST_BIN_HEADER*)decompressed_bin;
|
||||
validation_result = validate_quest_bin(bin_header, decompressed_bin_size, true);
|
||||
validation_result = handle_quest_bin_validation_issues(validation_result, bin_header, &decompressed_bin, &decompressed_bin_size);
|
||||
if (validation_result) {
|
||||
printf("Aborting due to invalid quest .bin data.\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
|
||||
/** prs decompress the .dat file and validate it **/
|
||||
printf("Decompressing and validating .dat file ...\n");
|
||||
|
||||
size_t decompressed_dat_size;
|
||||
result = fuzziqer_prs_decompress_buf(compressed_dat, &decompressed_dat, compressed_dat_size);
|
||||
if (result < 0) {
|
||||
printf("Error code %d decompressing .dat data.\n", result);
|
||||
goto error;
|
||||
}
|
||||
decompressed_dat_size = result;
|
||||
|
||||
validation_result = validate_quest_dat(decompressed_dat, decompressed_dat_size, true);
|
||||
validation_result = handle_quest_dat_validation_issues(validation_result, &decompressed_dat, &decompressed_dat_size);
|
||||
if (validation_result) {
|
||||
printf("Aborting due to invalid quest .dat data.\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
|
||||
print_quick_quest_info(bin_header, compressed_bin_size, compressed_dat_size);
|
||||
|
||||
|
||||
/** set the "download" flag in the .bin header and then re-compress the .bin data **/
|
||||
printf("Setting .bin header 'download' flag and re-compressing .bin file data ...\n");
|
||||
|
||||
bin_header->download = 1; // gamecube pso client will not find quests on a memory card if this is not set!
|
||||
|
||||
uint8_t *recompressed_bin;
|
||||
result = fuzziqer_prs_compress(decompressed_bin, &recompressed_bin, decompressed_bin_size);
|
||||
if (result < 0) {
|
||||
printf("Error code %d re-compressing .bin file data.\n", result);
|
||||
goto error;
|
||||
}
|
||||
|
||||
// overwrite old compressed bin data, since we don't need it anymore
|
||||
free(compressed_bin);
|
||||
compressed_bin = recompressed_bin;
|
||||
compressed_bin_size = (uint32_t)result;
|
||||
|
||||
|
||||
/** encrypt compressed .bin and .dat file data, using PC crypt method with randomly generated crypt key.
|
||||
prefix unencrypted download quest chunks header to prs compressed + encrypted .bin and .dat file data. **/
|
||||
printf("Preparing final .qst file data ... \n");
|
||||
|
||||
srand(time(NULL));
|
||||
|
||||
uint32_t final_bin_size = compressed_bin_size + sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER);
|
||||
final_bin = malloc(final_bin_size);
|
||||
memset(final_bin, 0, final_bin_size);
|
||||
uint8_t *crypt_compressed_bin = final_bin + sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER);
|
||||
DOWNLOAD_QUEST_CHUNKS_HEADER *bin_dlchunks_header = (DOWNLOAD_QUEST_CHUNKS_HEADER*)final_bin;
|
||||
bin_dlchunks_header->decompressed_size = decompressed_bin_size + sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER);
|
||||
bin_dlchunks_header->crypt_key = rand();
|
||||
memcpy(crypt_compressed_bin, compressed_bin, compressed_bin_size);
|
||||
|
||||
uint32_t final_dat_size = compressed_dat_size + sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER);
|
||||
final_dat = malloc(final_dat_size);
|
||||
memset(final_dat, 0, final_dat_size);
|
||||
uint8_t *crypt_compressed_dat = final_dat + sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER);
|
||||
DOWNLOAD_QUEST_CHUNKS_HEADER *dat_dlchunks_header = (DOWNLOAD_QUEST_CHUNKS_HEADER*)final_dat;
|
||||
dat_dlchunks_header->decompressed_size = decompressed_dat_size + sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER);
|
||||
dat_dlchunks_header->crypt_key = rand();
|
||||
memcpy(crypt_compressed_dat, compressed_dat, compressed_dat_size);
|
||||
|
||||
CRYPT_SETUP bin_cs, dat_cs;
|
||||
|
||||
// yes, we need to use PC encryption even for gamecube download quests
|
||||
CRYPT_CreateKeys(&bin_cs, &bin_dlchunks_header->crypt_key, CRYPT_PC);
|
||||
CRYPT_CreateKeys(&dat_cs, &dat_dlchunks_header->crypt_key, CRYPT_PC);
|
||||
|
||||
// NOTE: encrypts the compressed bin/dat data in-place
|
||||
CRYPT_CryptData(&bin_cs, crypt_compressed_bin, final_bin_size - sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER), 1);
|
||||
CRYPT_CryptData(&dat_cs, crypt_compressed_dat, final_dat_size - sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER), 1);
|
||||
|
||||
|
||||
/** generate .qst file header for both the encrypted+compressed .bin and .dat file data, using the .bin header data **/
|
||||
|
||||
QST_HEADER qst_bin_header, qst_dat_header;
|
||||
|
||||
generate_qst_header(bin_base_filename, final_bin_size, bin_header, &qst_bin_header);
|
||||
generate_qst_header(dat_base_filename, final_dat_size, bin_header, &qst_dat_header);
|
||||
|
||||
|
||||
/** write out the .qst file. chunk data is written out as interleaved 0xA7 packets containing 1024 bytes each */
|
||||
printf("Writing out %s ...\n", output_qst_filename);
|
||||
|
||||
FILE *fp = fopen(output_qst_filename, "wb");
|
||||
if (!fp) {
|
||||
printf("Error creating output .qst file: %s\n", output_qst_filename);
|
||||
goto error;
|
||||
}
|
||||
|
||||
fwrite(&qst_bin_header, sizeof(qst_bin_header), 1, fp);
|
||||
fwrite(&qst_dat_header, sizeof(qst_dat_header), 1, fp);
|
||||
|
||||
uint32_t bin_pos = 0, bin_done = 0;
|
||||
uint32_t dat_pos = 0, dat_done = 0;
|
||||
uint8_t bin_counter = 0, dat_counter = 0;
|
||||
QST_DATA_CHUNK chunk;
|
||||
|
||||
// note: .qst files actually do NOT need to be interleaved like this to work with the gamecube pso client. the
|
||||
// khyller server did not do this. it is possible that some .qst file tools (qedit?) expect it though? so, meh,
|
||||
// we'll just do it here because it's easy enough. also worth mentioning that khyller also put the .dat file data
|
||||
// first. so the order seems unimportant too ... ?
|
||||
|
||||
while (!bin_done || !dat_done) {
|
||||
if (!bin_done) {
|
||||
uint32_t size = (final_bin_size - bin_pos >= 1024) ? 1024 : (final_bin_size - bin_pos);
|
||||
|
||||
generate_qst_data_chunk(bin_base_filename, bin_counter, final_bin + bin_pos, size, &chunk);
|
||||
fwrite(&chunk, sizeof(QST_DATA_CHUNK), 1, fp);
|
||||
|
||||
bin_pos += size;
|
||||
++bin_counter;
|
||||
if (bin_pos >= final_bin_size)
|
||||
bin_done = 1;
|
||||
}
|
||||
|
||||
if (!dat_done) {
|
||||
uint32_t size = (final_dat_size - dat_pos >= 1024) ? 1024 : (final_dat_size - dat_pos);
|
||||
|
||||
generate_qst_data_chunk(dat_base_filename, dat_counter, final_dat + dat_pos, size, &chunk);
|
||||
fwrite(&chunk, sizeof(QST_DATA_CHUNK), 1, fp);
|
||||
|
||||
dat_pos += size;
|
||||
++dat_counter;
|
||||
if (dat_pos >= final_dat_size)
|
||||
dat_done = 1;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
|
||||
returncode = 0;
|
||||
goto quit;
|
||||
error:
|
||||
returncode = 1;
|
||||
quit:
|
||||
free(decompressed_bin);
|
||||
free(final_bin);
|
||||
free(final_dat);
|
||||
free(compressed_bin);
|
||||
free(compressed_dat);
|
||||
return returncode;
|
||||
}
|
16
bindat_to_gcdl.md
Normal file
16
bindat_to_gcdl.md
Normal file
|
@ -0,0 +1,16 @@
|
|||
# PSO Ep 1 & 2 (Gamecube) Quest .bin/.dat to Download .qst Tool
|
||||
|
||||
This tool takes a set of `.bin` and `.dat` files for a Gamecube quest and turns it into a `.qst` file that can be
|
||||
served up by a PSO server to Gamecube clients as "download quests" which can then be played by Gamecube users directly
|
||||
from a memory card.
|
||||
|
||||
This tool performs basically the same process that [Qedit's](https://qedit.info/) save file type
|
||||
"Download Quest file(GC)" does.
|
||||
|
||||
## Usage
|
||||
|
||||
Given two files, `quest.bin` and `quest.dat`, a download quest file, `download.qst`, could be created using:
|
||||
|
||||
```text
|
||||
bindat_to_gcdl quest.bin quest.dat download.qst
|
||||
```
|
127
decrypt_packets.c
Normal file
127
decrypt_packets.c
Normal file
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* PSO EP1&2 (Gamecube) Client/Server Packets Decrypter Tool
|
||||
*
|
||||
* This tool was made for myself as part of an investigative effort to figure out the undocumented "magic" behind
|
||||
* what PSO servers have done behind the scenes to prepare .bin/.dat quest files into something that works as an
|
||||
* offline/download quest which is playable from a Gamecube memory card.
|
||||
*
|
||||
* Of course, after gaining an understanding about how this all works, I only then realized that Qedit can save quests
|
||||
* directly to the necessary Gamecube download quest file format. Heh. Oh, well, that's just how it goes I guess! :-)
|
||||
* This tool may still prove useful to anyone interested in understanding the packet exchanges between a PSO client
|
||||
* and server I suppose.
|
||||
*
|
||||
* Given two binary files containing server->client and client->server packet data (separately), as long as the
|
||||
* packet data was captured from the very beginning of the connection, this will decrypt the packet data and display
|
||||
* it as raw packets.
|
||||
*
|
||||
* Gered King, March 2021
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <malloc.h>
|
||||
|
||||
#include <sylverant/encryption.h>
|
||||
|
||||
#include "defs.h"
|
||||
#include "utils.h"
|
||||
|
||||
typedef struct _PACKED_ {
|
||||
uint8_t pkt_id;
|
||||
uint8_t pkt_flags;
|
||||
uint16_t pkt_size;
|
||||
} PACKET_HEADER;
|
||||
|
||||
typedef struct _PACKED_ {
|
||||
PACKET_HEADER header;
|
||||
char message[64];
|
||||
uint32_t server_key;
|
||||
uint32_t client_key;
|
||||
// note: there may be more data. if so, it is likely just more text which can be ignored. check header.pkt_size
|
||||
} WELCOME_PACKET;
|
||||
|
||||
void decrypt_and_display_packets(CRYPT_SETUP *cs, uint8_t *packet_data, size_t size) {
|
||||
size_t pos = 0;
|
||||
|
||||
CRYPT_CryptData(cs, packet_data, size, 0);
|
||||
|
||||
while (pos < size) {
|
||||
PACKET_HEADER *header = (PACKET_HEADER*)&packet_data[pos];
|
||||
|
||||
printf("id=%x, flags=%x, size=%d\n", header->pkt_id, header->pkt_flags, header->pkt_size);
|
||||
CRYPT_PrintData(&packet_data[pos], header->pkt_size);
|
||||
printf("\n");
|
||||
|
||||
pos += header->pkt_size;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int returncode;
|
||||
uint8_t *server_data = NULL;
|
||||
uint8_t *client_data = NULL;
|
||||
|
||||
if (argc != 3) {
|
||||
printf("Usage: decrypt_packets server-packet-data.bin client-packet-data.bin\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *server_packet_file = argv[1];
|
||||
const char *client_packet_file = argv[2];
|
||||
|
||||
uint32_t server_data_size = 0;
|
||||
returncode = read_file(server_packet_file, &server_data, &server_data_size);
|
||||
if (returncode) {
|
||||
printf("Error code %d (%s) reading server packet data file: %s\n", returncode, get_error_message(returncode), server_packet_file);
|
||||
goto error;
|
||||
}
|
||||
|
||||
uint32_t client_data_size = 0;
|
||||
returncode = read_file(client_packet_file, &client_data, &client_data_size);
|
||||
if (returncode) {
|
||||
printf("Error code %d (%s) reading client packet data file: %s\n", returncode, get_error_message(returncode), client_packet_file);
|
||||
goto error;
|
||||
}
|
||||
|
||||
WELCOME_PACKET *welcome = (WELCOME_PACKET*)server_data;
|
||||
if (welcome->header.pkt_id != 0x02 && welcome->header.pkt_id != 0x17) {
|
||||
printf("Missing or unrecognized 'Welcome' packet:\n\n");
|
||||
CRYPT_PrintData(welcome, sizeof(WELCOME_PACKET));
|
||||
printf("\nWill not be able to successfully decrypt session. Aborting.\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
// read client & server crypt keys from the "Welcome" packet the server sends right away. always unencrypted.
|
||||
printf("'Welcome' packet. id=%x, flags=%x, size=%d\n",
|
||||
welcome->header.pkt_id,
|
||||
welcome->header.pkt_flags,
|
||||
welcome->header.pkt_size);
|
||||
CRYPT_PrintData(welcome, welcome->header.pkt_size);
|
||||
printf("\n");
|
||||
|
||||
printf("server_key = 0x%x\nclient_key = 0x%x\n\n", welcome->server_key, welcome->client_key);
|
||||
|
||||
// set up crypt functionality using those keys, so we can read the rest of the server and client packet data
|
||||
// (all of the rest of it will be encrypted)
|
||||
CRYPT_SETUP server_cs, client_cs;
|
||||
|
||||
CRYPT_CreateKeys(&server_cs, &welcome->server_key, CRYPT_GAMECUBE);
|
||||
CRYPT_CreateKeys(&client_cs, &welcome->client_key, CRYPT_GAMECUBE);
|
||||
|
||||
// display remainder of server packets first
|
||||
printf("**** SERVER -> CLIENT PACKETS ****\n\n");
|
||||
decrypt_and_display_packets(&server_cs, server_data + welcome->header.pkt_size, server_data_size - welcome->header.pkt_size);
|
||||
|
||||
// now display the client packets
|
||||
printf("**** CLIENT -> SERVER PACKETS ****\n\n");
|
||||
decrypt_and_display_packets(&client_cs, client_data, client_data_size);
|
||||
|
||||
returncode = 0;
|
||||
goto quit;
|
||||
error:
|
||||
returncode = 1;
|
||||
quit:
|
||||
free(server_data);
|
||||
free(client_data);
|
||||
return returncode;
|
||||
}
|
110
decrypt_packets.md
Normal file
110
decrypt_packets.md
Normal file
|
@ -0,0 +1,110 @@
|
|||
# PSO Ep 1 & 2 (Gamecube) Client/Server Packets Decrypter Tool
|
||||
|
||||
This is a tool that will take raw binary PSO client and server packet data dumps generated from a packet capture tool
|
||||
(such as Wireshark) and display the decrypted packet data.
|
||||
|
||||
I put this tool together for myself to help further my understanding of PSO's network communication. More specifically,
|
||||
to help me troubleshoot why my attempts at setting up Sylverant's open source [login_server](https://github.com/Sylverant/login_server)
|
||||
to serve up quests for download was resulting in unusable quest files on Gamecube memory cards. Understanding the
|
||||
quest download communication better by analyzing the packets being sent from a working implementation and comparing it
|
||||
to what my local login_server instance was sending proved invaluable to me.
|
||||
|
||||
Of course, once I spent a bunch of time looking at packet data and comparing things and gaining the understanding about
|
||||
how Gamecube download quest data is composed and sent to the game client, I only then learnt that [Qedit](https://qedit.info/)
|
||||
can save to a Gamecube download .qst file directly. Heh. :-) Oh well. This tool may still prove useful to anyone looking
|
||||
to gain a better understanding of how PSO server/client communication works by looking at the packet exchanges directly
|
||||
I guess.
|
||||
|
||||
## Network Protocol
|
||||
|
||||
After the initial `0x02` or `0x17` packet sent from the server to the client (which contains the client and server
|
||||
encryption keys), all subsequent communication between the server and client is encrypted. When you have the full set
|
||||
of packets, beginning with the `0x02` or `0x17` packet, it is pretty trivial to decrypt the entire set of data.
|
||||
|
||||
```text
|
||||
'Welcome' packet. id=17, flags=0, size=276
|
||||
0000 | 17 00 14 01 44 72 65 61 6D 43 61 73 74 20 50 6F | ....DreamCast Po
|
||||
0010 | 72 74 20 4D 61 70 2E 20 43 6F 70 79 72 69 67 68 | rt Map. Copyrigh
|
||||
0020 | 74 20 53 45 47 41 20 45 6E 74 65 72 70 72 69 73 | t SEGA Enterpris
|
||||
0030 | 65 73 2E 20 31 39 39 39 00 00 00 00 00 00 00 00 | es. 1999........
|
||||
0040 | 00 00 00 00 6B 81 4B 4F 01 A2 65 78 54 68 69 73 | ....k.KO..exThis
|
||||
0050 | 20 73 65 72 76 65 72 20 69 73 20 69 6E 20 6E 6F | server is in no
|
||||
0060 | 20 77 61 79 20 61 66 66 69 6C 69 61 74 65 64 2C | way affiliated,
|
||||
0070 | 20 73 70 6F 6E 73 6F 72 65 64 2C 20 6F 72 20 73 | sponsored, or s
|
||||
0080 | 75 70 70 6F 72 74 65 64 20 62 79 20 53 45 47 41 | upported by SEGA
|
||||
0090 | 20 45 6E 74 65 72 70 72 69 73 65 73 20 6F 72 20 | Enterprises or
|
||||
00A0 | 53 4F 4E 49 43 54 45 41 4D 2E 20 54 68 65 20 70 | SONICTEAM. The p
|
||||
00B0 | 72 65 63 65 64 69 6E 67 20 6D 65 73 73 61 67 65 | receding message
|
||||
00C0 | 20 65 78 69 73 74 73 20 6F 6E 6C 79 20 69 6E 20 | exists only in
|
||||
00D0 | 6F 72 64 65 72 20 74 6F 20 72 65 6D 61 69 6E 20 | order to remain
|
||||
00E0 | 63 6F 6D 70 61 74 69 62 6C 65 20 77 69 74 68 20 | compatible with
|
||||
00F0 | 70 72 6F 67 72 61 6D 73 20 74 68 61 74 20 65 78 | programs that ex
|
||||
0100 | 70 65 63 74 20 69 74 2E 00 00 00 00 00 00 00 00 | pect it.........
|
||||
0110 | 00 00 00 00 | ....
|
||||
|
||||
server_key = 0x4f4b816b
|
||||
client_key = 0x7865a201
|
||||
```
|
||||
|
||||
Note, sometimes the packet will contain significantly less text than what is shown above. As well, the first bit of
|
||||
text may be slightly different depending on if it was received from a login or ship server, etc. The above output is
|
||||
from a Fuzziqer [newserv](https://github.com/fuzziqersoftware/newserv) I was testing with.
|
||||
|
||||
Also of note is that Sylverant's login_server currently seems to always use identical server and client keys (I believe
|
||||
this is a bug in libsylverant's usage of its random number generator library). This does not cause problems, but it is
|
||||
weird to see when you first notice it.
|
||||
|
||||
Some relevant reading regarding PSO's network protocol:
|
||||
|
||||
* [Network Protocol](http://web.archive.org/web/20171201191557/http://sharnoth.com/psodevwiki/net/protocol)
|
||||
* [Network Protocol Messages](http://web.archive.org/web/20171201191532/http://sharnoth.com/psodevwiki/net/messages)
|
||||
* ["Detailed" Message Flow](http://web.archive.org/web/20171201191527/http://sharnoth.com/psodevwiki/net/message_flow)
|
||||
|
||||
Currently, [libsylverant](https://github.com/Sylverant/libsylverant) has the cleanest and easiest to use PSO encryption
|
||||
API, and that is what is used by this tool.
|
||||
|
||||
**Note that the PSO encryption method (and thus, the `CRYPT_` API provided by libsylverant) is stateful**. That is, you
|
||||
cannot just use it to arbitrarily decrypt any single random packet and expect it to result in readable data. To
|
||||
correctly decrypt any individual packet from either client or server, you need to work through the full sequence of
|
||||
packets (for either client or server) beginning with the very first client or server packet (**after** the `0x17`
|
||||
packet) up to the packet(s) you really wanted, decrypting all of it along the way.
|
||||
|
||||
## Usage
|
||||
|
||||
### Capturing Packets from PSO
|
||||
|
||||
This is probably easiest if you already have Dolphin set up to run PSO with a working network configuration. In such
|
||||
a configuration, you can capture from your local computer right away.
|
||||
|
||||
I do not have this set up and I cannot be bothered to figure out the janky "Tap" set up that Dolphin requires. Mostly
|
||||
because I am lazy. And because I have a router running [OpenWrt](https://openwrt.org/) which allows me to easily set up
|
||||
packet mirroring with a special iptables kernel module loaded so that I can capture packets directly from my Gamecube.
|
||||
|
||||
If you're running the PSO server yourself, then you can just capture packets from that same machine, no special setup
|
||||
required.
|
||||
|
||||
I'm not going to go into details here on setting up any of these methods. If you're knowledgeable enough to be
|
||||
considering doing packet capture analysis of any sort in the first place, then you should be able to set up either
|
||||
method yourself.
|
||||
|
||||
### Dumping PSO Server/Client Communication Data Dumps with Wireshark
|
||||
|
||||
It is easy to generate packet data dumps containing _just_ the PSO packet data we are interested in with Wireshark.
|
||||
|
||||
After taking a capture of a PSO server/client session, find the TCP packet sent from the server to the client that
|
||||
contains the `0x17` packet. This should be easy enough to find as it will be one of the first TCP packets sent from the
|
||||
server to the client and contains a clear-text string similar to `DreamCast Port Map. Copyright SEGA Enterprises. 1999`.
|
||||
|
||||
Once you've found this packet, right-click it from the top packet list and select "Follow" then "TCP Stream". This will
|
||||
bring up a window that shows the raw data, colour-coded to show data originating from the client and server. Use the
|
||||
drop-downs and buttons at the bottom of this window to save "Raw"-format data for the client and server in
|
||||
**individual** files.
|
||||
|
||||
### Decrypting
|
||||
|
||||
Assuming you saved the data to two files, `server.bin` (containing server-to-client packets) and `client.bin`
|
||||
(containing client-to-server packets), you can run the tool like so:
|
||||
|
||||
```text
|
||||
decrypt_packets /path/to/server.bin /path/to/client.bin
|
||||
```
|
|
@ -1,19 +0,0 @@
|
|||
[package]
|
||||
name = "decrypt_packets"
|
||||
version = "0.1.0"
|
||||
authors = ["gered <gered@blarg.ca>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.40"
|
||||
thiserror = "1.0.24"
|
||||
pcap = "0.8.1"
|
||||
etherparse = "0.9.0"
|
||||
libc = "0.2.94"
|
||||
pretty-hex = "0.2.1"
|
||||
chrono = "0.4.19"
|
||||
|
||||
[dependencies.psoutils]
|
||||
path = "../psoutils"
|
|
@ -1,65 +0,0 @@
|
|||
# Packet Capture Decryption and Display
|
||||
|
||||
This tool is intended for learning and development purposes. It was originally written by me to help myself be able
|
||||
to gain a better understanding of the PSO network communication protocol by being able to analyze the packets from
|
||||
existing sessions with various different private servers. This was with the end goal of being able to write a new
|
||||
minimal server implementation specifically and only to serve up download quests.
|
||||
|
||||
Unfortunately, with PSO being a fairly niche and old game, there is not exactly a burgeoning community of developers
|
||||
with tons of comprehensive documentation, so writing tools to help myself and supplement the dearth of documentation
|
||||
on PSO's network protocol was an important first step.
|
||||
|
||||
### Further Reading on PSO's Network Protocol
|
||||
|
||||
* http://web.archive.org/web/20171201191537/http://sharnoth.com/psodevwiki/
|
||||
* http://www.fuzziqersoftware.com/files/psoprotocol.rtf
|
||||
|
||||
## Usage
|
||||
|
||||
This tool reads `.pcap` or `.pcapng` files produced by a capture tool, such as [Wireshark](https://www.wireshark.org/).
|
||||
|
||||
```text
|
||||
decrypt_packets /path/to/capture.pcapng
|
||||
```
|
||||
|
||||
### Caveats
|
||||
|
||||
It is _probably_ unlikely that you can just throw **any** capture file at this tool and expect it to work.
|
||||
|
||||
For best results, you should try to limit your capture so that it includes PSO client/server communication only. This
|
||||
tool does internally apply a `"tcp"` [pcap filter](https://biot.com/capstats/bpf.html) (meaning that it will ignore any
|
||||
UDP packets present in a capture, for example), but afterwards it will look at every TCP packet it finds and try to
|
||||
find the start of two peers communicating using the PSO network protocol. It does this by inspecting each TCP packet
|
||||
and checking for one that contains a 0x02 or 0x17 packet. When found, those two peers get marked as a PSO client and
|
||||
server and then all subsequent packets sent from either of them are interpreted as PSO network communication and packets
|
||||
between them will be decrypted using the key information from that first 0x02 or 0x17 packet.
|
||||
|
||||
With this in mind, this tool _might_ be able to work with captures containing PSO network communication as well as a
|
||||
bunch of other intermingled TCP packets for other things all jumbled together. But this is not a scenario I have
|
||||
tested at all, so I make no promises!
|
||||
|
||||
This tool is also probably not properly dealing with a bunch of different TCP packet/connection scenarios. Noteably,
|
||||
it does not handle retransmissions and a capture containing these will cause this tool to mess up. In such a scenario,
|
||||
you could easily filter the retransmissions out via Wireshark by applying this filter `!(tcp.analysis.retransmission or tcp.analysis.fast_retransmission)`
|
||||
and then re-exporting the capture.
|
||||
|
||||
### Capturing Packets from PSO
|
||||
|
||||
This is probably easiest if you already have Dolphin set up to run PSO with a working network configuration. Setting up
|
||||
Dolphin in this way is not actually easy itself. But if you can overcome that obstacle, then you can begin capturing
|
||||
packets from your local computer right away.
|
||||
|
||||
I do not personally have such a setup configured because I am lazy and cannot be bothered to figure out the janky "Tap"
|
||||
set up that Dolphin requires. This is also because my home router runs [OpenWrt](https://openwrt.org/) which allows me
|
||||
to install and use the [iptables-mod-tee](https://openwrt.org/packages/pkgdata/iptables-mod-tee) iptables extension
|
||||
and then I can configure packet mirroring through my router's iptables configuration for any device on my network
|
||||
(such as my Gamecube running PSO) and capture Gamecube traffic from my PC.
|
||||
|
||||
Alternatively, if you are running the PSO server yourself, then you can capture PSO traffic from the same computer that
|
||||
runs the PSO server, as that would be far easier than setting something up to capture from your Gamecube directly.
|
||||
|
||||
In any event, the point of this section is not to provide a full how-to to set this up but just to get you pointed in
|
||||
the right direction if you were unsure. If you're knowledgeable enough to be considering doing packet capture analysis
|
||||
of any sort in the first place, then you should be able to set up one of these methods to enable you to capture that
|
||||
traffic without too much fuss. If you are not knowledgeable like this, please do not contact me for assistance. This
|
||||
kind of stuff is far too difficult and frustrating to remote troubleshoot with someone not well versed in this area!
|
|
@ -1 +0,0 @@
|
|||
pub mod pcap;
|
|
@ -1,31 +0,0 @@
|
|||
use std::env;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use decrypt_packets::pcap::analyze;
|
||||
|
||||
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
fn display_banner() {
|
||||
println!("decrypt_packets v{}", VERSION);
|
||||
}
|
||||
|
||||
fn display_help() {
|
||||
println!("Tool for decrypting and displaying raw packets captured from a PSO client/server session.\n");
|
||||
println!("USAGE: decrypt_packets <capture.pcapng>");
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() != 2 {
|
||||
display_help();
|
||||
} else {
|
||||
let pcap_path = Path::new(&args[1]);
|
||||
analyze(pcap_path).context("Failed to analyze pcap file")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1,375 +0,0 @@
|
|||
use std::collections::HashMap;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use std::io::Cursor;
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use etherparse::{IpHeader, PacketHeaders};
|
||||
use pcap::{Capture, Offline};
|
||||
use pretty_hex::*;
|
||||
use thiserror::Error;
|
||||
|
||||
use psoutils::encryption::{Crypter, GCCrypter};
|
||||
use psoutils::packets::init::InitEncryptionPacket;
|
||||
use psoutils::packets::{GenericPacket, PacketHeader};
|
||||
|
||||
fn timeval_to_dt(ts: &::libc::timeval) -> DateTime<Utc> {
|
||||
Utc.timestamp(ts.tv_sec, ts.tv_usec as u32 * 1000)
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
enum TcpDataPacketError {
|
||||
#[error("No IpHeader in packet")]
|
||||
NoIpHeader,
|
||||
|
||||
#[error("No TransportHeader in packet")]
|
||||
NoTransportHeader,
|
||||
|
||||
#[error("No TcpHeader in packet")]
|
||||
NoTcpHeader,
|
||||
}
|
||||
|
||||
struct TcpDataPacket {
|
||||
pub source: SocketAddr,
|
||||
pub destination: SocketAddr,
|
||||
pub tcp_fin: bool,
|
||||
pub tcp_rst: bool,
|
||||
pub data: Box<[u8]>,
|
||||
}
|
||||
|
||||
impl TcpDataPacket {
|
||||
pub fn as_init_encryption_packet(&self) -> Option<InitEncryptionPacket> {
|
||||
let mut reader = Cursor::new(&self.data);
|
||||
if let Ok(packet) = InitEncryptionPacket::from_bytes(&mut reader) {
|
||||
Some(packet)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<PacketHeaders<'a>> for TcpDataPacket {
|
||||
type Error = TcpDataPacketError;
|
||||
|
||||
fn try_from(value: PacketHeaders) -> Result<Self, Self::Error> {
|
||||
let source_ip: IpAddr;
|
||||
let source_port: u16;
|
||||
let destination_ip: IpAddr;
|
||||
let destination_port: u16;
|
||||
let payload_len: usize;
|
||||
let data_offset: usize;
|
||||
let tcp_fin: bool;
|
||||
let tcp_rst: bool;
|
||||
|
||||
if let Some(ip_header) = &value.ip {
|
||||
let (source, destination, len) = match ip_header {
|
||||
IpHeader::Version4(ipv4_header) => (
|
||||
IpAddr::from(ipv4_header.source),
|
||||
IpAddr::from(ipv4_header.destination),
|
||||
ipv4_header.payload_len,
|
||||
),
|
||||
IpHeader::Version6(ipv6_header) => (
|
||||
IpAddr::from(ipv6_header.source),
|
||||
IpAddr::from(ipv6_header.destination),
|
||||
ipv6_header.payload_length,
|
||||
),
|
||||
};
|
||||
source_ip = source;
|
||||
destination_ip = destination;
|
||||
payload_len = len as usize;
|
||||
} else {
|
||||
return Err(TcpDataPacketError::NoIpHeader);
|
||||
}
|
||||
|
||||
if let Some(transport_header) = value.transport {
|
||||
if let Some(tcp_header) = transport_header.tcp() {
|
||||
source_port = tcp_header.source_port;
|
||||
destination_port = tcp_header.destination_port;
|
||||
data_offset = tcp_header.header_len() as usize;
|
||||
tcp_fin = tcp_header.fin;
|
||||
tcp_rst = tcp_header.rst;
|
||||
} else {
|
||||
return Err(TcpDataPacketError::NoTcpHeader);
|
||||
}
|
||||
} else {
|
||||
return Err(TcpDataPacketError::NoTransportHeader);
|
||||
}
|
||||
|
||||
// this ensures we don't get any padding bytes that might have been added onto the end ...
|
||||
let data: Box<[u8]> = value.payload[0..(payload_len - data_offset)].into();
|
||||
|
||||
Ok(TcpDataPacket {
|
||||
source: SocketAddr::new(source_ip, source_port),
|
||||
destination: SocketAddr::new(destination_ip, destination_port),
|
||||
tcp_fin,
|
||||
tcp_rst,
|
||||
data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for TcpDataPacket {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"TcpDataPacket {{ source={}, destination={}, length={} }}",
|
||||
self.source,
|
||||
self.destination,
|
||||
self.data.len()
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct Peer {
|
||||
crypter: Option<GCCrypter>,
|
||||
address: SocketAddr,
|
||||
raw_buffer: Vec<u8>,
|
||||
decrypted_buffer: Vec<u8>,
|
||||
packets: Vec<GenericPacket>,
|
||||
}
|
||||
|
||||
impl Peer {
|
||||
pub fn new(address: SocketAddr) -> Peer {
|
||||
Peer {
|
||||
crypter: None,
|
||||
address,
|
||||
raw_buffer: Vec::new(),
|
||||
decrypted_buffer: Vec::new(),
|
||||
packets: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_pso_session(&mut self, crypt_key: u32) {
|
||||
self.crypter = Some(GCCrypter::new(crypt_key));
|
||||
self.raw_buffer.clear();
|
||||
self.decrypted_buffer.clear();
|
||||
}
|
||||
|
||||
pub fn push_pso_packet(&mut self, packet: GenericPacket) {
|
||||
self.packets.push(packet)
|
||||
}
|
||||
|
||||
pub fn process_packet(&mut self, packet: TcpDataPacket) -> Result<()> {
|
||||
if self.address != packet.source {
|
||||
return Err(anyhow!(
|
||||
"This Peer({}) cannot process TcpDataPacket originating from different source: {}",
|
||||
self.address,
|
||||
packet.source
|
||||
));
|
||||
}
|
||||
|
||||
// don't begin collecting data unless we're prepared to decrypt that data ...
|
||||
if let Some(crypter) = &mut self.crypter {
|
||||
// incoming bytes get added to the raw (encrypted) buffer first ...
|
||||
self.raw_buffer.append(&mut packet.data.into_vec());
|
||||
|
||||
// we should only be decrypting dword-sized bits of data (based on the way that the
|
||||
// encryption algorithm works) so if we have that much data, lets go ahead and decrypt that
|
||||
// much and move those bytes over to the decrypted buffer ...
|
||||
if self.raw_buffer.len() >= 4 {
|
||||
let length_to_decrypt = self.raw_buffer.len() - (self.raw_buffer.len() & 3); // dword-sized length only!
|
||||
let mut bytes_to_decrypt: Vec<u8> =
|
||||
self.raw_buffer.drain(0..length_to_decrypt).collect();
|
||||
crypter.crypt(&mut bytes_to_decrypt);
|
||||
self.decrypted_buffer.append(&mut bytes_to_decrypt);
|
||||
}
|
||||
}
|
||||
|
||||
// try to extract as many complete packets out of the decrypted buffer as we can
|
||||
while self.decrypted_buffer.len() >= PacketHeader::header_size() {
|
||||
// if we have at least enough bytes for a PacketHeader available, read one out and figure
|
||||
// out if we have enough remaining bytes for the full packet that this header is for
|
||||
let mut reader = &self.decrypted_buffer[0..PacketHeader::header_size()];
|
||||
if let Ok(header) = PacketHeader::from_bytes(&mut reader) {
|
||||
if self.decrypted_buffer.len() >= header.size as usize {
|
||||
// the buffer has enough bytes for this entire packet. read it out and add it
|
||||
// to our internal list of reconstructed packets
|
||||
let packet_length = header.size as usize;
|
||||
let packet_bytes: Vec<u8> =
|
||||
self.decrypted_buffer.drain(0..packet_length).collect();
|
||||
let mut reader = Cursor::new(packet_bytes);
|
||||
self.packets.push(GenericPacket::from_bytes(&mut reader)?);
|
||||
} else {
|
||||
// unable to read the full packet with the bytes currently in the decrypted
|
||||
// buffer ... so we'll need to try again later after receiving some more data
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Peer {
|
||||
type Item = GenericPacket;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.packets.is_empty() {
|
||||
return None;
|
||||
} else {
|
||||
Some(self.packets.remove(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Peer {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Peer {{ address={} }}", self.address)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct Session {
|
||||
peers: HashMap<SocketAddr, Peer>,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn new() -> Session {
|
||||
Session {
|
||||
peers: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_peer(&mut self, address: SocketAddr) -> Option<&mut Peer> {
|
||||
self.peers.get_mut(&address)
|
||||
}
|
||||
|
||||
fn get_or_create_peer(&mut self, address: SocketAddr) -> &mut Peer {
|
||||
if self.peers.contains_key(&address) {
|
||||
self.peers.get_mut(&address).unwrap()
|
||||
} else {
|
||||
println!("Encountered new peer: {}\n", address);
|
||||
let new_peer = Peer::new(address);
|
||||
self.peers.insert(address, new_peer);
|
||||
self.get_or_create_peer(address)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_packet(&mut self, packet: TcpDataPacket) -> Result<()> {
|
||||
if packet.tcp_rst {
|
||||
println!(
|
||||
"Encountered TCP RST. Removing peers {} and {}.\n",
|
||||
packet.source, packet.destination
|
||||
);
|
||||
self.peers.remove(&packet.source);
|
||||
self.peers.remove(&packet.destination);
|
||||
} else if packet.tcp_fin {
|
||||
println!("Peer {} sent TCP FIN. Removing peer.\n", packet.source);
|
||||
self.peers.remove(&packet.source);
|
||||
} else if let Some(init_packet) = packet.as_init_encryption_packet() {
|
||||
println!(
|
||||
"Encountered InitEncryptionPacket sent from peer {}. Starting new session.",
|
||||
packet.source
|
||||
);
|
||||
|
||||
// the "init packet" indicates the start of a PSO client/server session. this could
|
||||
// occur multiple times within the same pcap file as a client moves between different
|
||||
// servers (e.g. from login server to ship server, switching between ships, etc).
|
||||
|
||||
println!(
|
||||
"Treating peer {} as the client, setting client decryption key: {:#010x}",
|
||||
packet.destination,
|
||||
init_packet.client_key()
|
||||
);
|
||||
|
||||
let client = self.get_or_create_peer(packet.destination);
|
||||
client.init_pso_session(init_packet.client_key);
|
||||
|
||||
println!(
|
||||
"Treating peer {} as the server, setting server decryption key: {:#010x}",
|
||||
packet.source,
|
||||
init_packet.server_key()
|
||||
);
|
||||
|
||||
let server = self.get_or_create_peer(packet.source);
|
||||
server.init_pso_session(init_packet.server_key);
|
||||
server.push_pso_packet(
|
||||
init_packet
|
||||
.try_into()
|
||||
.context("Failed to convert InitEncryptionPacket into GenericPacket")?,
|
||||
);
|
||||
|
||||
println!();
|
||||
} else {
|
||||
// process the packet via the peer it was sent from
|
||||
let peer = self.get_or_create_peer(packet.source);
|
||||
peer.process_packet(packet)
|
||||
.with_context(|| format!("Failed to process packet for peer {:?}", peer))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn analyze(path: &Path) -> Result<()> {
|
||||
println!("Opening capture file: {}", path.to_string_lossy());
|
||||
|
||||
let mut cap: Capture<Offline> = Capture::from_file(path)
|
||||
.with_context(|| format!("Failed to open capture file: {:?}", path))?
|
||||
.into();
|
||||
cap.filter("tcp")
|
||||
.context("Failed to apply 'tcp' filter to opened capture")?;
|
||||
|
||||
let mut session = Session::new();
|
||||
|
||||
let hex_cfg = HexConfig {
|
||||
title: false,
|
||||
width: 16,
|
||||
group: 0,
|
||||
..HexConfig::default()
|
||||
};
|
||||
|
||||
println!("Beginning analysis ...\n");
|
||||
|
||||
while let Ok(raw_packet) = cap.next() {
|
||||
if let Ok(decoded_packet) = PacketHeaders::from_ethernet_slice(raw_packet.data) {
|
||||
if let Ok(our_packet) = TcpDataPacket::try_from(decoded_packet) {
|
||||
let dt = timeval_to_dt(&raw_packet.header.ts);
|
||||
|
||||
println!("<<<<< {} >>>>> - {:?}\n", dt, our_packet);
|
||||
|
||||
let peer_address = our_packet.source;
|
||||
|
||||
session
|
||||
.process_packet(our_packet)
|
||||
.context("Session failed to process packet")?;
|
||||
|
||||
if let Some(peer) = session.get_peer(peer_address) {
|
||||
while let Some(pso_packet) = peer.next() {
|
||||
println!(
|
||||
"id=0x{:02x}, flags=0x{:02x}, size={} (0x{2:04x})",
|
||||
pso_packet.header.id(),
|
||||
pso_packet.header.flags(),
|
||||
pso_packet.header.size()
|
||||
);
|
||||
|
||||
// get full packet bytes for the hex dump since it is probably useful most
|
||||
// of the time to include the header bytes alongside the body
|
||||
// TODO: this feels sloppy ...
|
||||
let mut packet_bytes = Vec::new();
|
||||
pso_packet.write_bytes(&mut packet_bytes)?;
|
||||
println!("{:?}", packet_bytes.hex_conf(hex_cfg));
|
||||
println!();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"*** TcpDataPacket::try_from failed for packet={:?}",
|
||||
raw_packet.header
|
||||
);
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"*** PacketHeaders::from_ethernet_slice failed for packet={:?}",
|
||||
raw_packet.header
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
6
defs.h
Normal file
6
defs.h
Normal file
|
@ -0,0 +1,6 @@
|
|||
#ifndef DEFS_H_INCLUDED
|
||||
#define DEFS_H_INCLUDED
|
||||
|
||||
#define _PACKED_ __attribute__((packed))
|
||||
|
||||
#endif
|
447
fuzziqer_prs.c
Normal file
447
fuzziqer_prs.c
Normal file
|
@ -0,0 +1,447 @@
|
|||
/*
|
||||
* This implementation of PRS compression/decompression comes from here:
|
||||
* https://github.com/Sylverant/libsylverant/blob/67074b719e5b52e6cf55898578e40c2dbccc0839/src/utils/prs.c
|
||||
*
|
||||
* The reason it is being brought back out into use instead of using
|
||||
* libsylverant's current PRS compression/decompression implementation (which is
|
||||
* by far the cleanest one out there in my opinion) is due to apparent
|
||||
* incompatibilities when used to generate Gamecube download quest .qst files.
|
||||
*
|
||||
* When using libsylverant's implementation, some generated .qst files work fine.
|
||||
* But some others were resulting in black-screen crashes when loaded from the
|
||||
* memory card. As soon as I switched to this older PRS implementation to
|
||||
* generate the same .qst file (using the same source .bin/.dat files in both
|
||||
* cases, obviously) the black-screen crashes disappeared.
|
||||
*
|
||||
* The externally accessible functions, prefixed "fuzziqer_" so as not to
|
||||
* conflict with libsylverant (which I am still using for encryption), are just
|
||||
* libsylverant-API-compatible wrappers over the original "prs_" functions found
|
||||
* in this source file. This will make it easier to switch away from this older
|
||||
* implementation in the future if a fix is found for these apparent
|
||||
* incompatibilities.
|
||||
*
|
||||
* March 2021, Gered King. Original header comment from 2011 follows.
|
||||
*/
|
||||
/*
|
||||
The PRS compressor/decompressor that this file implements to was originally
|
||||
written by Fuzziqer Software. The file was distributed with the message that
|
||||
it could be used in anything/for any purpose as long as credit was given. I
|
||||
have incorporated it into libsylverant for use with the Sylverant PSO server
|
||||
and related utilities.
|
||||
|
||||
Other than minor changes (making it compile cleanly as C) this file has been
|
||||
left relatively intact from its original distribution, which was obtained on
|
||||
June 21st, 2009 from http://www.fuzziqersoftware.com/files/prsutil.zip
|
||||
|
||||
Modified June 30, 2011 by Lawrence Sebald:
|
||||
Make the code work properly when compiled for a 64-bit target.
|
||||
*/
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <malloc.h>
|
||||
|
||||
#include "fuzziqer_prs.h"
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
typedef struct {
|
||||
uint8_t bitpos;
|
||||
uint8_t *controlbyteptr;
|
||||
uint8_t *srcptr_orig;
|
||||
uint8_t *dstptr_orig;
|
||||
uint8_t *srcptr;
|
||||
uint8_t *dstptr;
|
||||
} PRS_COMPRESSOR;
|
||||
|
||||
static void prs_put_control_bit(PRS_COMPRESSOR *pc, uint8_t bit) {
|
||||
*pc->controlbyteptr = *pc->controlbyteptr >> 1;
|
||||
*pc->controlbyteptr |= ((!!bit) << 7);
|
||||
pc->bitpos++;
|
||||
if (pc->bitpos >= 8) {
|
||||
pc->bitpos = 0;
|
||||
pc->controlbyteptr = pc->dstptr;
|
||||
pc->dstptr++;
|
||||
}
|
||||
}
|
||||
|
||||
static void prs_put_control_bit_nosave(PRS_COMPRESSOR *pc, uint8_t bit) {
|
||||
*pc->controlbyteptr = *pc->controlbyteptr >> 1;
|
||||
*pc->controlbyteptr |= ((!!bit) << 7);
|
||||
pc->bitpos++;
|
||||
}
|
||||
|
||||
static void prs_put_control_save(PRS_COMPRESSOR *pc) {
|
||||
if (pc->bitpos >= 8) {
|
||||
pc->bitpos = 0;
|
||||
pc->controlbyteptr = pc->dstptr;
|
||||
pc->dstptr++;
|
||||
}
|
||||
}
|
||||
|
||||
static void prs_put_static_data(PRS_COMPRESSOR *pc, uint8_t data) {
|
||||
*pc->dstptr = data;
|
||||
pc->dstptr++;
|
||||
}
|
||||
|
||||
static uint8_t prs_get_static_data(PRS_COMPRESSOR *pc) {
|
||||
uint8_t data = *pc->srcptr;
|
||||
pc->srcptr++;
|
||||
return data;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static void prs_init(PRS_COMPRESSOR *pc, const void *src, void *dst) {
|
||||
pc->bitpos = 0;
|
||||
pc->srcptr = (uint8_t *) src;
|
||||
pc->srcptr_orig = (uint8_t *) src;
|
||||
pc->dstptr = (uint8_t *) dst;
|
||||
pc->dstptr_orig = (uint8_t *) dst;
|
||||
pc->controlbyteptr = pc->dstptr;
|
||||
pc->dstptr++;
|
||||
}
|
||||
|
||||
static void prs_finish(PRS_COMPRESSOR *pc) {
|
||||
prs_put_control_bit(pc, 0);
|
||||
prs_put_control_bit(pc, 1);
|
||||
|
||||
if (pc->bitpos != 0) {
|
||||
*pc->controlbyteptr = ((*pc->controlbyteptr << pc->bitpos) >> 8);
|
||||
}
|
||||
|
||||
prs_put_static_data(pc, 0);
|
||||
prs_put_static_data(pc, 0);
|
||||
}
|
||||
|
||||
static void prs_rawbyte(PRS_COMPRESSOR *pc) {
|
||||
prs_put_control_bit_nosave(pc, 1);
|
||||
prs_put_static_data(pc, prs_get_static_data(pc));
|
||||
prs_put_control_save(pc);
|
||||
}
|
||||
|
||||
static void prs_shortcopy(PRS_COMPRESSOR *pc, int offset, uint8_t size) {
|
||||
size -= 2;
|
||||
prs_put_control_bit(pc, 0);
|
||||
prs_put_control_bit(pc, 0);
|
||||
prs_put_control_bit(pc, (size >> 1) & 1);
|
||||
prs_put_control_bit_nosave(pc, size & 1);
|
||||
prs_put_static_data(pc, offset & 0xFF);
|
||||
prs_put_control_save(pc);
|
||||
}
|
||||
|
||||
static void prs_longcopy(PRS_COMPRESSOR *pc, int offset, uint8_t size) {
|
||||
uint8_t byte1, byte2;
|
||||
if (size <= 9) {
|
||||
prs_put_control_bit(pc, 0);
|
||||
prs_put_control_bit_nosave(pc, 1);
|
||||
prs_put_static_data(pc, ((offset << 3) & 0xF8) | ((size - 2) & 0x07));
|
||||
prs_put_static_data(pc, (offset >> 5) & 0xFF);
|
||||
prs_put_control_save(pc);
|
||||
} else {
|
||||
prs_put_control_bit(pc, 0);
|
||||
prs_put_control_bit_nosave(pc, 1);
|
||||
prs_put_static_data(pc, (offset << 3) & 0xF8);
|
||||
prs_put_static_data(pc, (offset >> 5) & 0xFF);
|
||||
prs_put_static_data(pc, size - 1);
|
||||
prs_put_control_save(pc);
|
||||
}
|
||||
}
|
||||
|
||||
static void prs_copy(PRS_COMPRESSOR *pc, int offset, uint8_t size) {
|
||||
if ((offset > -0x100) && (size <= 5)) {
|
||||
prs_shortcopy(pc, offset, size);
|
||||
} else {
|
||||
prs_longcopy(pc, offset, size);
|
||||
}
|
||||
pc->srcptr += size;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static uint32_t prs_compress(const void *source, void *dest, uint32_t size) {
|
||||
PRS_COMPRESSOR pc;
|
||||
int x, y, z;
|
||||
uint32_t xsize;
|
||||
int lsoffset, lssize;
|
||||
uint8_t *src = (uint8_t *) source, *dst = (uint8_t *) dest;
|
||||
prs_init(&pc, source, dest);
|
||||
|
||||
for (x = 0; x < size; x++) {
|
||||
lsoffset = lssize = xsize = 0;
|
||||
for (y = x - 3; (y > 0) && (y > (x - 0x1FF0)) && (xsize < 255); y--) {
|
||||
xsize = 3;
|
||||
if (!memcmp(src + y, src + x, xsize)) {
|
||||
do xsize++;
|
||||
while (!memcmp(src + y, src + x, xsize) &&
|
||||
(xsize < 256) &&
|
||||
((y + xsize) < x) &&
|
||||
((x + xsize) <= size)
|
||||
);
|
||||
xsize--;
|
||||
if (xsize > lssize) {
|
||||
lsoffset = -(x - y);
|
||||
lssize = xsize;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lssize == 0) {
|
||||
prs_rawbyte(&pc);
|
||||
} else {
|
||||
prs_copy(&pc, lsoffset, lssize);
|
||||
x += (lssize - 1);
|
||||
}
|
||||
}
|
||||
prs_finish(&pc);
|
||||
return pc.dstptr - pc.dstptr_orig;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
static uint32_t prs_decompress(const void *source, void *dest) // 800F7CB0 through 800F7DE4 in mem
|
||||
{
|
||||
uint32_t r0, r3, r6, r9; // 6 unnamed registers
|
||||
uint32_t bitpos = 9; // 4 named registers
|
||||
uint8_t *sourceptr = (uint8_t *) source;
|
||||
uint8_t *sourceptr_orig = (uint8_t *) source;
|
||||
uint8_t *destptr = (uint8_t *) dest;
|
||||
uint8_t *destptr_orig = (uint8_t *) dest;
|
||||
uint8_t *ptr_reg;
|
||||
uint8_t currentbyte;
|
||||
int flag;
|
||||
int32_t offset;
|
||||
uint32_t x, t; // 2 placed variables
|
||||
|
||||
currentbyte = sourceptr[0];
|
||||
sourceptr++;
|
||||
for (;;) {
|
||||
bitpos--;
|
||||
if (bitpos == 0) {
|
||||
currentbyte = sourceptr[0];
|
||||
bitpos = 8;
|
||||
sourceptr++;
|
||||
}
|
||||
flag = currentbyte & 1;
|
||||
currentbyte = currentbyte >> 1;
|
||||
if (flag) {
|
||||
destptr[0] = sourceptr[0];
|
||||
sourceptr++;
|
||||
destptr++;
|
||||
continue;
|
||||
}
|
||||
bitpos--;
|
||||
if (bitpos == 0) {
|
||||
currentbyte = sourceptr[0];
|
||||
bitpos = 8;
|
||||
sourceptr++;
|
||||
}
|
||||
flag = currentbyte & 1;
|
||||
currentbyte = currentbyte >> 1;
|
||||
if (flag) {
|
||||
r3 = sourceptr[0] & 0xFF;
|
||||
offset = ((sourceptr[1] & 0xFF) << 8) | r3;
|
||||
sourceptr += 2;
|
||||
if (offset == 0) return (uint32_t) (destptr - destptr_orig);
|
||||
r3 = r3 & 0x00000007;
|
||||
//r5 = (offset >> 3) | 0xFFFFE000;
|
||||
if (r3 == 0) {
|
||||
flag = 0;
|
||||
r3 = sourceptr[0] & 0xFF;
|
||||
sourceptr++;
|
||||
r3++;
|
||||
} else r3 += 2;
|
||||
//r5 += (uint32_t)destptr;
|
||||
ptr_reg = destptr + ((int32_t) ((offset >> 3) | 0xFFFFE000));
|
||||
} else {
|
||||
r3 = 0;
|
||||
for (x = 0; x < 2; x++) {
|
||||
bitpos--;
|
||||
if (bitpos == 0) {
|
||||
currentbyte = sourceptr[0];
|
||||
bitpos = 8;
|
||||
sourceptr++;
|
||||
}
|
||||
flag = currentbyte & 1;
|
||||
currentbyte = currentbyte >> 1;
|
||||
offset = r3 << 1;
|
||||
r3 = offset | flag;
|
||||
}
|
||||
offset = sourceptr[0] | 0xFFFFFF00;
|
||||
r3 += 2;
|
||||
sourceptr++;
|
||||
//r5 = offset + (uint32_t)destptr;
|
||||
ptr_reg = destptr + offset;
|
||||
}
|
||||
if (r3 == 0) continue;
|
||||
t = r3;
|
||||
for (x = 0; x < t; x++) {
|
||||
//destptr[0] = *(uint8_t*)r5;
|
||||
//r5++;
|
||||
*destptr++ = *ptr_reg++;
|
||||
r3++;
|
||||
//destptr++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t prs_decompress_size(const void *source) {
|
||||
uint32_t r0, r3, r6, r9; // 6 unnamed registers
|
||||
uint32_t bitpos = 9; // 4 named registers
|
||||
uint8_t *sourceptr = (uint8_t *) source;
|
||||
uint8_t *destptr = NULL;
|
||||
uint8_t *destptr_orig = NULL;
|
||||
uint8_t *ptr_reg;
|
||||
uint8_t currentbyte, lastbyte;
|
||||
int flag;
|
||||
int32_t offset;
|
||||
uint32_t x, t; // 2 placed variables
|
||||
|
||||
currentbyte = sourceptr[0];
|
||||
sourceptr++;
|
||||
for (;;) {
|
||||
bitpos--;
|
||||
if (bitpos == 0) {
|
||||
lastbyte = currentbyte = sourceptr[0];
|
||||
bitpos = 8;
|
||||
sourceptr++;
|
||||
}
|
||||
flag = currentbyte & 1;
|
||||
currentbyte = currentbyte >> 1;
|
||||
if (flag) {
|
||||
sourceptr++;
|
||||
destptr++;
|
||||
continue;
|
||||
}
|
||||
bitpos--;
|
||||
if (bitpos == 0) {
|
||||
lastbyte = currentbyte = sourceptr[0];
|
||||
bitpos = 8;
|
||||
sourceptr++;
|
||||
}
|
||||
flag = currentbyte & 1;
|
||||
currentbyte = currentbyte >> 1;
|
||||
if (flag) {
|
||||
r3 = sourceptr[0];
|
||||
offset = (sourceptr[1] << 8) | r3;
|
||||
sourceptr += 2;
|
||||
if (offset == 0) return (uint32_t) (destptr - destptr_orig);
|
||||
r3 = r3 & 0x00000007;
|
||||
//r5 = (offset >> 3) | 0xFFFFE000;
|
||||
if (r3 == 0) {
|
||||
r3 = sourceptr[0];
|
||||
sourceptr++;
|
||||
r3++;
|
||||
} else r3 += 2;
|
||||
//r5 += (uint32_t)destptr;
|
||||
ptr_reg = destptr + ((int32_t) ((offset >> 3) | 0xFFFFE000));
|
||||
} else {
|
||||
r3 = 0;
|
||||
for (x = 0; x < 2; x++) {
|
||||
bitpos--;
|
||||
if (bitpos == 0) {
|
||||
lastbyte = currentbyte = sourceptr[0];
|
||||
bitpos = 8;
|
||||
sourceptr++;
|
||||
}
|
||||
flag = currentbyte & 1;
|
||||
currentbyte = currentbyte >> 1;
|
||||
offset = r3 << 1;
|
||||
r3 = offset | flag;
|
||||
}
|
||||
offset = sourceptr[0] | 0xFFFFFF00;
|
||||
r3 += 2;
|
||||
sourceptr++;
|
||||
//r5 = offset + (uint32_t)destptr;
|
||||
ptr_reg = destptr + offset;
|
||||
}
|
||||
if (r3 == 0) continue;
|
||||
t = r3;
|
||||
for (x = 0; x < t; x++) {
|
||||
//r5++;
|
||||
ptr_reg++;
|
||||
r3++;
|
||||
destptr++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// borrowed from libsylverant: https://github.com/Sylverant/libsylverant/blob/master/src/utils/prs-comp.c
|
||||
static size_t prs_max_compressed_size(size_t len) {
|
||||
len += 2;
|
||||
return len + (len >> 3) + ((len & 0x07) ? 1 : 0);
|
||||
}
|
||||
|
||||
/*
|
||||
* The below functions are included as wrappers for the above "prs_" functions in order to provide
|
||||
* API compatibility with libsylverant's PRS functions, with the goal being to make it easier to
|
||||
* switch back to that implementation of PRS compression/decompression in the future. Do note that
|
||||
* the error handling is NOT as robust as libsylverant's PRS functions!
|
||||
*/
|
||||
|
||||
int fuzziqer_prs_compress(const uint8_t *src, uint8_t **dst, size_t src_len) {
|
||||
if (!src || !dst)
|
||||
return -EFAULT;
|
||||
|
||||
if (!src_len)
|
||||
return -EINVAL;
|
||||
|
||||
if (src_len < 3)
|
||||
return -EBADMSG;
|
||||
|
||||
/* Allocate probably more than enough space for the compressed output. */
|
||||
uint8_t *temp_dst;
|
||||
size_t max_compressed_size = prs_max_compressed_size(src_len);
|
||||
if (!(temp_dst = (uint8_t *)malloc(max_compressed_size)))
|
||||
return -errno;
|
||||
|
||||
/* TODO: this version of prs_compress doesn't really do much in the way of error checking ... */
|
||||
uint32_t size = prs_compress(src, temp_dst, src_len);
|
||||
|
||||
/* Resize the output (if realloc fails to resize it, then just use the
|
||||
unshortened buffer). */
|
||||
if(!(*dst = realloc(temp_dst, size)))
|
||||
*dst = temp_dst;
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
int fuzziqer_prs_decompress_buf(const uint8_t *src, uint8_t **dst, size_t src_len) {
|
||||
if (!src || !dst)
|
||||
return -EFAULT;
|
||||
|
||||
if (!src_len)
|
||||
return -EINVAL;
|
||||
|
||||
/* The minimum length of a PRS compressed file (if you were to "compress" a
|
||||
zero-byte file) is 3 bytes. If we don't have that, then bail out now. */
|
||||
if (src_len < 3)
|
||||
return -EBADMSG;
|
||||
|
||||
uint32_t dst_len = prs_decompress_size(src);
|
||||
if (!(*dst = malloc(dst_len)))
|
||||
return -errno;
|
||||
|
||||
/* TODO: this version of prs_decompress doesn't really do much in the way of error checking ... */
|
||||
uint32_t size = prs_decompress(src, *dst);
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
int fuzziqer_prs_decompress_size(const uint8_t *src, size_t src_len) {
|
||||
if (!src)
|
||||
return -EFAULT;
|
||||
|
||||
if (!src_len)
|
||||
return -EINVAL;
|
||||
|
||||
/* The minimum length of a PRS compressed file (if you were to "compress" a
|
||||
zero-byte file) is 3 bytes. If we don't have that, then bail out now. */
|
||||
if(src_len < 3)
|
||||
return -EBADMSG;
|
||||
|
||||
return prs_decompress_size(src);
|
||||
}
|
10
fuzziqer_prs.h
Normal file
10
fuzziqer_prs.h
Normal file
|
@ -0,0 +1,10 @@
|
|||
#ifndef PRS_H_INCLUDED
|
||||
#define PRS_H_INCLUDED
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
int fuzziqer_prs_compress(const uint8_t *src, uint8_t **dst, size_t src_len);
|
||||
int fuzziqer_prs_decompress_buf(const uint8_t *src, uint8_t **dst, size_t src_len);
|
||||
int fuzziqer_prs_decompress_size(const uint8_t *src, size_t src_len);
|
||||
|
||||
#endif
|
267
gci_extract.c
Normal file
267
gci_extract.c
Normal file
|
@ -0,0 +1,267 @@
|
|||
/*
|
||||
* Unencrypted PRS-compressed GCI Download Quest Extractor Tool
|
||||
*
|
||||
* This tool is specifically made to extract Gamecube PSO quest .bin/.dat files from GCI download quests memory card
|
||||
* files generated using the "Decryption Key Saver" Action Replay code created by Ralf at the gc-forever forums.
|
||||
* However, this tool currently assumes the quest data has been pre-decrypted using the embedded decryption key.
|
||||
*
|
||||
* https://www.gc-forever.com/forums/viewtopic.php?f=38&t=2050&start=75
|
||||
*
|
||||
* To clarify: this tool can extract quest .bin/.dat file data from the quests available for download from the linked
|
||||
* thread ONLY if they are indicated to be "unencrypted PRS compressed quests." This tool will NOT currently work with
|
||||
* the quest downloads indicated to be "encrypted quests w/ embedded decryption key."
|
||||
*
|
||||
* A future update to this tool will likely include decryption capability. Maybe? :-)
|
||||
*
|
||||
* Gered King, March 2021
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <malloc.h>
|
||||
|
||||
#include <sylverant/encryption.h>
|
||||
#include <sylverant/prs.h>
|
||||
#include "fuzziqer_prs.h"
|
||||
|
||||
#include "defs.h"
|
||||
|
||||
#include "quests.h"
|
||||
#include "utils.h"
|
||||
|
||||
#define ENDIAN_SWAP_32(x) ( (((x) >> 24) & 0x000000FF) | \
|
||||
(((x) >> 8) & 0x0000FF00) | \
|
||||
(((x) << 8) & 0x00FF0000) | \
|
||||
(((x) << 24) & 0xFF000000) )
|
||||
|
||||
|
||||
// copied from https://github.com/suloku/gcmm/blob/master/source/gci.h
|
||||
typedef struct _PACKED_ {
|
||||
uint8_t gamecode[4];
|
||||
uint8_t company[2];
|
||||
uint8_t reserved01; /*** Always 0xff ***/
|
||||
uint8_t banner_fmt;
|
||||
uint8_t filename[32];
|
||||
uint32_t time;
|
||||
uint32_t icon_addr; /*** Offset to banner/icon data ***/
|
||||
uint16_t icon_fmt;
|
||||
uint16_t icon_speed;
|
||||
uint8_t unknown1; /*** Permission key ***/
|
||||
uint8_t unknown2; /*** Copy Counter ***/
|
||||
uint16_t index; /*** Start block of savegame in memory card (Ignore - and throw away) ***/
|
||||
uint16_t filesize8; /*** File size / 8192 ***/
|
||||
uint16_t reserved02; /*** Always 0xffff ***/
|
||||
uint32_t comment_addr;
|
||||
} GCI;
|
||||
|
||||
typedef struct _PACKED_ {
|
||||
GCI gci_header;
|
||||
uint8_t card_file_header[0x2040]; // big area containing the icon and such other things. ignored
|
||||
|
||||
// this is stored in big-endian format in the original card data. we will convert right after loading...
|
||||
// this size value indicates the size of the quest data. it DOES NOT include the size value itself, 'nor
|
||||
// the subsequent "unknown" bytes (which we are not interested in and will be skipping during load)
|
||||
uint32_t size;
|
||||
|
||||
uint32_t unknown1;
|
||||
uint8_t unknown2[16];
|
||||
} GCI_DECRYPTED_DLQUEST_HEADER;
|
||||
|
||||
int get_quest_data(const char *filename, uint8_t **dest, uint32_t *dest_size, GCI_DECRYPTED_DLQUEST_HEADER *header) {
|
||||
if (!filename || !dest || !dest_size)
|
||||
return ERROR_INVALID_PARAMS;
|
||||
|
||||
FILE *fp = fopen(filename, "rb");
|
||||
if (!fp)
|
||||
return ERROR_FILE_NOT_FOUND;
|
||||
|
||||
int bytes_read;
|
||||
|
||||
bytes_read = fread(header, 1, sizeof(GCI_DECRYPTED_DLQUEST_HEADER), fp);
|
||||
if (bytes_read != sizeof(GCI_DECRYPTED_DLQUEST_HEADER)) {
|
||||
fclose(fp);
|
||||
return ERROR_BAD_DATA;
|
||||
}
|
||||
|
||||
// think this is all the game codes we could encounter ... ?
|
||||
if (memcmp("GPOJ", header->gci_header.gamecode, 4) &&
|
||||
memcmp("GPOE", header->gci_header.gamecode, 4) &&
|
||||
memcmp("GPOP", header->gci_header.gamecode, 4)) {
|
||||
fclose(fp);
|
||||
return ERROR_BAD_DATA;
|
||||
}
|
||||
|
||||
if (memcmp("8P", header->gci_header.company, 2)) {
|
||||
fclose(fp);
|
||||
return ERROR_BAD_DATA;
|
||||
}
|
||||
|
||||
if (!header->size) {
|
||||
fclose(fp);
|
||||
return ERROR_BAD_DATA;
|
||||
}
|
||||
|
||||
header->size = ENDIAN_SWAP_32(header->size);
|
||||
uint32_t quest_data_size = header->size - sizeof(header->unknown1);
|
||||
uint8_t *data = malloc(quest_data_size);
|
||||
bytes_read = fread(data, 1, quest_data_size, fp);
|
||||
if (bytes_read != quest_data_size) {
|
||||
fclose(fp);
|
||||
free(data);
|
||||
return ERROR_BAD_DATA;
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
*dest = data;
|
||||
*dest_size = quest_data_size;
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int returncode, validation_result;
|
||||
int32_t result;
|
||||
uint8_t *bin_data = NULL;
|
||||
uint8_t *dat_data = NULL;
|
||||
uint8_t *decompressed_bin_data = NULL;
|
||||
uint8_t *decompressed_dat_data = NULL;
|
||||
uint32_t bin_data_size, dat_data_size;
|
||||
size_t decompressed_bin_size, decompressed_dat_size;
|
||||
char out_filename[FILENAME_MAX];
|
||||
|
||||
if (argc != 3 && argc != 5) {
|
||||
printf("Usage: gci quest-bin.gci quest-dat.gci [output.bin] [output.dat]\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *bin_gci_filename = argv[1];
|
||||
const char *dat_gci_filename = argv[2];
|
||||
const char *out_bin_filename = (argc == 5 ? argv[3] : NULL);
|
||||
const char *out_dat_filename = (argc == 5 ? argv[4] : NULL);
|
||||
|
||||
/** extract quest .bin and .dat files from pre-decrypted GCI files **/
|
||||
|
||||
printf("Reading quest .bin data from %s ...\n", bin_gci_filename);
|
||||
GCI_DECRYPTED_DLQUEST_HEADER bin_gci_header;
|
||||
result = get_quest_data(bin_gci_filename, &bin_data, &bin_data_size, &bin_gci_header);
|
||||
if (result) {
|
||||
printf("Error code %d reading quest .bin data: %s\n", result, get_error_message(result));
|
||||
goto error;
|
||||
}
|
||||
|
||||
printf("Reading quest .dat data from %s ...\n", dat_gci_filename);
|
||||
GCI_DECRYPTED_DLQUEST_HEADER dat_gci_header;
|
||||
result = get_quest_data(dat_gci_filename, &dat_data, &dat_data_size, &dat_gci_header);
|
||||
if (result) {
|
||||
printf("Error code %d reading quest .dat data: %s\n", result, get_error_message(result));
|
||||
goto error;
|
||||
}
|
||||
|
||||
|
||||
/** decompress loaded quest .bin data and validate it **/
|
||||
printf("Validating quest .bin data ...\n");
|
||||
|
||||
//result = prs_decompress_buf(bin_data, &decompressed_bin_data, bin_data_size);
|
||||
result = fuzziqer_prs_decompress_buf(bin_data, &decompressed_bin_data, bin_data_size);
|
||||
if (result < 0) {
|
||||
printf("Error code %d decompressing .bin data.\n", result);
|
||||
goto error;
|
||||
}
|
||||
decompressed_bin_size = result;
|
||||
|
||||
QUEST_BIN_HEADER *bin_header = (QUEST_BIN_HEADER*)decompressed_bin_data;
|
||||
validation_result = validate_quest_bin(bin_header, decompressed_bin_size, true);
|
||||
validation_result = handle_quest_bin_validation_issues(validation_result, bin_header, &decompressed_bin_data, &decompressed_bin_size);
|
||||
if (validation_result) {
|
||||
printf("Aborting due to invalid quest .bin data.\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
|
||||
/** decompress loaded quest .dat data and validate it. this decompressed data is not used otherwise **/
|
||||
printf("Validating quest .dat data ...\n");
|
||||
|
||||
result = prs_decompress_buf(dat_data, &decompressed_dat_data, dat_data_size);
|
||||
if (result < 0) {
|
||||
printf("Error code %d decompressing .dat data.\n", result);
|
||||
goto error;
|
||||
}
|
||||
decompressed_dat_size = result;
|
||||
|
||||
validation_result = validate_quest_dat(decompressed_dat_data, decompressed_dat_size, true);
|
||||
validation_result = handle_quest_dat_validation_issues(validation_result, &decompressed_dat_data, &decompressed_dat_size);
|
||||
if (validation_result) {
|
||||
printf("Aborting due to invalid quest .dat data.\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
|
||||
print_quick_quest_info(bin_header, bin_data_size, dat_data_size);
|
||||
|
||||
|
||||
/** clear "download" flag from .bin data and re-compress **/
|
||||
printf("Clearing .bin header 'download' flag and re-compressing ...\n");
|
||||
|
||||
// we are clearing this here because this is normally how you would want this .bin file to be. this way it is
|
||||
// suitable as-is for use in online-play with a server. the .bin file needs to be specially prepared for use
|
||||
// as a downloadable quest anyway (see bindat_to_gcdl), and that process can (should) turn this flag back on.
|
||||
bin_header->download = 0;
|
||||
|
||||
uint8_t *recompressed_bin = NULL;
|
||||
// note: see header comment in fuzziqer_prs.c for explanation on why this is used instead of prs_compress()
|
||||
result = fuzziqer_prs_compress(decompressed_bin_data, &recompressed_bin, decompressed_bin_size);
|
||||
if (result < 0) {
|
||||
printf("Error code %d re-compressing .bin file data.\n", result);
|
||||
goto error;
|
||||
}
|
||||
|
||||
// overwrite old compressed bin data, since we don't need it anymore
|
||||
free(bin_data);
|
||||
bin_data = recompressed_bin;
|
||||
bin_data_size = (uint32_t)result;
|
||||
|
||||
|
||||
/** write out .bin data file **/
|
||||
|
||||
if (out_bin_filename)
|
||||
strncpy(out_filename, out_bin_filename, FILENAME_MAX-1);
|
||||
else
|
||||
snprintf(out_filename, FILENAME_MAX-1, "q%05d.bin", bin_header->quest_number_word);
|
||||
|
||||
printf("Writing compressed quest .bin data to %s ...\n", out_filename);
|
||||
result = write_file(out_filename, bin_data, bin_data_size);
|
||||
if (result) {
|
||||
printf("Error code %d writing out file: %s\n", result, get_error_message(result));
|
||||
goto error;
|
||||
}
|
||||
|
||||
|
||||
/** write out .dat data file **/
|
||||
|
||||
if (out_dat_filename)
|
||||
strncpy(out_filename, out_dat_filename, FILENAME_MAX-1);
|
||||
else
|
||||
snprintf(out_filename, FILENAME_MAX-1, "q%05d.dat", bin_header->quest_number_word);
|
||||
|
||||
printf("Writing compressed quest .dat data to %s ...\n", out_filename);
|
||||
result = write_file(out_filename, dat_data, dat_data_size);
|
||||
if (result) {
|
||||
printf("Error code %d writing out file: %s\n", result, get_error_message(result));
|
||||
goto error;
|
||||
}
|
||||
|
||||
|
||||
printf("Success!\n");
|
||||
|
||||
returncode = 0;
|
||||
goto quit;
|
||||
error:
|
||||
returncode = 1;
|
||||
quit:
|
||||
free(bin_data);
|
||||
free(dat_data);
|
||||
free(decompressed_dat_data);
|
||||
free(decompressed_bin_data);
|
||||
return returncode;
|
||||
}
|
36
gci_extract.md
Normal file
36
gci_extract.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
# PSO Ep 1 & 2 (Gamecube) Unencrypted PRS-compressed .gci Quest Extractor
|
||||
|
||||
This is a specialized tool written **specifically** to extract quest `.bin` and `.dat` files from `.gci` dumps of
|
||||
Gamecube memory card quest files that were saved using a special Action Replay code which included an embedded
|
||||
decryption key in the save file and then were manually decrypted with that decryption key.
|
||||
|
||||
Put another way, this tool will **only** work on `.gci` files found on [this gc-forever.com forum thread](https://www.gc-forever.com/forums/viewtopic.php?f=38&t=2050&start=75).
|
||||
And only if they are indicated to be "unencrypted PRS compressed quests" and **not** "encrypted quests w/ embedded
|
||||
decryption key".
|
||||
|
||||
**You cannot use this tool to extract quests from any arbitrary `.gci` file you have on your Gamecube memory cards!**
|
||||
|
||||
(Maybe one day someone will reverse-engineer the method in which the Gamecube client derives the encryption key
|
||||
from the player's serial number and access key. But until then, it is not possible for this tool to work with any
|
||||
arbitrary `.gci` file.)
|
||||
|
||||
## Usage
|
||||
|
||||
Quest files on a Gamecube memory card are split into two files per quest. One file contains the quest `.bin` data, and
|
||||
the other contains the quest `.dat` data. Therefore, two `.gci` files need to be provided to this tool.
|
||||
|
||||
Special care should be taken to ensure that the two `.gci` files you provide are a matching pair **and** that they are
|
||||
provided in the right order! **The file containing the `.bin` data should be specified first.**
|
||||
|
||||
```text
|
||||
gci_extract 8P-GPOE-PSO______NNN.gci 8P-GPOE-PSO______NNN+1.gci
|
||||
```
|
||||
|
||||
This will read out the data, parse and validate the quest information and save the raw (compressed) `.bin` and `.dat`
|
||||
file using a filename automatically derived from the quest's ID number.
|
||||
|
||||
Or, you can provide your own `.bin` and `.dat` filenames if you wish:
|
||||
|
||||
```text
|
||||
gci_extract 8P-GPOE-PSO______NNN.gci 8P-GPOE-PSO______NNN+1.gci myquest.bin myquest.dat
|
||||
```
|
|
@ -1,14 +0,0 @@
|
|||
[package]
|
||||
name = "gci_quest_extract"
|
||||
version = "0.1.0"
|
||||
authors = ["gered <gered@blarg.ca>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.40"
|
||||
byteorder = "1.4.3"
|
||||
|
||||
[dependencies.psoutils]
|
||||
path = "../psoutils"
|
|
@ -1,37 +0,0 @@
|
|||
# Unencrypted PRS-compressed `.gci` Quest Extractor
|
||||
|
||||
This is a specialized tool written **specifically** to extract quest `.bin` and `.dat` files from `.gci` dumps of
|
||||
Gamecube memory card quest files that were saved using a special Action Replay code which enabled an embedded
|
||||
decryption key to be included in the save file, following this the data was manually decrypted with that key.
|
||||
|
||||
Put another way, this tool will **only** work on `.gci` files found on [this gc-forever.com forum thread](https://www.gc-forever.com/forums/viewtopic.php?f=38&t=2050&start=75).
|
||||
And even then, **only** if they are labelled as "unencrypted PRS compressed quests". It will **not** work on the quests
|
||||
found there which are labelled as "encrypted quests w/ embedded decryption key."
|
||||
|
||||
**You CANNOT use this tool to extract quests from any arbitrary `.gci` file you have on your Gamecube memory cards!**
|
||||
|
||||
(Maybe one day someone will reverse-engineer the method in which PSO derives the encryption from the player's serial
|
||||
number and access key. But until then, it is not possible for this tool to work with any arbitrary `.gci` file.)
|
||||
|
||||
## Usage
|
||||
|
||||
Quest files on a Gamecube memory card are split into two files per quest. One file contains the quest `.bin` data, and
|
||||
the other contains the quest `.dat` data. Therefore, two `.gci` files are needed for each single quest.
|
||||
|
||||
```text
|
||||
gci_quest_extract <quest_1.gci> <quest_2.gci> <output.bin> <output.dat>
|
||||
```
|
||||
|
||||
For example, in the aforementioned gc-forever forum thread, you can download a zip of quests in `.gci` files. Consult
|
||||
the included text file for information about which files are for which quest. Then you can run this tool using
|
||||
something like this:
|
||||
|
||||
```text
|
||||
gci_quest_extract /path/to/8P-GPOE-PSO______022.gci /path/to/8P-GPOE-PSO______023.gci quest.bin quest.dat
|
||||
```
|
||||
|
||||
This will extract the quest found out into the files `quest.bin` and `quest.dat`.
|
||||
|
||||
This tool will automatically try to figure out which of the `.gci` files provided is the quest `.bin` file and which
|
||||
is the quest `.dat` file, so if you mix up the order, it should not matter. However, it is entirely up to you to make
|
||||
sure you provide matching files for the same quest!
|
|
@ -1,121 +0,0 @@
|
|||
use std::fs::File;
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
|
||||
use psoutils::bytes::ReadFixedLengthByteArray;
|
||||
use psoutils::quest::bin::QuestBin;
|
||||
use psoutils::quest::dat::QuestDat;
|
||||
use psoutils::quest::Quest;
|
||||
|
||||
// see https://github.com/suloku/gcmm/blob/master/source/gci.h for detailed GCI file format header
|
||||
// we will not be re-defining that struct here, since we're only interested in a handful of fields
|
||||
|
||||
const GCI_HEADER_SIZE: usize = 64;
|
||||
const CARD_FILE_HEADER: usize = 0x2040;
|
||||
const DATA_START_OFFSET: usize = GCI_HEADER_SIZE + CARD_FILE_HEADER;
|
||||
|
||||
fn extract_quest_data(path: &Path) -> Result<Box<[u8]>> {
|
||||
let mut file = File::open(path)?;
|
||||
|
||||
let gamecode: [u8; 4] = file.read_bytes()?;
|
||||
if &gamecode != b"GPOJ" && &gamecode != b"GPOE" && &gamecode != b"GPOP" {
|
||||
return Err(anyhow!(
|
||||
"GCI header 'gamecode' field does not match any expected string: {:02x?}",
|
||||
gamecode
|
||||
));
|
||||
}
|
||||
|
||||
let company: [u8; 2] = file.read_bytes()?;
|
||||
if &company != b"8P" {
|
||||
return Err(anyhow!(
|
||||
"GCI header 'company' field is not the expected value: {:02x?}",
|
||||
company
|
||||
));
|
||||
}
|
||||
|
||||
// move past the majority of GCI header and the actual Gamecube memory card header
|
||||
file.seek(SeekFrom::Start(DATA_START_OFFSET as u64))?;
|
||||
|
||||
// this "size" value actually accounts for an extra dword value that we do not care about
|
||||
let data_size = file.read_u32::<BigEndian>()? - 4;
|
||||
|
||||
// move past the remaining bits of the header to the actual start of the quest data
|
||||
file.seek(SeekFrom::Current(20))?;
|
||||
|
||||
// there will be remaining junk after the data which we probably don't want, so only read
|
||||
// the exact amount of bytes indicated in the header
|
||||
let mut buffer = vec![0u8; data_size as usize];
|
||||
file.read_exact(&mut buffer)?;
|
||||
|
||||
Ok(buffer.into_boxed_slice())
|
||||
}
|
||||
|
||||
fn load_quest_from_gci_files(gci1: &Path, gci2: &Path) -> Result<Quest> {
|
||||
let gci1_bytes = extract_quest_data(gci1).context(format!(
|
||||
"Failed to extract quest data from: {}",
|
||||
gci1.to_string_lossy()
|
||||
))?;
|
||||
let gci2_bytes = extract_quest_data(gci2).context(format!(
|
||||
"Failed to extract quest data from: {}",
|
||||
gci2.to_string_lossy()
|
||||
))?;
|
||||
|
||||
// now try to figure out which is the .bin and which is the .dat
|
||||
let bin: QuestBin;
|
||||
let dat: QuestDat;
|
||||
if let Ok(loaded) = QuestBin::from_compressed_bytes(gci1_bytes.as_ref()) {
|
||||
bin = loaded;
|
||||
dat = QuestDat::from_compressed_bytes(gci2_bytes.as_ref())
|
||||
.context("Failed to load second GCI file as quest .dat")?;
|
||||
} else if let Ok(loaded) = QuestDat::from_compressed_bytes(gci1_bytes.as_ref()) {
|
||||
dat = loaded;
|
||||
bin = QuestBin::from_compressed_bytes(gci2_bytes.as_ref())
|
||||
.context("Failed to load second GCI file as quest .bin")?;
|
||||
} else {
|
||||
return Err(anyhow!("Unable to load first GCI file as either a quest .bin or .dat file. It might not contain quest data, or it might not be pre-decrypted, or it might be corrupted."));
|
||||
}
|
||||
|
||||
Ok(Quest { bin, dat })
|
||||
}
|
||||
|
||||
pub fn extract_to_bindat(
|
||||
gci1: &Path,
|
||||
gci2: &Path,
|
||||
output_bin: &Path,
|
||||
output_dat: &Path,
|
||||
) -> Result<()> {
|
||||
println!(
|
||||
"Reading quest data from GCI files:\n - {}\n - {}",
|
||||
gci1.to_string_lossy(),
|
||||
gci2.to_string_lossy()
|
||||
);
|
||||
|
||||
let mut quest = load_quest_from_gci_files(gci1, gci2)?;
|
||||
|
||||
println!("Loaded quest .bin and .dat data successfully.\n");
|
||||
println!(
|
||||
"{}\n{}\n",
|
||||
quest.display_bin_info(),
|
||||
quest.display_dat_info()
|
||||
);
|
||||
|
||||
if quest.is_download() {
|
||||
println!("Turning 'download' flag off before saving.");
|
||||
quest.set_is_download(false);
|
||||
}
|
||||
|
||||
println!(
|
||||
"Saving quest as PRS-compressed bin/dat files:\n .bin file: {}\n .dat file: {}",
|
||||
output_bin.to_string_lossy(),
|
||||
output_dat.to_string_lossy()
|
||||
);
|
||||
|
||||
quest
|
||||
.to_compressed_bindat_files(output_bin, output_dat)
|
||||
.context("Failed to save quest to bin/dat files")?;
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
pub mod gci;
|
|
@ -1,35 +0,0 @@
|
|||
use std::env;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use gci_quest_extract::gci::extract_to_bindat;
|
||||
|
||||
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
fn display_banner() {
|
||||
println!("gci_quest_extract v{}", VERSION);
|
||||
}
|
||||
|
||||
fn display_help() {
|
||||
println!("Tool for extracting PSO Gamecube quests out of pre-decrypted .gci files.\n");
|
||||
println!("USAGE: gci_quest_extract <quest_1.gci> <quest_2.gci> <output.bin> <output.dat>");
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() != 5 {
|
||||
display_help();
|
||||
} else {
|
||||
let gci1_path = Path::new(&args[1]);
|
||||
let gci2_path = Path::new(&args[2]);
|
||||
let output_bin_path = Path::new(&args[3]);
|
||||
let output_dat_path = Path::new(&args[4]);
|
||||
extract_to_bindat(gci1_path, gci2_path, output_bin_path, output_dat_path)
|
||||
.context("Failed to extract quest from GCI files")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
121
gen_qst_header.c
Normal file
121
gen_qst_header.c
Normal file
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* PSO EP1&2 (Gamecube) .qst Header Generator Tool
|
||||
*
|
||||
* Given a set of input .bin/.dat quest files, this will automatically generate .hdr files for each appropriate for
|
||||
* a .qst file containing these .bin/.dat files.
|
||||
*
|
||||
* This tool was originally made to supplement the "qst_tool" found here https://github.com/Sylverant/pso_tools
|
||||
* which has somewhat primitive support for automatically generating .qst header information.
|
||||
*
|
||||
* Gered King, March 2021
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <malloc.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <sylverant/prs.h>
|
||||
|
||||
#include "utils.h"
|
||||
#include "quests.h"
|
||||
//#include "textconv.h"
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int returncode, validation_result;
|
||||
uint8_t *bin_data = NULL;
|
||||
uint8_t *dat_data = NULL;
|
||||
char *bin_hdr_file = NULL;
|
||||
char *dat_hdr_file = NULL;
|
||||
|
||||
if (argc != 3) {
|
||||
printf("Usage: gen_qst_header quest.bin quest.dat\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *bin_file = argv[1];
|
||||
const char *dat_file = argv[2];
|
||||
|
||||
const char *bin_base_filename = path_to_filename(bin_file);
|
||||
if (strlen(bin_base_filename) > QUEST_FILENAME_MAX_LENGTH) {
|
||||
printf("Bin filename is too long to fit in a QST header. Maximum length is 16 including file extension.\n");
|
||||
goto error;
|
||||
}
|
||||
const char *dat_base_filename = path_to_filename(dat_file);
|
||||
if (strlen(dat_base_filename) > QUEST_FILENAME_MAX_LENGTH) {
|
||||
printf("Dat filename is too long to fit in a QST header. Maximum length is 16 including file extension.\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
size_t bin_compressed_size, dat_compressed_size;
|
||||
|
||||
returncode = get_filesize(bin_file, &bin_compressed_size);
|
||||
if (returncode) {
|
||||
printf("Error code %d (%s) getting size of bin file: %s\n", returncode, get_error_message(returncode), bin_file);
|
||||
goto error;
|
||||
}
|
||||
returncode = get_filesize(dat_file, &dat_compressed_size);
|
||||
if (returncode) {
|
||||
printf("Error code %d (%s) getting size of dat file: %s\n", returncode, get_error_message(returncode), dat_file);
|
||||
goto error;
|
||||
}
|
||||
|
||||
|
||||
size_t bin_decompressed_size = prs_decompress_file(bin_file, &bin_data);
|
||||
if (bin_decompressed_size < 0) {
|
||||
printf("Error opening and decompressing bin file: %s\n", bin_file);
|
||||
goto error;
|
||||
}
|
||||
|
||||
size_t dat_decompressed_size = prs_decompress_file(dat_file, &dat_data);
|
||||
if (dat_decompressed_size < 0) {
|
||||
printf("Error opening and decompressing dat file: %s\n", dat_file);
|
||||
goto error;
|
||||
}
|
||||
|
||||
|
||||
QUEST_BIN_HEADER *bin_header = (QUEST_BIN_HEADER*)bin_data;
|
||||
validation_result = validate_quest_bin(bin_header, bin_decompressed_size, true);
|
||||
if (validation_result) {
|
||||
printf("Aborting due to invalid quest .bin data.\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
//sjis_to_utf8(bin_header->name, sizeof(bin_header->name));
|
||||
//sjis_to_utf8(bin_header->short_description, sizeof(bin_header->short_description));
|
||||
//sjis_to_utf8(bin_header->long_description, sizeof(bin_header->long_description));
|
||||
|
||||
print_quick_quest_info(bin_header, bin_compressed_size, dat_compressed_size);
|
||||
|
||||
|
||||
QST_HEADER qst_bin_header, qst_dat_header;
|
||||
generate_qst_header(bin_base_filename, bin_compressed_size, bin_header, &qst_bin_header);
|
||||
generate_qst_header(dat_base_filename, dat_compressed_size, bin_header, &qst_dat_header);
|
||||
|
||||
bin_hdr_file = append_string(bin_file, ".hdr");
|
||||
dat_hdr_file = append_string(dat_file, ".hdr");
|
||||
|
||||
returncode = write_file(bin_hdr_file, &qst_bin_header, sizeof(QST_HEADER));
|
||||
if (returncode) {
|
||||
printf("Error code %d (%s) writing out bin header file: %s\n", returncode, get_error_message(returncode), bin_hdr_file);
|
||||
goto error;
|
||||
}
|
||||
|
||||
returncode = write_file(dat_hdr_file, &qst_dat_header, sizeof(QST_HEADER));
|
||||
if (returncode) {
|
||||
printf("Error code %d (%s) writing out dat header file: %s\n", returncode, get_error_message(returncode), dat_hdr_file);
|
||||
goto error;
|
||||
}
|
||||
|
||||
|
||||
returncode = 0;
|
||||
goto quit;
|
||||
error:
|
||||
returncode = 1;
|
||||
quit:
|
||||
free(bin_hdr_file);
|
||||
free(dat_hdr_file);
|
||||
free(bin_data);
|
||||
free(dat_data);
|
||||
return returncode;
|
||||
}
|
29
gen_qst_header.md
Normal file
29
gen_qst_header.md
Normal file
|
@ -0,0 +1,29 @@
|
|||
# PSO Ep 1 & 2 (Gamecube) .qst Header Generator Tool
|
||||
|
||||
This is a simple tool that can generate quest `.qst` file headers for a given set of `.bin` and `.dat` files. This tool
|
||||
was written to complement Sylverant's [qst_tool](https://github.com/Sylverant/pso_tools/tree/master/qst_tool) which
|
||||
has primitive support for automatically generating a `.qst` file header if one is not provided.
|
||||
|
||||
**This tool is NOT required if you are using the "bindat_to_gcdl" tool also included in this repository. That tool
|
||||
automatically generates the necessary header information in an identical manner to how this tool does.**
|
||||
|
||||
It is probably not necessary to use this tool to be perfectly honest. It was something I originally created thinking
|
||||
I would need it, but then realized that I did not ("bindat_to_gcdl" evolved and basically rendered this tool
|
||||
redundant for me). It is still included here just for completeness sake.
|
||||
|
||||
## Usage
|
||||
|
||||
Given two quest `.bin` and `.dat` files ...
|
||||
|
||||
```
|
||||
gen_qst_header quest.bin quest.dat
|
||||
```
|
||||
|
||||
Will result in the `.bin` file's header information being saved to a file called `quest.bin.hdr` and the `.dat` file's
|
||||
header information being saved to a file called `quest.dat.hdr`.
|
||||
|
||||
This can then be used with "qst_tool" to generate a `.qst` file if you wish:
|
||||
|
||||
```
|
||||
qst_tool -m gc quest.bin quest.dat quest.bin.hdr quest.dat.hdr
|
||||
```
|
|
@ -1,17 +0,0 @@
|
|||
[package]
|
||||
name = "psogc_quest_tool"
|
||||
version = "0.1.0"
|
||||
authors = ["gered <gered@blarg.ca>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.40"
|
||||
|
||||
[dependencies.psoutils]
|
||||
path = "../psoutils"
|
||||
|
||||
[dev-dependencies]
|
||||
claim = "0.5.0"
|
||||
tempfile = "3.2.0"
|
|
@ -1,37 +0,0 @@
|
|||
# Quest Info and Conversion Tool
|
||||
|
||||
This tool can be used to display information about and perform simple data validations for any given PSO Gamecube quest
|
||||
as well as convert between various different quest file formats.
|
||||
|
||||
## Usage
|
||||
|
||||
### Quest Info
|
||||
|
||||
Use the `info` command argument and pass the quest file(s).
|
||||
|
||||
```text
|
||||
psogc_quest_tool info quest.bin quest.dat
|
||||
|
||||
psogc_quest_tool info quest.qst
|
||||
```
|
||||
|
||||
When providing .bin and .dat files, this tool will automatically figure out which is the .bin and .dat file, so if you
|
||||
mix up the order of these files it does not matter.
|
||||
|
||||
### Quest Conversion
|
||||
|
||||
Use the `convert` command argument and pass the input quest file(s) and output quest file(s).
|
||||
|
||||
```text
|
||||
psogc_quest_tool convert <input files> <output_format_type> <output_files>
|
||||
```
|
||||
|
||||
Where:
|
||||
|
||||
* `<input files>` should be either two files, a .bin and .dat file, or a single .qst file.
|
||||
* `<output_files>` same as the above, but the `<output_format_type>` dictates the files you should specify here.
|
||||
* `<output_format_type>` should be one of the following:
|
||||
* `raw_bindat` - Produces a .bin and .dat file, both uncompressed.
|
||||
* `prs_bindat` - Produces a .bin and .dat file, both PRS compressed.
|
||||
* `online_qst` - Produces a .qst file using packets 0x44 and 0x13 for online play via a server.
|
||||
* `offline_qst` - Produces a .qst file using packets 0xA6 and 0xA7 for offline play from a memory card when downloaded from a server.
|
|
@ -1,388 +0,0 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
|
||||
use psoutils::quest::Quest;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum ConvertFormat {
|
||||
RawBinDat,
|
||||
PrsBinDat,
|
||||
OnlineQst,
|
||||
OfflineQst,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ConvertFormat {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
use ConvertFormat::*;
|
||||
match value.to_lowercase().as_str() {
|
||||
"raw_bindat" => Ok(RawBinDat),
|
||||
"prs_bindat" => Ok(PrsBinDat),
|
||||
"online_qst" => Ok(OnlineQst),
|
||||
"offline_qst" => Ok(OfflineQst),
|
||||
other => Err(format!("Not a valid conversion format: {}", other)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_args(args: &[String]) -> Result<(&[String], ConvertFormat, &[String])> {
|
||||
if args.len() < 3 {
|
||||
return Err(anyhow!("Not enough arguments supplied"));
|
||||
}
|
||||
|
||||
let mut convert_format_arg_index = None;
|
||||
let mut convert_format = None;
|
||||
// find the ConvertFormat argument, wherever it may be
|
||||
for (index, arg) in args.iter().enumerate() {
|
||||
if let Ok(format) = ConvertFormat::try_from(arg.as_str()) {
|
||||
if convert_format.is_some() {
|
||||
return Err(anyhow!("More than one conversion format specified"));
|
||||
}
|
||||
|
||||
convert_format_arg_index = Some(index);
|
||||
convert_format = Some(format);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(index) = convert_format_arg_index {
|
||||
// the ConvertFormat arg should be specified in-between the input file argument(s) and the
|
||||
// output file argument(s), so it should never exist at the very beginning or very end of
|
||||
// the arguments list.
|
||||
if index == 0 {
|
||||
return Err(anyhow!("No input file(s) provided"));
|
||||
} else if index == (args.len() - 1) {
|
||||
return Err(anyhow!("No output file(s) provided"));
|
||||
}
|
||||
|
||||
let input_file_args = &args[0..index];
|
||||
let convert_format = convert_format.unwrap();
|
||||
let output_file_args = &args[(index + 1)..];
|
||||
Ok((input_file_args, convert_format, output_file_args))
|
||||
} else {
|
||||
return Err(anyhow!("No conversion format specified"));
|
||||
}
|
||||
}
|
||||
|
||||
fn load_quest(input_files: &[String]) -> Result<Quest> {
|
||||
if input_files.len() == 2 {
|
||||
println!(
|
||||
"Loading quest from:\n .bin file: {}\n .dat file: {}",
|
||||
&input_files[0], &input_files[1]
|
||||
);
|
||||
let bin_path = Path::new(&input_files[0]);
|
||||
let dat_path = Path::new(&input_files[1]);
|
||||
Quest::from_bindat_files(bin_path, dat_path)
|
||||
.context("Failed to load quest from .bin/.dat files")
|
||||
} else {
|
||||
println!("Loading quest from:\n .qst file: {}", &input_files[0]);
|
||||
let qst_path = Path::new(&input_files[0]);
|
||||
Quest::from_qst_file(qst_path).context("Failed to load quest from .qst file")
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_to_raw_bindat(input_files: &[String], output_files: &[String]) -> Result<()> {
|
||||
println!("Performing conversion to raw/uncompressed .bin/.dat quest files");
|
||||
|
||||
if input_files.len() > 2 {
|
||||
return Err(anyhow!(
|
||||
"Too many input files specified. Expected either: two (.bin + .dat) or one (.qst)"
|
||||
));
|
||||
}
|
||||
if output_files.len() != 2 {
|
||||
return Err(anyhow!(
|
||||
"Incorrect number of output files specified. Expected two: a .bin and a .dat file."
|
||||
));
|
||||
}
|
||||
|
||||
let quest = load_quest(input_files)?;
|
||||
|
||||
println!(
|
||||
"Saving converted quest to:\n .bin file: {}\n .dat file: {}",
|
||||
&output_files[0], &output_files[1]
|
||||
);
|
||||
let output_bin_path = Path::new(&output_files[0]);
|
||||
let output_dat_path = Path::new(&output_files[1]);
|
||||
quest
|
||||
.to_uncompressed_bindat_files(output_bin_path, output_dat_path)
|
||||
.context("Failed to save quest to uncompressed .bin/.dat files")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn convert_to_prs_bindat(input_files: &[String], output_files: &[String]) -> Result<()> {
|
||||
println!("Performing conversion to PRS-compressed .bin/.dat quest files");
|
||||
|
||||
if input_files.len() > 2 {
|
||||
return Err(anyhow!(
|
||||
"Too many input files specified. Expected either: two (.bin + .dat) or one (.qst)"
|
||||
));
|
||||
}
|
||||
if output_files.len() != 2 {
|
||||
return Err(anyhow!(
|
||||
"Incorrect number of output files specified. Expected two: a .bin and a .dat file."
|
||||
));
|
||||
}
|
||||
|
||||
let quest = load_quest(input_files)?;
|
||||
|
||||
println!(
|
||||
"Saving converted quest to:\n .bin file: {}\n .dat file: {}",
|
||||
&output_files[0], &output_files[1]
|
||||
);
|
||||
let output_bin_path = Path::new(&output_files[0]);
|
||||
let output_dat_path = Path::new(&output_files[1]);
|
||||
quest
|
||||
.to_compressed_bindat_files(output_bin_path, output_dat_path)
|
||||
.context("Failed to save quest to compressed .bin/.dat files")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn convert_to_online_qst(input_files: &[String], output_files: &[String]) -> Result<()> {
|
||||
println!("Performing conversion to server/online .qst quest file");
|
||||
|
||||
if input_files.len() > 2 {
|
||||
return Err(anyhow!(
|
||||
"Too many input files specified. Expected either: two (.bin + .dat) or one (.qst)"
|
||||
));
|
||||
}
|
||||
if output_files.len() != 1 {
|
||||
return Err(anyhow!(
|
||||
"Incorrect number of output files specified. Expected one .qst file."
|
||||
));
|
||||
}
|
||||
|
||||
let mut quest = load_quest(input_files)?;
|
||||
|
||||
// turn download flag off (download = offline)
|
||||
quest.set_is_download(false);
|
||||
|
||||
println!(
|
||||
"Saving converted quest to:\n .qst file: {}",
|
||||
&output_files[0]
|
||||
);
|
||||
let output_qst_path = Path::new(&output_files[0]);
|
||||
quest
|
||||
.to_qst_file(output_qst_path)
|
||||
.context("Failed to save quest to server/online .qst file")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn convert_to_offline_qst(input_files: &[String], output_files: &[String]) -> Result<()> {
|
||||
println!("Performing conversion to download/offline .qst quest file");
|
||||
|
||||
if input_files.len() > 2 {
|
||||
return Err(anyhow!(
|
||||
"Too many input files specified. Expected either: two (.bin + .dat) or one (.qst)"
|
||||
));
|
||||
}
|
||||
if output_files.len() != 1 {
|
||||
return Err(anyhow!(
|
||||
"Incorrect number of output files specified. Expected one .qst file."
|
||||
));
|
||||
}
|
||||
|
||||
let mut quest = load_quest(input_files)?;
|
||||
|
||||
// turn download flag on (download = offline)
|
||||
quest.set_is_download(true);
|
||||
|
||||
println!(
|
||||
"Saving converted quest to:\n .qst file: {}",
|
||||
&output_files[0]
|
||||
);
|
||||
let output_qst_path = Path::new(&output_files[0]);
|
||||
quest
|
||||
.to_qst_file(output_qst_path)
|
||||
.context("Failed to save quest to download/offline .qst file")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn quest_convert(args: &[String]) -> Result<()> {
|
||||
use ConvertFormat::*;
|
||||
|
||||
let (input_file_args, convert_format, output_file_args) = collect_args(args)?;
|
||||
|
||||
match convert_format {
|
||||
RawBinDat => convert_to_raw_bindat(input_file_args, output_file_args)
|
||||
.context("Failed converting to raw/uncompressed .bin/.dat quest")?,
|
||||
PrsBinDat => convert_to_prs_bindat(input_file_args, output_file_args)
|
||||
.context("Failed converting to PRS-compressed .bin/.dat quest")?,
|
||||
OnlineQst => convert_to_online_qst(input_file_args, output_file_args)
|
||||
.context("Failed converting to online .qst quest")?,
|
||||
OfflineQst => convert_to_offline_qst(input_file_args, output_file_args)
|
||||
.context("Failed converting to offline .qst quest")?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use claim::*;
|
||||
use tempfile::*;
|
||||
|
||||
use psoutils::quest::bin::QuestBin;
|
||||
use psoutils::quest::dat::QuestDat;
|
||||
use psoutils::quest::qst::QuestQst;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
pub fn collect_args_fails_with_less_than_minimum_arg_count() {
|
||||
let args: &[String] = &[];
|
||||
assert_matches!(collect_args(args), Err(_));
|
||||
|
||||
let args = &["a".to_string(), "b".to_string()];
|
||||
assert_matches!(collect_args(args), Err(_));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn collect_args_succeeds_in_expected_cases() {
|
||||
let args = &[
|
||||
"input.bin".to_string(),
|
||||
"input.dat".to_string(),
|
||||
"raw_bindat".to_string(),
|
||||
"output.bin".to_string(),
|
||||
"output.dat".to_string(),
|
||||
];
|
||||
let (input, format, output) = collect_args(args).unwrap();
|
||||
assert_eq!(input, ["input.bin", "input.dat"]);
|
||||
assert_eq!(format, ConvertFormat::RawBinDat);
|
||||
assert_eq!(output, ["output.bin", "output.dat"]);
|
||||
|
||||
let args = &[
|
||||
"input.qst".to_string(),
|
||||
"prs_bindat".to_string(),
|
||||
"output.bin".to_string(),
|
||||
"output.dat".to_string(),
|
||||
];
|
||||
let (input, format, output) = collect_args(args).unwrap();
|
||||
assert_eq!(input, ["input.qst"]);
|
||||
assert_eq!(format, ConvertFormat::PrsBinDat);
|
||||
assert_eq!(output, ["output.bin", "output.dat"]);
|
||||
|
||||
let args = &[
|
||||
"input.bin".to_string(),
|
||||
"input.dat".to_string(),
|
||||
"online_qst".to_string(),
|
||||
"output.qst".to_string(),
|
||||
];
|
||||
let (input, format, output) = collect_args(args).unwrap();
|
||||
assert_eq!(input, ["input.bin", "input.dat"]);
|
||||
assert_eq!(format, ConvertFormat::OnlineQst);
|
||||
assert_eq!(output, ["output.qst"]);
|
||||
|
||||
let args = &[
|
||||
"input.qst".to_string(),
|
||||
"offline_qst".to_string(),
|
||||
"output.qst".to_string(),
|
||||
];
|
||||
let (input, format, output) = collect_args(args).unwrap();
|
||||
assert_eq!(input, ["input.qst"]);
|
||||
assert_eq!(format, ConvertFormat::OfflineQst);
|
||||
assert_eq!(output, ["output.qst"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn collect_args_fails_when_no_convert_format_arg_is_provided() {
|
||||
let args = &[
|
||||
"input.bin".to_string(),
|
||||
"input.dat".to_string(),
|
||||
"output.bin".to_string(),
|
||||
"output.dat".to_string(),
|
||||
];
|
||||
assert_matches!(collect_args(args), Err(_));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn collect_args_fails_when_convert_format_arg_is_provided_multiple_times() {
|
||||
let args = &[
|
||||
"input.bin".to_string(),
|
||||
"input.dat".to_string(),
|
||||
"online_qst".to_string(),
|
||||
"online_qst".to_string(),
|
||||
"output.qst".to_string(),
|
||||
];
|
||||
assert_matches!(collect_args(args), Err(_));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn collect_args_fails_when_no_output_file_args_provided() {
|
||||
let args = &[
|
||||
"input.bin".to_string(),
|
||||
"input.dat".to_string(),
|
||||
"online_qst".to_string(),
|
||||
];
|
||||
assert_matches!(collect_args(args), Err(_));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn can_convert_to_raw_bindat() {
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
let bin_save_path = tmp_dir.path().join("quest58.bin");
|
||||
let dat_save_path = tmp_dir.path().join("quest58.dat");
|
||||
|
||||
let args = &[
|
||||
"../test-assets/q058-ret-gc.online.qst".to_string(),
|
||||
"raw_bindat".to_string(),
|
||||
bin_save_path.to_string_lossy().into_owned(),
|
||||
dat_save_path.to_string_lossy().into_owned(),
|
||||
];
|
||||
assert_ok!(quest_convert(args));
|
||||
assert_ok!(QuestBin::from_uncompressed_file(&bin_save_path));
|
||||
assert_ok!(QuestDat::from_uncompressed_file(&dat_save_path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn can_convert_to_prs_bindat() {
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
let bin_save_path = tmp_dir.path().join("quest58.bin");
|
||||
let dat_save_path = tmp_dir.path().join("quest58.dat");
|
||||
|
||||
let args = &[
|
||||
"../test-assets/q058-ret-gc.offline.qst".to_string(),
|
||||
"prs_bindat".to_string(),
|
||||
bin_save_path.to_string_lossy().into_owned(),
|
||||
dat_save_path.to_string_lossy().into_owned(),
|
||||
];
|
||||
assert_ok!(quest_convert(args));
|
||||
assert_ok!(QuestBin::from_compressed_file(&bin_save_path));
|
||||
assert_ok!(QuestDat::from_compressed_file(&dat_save_path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn can_convert_to_online_qst() {
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
let qst_save_path = tmp_dir.path().join("quest58.qst");
|
||||
|
||||
let args = &[
|
||||
"../test-assets/q058-ret-gc.bin".to_string(),
|
||||
"../test-assets/q058-ret-gc.dat".to_string(),
|
||||
"online_qst".to_string(),
|
||||
qst_save_path.to_string_lossy().into_owned(),
|
||||
];
|
||||
assert_ok!(quest_convert(args));
|
||||
assert_ok!(QuestQst::from_file(&qst_save_path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn can_convert_to_offline_qst() {
|
||||
let tmp_dir = TempDir::new().unwrap();
|
||||
let qst_save_path = tmp_dir.path().join("quest58.qst");
|
||||
|
||||
let args = &[
|
||||
"../test-assets/q058-ret-gc.uncompressed.bin".to_string(),
|
||||
"../test-assets/q058-ret-gc.uncompressed.dat".to_string(),
|
||||
"offline_qst".to_string(),
|
||||
qst_save_path.to_string_lossy().into_owned(),
|
||||
];
|
||||
assert_ok!(quest_convert(args));
|
||||
assert_ok!(QuestQst::from_file(&qst_save_path));
|
||||
}
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
use std::path::Path;
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
|
||||
use psoutils::quest::Quest;
|
||||
|
||||
pub fn quest_info(args: &[String]) -> Result<()> {
|
||||
println!("Showing quest information");
|
||||
|
||||
let quest = match args.len() {
|
||||
0 => {
|
||||
return Err(anyhow!("No quest file(s) specified."));
|
||||
}
|
||||
1 => {
|
||||
println!("Loading quest from:\n .qst file: {}", &args[0]);
|
||||
let qst_path = Path::new(&args[0]);
|
||||
Quest::from_qst_file(qst_path).context("Failed to load quest from .qst file")?
|
||||
}
|
||||
2 => {
|
||||
println!(
|
||||
"Loading quest from:\n .bin file: {}\n .dat file: {}",
|
||||
&args[0], &args[1]
|
||||
);
|
||||
let bin_path = Path::new(&args[0]);
|
||||
let dat_path = Path::new(&args[1]);
|
||||
Quest::from_bindat_files(bin_path, dat_path)
|
||||
.context("Failed to load quest from .bin/.dat files")?
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow!("Too many arguments. Should only specify either a single .qst file, or a .bin and .dat file."));
|
||||
}
|
||||
};
|
||||
|
||||
println!();
|
||||
println!("{}", quest.display_bin_info());
|
||||
println!();
|
||||
println!("{}", quest.display_dat_info());
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use claim::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
// TODO: some way to match the specific error message string? or probably should just replace
|
||||
// anyhow usage with a specific error type ...
|
||||
|
||||
#[test]
|
||||
pub fn no_args_fails_with_error() {
|
||||
let args: &[String] = &[];
|
||||
assert_matches!(quest_info(args), Err(_));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn too_many_args_fails_with_error() {
|
||||
let args = &["a".to_string(), "b".to_string(), "c".to_string()];
|
||||
assert_matches!(quest_info(args), Err(_));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn succeeds_with_single_file_arg() {
|
||||
let args = &["../test-assets/q058-ret-gc.online.qst".to_string()];
|
||||
assert_ok!(quest_info(args));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn succeeds_with_two_file_args() {
|
||||
let args = &[
|
||||
"../test-assets/q058-ret-gc.bin".to_string(),
|
||||
"../test-assets/q058-ret-gc.dat".to_string(),
|
||||
];
|
||||
assert_ok!(quest_info(args));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn fails_when_bin_dat_file_args_in_wrong_order() {
|
||||
let args = &[
|
||||
"../test-assets/q058-ret-gc.dat".to_string(),
|
||||
"../test-assets/q058-ret-gc.bin".to_string(),
|
||||
];
|
||||
assert_matches!(quest_info(args), Err(_));
|
||||
}
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
pub mod convert;
|
||||
pub mod info;
|
|
@ -1,54 +0,0 @@
|
|||
use std::env;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use psogc_quest_tool::convert::quest_convert;
|
||||
use psogc_quest_tool::info::quest_info;
|
||||
|
||||
const VERSION: &'static str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
fn display_banner() {
|
||||
println!("psogc_quest_tool v{}", VERSION);
|
||||
}
|
||||
|
||||
fn display_help() {
|
||||
println!("Tool for PSO Gamecube quest bin/dat and/or qst files.\n");
|
||||
println!("USAGE: psogc_quest_tool <COMMAND> <ARGS...>\n");
|
||||
println!("COMMANDS:");
|
||||
println!(" info - Displays info about a quest.");
|
||||
println!(" - info <input.bin> <input.dat>");
|
||||
println!(" - info <input.qst>");
|
||||
println!(" convert - Converts a quest to a different file format");
|
||||
println!(" - convert <input files> <output_format_type> <output files>");
|
||||
println!(" Where the arguments:");
|
||||
println!(" - \"input files\" and \"output files\" should either be:");
|
||||
println!(" a) two files, a .bin and .dat file; or");
|
||||
println!(" b) a single .qst file");
|
||||
println!(" - \"output_format_type\" should be one of: ");
|
||||
println!(" - raw_bindat (produces a .bin and .dat, both uncompressed)");
|
||||
println!(" - prs_bindat (produces a .bin and .dat, both PRS compressed)");
|
||||
println!(" - online_qst (produces a .qst, for online play via a server)");
|
||||
println!(" - offline_qst (produces a .qst, for offline play from a mem");
|
||||
println!(" card when downloaded from a server)");
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
display_banner();
|
||||
|
||||
let args: Vec<String> = env::args().collect();
|
||||
if args.len() < 2 {
|
||||
display_help();
|
||||
} else {
|
||||
let command = &args[1];
|
||||
let remaining_args = &args[2..];
|
||||
match command.to_lowercase().as_str() {
|
||||
"info" => quest_info(&remaining_args)?,
|
||||
"convert" => quest_convert(&remaining_args)?,
|
||||
_ => {
|
||||
println!("Unrecognized command");
|
||||
display_help();
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
[package]
|
||||
name = "psoutils"
|
||||
version = "0.1.0"
|
||||
authors = ["gered <gered@blarg.ca>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
byteorder = "1.4.3"
|
||||
thiserror = "1.0.24"
|
||||
encoding_rs = "0.8.28"
|
||||
libc = "0.2.94"
|
||||
rand = "0.8.3"
|
||||
itertools = "0.10.0"
|
||||
crc = "1.8.1"
|
||||
|
||||
[dev-dependencies]
|
||||
claim = "0.5.0"
|
||||
pretty-hex = "0.2.1"
|
||||
tempfile = "3.2.0"
|
|
@ -1,9 +0,0 @@
|
|||
# psoutils
|
||||
|
||||
This is a library that includes some useful things when working on with PSO Gamecube related projects:
|
||||
|
||||
* **PRS compression**. This is a conversion of the original Fuzziqer C++ implementation found in [newserv](https://github.com/fuzziqersoftware/newserv) with some minor bug fixes.
|
||||
* **Encryption**. This is also a conversion of the original Fuzziqer C++ implementation found in [newserv](https://github.com/fuzziqersoftware/newserv).
|
||||
* **Quest Files**. Support for reading and writing both .bin/.dat and .qst file formats.
|
||||
* **Text Encoding**. Basic support for the language/text encoding that PSO Gamecube files use.
|
||||
* **Packet Structures**. Fairly incomplete (will be expanded on over time) structures used in PSO's network protocol.
|
|
@ -1,90 +0,0 @@
|
|||
pub trait FixedLengthByteArrays {
|
||||
fn as_unpadded_slice(&self) -> &[u8];
|
||||
fn to_array<const N: usize>(&self) -> [u8; N];
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]> + ?Sized> FixedLengthByteArrays for T {
|
||||
fn as_unpadded_slice(&self) -> &[u8] {
|
||||
let end = self.as_ref().iter().take_while(|&b| *b != 0).count();
|
||||
&self.as_ref()[0..end]
|
||||
}
|
||||
|
||||
fn to_array<const N: usize>(&self) -> [u8; N] {
|
||||
assert_ne!(N, 0);
|
||||
let mut array = [0u8; N];
|
||||
if N <= self.as_ref().len() {
|
||||
array.copy_from_slice(&self.as_ref()[0..N]);
|
||||
} else {
|
||||
array[0..self.as_ref().len()].copy_from_slice(&self.as_ref())
|
||||
}
|
||||
array
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ReadFixedLengthByteArray {
|
||||
fn read_bytes<const N: usize>(&mut self) -> Result<[u8; N], std::io::Error>;
|
||||
}
|
||||
|
||||
impl<T: std::io::Read> ReadFixedLengthByteArray for T {
|
||||
fn read_bytes<const N: usize>(&mut self) -> Result<[u8; N], std::io::Error> {
|
||||
assert_ne!(N, 0);
|
||||
let mut array = [0u8; N];
|
||||
self.read_exact(&mut array)?;
|
||||
Ok(array)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
pub fn fixed_length_byte_arrays() {
|
||||
let bytes: &[u8] = &[
|
||||
0x54, 0x68, 0x65, 0x20, 0x45, 0x61, 0x73, 0x74, 0x20, 0x54, 0x6f, 0x77, 0x65, 0x72,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
];
|
||||
assert_eq!(
|
||||
vec![
|
||||
0x54, 0x68, 0x65, 0x20, 0x45, 0x61, 0x73, 0x74, 0x20, 0x54, 0x6f, 0x77, 0x65, 0x72
|
||||
],
|
||||
bytes.as_unpadded_slice()
|
||||
);
|
||||
|
||||
let bytes: &[u8] = &[
|
||||
0x54, 0x68, 0x65, 0x20, 0x45, 0x61, 0x73, 0x74, 0x20, 0x54, 0x6f, 0x77, 0x65, 0x72,
|
||||
];
|
||||
assert_eq!(
|
||||
vec![
|
||||
0x54, 0x68, 0x65, 0x20, 0x45, 0x61, 0x73, 0x74, 0x20, 0x54, 0x6f, 0x77, 0x65, 0x72
|
||||
],
|
||||
bytes.as_unpadded_slice()
|
||||
);
|
||||
|
||||
let bytes: &[u8] = &[
|
||||
0x54, 0x68, 0x65, 0x20, 0x45, 0x61, 0x73, 0x74, 0x20, 0x54, 0x6f, 0x77, 0x65, 0x72,
|
||||
];
|
||||
assert_eq!(
|
||||
vec![
|
||||
0x54, 0x68, 0x65, 0x20, 0x45, 0x61, 0x73, 0x74, 0x20, 0x54, 0x6f, 0x77, 0x65, 0x72,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
],
|
||||
bytes.to_array::<32>()
|
||||
);
|
||||
|
||||
let bytes: &[u8] = &[
|
||||
0x54, 0x68, 0x65, 0x20, 0x45, 0x61, 0x73, 0x74, 0x20, 0x54, 0x6f, 0x77, 0x65, 0x72,
|
||||
];
|
||||
assert_eq!(
|
||||
vec![
|
||||
0x54, 0x68, 0x65, 0x20, 0x45, 0x61, 0x73, 0x74, 0x20, 0x54, 0x6f, 0x77, 0x65, 0x72,
|
||||
],
|
||||
bytes.to_array::<14>()
|
||||
);
|
||||
|
||||
let bytes: &[u8] = &[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
|
||||
assert_eq!(vec![0x01, 0x02, 0x03, 0x04], bytes.to_array::<4>());
|
||||
}
|
||||
}
|
|
@ -1,728 +0,0 @@
|
|||
/*
|
||||
* The contents of this module are ported from the Fuzziqer "newserv" project with some minor
|
||||
* alterations by me.
|
||||
* https://github.com/fuzziqersoftware/newserv (Compression.cc + Compression.hh)
|
||||
*/
|
||||
|
||||
use std::ffi::c_void;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PrsCompressionError {
|
||||
#[error("Error due to bad input data: {0}")]
|
||||
BadData(String),
|
||||
}
|
||||
|
||||
struct Context {
|
||||
bitpos: u8,
|
||||
forward_log: Vec<u8>,
|
||||
output: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn new() -> Context {
|
||||
// tiny bug from the fuzziqer implementation? it never really initializes the forward log
|
||||
// anywhere (except, in newserv, as a zero-length std::string) and will ALWAYS start doing
|
||||
// some bit twiddling on the first byte before it ever actually explicitly adds the first
|
||||
// byte to the forward log ...
|
||||
let mut forward_log = Vec::new();
|
||||
forward_log.push(0);
|
||||
|
||||
Context {
|
||||
bitpos: 0,
|
||||
forward_log,
|
||||
output: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn put_control_bit_nosave(&mut self, bit: bool) {
|
||||
self.forward_log[0] >>= 1;
|
||||
self.forward_log[0] |= (bit as u8) << 7;
|
||||
self.bitpos += 1;
|
||||
}
|
||||
|
||||
pub fn put_control_save(&mut self) {
|
||||
if self.bitpos >= 8 {
|
||||
self.bitpos = 0;
|
||||
self.output.append(&mut self.forward_log);
|
||||
self.forward_log.resize(1, 0);
|
||||
self.forward_log[0] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn put_static_data(&mut self, data: u8) {
|
||||
self.forward_log.push(data);
|
||||
}
|
||||
|
||||
pub fn put_control_bit(&mut self, bit: bool) {
|
||||
self.put_control_bit_nosave(bit);
|
||||
self.put_control_save();
|
||||
}
|
||||
|
||||
pub fn raw_byte(&mut self, value: u8) {
|
||||
self.put_control_bit_nosave(true);
|
||||
self.put_static_data(value);
|
||||
self.put_control_save();
|
||||
}
|
||||
|
||||
pub fn short_copy(&mut self, offset: isize, size: u8) {
|
||||
let size = size - 2;
|
||||
self.put_control_bit(false);
|
||||
self.put_control_bit(false);
|
||||
self.put_control_bit((size >> 1) & 1 == 1);
|
||||
self.put_control_bit_nosave(size & 1 == 1);
|
||||
self.put_static_data(offset as u8);
|
||||
self.put_control_save();
|
||||
}
|
||||
|
||||
pub fn long_copy(&mut self, offset: isize, size: u8) {
|
||||
if size <= 9 {
|
||||
self.put_control_bit(false);
|
||||
self.put_control_bit_nosave(true);
|
||||
self.put_static_data((((offset << 3) & 0xf8) as u8) | ((size - 2) & 0x07));
|
||||
self.put_static_data((offset >> 5) as u8);
|
||||
self.put_control_save();
|
||||
} else {
|
||||
self.put_control_bit(false);
|
||||
self.put_control_bit_nosave(true);
|
||||
self.put_static_data(((offset << 3) & 0xf8) as u8);
|
||||
self.put_static_data((offset >> 5) as u8);
|
||||
self.put_static_data(size - 1);
|
||||
self.put_control_save();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy(&mut self, offset: isize, size: u8) {
|
||||
if (offset > -0x100) && (size <= 5) {
|
||||
self.short_copy(offset, size);
|
||||
} else {
|
||||
self.long_copy(offset, size);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish(mut self) -> Box<[u8]> {
|
||||
self.put_control_bit(false);
|
||||
self.put_control_bit(true);
|
||||
if self.bitpos != 0 {
|
||||
self.forward_log[0] =
|
||||
(((self.forward_log[0] as u32) << (self.bitpos as u32)) >> 8) as u8;
|
||||
};
|
||||
self.put_static_data(0);
|
||||
self.put_static_data(0);
|
||||
self.output.append(&mut self.forward_log);
|
||||
self.output.into_boxed_slice()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_mem_equal(base: &[u8], offset1: isize, offset2: isize, length: usize) -> bool {
|
||||
// the fuzziqer prs compression implementation performs memcmp's that check memory slightly
|
||||
// outside of the buffers it is working with fairly often actually, despite the checks it
|
||||
// does in the main prs_compress loops ... ugh
|
||||
if offset1 < 0 || offset2 < 0 {
|
||||
false
|
||||
} else {
|
||||
let offset1 = offset1 as usize;
|
||||
let offset2 = offset2 as usize;
|
||||
if ((offset1 + length) > base.len()) || ((offset2 + length) > base.len()) {
|
||||
false
|
||||
} else {
|
||||
// calling memcmp directly here instead of doing slice comparisons is at least twice as
|
||||
// fast in non-release builds right now. since we're doing a bunch of pre-checks anyway
|
||||
// here (else, even the original slice comparisons would occasionally panic), just
|
||||
// calling memcmp in all cases doesn't seem to be too bad an idea?
|
||||
// NOTE: i actually wanted to use the memcmp from compiler_builtins but that seems to
|
||||
// be nightly-only at the moment??
|
||||
|
||||
//base[offset1..(offset1 + length)] == base[offset2..(offset2 + length)]
|
||||
let a = (&base[offset1] as *const u8) as *const c_void;
|
||||
let b = (&base[offset2] as *const u8) as *const c_void;
|
||||
unsafe { libc::memcmp(a, b, length) == 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prs_compress(source: &[u8]) -> Result<Box<[u8]>, PrsCompressionError> {
|
||||
let mut pc = Context::new();
|
||||
|
||||
let mut x: isize = 0;
|
||||
while x < source.len() as isize {
|
||||
let mut lsoffset: isize = 0;
|
||||
let mut lssize: isize = 0;
|
||||
let mut xsize: usize = 0;
|
||||
|
||||
let mut y: isize = x - 3;
|
||||
while (y > 0) && (y > (x - 0x1ff0)) && (xsize < 255) {
|
||||
xsize = 3;
|
||||
if is_mem_equal(source, y, x, xsize) {
|
||||
xsize += 1;
|
||||
while (xsize < 256)
|
||||
&& ((y + xsize as isize) < x)
|
||||
&& ((x + xsize as isize) <= source.len() as isize)
|
||||
&& is_mem_equal(source, y, x, xsize)
|
||||
{
|
||||
xsize += 1;
|
||||
}
|
||||
xsize -= 1;
|
||||
|
||||
if (xsize as isize) > lssize {
|
||||
lsoffset = -(x - y);
|
||||
lssize = xsize as isize;
|
||||
}
|
||||
}
|
||||
y -= 1;
|
||||
}
|
||||
|
||||
if lssize == 0 {
|
||||
pc.raw_byte(match source.get(x as usize) {
|
||||
Some(value) => *value,
|
||||
None => {
|
||||
return Err(PrsCompressionError::BadData(format!(
|
||||
"tried to add raw byte from source at out-of-bounds index {}",
|
||||
x
|
||||
)))
|
||||
}
|
||||
});
|
||||
} else {
|
||||
pc.copy(lsoffset, lssize as u8);
|
||||
x += lssize - 1;
|
||||
}
|
||||
|
||||
x += 1;
|
||||
}
|
||||
|
||||
Ok(pc.finish())
|
||||
}
|
||||
|
||||
enum Next {
|
||||
Byte(u8),
|
||||
Eof(),
|
||||
}
|
||||
|
||||
struct ByteReader<'a> {
|
||||
source: &'a [u8],
|
||||
offset: usize,
|
||||
}
|
||||
|
||||
impl<'a> ByteReader<'a> {
|
||||
pub fn new(source: &[u8]) -> ByteReader {
|
||||
ByteReader { source, offset: 0 }
|
||||
}
|
||||
|
||||
pub fn next(&mut self) -> Next {
|
||||
if self.offset < self.source.len() {
|
||||
let result = Next::Byte(self.source[self.offset]);
|
||||
self.offset += 1;
|
||||
result
|
||||
} else {
|
||||
Next::Eof()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prs_decompress(source: &[u8]) -> Result<Box<[u8]>, PrsCompressionError> {
|
||||
let mut output = Vec::new();
|
||||
let mut reader = ByteReader::new(source);
|
||||
let mut r3: i32;
|
||||
let mut r5: i32;
|
||||
let mut bitpos = 9;
|
||||
let mut current_byte: u8;
|
||||
let mut flag: bool;
|
||||
let mut offset: i32;
|
||||
|
||||
// if you prs_compress a zero-length buffer, you get a 3-byte "compressed" result.
|
||||
// therefore, 3 byte minimum input buffer is required to get any kind of "meaningful"
|
||||
// decompression result back out
|
||||
if source.len() < 3 {
|
||||
return Err(PrsCompressionError::BadData(format!(
|
||||
"Input data is too short: {} bytes",
|
||||
source.len()
|
||||
)));
|
||||
}
|
||||
|
||||
current_byte = match reader.next() {
|
||||
Next::Byte(byte) => byte,
|
||||
Next::Eof() => return Ok(output.into_boxed_slice()),
|
||||
};
|
||||
|
||||
loop {
|
||||
bitpos -= 1;
|
||||
if bitpos == 0 {
|
||||
current_byte = match reader.next() {
|
||||
Next::Byte(byte) => byte,
|
||||
Next::Eof() => return Ok(output.into_boxed_slice()),
|
||||
};
|
||||
bitpos = 8;
|
||||
}
|
||||
|
||||
flag = (current_byte & 1) == 1;
|
||||
current_byte >>= 1;
|
||||
if flag {
|
||||
output.push(match reader.next() {
|
||||
Next::Byte(byte) => byte,
|
||||
Next::Eof() => return Ok(output.into_boxed_slice()),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
bitpos -= 1;
|
||||
if bitpos == 0 {
|
||||
current_byte = match reader.next() {
|
||||
Next::Byte(byte) => byte,
|
||||
Next::Eof() => return Ok(output.into_boxed_slice()),
|
||||
};
|
||||
bitpos = 8;
|
||||
}
|
||||
|
||||
flag = (current_byte & 1) == 1;
|
||||
current_byte >>= 1;
|
||||
if flag {
|
||||
r3 = match reader.next() {
|
||||
Next::Byte(byte) => byte as i32,
|
||||
Next::Eof() => return Ok(output.into_boxed_slice()),
|
||||
};
|
||||
let high_byte = match reader.next() {
|
||||
Next::Byte(byte) => byte as i32,
|
||||
Next::Eof() => return Ok(output.into_boxed_slice()),
|
||||
};
|
||||
offset = ((high_byte & 0xff) << 8) | (r3 & 0xff);
|
||||
if offset == 0 {
|
||||
return Ok(output.into_boxed_slice());
|
||||
}
|
||||
r3 &= 0x00000007;
|
||||
r5 = (offset >> 3) | -8192i32; // 0xffffe000
|
||||
if r3 == 0 {
|
||||
r3 = match reader.next() {
|
||||
Next::Byte(byte) => byte as i32,
|
||||
Next::Eof() => return Ok(output.into_boxed_slice()),
|
||||
};
|
||||
r3 = (r3 & 0xff) + 1;
|
||||
} else {
|
||||
r3 += 2;
|
||||
}
|
||||
} else {
|
||||
r3 = 0;
|
||||
for _ in 0..2 {
|
||||
bitpos -= 1;
|
||||
if bitpos == 0 {
|
||||
current_byte = match reader.next() {
|
||||
Next::Byte(byte) => byte,
|
||||
Next::Eof() => return Ok(output.into_boxed_slice()),
|
||||
};
|
||||
bitpos = 8;
|
||||
}
|
||||
flag = (current_byte & 1) == 1;
|
||||
current_byte >>= 1;
|
||||
offset = r3 << 1;
|
||||
r3 = offset | (flag as i32);
|
||||
}
|
||||
offset = match reader.next() {
|
||||
Next::Byte(byte) => byte as i32,
|
||||
Next::Eof() => return Ok(output.into_boxed_slice()),
|
||||
};
|
||||
r3 += 2;
|
||||
r5 = offset | -256i32; // 0xffffff00
|
||||
}
|
||||
if r3 == 0 {
|
||||
continue;
|
||||
}
|
||||
for _ in 0..r3 {
|
||||
let index = output.len() as i32 + r5;
|
||||
output.push(match output.get(index as usize) {
|
||||
Some(value) => *value,
|
||||
None => {
|
||||
return Err(PrsCompressionError::BadData(format!(
|
||||
"tried to push copy of byte at out-of-bounds index {}",
|
||||
index
|
||||
)))
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use claim::*;
|
||||
use rand::rngs::StdRng;
|
||||
use rand::{Fill, SeedableRng};
|
||||
|
||||
use super::*;
|
||||
|
||||
struct TestData<'a> {
|
||||
uncompressed: &'a [u8],
|
||||
compressed: &'a [u8],
|
||||
}
|
||||
|
||||
static TEST_DATA: &[TestData] = &[
|
||||
TestData {
|
||||
uncompressed: "Hello, world!\0".as_bytes(),
|
||||
compressed: &[
|
||||
0xff, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x77, 0xbf, 0x6f, 0x72, 0x6c, 0x64,
|
||||
0x21, 0x00, 0x00, 0x00, 0x00,
|
||||
],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: "I am Sam
|
||||
|
||||
Sam I am
|
||||
|
||||
That Sam-I-am!
|
||||
That Sam-I-am!
|
||||
I do not like
|
||||
that Sam-I-am!
|
||||
|
||||
Do you like green eggs and ham?
|
||||
|
||||
I do not like them, Sam-I-am.
|
||||
I do not like green eggs and ham."
|
||||
.as_bytes(),
|
||||
compressed: &[
|
||||
0xff, 0x49, 0x20, 0x61, 0x6d, 0x20, 0x53, 0x61, 0x6d, 0xe3, 0x0a, 0x0a, 0xfb, 0x20,
|
||||
0x49, 0xf8, 0xf2, 0x0a, 0x0a, 0x54, 0x68, 0xd3, 0x61, 0x74, 0xec, 0x2d, 0x49, 0xef,
|
||||
0x2d, 0x61, 0x6d, 0x21, 0x88, 0xff, 0x0d, 0x21, 0x0a, 0xff, 0x49, 0x20, 0x64, 0x6f,
|
||||
0x20, 0x6e, 0x6f, 0x74, 0x7f, 0x20, 0x6c, 0x69, 0x6b, 0x65, 0x0a, 0x74, 0xff, 0x18,
|
||||
0xff, 0x0d, 0x0a, 0x44, 0x6f, 0x20, 0x79, 0x6f, 0x75, 0xfc, 0xe4, 0x20, 0x67, 0x72,
|
||||
0x65, 0xff, 0x65, 0x6e, 0x20, 0x65, 0x67, 0x67, 0x73, 0x20, 0xff, 0x61, 0x6e, 0x64,
|
||||
0x20, 0x68, 0x61, 0x6d, 0x3f, 0xfd, 0x0a, 0x08, 0xfe, 0x0d, 0x20, 0x74, 0x68, 0x65,
|
||||
0x6d, 0xad, 0x2c, 0x07, 0xfe, 0x2e, 0x10, 0xff, 0x0e, 0xf8, 0xfd, 0x11, 0x05, 0x2e,
|
||||
0x00, 0x00,
|
||||
],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: &[],
|
||||
compressed: &[0x02, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: b"a",
|
||||
compressed: &[0x05, 0x61, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: b"aa",
|
||||
compressed: &[0x0b, 0x61, 0x61, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: b"aaa",
|
||||
compressed: &[0x17, 0x61, 0x61, 0x61, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: b"aaaa",
|
||||
compressed: &[0x2f, 0x61, 0x61, 0x61, 0x61, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: b"aaaaa",
|
||||
compressed: &[0x5f, 0x61, 0x61, 0x61, 0x61, 0x61, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: b"aaaaaa",
|
||||
compressed: &[0xbf, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x00, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: b"aaaaaaa",
|
||||
compressed: &[0x8f, 0x61, 0x61, 0x61, 0x61, 0xfd, 0x02, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: b"aaaaaaaa",
|
||||
compressed: &[0x8f, 0x61, 0x61, 0x61, 0x61, 0xfd, 0x05, 0x61, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: b"aaaaaaaaa",
|
||||
compressed: &[
|
||||
0x8f, 0x61, 0x61, 0x61, 0x61, 0xfd, 0x0b, 0x61, 0x61, 0x00, 0x00,
|
||||
],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: b"aaaaaaaaaa",
|
||||
compressed: &[0x8f, 0x61, 0x61, 0x61, 0x61, 0xfd, 0x28, 0xfd, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: b"aaaaaaaaaaa",
|
||||
compressed: &[0x8f, 0x61, 0x61, 0x61, 0x61, 0xfd, 0x24, 0xfb, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: b"aaaaaaaaaaaa",
|
||||
compressed: &[0x8f, 0x61, 0x61, 0x61, 0x61, 0xfd, 0x2c, 0xfa, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: b"aaaaaaaaaaaaa",
|
||||
compressed: &[
|
||||
0x8f, 0x61, 0x61, 0x61, 0x61, 0xfd, 0x5c, 0xfa, 0x61, 0x00, 0x00,
|
||||
],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: b"aaaaaaaaaaaaaa",
|
||||
compressed: &[
|
||||
0x8f, 0x61, 0x61, 0x61, 0x61, 0xfd, 0xbc, 0xfa, 0x61, 0x61, 0x00, 0x00, 0x00,
|
||||
],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: b"aaaaaaaaaaaaaaa",
|
||||
compressed: &[
|
||||
0x8f, 0x61, 0x61, 0x61, 0x61, 0xfd, 0x8c, 0xfa, 0xfd, 0x02, 0x00, 0x00,
|
||||
],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: b"aaaaaaaaaaaaaaaa",
|
||||
compressed: &[
|
||||
0x8f, 0x61, 0x61, 0x61, 0x61, 0xfd, 0x4c, 0xfa, 0xfb, 0x02, 0x00, 0x00,
|
||||
],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: b"aaaaaaaaaaaaaaaaa",
|
||||
compressed: &[
|
||||
0x8f, 0x61, 0x61, 0x61, 0x61, 0xfd, 0xcc, 0xfa, 0xfa, 0x02, 0x00, 0x00,
|
||||
],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: &[0xff],
|
||||
compressed: &[0x05, 0xff, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: &[0xff, 0xff],
|
||||
compressed: &[0x0b, 0xff, 0xff, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: &[0xff, 0xff, 0xff],
|
||||
compressed: &[0x17, 0xff, 0xff, 0xff, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: &[0xff, 0xff, 0xff, 0xff],
|
||||
compressed: &[0x2f, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: &[0xff, 0xff, 0xff, 0xff, 0xff],
|
||||
compressed: &[0x5f, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: &[0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
|
||||
compressed: &[0xbf, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: &[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
|
||||
compressed: &[0x8f, 0xff, 0xff, 0xff, 0xff, 0xfd, 0x02, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: &[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff],
|
||||
compressed: &[0x8f, 0xff, 0xff, 0xff, 0xff, 0xfd, 0x05, 0xff, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: &[0x00],
|
||||
compressed: &[0x05, 0x00, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: &[0x00, 0x00],
|
||||
compressed: &[0x0b, 0x00, 0x00, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: &[0x00, 0x00, 0x00],
|
||||
compressed: &[0x17, 0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: &[0x00, 0x00, 0x00, 0x00],
|
||||
compressed: &[0x2f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: &[0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
compressed: &[0x5f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
},
|
||||
// when comparing these results to the fuzziqer implementation, these zero-byte run tests
|
||||
// start to get a little interesting from this point onward. at this point, the fuzziqer
|
||||
// implementation will sometimes start to perform memcmp() calls that check memory slightly
|
||||
// out of the bounds of its buffers, and when that memory also happens to contain zeros (as
|
||||
// seems to always be the case for me right now), you get things like the 6 and 7 length
|
||||
// zero-byte runs compressing identically (which is obviously a bug). this buggy behaviour
|
||||
// carries on for some larger run sizes too, probably indefinitely.
|
||||
TestData {
|
||||
uncompressed: &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
compressed: &[0xbf, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
compressed: &[0x8f, 0x00, 0x00, 0x00, 0x00, 0xfd, 0x02, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
compressed: &[0x8f, 0x00, 0x00, 0x00, 0x00, 0xfd, 0x05, 0x00, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
compressed: &[
|
||||
0x8f, 0x00, 0x00, 0x00, 0x00, 0xfd, 0x0b, 0x00, 0x00, 0x00, 0x00,
|
||||
],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: &[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],
|
||||
compressed: &[0x8f, 0x00, 0x00, 0x00, 0x00, 0xfd, 0x28, 0xfd, 0x00, 0x00],
|
||||
},
|
||||
TestData {
|
||||
uncompressed: &[
|
||||
0x04, 0x00, 0x02, 0x01, 0x05, 0x04, 0x08, 0x00, 0x04, 0x02, 0x07, 0x0d, 0x0c, 0x11,
|
||||
0x02, 0x00, 0x03, 0x04, 0x04, 0x04, 0x03, 0x02, 0x09, 0x02, 0x03, 0x01, 0x0b, 0x0a,
|
||||
0x0d, 0x0e, 0x04, 0x03, 0x03, 0x04, 0x02, 0x00, 0x07, 0x00, 0x08, 0x00, 0x03, 0x03,
|
||||
0x0b, 0x0a, 0x0b, 0x10, 0x03, 0x03, 0x04, 0x02, 0x06, 0x04, 0x07, 0x03, 0x07, 0x04,
|
||||
0x01, 0x03, 0x0a, 0x0c, 0x0c, 0x0f, 0x02, 0x04, 0x01, 0x04, 0x04, 0x02, 0x07, 0x02,
|
||||
0x09, 0x04, 0x02, 0x03, 0x09, 0x0b, 0x0f, 0x0d, 0x02, 0x03, 0x04, 0x01, 0x02, 0x03,
|
||||
0x04, 0x02, 0x05, 0x02, 0x00, 0x00, 0x0a, 0x0b, 0x0d, 0x0f, 0x03, 0x00, 0x01, 0x02,
|
||||
0x02, 0x02, 0x07, 0x04, 0x09, 0x02, 0x00, 0x03, 0x08, 0x0a, 0x0c, 0x11, 0x04, 0x00,
|
||||
0x00, 0x04, 0x03, 0x04, 0x06, 0x01, 0x06, 0x01, 0x03, 0x01, 0x07, 0x09, 0x0e, 0x10,
|
||||
0x02, 0x01, 0x03, 0x04, 0x03, 0x02, 0x04, 0x00, 0x06, 0x01, 0x00, 0x03, 0x09, 0x0a,
|
||||
0x0d, 0x10, 0x02, 0x04, 0x03, 0x03, 0x05, 0x03, 0x04, 0x02, 0x09, 0x04, 0x03, 0x04,
|
||||
0x08, 0x0b, 0x0b, 0x0d, 0x00, 0x03, 0x00, 0x01, 0x04, 0x01, 0x06, 0x04, 0x09, 0x04,
|
||||
0x04, 0x03, 0x07, 0x0a, 0x0c, 0x0f, 0x02, 0x01, 0x02, 0x03, 0x02, 0x03, 0x05, 0x01,
|
||||
0x09, 0x00, 0x01, 0x02, 0x0b, 0x0c, 0x0e, 0x0d, 0x03, 0x00, 0x03, 0x00, 0x03, 0x02,
|
||||
0x04, 0x02, 0x06, 0x00, 0x00, 0x01, 0x0a, 0x0c, 0x0c, 0x0e, 0x02, 0x03, 0x01, 0x02,
|
||||
0x06, 0x03, 0x03, 0x00, 0x05, 0x03, 0x03, 0x02, 0x08, 0x0c, 0x0f, 0x0e, 0x03, 0x02,
|
||||
0x02, 0x01, 0x06, 0x03, 0x03, 0x02, 0x06, 0x02, 0x04, 0x04, 0x07, 0x0b, 0x0b, 0x0f,
|
||||
0x00, 0x01, 0x01, 0x01, 0x06, 0x04, 0x05, 0x02, 0x07, 0x02, 0x04, 0x04, 0x09, 0x0c,
|
||||
0x0d, 0x0d, 0x00, 0x04, 0x03, 0x02, 0x02, 0x00, 0x07, 0x01, 0x07, 0x00, 0x00, 0x04,
|
||||
0x09, 0x0c, 0x0f, 0x10, 0x04, 0x00, 0x01, 0x01, 0x06, 0x03, 0x03, 0x04, 0x07, 0x04,
|
||||
0x03, 0x04, 0x09, 0x09, 0x0c, 0x11, 0x02, 0x01, 0x03, 0x04, 0x03, 0x03, 0x03, 0x03,
|
||||
0x08, 0x02, 0x03, 0x01, 0x07, 0x0b, 0x0c, 0x0f, 0x04, 0x04, 0x00, 0x01, 0x02, 0x00,
|
||||
0x03, 0x02, 0x09, 0x00, 0x04, 0x03, 0x09, 0x09, 0x0f, 0x0e, 0x02, 0x03, 0x00, 0x00,
|
||||
0x03, 0x02, 0x04, 0x01, 0x05, 0x01, 0x04, 0x02, 0x07, 0x0b, 0x0f, 0x11, 0x02, 0x04,
|
||||
0x02, 0x02, 0x03, 0x04, 0x07, 0x00, 0x09, 0x03, 0x00, 0x04, 0x08, 0x09, 0x0b, 0x0d,
|
||||
0x03, 0x01, 0x00, 0x01, 0x02, 0x01, 0x05, 0x00, 0x07, 0x04, 0x03, 0x02, 0x08, 0x0d,
|
||||
0x0f, 0x10, 0x01, 0x03, 0x00, 0x02, 0x05, 0x02, 0x03, 0x02, 0x07, 0x00, 0x03, 0x03,
|
||||
0x09, 0x0d, 0x0b, 0x0f, 0x02, 0x01, 0x03, 0x02, 0x06, 0x03, 0x03, 0x04, 0x07, 0x00,
|
||||
0x03, 0x03, 0x0b, 0x0b, 0x0f, 0x0f, 0x03, 0x01, 0x00, 0x01, 0x05, 0x02, 0x03, 0x03,
|
||||
0x07, 0x04, 0x03, 0x02, 0x0a, 0x0d, 0x0f, 0x0d, 0x02, 0x00, 0x04, 0x01, 0x05, 0x04,
|
||||
0x05, 0x02, 0x06, 0x01, 0x00, 0x03, 0x07, 0x0a, 0x0b, 0x10, 0x03, 0x02, 0x04, 0x03,
|
||||
0x06, 0x00, 0x04, 0x04, 0x06, 0x00, 0x01, 0x04, 0x08, 0x09, 0x0c, 0x10, 0x00, 0x02,
|
||||
0x01, 0x00, 0x04, 0x04, 0x05, 0x00, 0x07, 0x00, 0x03, 0x02, 0x08, 0x0d, 0x0e, 0x0e,
|
||||
0x01, 0x04, 0x00, 0x01, 0x03, 0x01, 0x05, 0x02, 0x08, 0x03, 0x01, 0x04, 0x07, 0x0d,
|
||||
0x0f, 0x10, 0x02, 0x01, 0x00, 0x01, 0x04, 0x03, 0x04, 0x04, 0x05, 0x00, 0x03, 0x01,
|
||||
0x0b, 0x0c, 0x0b, 0x0f, 0x03, 0x00, 0x00, 0x04, 0x05, 0x02, 0x05, 0x02, 0x05, 0x00,
|
||||
0x03, 0x03, 0x09, 0x09, 0x0e, 0x11, 0x03, 0x03, 0x00, 0x00, 0x03, 0x01, 0x04, 0x01,
|
||||
0x08, 0x01, 0x00, 0x02, 0x07, 0x09, 0x0d, 0x10, 0x00, 0x02, 0x04, 0x00, 0x02, 0x01,
|
||||
0x05, 0x02, 0x09, 0x03, 0x00, 0x01, 0x0a, 0x0c, 0x0d, 0x0e, 0x02, 0x02, 0x03, 0x00,
|
||||
0x02, 0x04, 0x05, 0x01, 0x07, 0x04, 0x03, 0x02, 0x08, 0x09, 0x0b, 0x10, 0x03, 0x00,
|
||||
0x03, 0x00, 0x05, 0x03, 0x05, 0x04, 0x06, 0x03, 0x02, 0x01, 0x0a, 0x0d, 0x0f, 0x0d,
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x02, 0x03, 0x02, 0x06, 0x00, 0x00, 0x02, 0x0a, 0x0b,
|
||||
0x0b, 0x10, 0x04, 0x00, 0x03, 0x03, 0x05, 0x02, 0x07, 0x01, 0x05, 0x02, 0x04, 0x01,
|
||||
0x08, 0x0c, 0x0e, 0x0d, 0x02, 0x01, 0x01, 0x02, 0x05, 0x01, 0x03, 0x01, 0x08, 0x00,
|
||||
0x00, 0x03, 0x0b, 0x0b, 0x0c, 0x11, 0x03, 0x01, 0x02, 0x01, 0x06, 0x01, 0x03, 0x01,
|
||||
0x05, 0x04, 0x02, 0x02, 0x0a, 0x0c, 0x0d, 0x0f, 0x04, 0x03, 0x02, 0x00, 0x03, 0x02,
|
||||
0x04, 0x01, 0x09, 0x02, 0x00, 0x03, 0x0b, 0x0c, 0x0d, 0x0f, 0x02, 0x01, 0x01, 0x03,
|
||||
0x02, 0x01, 0x07, 0x00, 0x07, 0x04, 0x02, 0x02, 0x09, 0x0a, 0x0b, 0x10, 0x01, 0x02,
|
||||
0x03, 0x02, 0x03, 0x00, 0x07, 0x02, 0x09, 0x01, 0x00, 0x00, 0x0b, 0x09, 0x0e, 0x0e,
|
||||
0x01, 0x01, 0x04, 0x03, 0x06, 0x01, 0x07, 0x01, 0x07, 0x03, 0x04, 0x01, 0x09, 0x0a,
|
||||
0x0f, 0x10, 0x03, 0x03, 0x01, 0x01, 0x02, 0x02, 0x06, 0x01, 0x08, 0x00, 0x01, 0x04,
|
||||
0x07, 0x0a, 0x0e, 0x11, 0x02, 0x04, 0x02, 0x01, 0x02, 0x03, 0x03, 0x02, 0x07, 0x04,
|
||||
0x03, 0x01, 0x07, 0x09, 0x0f, 0x0d, 0x03, 0x02, 0x01, 0x00, 0x06, 0x01, 0x04, 0x04,
|
||||
0x06, 0x02, 0x01, 0x04, 0x08, 0x0d, 0x0e, 0x10, 0x03, 0x00, 0x02, 0x01, 0x03, 0x02,
|
||||
0x03, 0x00, 0x08, 0x04, 0x01, 0x03, 0x08, 0x09, 0x0b, 0x11, 0x03, 0x03, 0x01, 0x04,
|
||||
0x06, 0x04, 0x04, 0x02, 0x08, 0x04, 0x01, 0x04, 0x0a, 0x0d, 0x0e, 0x10, 0x02, 0x02,
|
||||
0x01, 0x03, 0x06, 0x02, 0x03, 0x04, 0x08, 0x01, 0x02, 0x04, 0x08, 0x0b, 0x0e, 0x0e,
|
||||
0x00, 0x01, 0x03, 0x01, 0x02, 0x04, 0x06, 0x00, 0x05, 0x00, 0x00, 0x00, 0x08, 0x09,
|
||||
0x0e, 0x10, 0x02, 0x00, 0x03, 0x03, 0x06, 0x03, 0x07, 0x02, 0x09, 0x01, 0x01, 0x03,
|
||||
0x07, 0x0a, 0x0f, 0x0f, 0x02, 0x04, 0x03, 0x04, 0x05, 0x03, 0x07, 0x00, 0x08, 0x01,
|
||||
0x00, 0x00, 0x0a, 0x0d, 0x0b, 0x0f, 0x01, 0x04, 0x00, 0x00, 0x06, 0x04, 0x07, 0x01,
|
||||
0x07, 0x00, 0x04, 0x04, 0x08, 0x09, 0x0c, 0x0d, 0x00, 0x01, 0x04, 0x00, 0x02, 0x00,
|
||||
0x04, 0x00, 0x09, 0x01, 0x02, 0x02, 0x09, 0x0c, 0x0b, 0x0d, 0x04, 0x02, 0x02, 0x03,
|
||||
0x06, 0x01, 0x07, 0x01, 0x06, 0x00, 0x01, 0x04, 0x08, 0x0d, 0x0f, 0x10, 0x03, 0x00,
|
||||
0x03, 0x03, 0x03, 0x01, 0x03, 0x00, 0x05, 0x03, 0x02, 0x02, 0x0a, 0x0b, 0x0b, 0x0f,
|
||||
0x00, 0x02, 0x02, 0x04, 0x03, 0x04, 0x05, 0x02, 0x09, 0x00, 0x04, 0x02, 0x09, 0x0c,
|
||||
0x0b, 0x0d, 0x04, 0x01, 0x00, 0x02, 0x04, 0x00, 0x05, 0x02, 0x05, 0x01, 0x02, 0x03,
|
||||
0x08, 0x0b, 0x0d, 0x10, 0x01, 0x00, 0x04, 0x02, 0x03, 0x01, 0x05, 0x02, 0x09, 0x01,
|
||||
0x00, 0x01, 0x08, 0x0b, 0x0c, 0x0d, 0x03, 0x03, 0x02, 0x03, 0x05, 0x01, 0x05, 0x04,
|
||||
0x05, 0x04, 0x04, 0x01, 0x0a, 0x0b, 0x0f, 0x0d, 0x04, 0x03, 0x02, 0x00, 0x03, 0x01,
|
||||
0x05, 0x02, 0x07, 0x04, 0x03, 0x04, 0x09, 0x0a, 0x0c, 0x0f, 0x04, 0x01, 0x00, 0x00,
|
||||
0x04, 0x03, 0x04, 0x04, 0x09, 0x00, 0x00, 0x03, 0x0b, 0x0a, 0x0b, 0x10, 0x01, 0x04,
|
||||
0x00, 0x00, 0x03, 0x03, 0x05, 0x00, 0x09, 0x01, 0x01, 0x01, 0x0b, 0x0c, 0x0f, 0x11,
|
||||
0x01, 0x04,
|
||||
],
|
||||
compressed: &[
|
||||
0xff, 0x04, 0x00, 0x02, 0x01, 0x05, 0x04, 0x08, 0x00, 0xff, 0x04, 0x02, 0x07, 0x0d,
|
||||
0x0c, 0x11, 0x02, 0x00, 0xff, 0x03, 0x04, 0x04, 0x04, 0x03, 0x02, 0x09, 0x02, 0xff,
|
||||
0x03, 0x01, 0x0b, 0x0a, 0x0d, 0x0e, 0x04, 0x03, 0xff, 0x03, 0x04, 0x02, 0x00, 0x07,
|
||||
0x00, 0x08, 0x00, 0x3f, 0x03, 0x03, 0x0b, 0x0a, 0x0b, 0x10, 0xfd, 0xf1, 0x06, 0x04,
|
||||
0x07, 0x03, 0x07, 0x04, 0xff, 0x01, 0x03, 0x0a, 0x0c, 0x0c, 0x0f, 0x02, 0x04, 0xe3,
|
||||
0x01, 0x04, 0xc6, 0x02, 0x09, 0xff, 0x04, 0x02, 0x03, 0x09, 0x0b, 0x0f, 0x0d, 0x02,
|
||||
0xc7, 0x03, 0x04, 0x01, 0xfc, 0x02, 0xff, 0x05, 0x02, 0x00, 0x00, 0x0a, 0x0b, 0x0d,
|
||||
0x0f, 0xff, 0x03, 0x00, 0x01, 0x02, 0x02, 0x02, 0x07, 0x04, 0xf1, 0x09, 0xa7, 0x08,
|
||||
0x0a, 0x0c, 0xff, 0x11, 0x04, 0x00, 0x00, 0x04, 0x03, 0x04, 0x06, 0xff, 0x01, 0x06,
|
||||
0x01, 0x03, 0x01, 0x07, 0x09, 0x0e, 0x8f, 0x10, 0x02, 0x01, 0x03, 0x92, 0xff, 0x04,
|
||||
0x00, 0x06, 0x01, 0x00, 0x03, 0x09, 0x0a, 0xc7, 0x0d, 0x10, 0x02, 0x8f, 0x05, 0x18,
|
||||
0xc0, 0x09, 0x3f, 0xda, 0x08, 0x0b, 0x0b, 0x0d, 0x00, 0x7e, 0xbf, 0x04, 0x01, 0x06,
|
||||
0x04, 0x09, 0x1c, 0x6b, 0x07, 0x0a, 0xf1, 0x90, 0xa2, 0x02, 0x03, 0x05, 0xe3, 0x01,
|
||||
0x09, 0xa8, 0x0b, 0x0c, 0x47, 0x0e, 0x0d, 0x03, 0xdf, 0xfc, 0xc0, 0x02, 0x06, 0x00,
|
||||
0x00, 0x01, 0x18, 0x70, 0x0e, 0xff, 0x49, 0x02, 0x06, 0x03, 0x03, 0x00, 0x05, 0x03,
|
||||
0xff, 0x03, 0x02, 0x08, 0x0c, 0x0f, 0x0e, 0x03, 0x02, 0xe3, 0x02, 0x01, 0xf0, 0x02,
|
||||
0x06, 0xff, 0x02, 0x04, 0x04, 0x07, 0x0b, 0x0b, 0x0f, 0x00, 0x63, 0x01, 0x01, 0xb2,
|
||||
0x05, 0xfc, 0x4e, 0x04, 0x04, 0x09, 0x0c, 0x0d, 0x31, 0x0d, 0x72, 0x02, 0x8e, 0x20,
|
||||
0x01, 0x07, 0x68, 0x1f, 0x09, 0x0c, 0x0f, 0x10, 0x04, 0x71, 0xdf, 0xd0, 0x04, 0x07,
|
||||
0x5c, 0x80, 0x09, 0x09, 0x81, 0xf7, 0x7a, 0x60, 0x03, 0x03, 0x03, 0x08, 0xfc, 0xa7,
|
||||
0x07, 0x0b, 0x0c, 0x0f, 0x04, 0x88, 0xdf, 0x35, 0xe3, 0x02, 0x09, 0xc7, 0x09, 0x09,
|
||||
0x31, 0x0f, 0x90, 0x00, 0xdd, 0x80, 0x01, 0x05, 0x01, 0xd1, 0xf7, 0x0b, 0x23, 0x0f,
|
||||
0x11, 0x75, 0xfe, 0x01, 0x07, 0x00, 0x09, 0x03, 0x00, 0x04, 0x3f, 0x08, 0x09, 0x0b,
|
||||
0x0d, 0x03, 0x01, 0x1e, 0xd0, 0x01, 0x05, 0x00, 0xff, 0xb0, 0x02, 0x08, 0x0d, 0x0f,
|
||||
0x10, 0x01, 0x03, 0xbd, 0x00, 0x21, 0xf7, 0x03, 0x02, 0x07, 0x81, 0xf5, 0x47, 0x09,
|
||||
0x0d, 0x0b, 0x30, 0x24, 0x64, 0x90, 0x4f, 0x02, 0xf5, 0x0b, 0x0f, 0x0f, 0xd0, 0xe8,
|
||||
0xe0, 0x01, 0xf5, 0x03, 0x02, 0x1b, 0x0a, 0x0d, 0x81, 0xf5, 0x00, 0xd1, 0xa4, 0x50,
|
||||
0x02, 0xf7, 0x07, 0xfa, 0x02, 0xf4, 0xf9, 0xf6, 0x06, 0x00, 0x04, 0x04, 0x7d, 0x06,
|
||||
0x49, 0xf7, 0x08, 0x09, 0x0c, 0x10, 0x11, 0x19, 0xf2, 0xf2, 0xf1, 0xa0, 0x7a, 0x08,
|
||||
0x0d, 0x0e, 0x63, 0x0e, 0x01, 0x60, 0x03, 0xfc, 0xbe, 0x08, 0x03, 0x01, 0x04, 0x07,
|
||||
0x88, 0x90, 0xe1, 0x11, 0x01, 0x35, 0xdd, 0xde, 0x81, 0xf1, 0x0c, 0x0b, 0x81, 0xf3,
|
||||
0x00, 0x38, 0xb2, 0x05, 0x02, 0xe2, 0xf0, 0x40, 0x0e, 0x11, 0xe2, 0xa9, 0xf6, 0xe6,
|
||||
0x04, 0x01, 0x7f, 0x08, 0x01, 0x00, 0x02, 0x07, 0x09, 0x0d, 0x6c, 0xb0, 0x04, 0x82,
|
||||
0xef, 0x02, 0x74, 0x40, 0x81, 0xf5, 0x0d, 0x0e, 0xc4, 0x32, 0xed, 0x05, 0x39, 0x01,
|
||||
0x40, 0x09, 0xfa, 0x80, 0x81, 0xf4, 0x05, 0x03, 0x05, 0x04, 0x4f, 0x06, 0x03, 0x02,
|
||||
0x01, 0x60, 0x92, 0x9a, 0xf0, 0x30, 0x01, 0xf4, 0xaf, 0x02, 0x0a, 0x0b, 0x0b, 0x01,
|
||||
0xf6, 0x01, 0xf2, 0x23, 0x02, 0x07, 0xbe, 0xfa, 0xac, 0x01, 0xf3, 0x02, 0x01, 0x01,
|
||||
0x02, 0x31, 0x05, 0x6e, 0x08, 0xbe, 0x99, 0x0b, 0x0b, 0x0c, 0x11, 0x09, 0xf3, 0x16,
|
||||
0x23, 0xf0, 0x05, 0xc1, 0xf6, 0xa3, 0xa0, 0x0f, 0xa9, 0x03, 0xf6, 0x46, 0x02, 0xef,
|
||||
0x0b, 0xf0, 0xc4, 0xd0, 0xa6, 0x07, 0x1e, 0x81, 0xf6, 0x02, 0x02, 0x09, 0xed, 0x10,
|
||||
0x8b, 0xf0, 0x00, 0x01, 0xed, 0x01, 0x00, 0x47, 0x00, 0x0b, 0x09, 0x20, 0x5c, 0x32,
|
||||
0x06, 0x01, 0x81, 0xf2, 0x2f, 0xc1, 0xec, 0x09, 0x0a, 0x0f, 0x81, 0xeb, 0x1a, 0x9f,
|
||||
0x11, 0xf7, 0x08, 0x2f, 0x19, 0x07, 0x0a, 0x0e, 0x02, 0xf4, 0xda, 0xcd, 0x01, 0xf5,
|
||||
0x04, 0x02, 0xed, 0x0f, 0x51, 0x0d, 0xb3, 0x21, 0xed, 0x3b, 0x81, 0xf6, 0x02, 0x81,
|
||||
0xf6, 0x0d, 0x0e, 0x7a, 0x40, 0x72, 0xf4, 0x03, 0x00, 0x08, 0xd1, 0x01, 0xea, 0x30,
|
||||
0x01, 0xf8, 0x01, 0x8b, 0x04, 0x06, 0xf1, 0xe9, 0xf0, 0xed, 0x04, 0x89, 0xe8, 0x10,
|
||||
0x89, 0xee, 0x03, 0x06, 0x78, 0x2c, 0x08, 0x01, 0x02, 0xb7, 0x01, 0xec, 0x0e, 0x0e,
|
||||
0x92, 0xf5, 0x02, 0x91, 0xf4, 0x5f, 0x05, 0x00, 0x00, 0x00, 0x08, 0x82, 0xea, 0x9c,
|
||||
0x20, 0x06, 0x03, 0x5a, 0x70, 0x01, 0x81, 0xf3, 0x0f, 0x01, 0xe8, 0x8b, 0x01, 0xf8,
|
||||
0x03, 0x01, 0xe7, 0x60, 0xb5, 0x0a, 0x81, 0xf1, 0x01, 0xf4, 0x00, 0x01, 0xe7, 0xd8,
|
||||
0x3e, 0x04, 0x02, 0xf3, 0x0d, 0xd8, 0x69, 0x00, 0xe1, 0xf1, 0x00, 0xf1, 0x09, 0x5a,
|
||||
0x09, 0x0c, 0x0b, 0x25, 0x0d, 0x0a, 0xef, 0x40, 0x15, 0x03, 0xf2, 0x01, 0xf3, 0x81,
|
||||
0xf5, 0xd5, 0x7d, 0x62, 0xf5, 0x81, 0xf7, 0x02, 0xeb, 0x02, 0xac, 0xae, 0x02, 0xed,
|
||||
0x84, 0xfe, 0x2e, 0x41, 0xf3, 0x04, 0x00, 0x81, 0xf2, 0xbe, 0x2a, 0x08, 0x0b, 0x0d,
|
||||
0x10, 0x91, 0xf0, 0xa2, 0xb1, 0xeb, 0xe0, 0x41, 0xf1, 0x45, 0x08, 0x79, 0xf6, 0x15,
|
||||
0x7d, 0x91, 0xe7, 0xf1, 0xee, 0x04, 0x04, 0x01, 0x0a, 0x45, 0x01, 0xe4, 0x83, 0xf5,
|
||||
0xe0, 0x45, 0x03, 0xea, 0x81, 0xe6, 0xc0, 0x57, 0x7a, 0xe4, 0x04, 0x09, 0x02, 0xf4,
|
||||
0x82, 0xf5, 0x14, 0x60, 0xf1, 0xf2, 0xfb, 0x70, 0x01, 0x81, 0xef, 0x0f, 0x11, 0x01,
|
||||
0x04, 0x02, 0x00, 0x00,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
#[test]
|
||||
pub fn compresses_things() -> Result<(), PrsCompressionError> {
|
||||
for (index, test) in TEST_DATA.iter().enumerate() {
|
||||
println!("\ntest #{}", index);
|
||||
println!(" prs_compress({:02x?})", test.uncompressed);
|
||||
assert_eq!(*test.compressed, *prs_compress(&test.uncompressed)?);
|
||||
println!(" prs_decompress({:02x?})", test.compressed);
|
||||
assert_eq!(*test.uncompressed, *prs_decompress(&test.compressed)?);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn decompress_bad_data_error_result() -> Result<(), PrsCompressionError> {
|
||||
let data: &[u8] = &[];
|
||||
assert_matches!(prs_decompress(data), Err(PrsCompressionError::BadData(..)));
|
||||
|
||||
let data: &[u8] = &[1, 2];
|
||||
assert_matches!(prs_decompress(data), Err(PrsCompressionError::BadData(..)));
|
||||
|
||||
let data: &[u8] = &[1, 2, 3];
|
||||
assert_matches!(prs_decompress(data), Err(PrsCompressionError::BadData(..)));
|
||||
|
||||
let mut data = [0u8; 1024];
|
||||
let mut rng = StdRng::seed_from_u64(42);
|
||||
data.try_fill(&mut rng).unwrap();
|
||||
assert_matches!(prs_decompress(&data), Err(PrsCompressionError::BadData(..)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,641 +0,0 @@
|
|||
/*
|
||||
* The contents of this module are ported from the Fuzziqer "newserv" project with some minor
|
||||
* alterations by me.
|
||||
* https://github.com/fuzziqersoftware/newserv (PSOEncryption.cc + PSOEncryption.hh)
|
||||
*/
|
||||
|
||||
const PC_STREAM_LENGTH: usize = 57;
|
||||
const GC_STREAM_LENGTH: usize = 521;
|
||||
|
||||
pub trait Crypter {
|
||||
fn crypt_u32(&mut self, value: u32) -> u32;
|
||||
|
||||
fn crypt(&mut self, data: &mut [u8]) {
|
||||
let remaining_bytes_count = data.len() % 4;
|
||||
let dword_length = if remaining_bytes_count > 0 {
|
||||
data.len() - remaining_bytes_count
|
||||
} else {
|
||||
data.len()
|
||||
};
|
||||
|
||||
// encrypt all of the dword-sized data in the given buffer
|
||||
if dword_length > 0 {
|
||||
let mut dword: *mut u32 = data.as_mut_ptr().cast();
|
||||
for _ in 0..(dword_length / 4) {
|
||||
unsafe {
|
||||
*dword = self.crypt_u32(*dword);
|
||||
dword = dword.add(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if there is a remaining 1-3 bytes at the end of the buffer ...
|
||||
if remaining_bytes_count > 0 {
|
||||
// copy those 1-3 bytes into a temporary dword buffer
|
||||
let mut remaining_bytes = [0u8; 4];
|
||||
remaining_bytes[0..remaining_bytes_count].copy_from_slice(&data[dword_length..]);
|
||||
|
||||
// encrypt the temp dword buffer
|
||||
let dword: *mut u32 = remaining_bytes.as_mut_ptr().cast();
|
||||
unsafe {
|
||||
*dword = self.crypt_u32(*dword);
|
||||
}
|
||||
|
||||
// copy those now-encrypted 1-3 bytes back out of the temp buffer
|
||||
data[dword_length..].copy_from_slice(&remaining_bytes[0..remaining_bytes_count]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GCCrypter {
|
||||
stream: [u32; GC_STREAM_LENGTH],
|
||||
offset: usize,
|
||||
}
|
||||
|
||||
impl GCCrypter {
|
||||
pub fn new(seed: u32) -> GCCrypter {
|
||||
let mut seed = seed;
|
||||
let mut basekey = 0;
|
||||
let mut stream = [0u32; GC_STREAM_LENGTH];
|
||||
let mut offset = 0;
|
||||
|
||||
for _ in 0..=16 {
|
||||
for _ in 0..32 {
|
||||
seed = seed.wrapping_mul(0x5d588b65);
|
||||
basekey >>= 1;
|
||||
seed = seed.wrapping_add(1);
|
||||
if seed & 0x80000000 != 0 {
|
||||
basekey |= 0x80000000;
|
||||
} else {
|
||||
basekey &= 0x7fffffff;
|
||||
}
|
||||
}
|
||||
stream[offset] = basekey;
|
||||
offset += 1;
|
||||
}
|
||||
|
||||
stream[offset - 1] = ((stream[0] >> 9) ^ (stream[offset - 1] << 23)) ^ stream[15];
|
||||
let mut source1 = 0;
|
||||
let mut source2 = 1;
|
||||
let mut source3 = offset - 1;
|
||||
while offset != GC_STREAM_LENGTH {
|
||||
stream[offset] = stream[source3]
|
||||
^ (((stream[source1] << 23) & 0xff800000) ^ ((stream[source2] >> 9) & 0x007fffff));
|
||||
offset += 1;
|
||||
source1 += 1;
|
||||
source2 += 1;
|
||||
source3 += 1;
|
||||
}
|
||||
|
||||
let mut crypter = GCCrypter { stream, offset };
|
||||
crypter.update_stream();
|
||||
crypter.update_stream();
|
||||
crypter.update_stream();
|
||||
crypter.offset = GC_STREAM_LENGTH - 1;
|
||||
|
||||
crypter
|
||||
}
|
||||
|
||||
fn update_stream(&mut self) {
|
||||
let mut r5: u32 = 0;
|
||||
let mut r6: u32 = 489;
|
||||
let mut r7: u32 = 0;
|
||||
|
||||
while r6 != GC_STREAM_LENGTH as u32 {
|
||||
self.stream[r5 as usize] ^= self.stream[r6 as usize];
|
||||
r5 += 1;
|
||||
r6 += 1;
|
||||
}
|
||||
|
||||
while r5 != GC_STREAM_LENGTH as u32 {
|
||||
self.stream[r5 as usize] ^= self.stream[r7 as usize];
|
||||
r5 += 1;
|
||||
r7 += 1;
|
||||
}
|
||||
|
||||
self.offset = 0;
|
||||
}
|
||||
|
||||
fn next(&mut self) -> u32 {
|
||||
self.offset += 1;
|
||||
if self.offset == GC_STREAM_LENGTH {
|
||||
self.update_stream();
|
||||
}
|
||||
self.stream[self.offset]
|
||||
}
|
||||
}
|
||||
|
||||
impl Crypter for GCCrypter {
|
||||
fn crypt_u32(&mut self, mut value: u32) -> u32 {
|
||||
value ^= self.next().to_le();
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PCCrypter {
|
||||
stream: [u32; PC_STREAM_LENGTH],
|
||||
offset: usize,
|
||||
}
|
||||
|
||||
impl PCCrypter {
|
||||
pub fn new(seed: u32) -> PCCrypter {
|
||||
let mut esi: u32 = 1;
|
||||
let mut ebx: u32 = seed;
|
||||
let mut edi: u32 = 0x15;
|
||||
|
||||
let mut stream = [0u32; PC_STREAM_LENGTH];
|
||||
stream[56] = ebx;
|
||||
stream[55] = ebx;
|
||||
|
||||
while edi <= 0x46e {
|
||||
let eax = edi;
|
||||
let var1 = eax / 55;
|
||||
let edx = eax.wrapping_sub(var1 * 55);
|
||||
ebx = ebx.wrapping_sub(esi);
|
||||
edi = edi.wrapping_add(0x15);
|
||||
stream[edx as usize] = esi;
|
||||
esi = ebx;
|
||||
ebx = stream[edx as usize];
|
||||
}
|
||||
|
||||
let mut crypter = PCCrypter {
|
||||
stream,
|
||||
offset: PC_STREAM_LENGTH - 1,
|
||||
};
|
||||
|
||||
crypter.update_stream();
|
||||
crypter.update_stream();
|
||||
crypter.update_stream();
|
||||
crypter.update_stream();
|
||||
|
||||
crypter
|
||||
}
|
||||
|
||||
fn update_stream(&mut self) {
|
||||
let mut edi: u32 = 1;
|
||||
let mut edx: u32 = 0x18;
|
||||
let mut eax = edi;
|
||||
while edx > 0 {
|
||||
let esi = self.stream[eax.wrapping_add(0x1f) as usize];
|
||||
let ebp = self.stream[eax as usize].wrapping_sub(esi);
|
||||
self.stream[eax as usize] = ebp;
|
||||
eax = eax.wrapping_add(1);
|
||||
edx = edx.wrapping_sub(1);
|
||||
}
|
||||
|
||||
edi = 0x19;
|
||||
edx = 0x1f;
|
||||
eax = edi;
|
||||
while edx > 0 {
|
||||
let esi = self.stream[eax.wrapping_sub(0x18) as usize];
|
||||
let ebp = self.stream[eax as usize].wrapping_sub(esi);
|
||||
self.stream[eax as usize] = ebp;
|
||||
eax = eax.wrapping_add(1);
|
||||
edx = edx.wrapping_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn next(&mut self) -> u32 {
|
||||
if self.offset == PC_STREAM_LENGTH - 1 {
|
||||
self.update_stream();
|
||||
self.offset = 1;
|
||||
}
|
||||
let next = self.stream[self.offset];
|
||||
self.offset += 1;
|
||||
next
|
||||
}
|
||||
}
|
||||
|
||||
impl Crypter for PCCrypter {
|
||||
fn crypt_u32(&mut self, mut value: u32) -> u32 {
|
||||
value ^= self.next().to_le();
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn pc_encrypt_decrypt() {
|
||||
let seed: u32 = 0x12345678;
|
||||
|
||||
let decrypted = [
|
||||
0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21, 0x00,
|
||||
0x00, 0x00,
|
||||
];
|
||||
let encrypted = [
|
||||
0xde, 0xee, 0x84, 0xb6, 0xd6, 0x4c, 0x10, 0xbc, 0x07, 0x3c, 0x20, 0xca, 0x08, 0x20,
|
||||
0xee, 0xf0,
|
||||
];
|
||||
|
||||
let mut buffer = decrypted.clone();
|
||||
|
||||
// encrypt data
|
||||
let mut encrypter = PCCrypter::new(seed);
|
||||
encrypter.crypt(&mut buffer);
|
||||
assert_eq!(buffer, encrypted);
|
||||
|
||||
// crypting the same buffer again with the same Crypter instance won't decrypt it
|
||||
let mut temp_buffer = buffer.clone();
|
||||
encrypter.crypt(&mut temp_buffer);
|
||||
assert_ne!(temp_buffer, decrypted);
|
||||
|
||||
// crypting the previous buffer with a new Crypter using the same seed, will decrypt it
|
||||
let mut decrypter = PCCrypter::new(seed);
|
||||
decrypter.crypt(&mut buffer);
|
||||
assert_eq!(buffer, decrypted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gc_encrypt_decrypt() {
|
||||
let seed: u32 = 0x12345678;
|
||||
|
||||
let decrypted = [
|
||||
0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x21, 0x00,
|
||||
0x00, 0x00,
|
||||
];
|
||||
let encrypted = [
|
||||
0x8a, 0x87, 0x5e, 0x68, 0x24, 0x01, 0xee, 0xac, 0xd6, 0x82, 0x07, 0xff, 0x2b, 0xa5,
|
||||
0x92, 0x2b,
|
||||
];
|
||||
|
||||
let mut buffer = decrypted.clone();
|
||||
|
||||
// encrypt data
|
||||
let mut encrypter = GCCrypter::new(seed);
|
||||
encrypter.crypt(&mut buffer);
|
||||
assert_eq!(buffer, encrypted);
|
||||
|
||||
// crypting the same buffer again with the same Crypter instance won't decrypt it
|
||||
let mut temp_buffer = buffer.clone();
|
||||
encrypter.crypt(&mut temp_buffer);
|
||||
assert_ne!(temp_buffer, decrypted);
|
||||
|
||||
// crypting the previous buffer with a new Crypter using the same seed, will decrypt it
|
||||
let mut decrypter = GCCrypter::new(seed);
|
||||
decrypter.crypt(&mut buffer);
|
||||
assert_eq!(buffer, decrypted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pc_crypt_non_dword_sized_data_works() {
|
||||
let mut crypter = PCCrypter::new(0x12345678);
|
||||
|
||||
// 3 bytes
|
||||
let mut first = [0x01, 0x02, 0x03];
|
||||
crypter.crypt(&mut first);
|
||||
assert_eq!(first, [0x97, 0x89, 0xeb]);
|
||||
|
||||
// 5 bytes
|
||||
let mut second = [0x01, 0x02, 0x03, 0x04, 0x05];
|
||||
crypter.crypt(&mut second);
|
||||
assert_eq!(second, [0xb8, 0x62, 0x33, 0xcf, 0x6d]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gc_crypt_non_dword_sized_data_returns_error() {
|
||||
let mut crypter = GCCrypter::new(0x12345678);
|
||||
|
||||
// 3 bytes
|
||||
let mut first = [0x01, 0x02, 0x03];
|
||||
crypter.crypt(&mut first);
|
||||
assert_eq!(first, [0xc3, 0xe0, 0x31]);
|
||||
|
||||
// 5 bytes
|
||||
let mut second = [0x01, 0x02, 0x03, 0x04, 0x05];
|
||||
crypter.crypt(&mut second);
|
||||
assert_eq!(second, [0x4a, 0x2f, 0xcd, 0xdf, 0xbc]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pc_encrypt_multiple_things_and_decrypt_multiple_things() {
|
||||
let seed: u32 = 0x42424242;
|
||||
|
||||
let first_decrypted = [0x46, 0x69, 0x72, 0x73, 0x74, 0x21, 0x21, 0x00];
|
||||
let second_decrypted = [
|
||||
0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x62, 0x69, 0x74, 0x20, 0x6f, 0x66, 0x20,
|
||||
0x64, 0x61, 0x74, 0x61, 0x00, 0x00,
|
||||
];
|
||||
|
||||
let first_encrypted = [0xf4, 0x41, 0x19, 0x58, 0xa3, 0x2d, 0xbc, 0x67];
|
||||
let second_encrypted = [
|
||||
0x9d, 0x08, 0xee, 0xec, 0x89, 0x7f, 0xac, 0x66, 0xef, 0x18, 0x9c, 0xc4, 0xa9, 0x84,
|
||||
0x34, 0xa1, 0x90, 0x76, 0x71, 0xea,
|
||||
];
|
||||
|
||||
let mut encrypter = PCCrypter::new(seed);
|
||||
|
||||
let mut first_buffer = first_decrypted.clone();
|
||||
encrypter.crypt(&mut first_buffer);
|
||||
assert_eq!(first_encrypted, first_buffer);
|
||||
|
||||
let mut second_buffer = second_decrypted.clone();
|
||||
encrypter.crypt(&mut second_buffer);
|
||||
assert_eq!(second_encrypted, second_buffer);
|
||||
|
||||
let mut decrypter = PCCrypter::new(seed);
|
||||
|
||||
decrypter.crypt(&mut first_buffer);
|
||||
assert_eq!(first_decrypted, first_buffer);
|
||||
|
||||
decrypter.crypt(&mut second_buffer);
|
||||
assert_eq!(second_decrypted, second_buffer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gc_encrypt_multiple_things_and_decrypt_multiple_things() {
|
||||
let seed: u32 = 0x42424242;
|
||||
|
||||
let first_decrypted = [0x46, 0x69, 0x72, 0x73, 0x74, 0x21, 0x21, 0x00];
|
||||
let second_decrypted = [
|
||||
0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x62, 0x69, 0x74, 0x20, 0x6f, 0x66, 0x20,
|
||||
0x64, 0x61, 0x74, 0x61, 0x00, 0x00,
|
||||
];
|
||||
|
||||
let first_encrypted = [0xda, 0x5a, 0x14, 0xab, 0x2c, 0x0a, 0x50, 0x07];
|
||||
let second_encrypted = [
|
||||
0xc4, 0x17, 0x16, 0xa3, 0x48, 0xf1, 0x9c, 0x8d, 0x8e, 0x71, 0xdd, 0x46, 0xe2, 0x09,
|
||||
0xce, 0x38, 0xf9, 0xd3, 0xdb, 0x7c,
|
||||
];
|
||||
|
||||
let mut encrypter = GCCrypter::new(seed);
|
||||
|
||||
let mut first_buffer = first_decrypted.clone();
|
||||
encrypter.crypt(&mut first_buffer);
|
||||
assert_eq!(first_encrypted, first_buffer);
|
||||
|
||||
let mut second_buffer = second_decrypted.clone();
|
||||
encrypter.crypt(&mut second_buffer);
|
||||
assert_eq!(second_encrypted, second_buffer);
|
||||
|
||||
let mut decrypter = GCCrypter::new(seed);
|
||||
|
||||
decrypter.crypt(&mut first_buffer);
|
||||
assert_eq!(first_decrypted, first_buffer);
|
||||
|
||||
decrypter.crypt(&mut second_buffer);
|
||||
assert_eq!(second_decrypted, second_buffer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pc_encrypt_and_decrypt_bigger_data() {
|
||||
let seed: u32 = 0xabcdef;
|
||||
|
||||
// these blocks of data are specifically intended to be larger than the PC encryption
|
||||
// algorithm's "stream length", so we can test the wrap-around logic works too
|
||||
|
||||
let decrypted = [
|
||||
0x4c, 0x6f, 0x72, 0x65, 0x6d, 0x20, 0x69, 0x70, 0x73, 0x75, 0x6d, 0x20, 0x64, 0x6f,
|
||||
0x6c, 0x6f, 0x72, 0x20, 0x73, 0x69, 0x74, 0x20, 0x61, 0x6d, 0x65, 0x74, 0x2c, 0x20,
|
||||
0x63, 0x6f, 0x6e, 0x73, 0x65, 0x63, 0x74, 0x65, 0x74, 0x75, 0x72, 0x20, 0x61, 0x64,
|
||||
0x69, 0x70, 0x69, 0x73, 0x63, 0x69, 0x6e, 0x67, 0x20, 0x65, 0x6c, 0x69, 0x74, 0x2e,
|
||||
0x20, 0x4e, 0x61, 0x6d, 0x20, 0x65, 0x67, 0x65, 0x73, 0x74, 0x61, 0x73, 0x20, 0x64,
|
||||
0x69, 0x63, 0x74, 0x75, 0x6d, 0x20, 0x65, 0x72, 0x6f, 0x73, 0x20, 0x6e, 0x6f, 0x6e,
|
||||
0x20, 0x6c, 0x75, 0x63, 0x74, 0x75, 0x73, 0x2e, 0x20, 0x50, 0x65, 0x6c, 0x6c, 0x65,
|
||||
0x6e, 0x74, 0x65, 0x73, 0x71, 0x75, 0x65, 0x20, 0x6e, 0x75, 0x6e, 0x63, 0x20, 0x70,
|
||||
0x75, 0x72, 0x75, 0x73, 0x2c, 0x20, 0x73, 0x75, 0x73, 0x63, 0x69, 0x70, 0x69, 0x74,
|
||||
0x20, 0x76, 0x65, 0x6c, 0x20, 0x65, 0x78, 0x20, 0x69, 0x6e, 0x2c, 0x20, 0x73, 0x6f,
|
||||
0x6c, 0x6c, 0x69, 0x63, 0x69, 0x74, 0x75, 0x64, 0x69, 0x6e, 0x20, 0x66, 0x69, 0x6e,
|
||||
0x69, 0x62, 0x75, 0x73, 0x20, 0x64, 0x6f, 0x6c, 0x6f, 0x72, 0x2e, 0x20, 0x41, 0x6c,
|
||||
0x69, 0x71, 0x75, 0x61, 0x6d, 0x20, 0x61, 0x6c, 0x69, 0x71, 0x75, 0x61, 0x6d, 0x20,
|
||||
0x73, 0x65, 0x6d, 0x20, 0x6a, 0x75, 0x73, 0x74, 0x6f, 0x2c, 0x20, 0x76, 0x69, 0x74,
|
||||
0x61, 0x65, 0x20, 0x70, 0x6f, 0x73, 0x75, 0x65, 0x72, 0x65, 0x20, 0x65, 0x72, 0x61,
|
||||
0x74, 0x20, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x64, 0x75, 0x6d, 0x20, 0x6e, 0x65, 0x63,
|
||||
0x2e, 0x20, 0x4e, 0x75, 0x6e, 0x63, 0x20, 0x73, 0x69, 0x74, 0x20, 0x61, 0x6d, 0x65,
|
||||
0x74, 0x20, 0x65, 0x6c, 0x65, 0x69, 0x66, 0x65, 0x6e, 0x64, 0x20, 0x65, 0x6e, 0x69,
|
||||
0x6d, 0x2e, 0x20, 0x4d, 0x6f, 0x72, 0x62, 0x69, 0x20, 0x71, 0x75, 0x69, 0x73, 0x20,
|
||||
0x75, 0x6c, 0x6c, 0x61, 0x6d, 0x63, 0x6f, 0x72, 0x70, 0x65, 0x72, 0x20, 0x6d, 0x61,
|
||||
0x75, 0x72, 0x69, 0x73, 0x2e, 0x20, 0x50, 0x72, 0x6f, 0x69, 0x6e, 0x20, 0x6c, 0x61,
|
||||
0x63, 0x75, 0x73, 0x20, 0x74, 0x65, 0x6c, 0x6c, 0x75, 0x73, 0x2c, 0x20, 0x61, 0x75,
|
||||
0x63, 0x74, 0x6f, 0x72, 0x20, 0x71, 0x75, 0x69, 0x73, 0x20, 0x6f, 0x64, 0x69, 0x6f,
|
||||
0x20, 0x6e, 0x6f, 0x6e, 0x2c, 0x20, 0x6d, 0x6f, 0x6c, 0x6c, 0x69, 0x73, 0x20, 0x74,
|
||||
0x65, 0x6d, 0x70, 0x6f, 0x72, 0x20, 0x6d, 0x61, 0x73, 0x73, 0x61, 0x2e, 0x20, 0x50,
|
||||
0x68, 0x61, 0x73, 0x65, 0x6c, 0x6c, 0x75, 0x73, 0x20, 0x66, 0x65, 0x75, 0x67, 0x69,
|
||||
0x61, 0x74, 0x20, 0x69, 0x70, 0x73, 0x75, 0x6d, 0x20, 0x61, 0x74, 0x20, 0x69, 0x6d,
|
||||
0x70, 0x65, 0x72, 0x64, 0x69, 0x65, 0x74, 0x20, 0x66, 0x61, 0x63, 0x69, 0x6c, 0x69,
|
||||
0x73, 0x69, 0x73, 0x2e, 0x20, 0x50, 0x72, 0x61, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x20,
|
||||
0x70, 0x68, 0x61, 0x72, 0x65, 0x74, 0x72, 0x61, 0x20, 0x61, 0x75, 0x67, 0x75, 0x65,
|
||||
0x20, 0x6e, 0x6f, 0x6e, 0x20, 0x6f, 0x64, 0x69, 0x6f, 0x20, 0x63, 0x6f, 0x6e, 0x67,
|
||||
0x75, 0x65, 0x20, 0x74, 0x72, 0x69, 0x73, 0x74, 0x69, 0x71, 0x75, 0x65, 0x2e, 0x20,
|
||||
0x50, 0x72, 0x6f, 0x69, 0x6e, 0x20, 0x73, 0x61, 0x67, 0x69, 0x74, 0x74, 0x69, 0x73,
|
||||
0x20, 0x66, 0x65, 0x72, 0x6d, 0x65, 0x6e, 0x74, 0x75, 0x6d, 0x20, 0x6c, 0x61, 0x63,
|
||||
0x75, 0x73, 0x2c, 0x20, 0x73, 0x69, 0x74, 0x20, 0x61, 0x6d, 0x65, 0x74, 0x20, 0x76,
|
||||
0x69, 0x76, 0x65, 0x72, 0x72, 0x61, 0x20, 0x61, 0x72, 0x63, 0x75, 0x20, 0x63, 0x6f,
|
||||
0x6e, 0x73, 0x65, 0x63, 0x74, 0x65, 0x74, 0x75, 0x72, 0x20, 0x61, 0x2e, 0x20, 0x43,
|
||||
0x75, 0x72, 0x61, 0x62, 0x69, 0x74, 0x75, 0x72, 0x20, 0x74, 0x69, 0x6e, 0x63, 0x69,
|
||||
0x64, 0x75, 0x6e, 0x74, 0x20, 0x6e, 0x6f, 0x6e, 0x20, 0x6c, 0x6f, 0x72, 0x65, 0x6d,
|
||||
0x20, 0x76, 0x69, 0x74, 0x61, 0x65, 0x20, 0x6c, 0x61, 0x6f, 0x72, 0x65, 0x65, 0x74,
|
||||
0x2e, 0x20, 0x49, 0x6e, 0x20, 0x64, 0x69, 0x63, 0x74, 0x75, 0x6d, 0x20, 0x74, 0x65,
|
||||
0x6d, 0x70, 0x75, 0x73, 0x20, 0x74, 0x69, 0x6e, 0x63, 0x69, 0x64, 0x75, 0x6e, 0x74,
|
||||
0x2e, 0x20, 0x46, 0x75, 0x73, 0x63, 0x65, 0x20, 0x71, 0x75, 0x69, 0x73, 0x20, 0x6d,
|
||||
0x69, 0x20, 0x73, 0x65, 0x64, 0x20, 0x65, 0x72, 0x6f, 0x73, 0x20, 0x63, 0x6f, 0x6d,
|
||||
0x6d, 0x6f, 0x64, 0x6f, 0x20, 0x76, 0x65, 0x6e, 0x65, 0x6e, 0x61, 0x74, 0x69, 0x73,
|
||||
0x2e, 0x20, 0x51, 0x75, 0x69, 0x73, 0x71, 0x75, 0x65, 0x20, 0x65, 0x67, 0x65, 0x73,
|
||||
0x74, 0x61, 0x73, 0x20, 0x64, 0x6f, 0x6c, 0x6f, 0x72, 0x20, 0x65, 0x74, 0x20, 0x6e,
|
||||
0x75, 0x6e, 0x63, 0x20, 0x64, 0x69, 0x63, 0x74, 0x75, 0x6d, 0x20, 0x62, 0x6c, 0x61,
|
||||
0x6e, 0x64, 0x69, 0x74, 0x2e, 0x20, 0x56, 0x65, 0x73, 0x74, 0x69, 0x62, 0x75, 0x6c,
|
||||
0x75, 0x6d, 0x20, 0x65, 0x75, 0x20, 0x6c, 0x69, 0x62, 0x65, 0x72, 0x6f, 0x20, 0x65,
|
||||
0x67, 0x65, 0x74, 0x20, 0x61, 0x6e, 0x74, 0x65, 0x20, 0x76, 0x61, 0x72, 0x69, 0x75,
|
||||
0x73, 0x20, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x72, 0x61, 0x74, 0x20, 0x65, 0x67, 0x65,
|
||||
0x74, 0x20, 0x75, 0x74, 0x20, 0x6e, 0x69, 0x62, 0x68, 0x2e, 0x00, 0x00,
|
||||
];
|
||||
|
||||
let encrypted = [
|
||||
0x3c, 0x76, 0x78, 0x22, 0x53, 0x33, 0x9b, 0x87, 0x0a, 0x02, 0x45, 0xf6, 0xfa, 0xcd,
|
||||
0x95, 0x84, 0xc6, 0xc9, 0x3e, 0x89, 0x23, 0x51, 0x08, 0x77, 0x30, 0xaf, 0x34, 0xd3,
|
||||
0xb0, 0x44, 0xe1, 0x17, 0x29, 0x23, 0x51, 0x0d, 0x0e, 0x3d, 0xff, 0xe1, 0x0c, 0xd2,
|
||||
0xe0, 0xa1, 0xce, 0xd3, 0x2c, 0x6d, 0xc1, 0x03, 0x86, 0x85, 0x0c, 0x10, 0xce, 0x02,
|
||||
0x15, 0xb3, 0x0a, 0x3c, 0x6a, 0x43, 0x76, 0x49, 0xd7, 0x11, 0xe9, 0x4e, 0x5b, 0x8f,
|
||||
0x43, 0x1b, 0x0f, 0xfa, 0x3a, 0xd2, 0x62, 0xc5, 0x51, 0x2b, 0x0f, 0xf8, 0x18, 0xbc,
|
||||
0xa3, 0x4a, 0xc8, 0xe0, 0x7c, 0xb8, 0xc1, 0x06, 0x36, 0xa1, 0xa4, 0xbe, 0x75, 0x4f,
|
||||
0xc0, 0xe2, 0xe6, 0xd4, 0x7d, 0x3c, 0x4e, 0x1d, 0x72, 0xc1, 0x38, 0xc2, 0xf0, 0x3e,
|
||||
0x8d, 0x28, 0x11, 0xae, 0x4b, 0x2d, 0xf2, 0x89, 0x32, 0xd8, 0x2d, 0x89, 0xb6, 0x33,
|
||||
0xe7, 0x2d, 0xa1, 0xd9, 0x46, 0x8e, 0xf0, 0x0d, 0x9f, 0xf3, 0xa1, 0xe0, 0x7a, 0xe9,
|
||||
0x50, 0xce, 0x34, 0x0f, 0xff, 0xd2, 0x4d, 0x0b, 0x30, 0xc5, 0xb5, 0x8c, 0x58, 0x75,
|
||||
0x84, 0x3b, 0x7e, 0xa5, 0x95, 0x99, 0xac, 0x7c, 0x22, 0x9b, 0xfe, 0x26, 0xd2, 0x3c,
|
||||
0xf3, 0xa7, 0xbd, 0x5f, 0x02, 0xcb, 0xa5, 0xcc, 0xa7, 0xc9, 0x78, 0xc2, 0x39, 0x7e,
|
||||
0xf2, 0x76, 0xf4, 0x38, 0x67, 0xbf, 0x8e, 0xad, 0x6f, 0x02, 0xdb, 0x4b, 0x6a, 0x5b,
|
||||
0x59, 0xd9, 0xbb, 0x0b, 0xe9, 0xf0, 0xb3, 0x44, 0x52, 0x53, 0x0d, 0x20, 0xb6, 0x4b,
|
||||
0x32, 0x0f, 0x7c, 0x5c, 0x67, 0x2f, 0xd9, 0x1a, 0x75, 0xde, 0xb1, 0xbf, 0x27, 0x88,
|
||||
0x54, 0x7d, 0xc5, 0x79, 0x9f, 0x2a, 0x12, 0x4b, 0x78, 0x96, 0xcf, 0x04, 0x15, 0x22,
|
||||
0x84, 0x53, 0xa4, 0xa6, 0x55, 0xc2, 0x9a, 0x4a, 0xed, 0x6c, 0x82, 0x75, 0xcc, 0x63,
|
||||
0x2c, 0x44, 0x4f, 0x27, 0xd8, 0x45, 0x22, 0xb1, 0xbd, 0xde, 0x83, 0xe9, 0x7e, 0xea,
|
||||
0xf3, 0xa9, 0x2c, 0x18, 0x8c, 0x5c, 0xfd, 0xb2, 0xdc, 0xec, 0x93, 0xbe, 0x87, 0x5c,
|
||||
0xc4, 0x7f, 0x6d, 0x11, 0x89, 0xab, 0xd7, 0x7d, 0xef, 0xc4, 0x49, 0x69, 0x2f, 0xb2,
|
||||
0xd8, 0x03, 0xf2, 0x13, 0x0c, 0x53, 0x63, 0x0c, 0x3f, 0xfe, 0x93, 0xdb, 0x17, 0x21,
|
||||
0x90, 0xee, 0xf0, 0xac, 0x4b, 0x03, 0xb4, 0x76, 0xfb, 0x78, 0x04, 0xcf, 0x60, 0x25,
|
||||
0xa1, 0x52, 0x55, 0x9d, 0xc5, 0x5b, 0x28, 0xd0, 0x8c, 0x84, 0xe9, 0x60, 0x54, 0x1d,
|
||||
0xc3, 0x2f, 0x20, 0x3e, 0x37, 0xab, 0xac, 0x91, 0x4e, 0x44, 0x44, 0x7f, 0xa3, 0x1b,
|
||||
0x9f, 0xe1, 0xa2, 0x90, 0xd9, 0xa9, 0x85, 0x63, 0x33, 0x63, 0x4a, 0xad, 0xb1, 0xcf,
|
||||
0x37, 0x59, 0x77, 0x46, 0xb7, 0x99, 0x9d, 0x0d, 0x70, 0x1d, 0x76, 0x3c, 0x33, 0xa5,
|
||||
0xc1, 0xfe, 0x6e, 0xe1, 0xac, 0xbc, 0x24, 0x79, 0x0d, 0x66, 0x34, 0x6a, 0x61, 0xa1,
|
||||
0x9d, 0xde, 0x3f, 0x44, 0x9f, 0x08, 0xb1, 0x74, 0xf0, 0x11, 0x6f, 0xd1, 0xd2, 0x5d,
|
||||
0x1d, 0x83, 0xf3, 0x15, 0x5a, 0x7a, 0x01, 0x84, 0xb7, 0xe2, 0x5a, 0x15, 0x6f, 0x5a,
|
||||
0x6c, 0xfe, 0xb3, 0xcb, 0xfb, 0x19, 0x28, 0x35, 0x2b, 0x37, 0xb1, 0xaa, 0x01, 0x88,
|
||||
0xb7, 0x9d, 0x46, 0x87, 0x4c, 0xab, 0x27, 0xee, 0x74, 0xeb, 0x82, 0x74, 0xba, 0xab,
|
||||
0x70, 0x26, 0x13, 0x1b, 0x4f, 0xf1, 0xaf, 0x01, 0x2e, 0x06, 0x6d, 0xb9, 0x02, 0xee,
|
||||
0xf9, 0x1d, 0x50, 0x37, 0xf7, 0xc2, 0x3c, 0xe0, 0xea, 0x83, 0xc7, 0xcd, 0xdc, 0xad,
|
||||
0xee, 0xc1, 0x56, 0xde, 0x3e, 0x3f, 0xff, 0x59, 0xd7, 0xab, 0x1c, 0x89, 0x72, 0xb7,
|
||||
0xfd, 0xa3, 0xb6, 0x15, 0x9b, 0x12, 0x6c, 0x5d, 0x92, 0x1d, 0x7e, 0xb0, 0xf5, 0x19,
|
||||
0x7b, 0x57, 0x2d, 0x62, 0x79, 0xad, 0xfb, 0xb0, 0x66, 0x41, 0xc0, 0x19, 0x15, 0xe0,
|
||||
0xee, 0xe2, 0x55, 0x8b, 0x94, 0x44, 0x0e, 0x96, 0x84, 0xfa, 0xed, 0xc5, 0xbf, 0x8c,
|
||||
0x61, 0x0a, 0xec, 0x29, 0x14, 0xd0, 0x22, 0x7f, 0x32, 0x54, 0x82, 0xc2, 0x7f, 0xf2,
|
||||
0x4d, 0x7f, 0x4d, 0x9a, 0x62, 0xed, 0x17, 0xc8, 0x3b, 0xf3, 0x49, 0xc0, 0x13, 0xa1,
|
||||
0x3e, 0x66, 0x6e, 0x27, 0xcb, 0xc6, 0xec, 0x01, 0xe8, 0xdc, 0x54, 0x92, 0x42, 0x26,
|
||||
0x56, 0xb7, 0xd6, 0xc9, 0xa7, 0xff, 0x10, 0x7f, 0x3e, 0xc0, 0x60, 0x19, 0xac, 0x2d,
|
||||
0xda, 0xa2, 0xb9, 0x99, 0x77, 0x23, 0x47, 0xbd, 0x3e, 0x4d, 0x72, 0x56, 0x27, 0x0c,
|
||||
0x14, 0xf8, 0x30, 0xf4, 0xbf, 0x61, 0x26, 0xd0, 0x04, 0xe3, 0x99, 0x77, 0xde, 0xb4,
|
||||
0xe6, 0x00, 0xa1, 0x8b, 0x3a, 0x08, 0x00, 0x5e, 0x47, 0xbc, 0xf1, 0x71, 0xe4, 0x9b,
|
||||
0x92, 0x90, 0x6e, 0x52, 0x23, 0x01, 0x6c, 0x4f, 0x48, 0xae, 0x57, 0x96, 0x0b, 0xef,
|
||||
0xc3, 0xe9, 0x3b, 0xf4, 0x69, 0x1c, 0x1b, 0x46, 0x46, 0x6a, 0x29, 0x57, 0x76, 0xc3,
|
||||
0x62, 0x17, 0x0a, 0xd7, 0xf3, 0x5e, 0x38, 0x1c, 0x2f, 0xb4, 0xca, 0x72, 0x2d, 0xca,
|
||||
0x10, 0x72, 0x3c, 0xa1, 0xfe, 0x7d, 0xea, 0x46, 0x14, 0x45, 0x7e, 0x40, 0x34, 0xae,
|
||||
0xef, 0xd7, 0x6e, 0x31, 0x08, 0x71, 0xf4, 0x00, 0xc0, 0xcc, 0xe6, 0x3e, 0xdd, 0x40,
|
||||
0x6d, 0xa0, 0xdb, 0x17, 0x12, 0x4a, 0x7a, 0x08, 0xb9, 0xda, 0x82, 0x89, 0x21, 0x8d,
|
||||
0x50, 0xaf, 0x42, 0xd2, 0x1b, 0x2d, 0x8c, 0xcf, 0x64, 0x05, 0xa8, 0x5e, 0xec, 0x35,
|
||||
0xba, 0x80, 0x30, 0x27, 0xd7, 0x48, 0x1d, 0xcb, 0x6b, 0x9c, 0x2c, 0xf4,
|
||||
];
|
||||
|
||||
let mut buffer = decrypted.clone();
|
||||
|
||||
let mut encrypter = PCCrypter::new(seed);
|
||||
encrypter.crypt(&mut buffer);
|
||||
assert_eq!(encrypted, buffer);
|
||||
|
||||
let mut decrypter = PCCrypter::new(seed);
|
||||
decrypter.crypt(&mut buffer);
|
||||
assert_eq!(decrypted, buffer);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gc_encrypt_and_decrypt_bigger_data() {
|
||||
let seed: u32 = 0xabcdef;
|
||||
|
||||
// these blocks of data are specifically intended to be larger than the Gamecube encryption
|
||||
// algorithm's "stream length", so we can test the wrap-around logic works too
|
||||
|
||||
let decrypted = [
|
||||
0x4c, 0x6f, 0x72, 0x65, 0x6d, 0x20, 0x69, 0x70, 0x73, 0x75, 0x6d, 0x20, 0x64, 0x6f,
|
||||
0x6c, 0x6f, 0x72, 0x20, 0x73, 0x69, 0x74, 0x20, 0x61, 0x6d, 0x65, 0x74, 0x2c, 0x20,
|
||||
0x63, 0x6f, 0x6e, 0x73, 0x65, 0x63, 0x74, 0x65, 0x74, 0x75, 0x72, 0x20, 0x61, 0x64,
|
||||
0x69, 0x70, 0x69, 0x73, 0x63, 0x69, 0x6e, 0x67, 0x20, 0x65, 0x6c, 0x69, 0x74, 0x2e,
|
||||
0x20, 0x4e, 0x61, 0x6d, 0x20, 0x65, 0x67, 0x65, 0x73, 0x74, 0x61, 0x73, 0x20, 0x64,
|
||||
0x69, 0x63, 0x74, 0x75, 0x6d, 0x20, 0x65, 0x72, 0x6f, 0x73, 0x20, 0x6e, 0x6f, 0x6e,
|
||||
0x20, 0x6c, 0x75, 0x63, 0x74, 0x75, 0x73, 0x2e, 0x20, 0x50, 0x65, 0x6c, 0x6c, 0x65,
|
||||
0x6e, 0x74, 0x65, 0x73, 0x71, 0x75, 0x65, 0x20, 0x6e, 0x75, 0x6e, 0x63, 0x20, 0x70,
|
||||
0x75, 0x72, 0x75, 0x73, 0x2c, 0x20, 0x73, 0x75, 0x73, 0x63, 0x69, 0x70, 0x69, 0x74,
|
||||
0x20, 0x76, 0x65, 0x6c, 0x20, 0x65, 0x78, 0x20, 0x69, 0x6e, 0x2c, 0x20, 0x73, 0x6f,
|
||||
0x6c, 0x6c, 0x69, 0x63, 0x69, 0x74, 0x75, 0x64, 0x69, 0x6e, 0x20, 0x66, 0x69, 0x6e,
|
||||
0x69, 0x62, 0x75, 0x73, 0x20, 0x64, 0x6f, 0x6c, 0x6f, 0x72, 0x2e, 0x20, 0x41, 0x6c,
|
||||
0x69, 0x71, 0x75, 0x61, 0x6d, 0x20, 0x61, 0x6c, 0x69, 0x71, 0x75, 0x61, 0x6d, 0x20,
|
||||
0x73, 0x65, 0x6d, 0x20, 0x6a, 0x75, 0x73, 0x74, 0x6f, 0x2c, 0x20, 0x76, 0x69, 0x74,
|
||||
0x61, 0x65, 0x20, 0x70, 0x6f, 0x73, 0x75, 0x65, 0x72, 0x65, 0x20, 0x65, 0x72, 0x61,
|
||||
0x74, 0x20, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x64, 0x75, 0x6d, 0x20, 0x6e, 0x65, 0x63,
|
||||
0x2e, 0x20, 0x4e, 0x75, 0x6e, 0x63, 0x20, 0x73, 0x69, 0x74, 0x20, 0x61, 0x6d, 0x65,
|
||||
0x74, 0x20, 0x65, 0x6c, 0x65, 0x69, 0x66, 0x65, 0x6e, 0x64, 0x20, 0x65, 0x6e, 0x69,
|
||||
0x6d, 0x2e, 0x20, 0x4d, 0x6f, 0x72, 0x62, 0x69, 0x20, 0x71, 0x75, 0x69, 0x73, 0x20,
|
||||
0x75, 0x6c, 0x6c, 0x61, 0x6d, 0x63, 0x6f, 0x72, 0x70, 0x65, 0x72, 0x20, 0x6d, 0x61,
|
||||
0x75, 0x72, 0x69, 0x73, 0x2e, 0x20, 0x50, 0x72, 0x6f, 0x69, 0x6e, 0x20, 0x6c, 0x61,
|
||||
0x63, 0x75, 0x73, 0x20, 0x74, 0x65, 0x6c, 0x6c, 0x75, 0x73, 0x2c, 0x20, 0x61, 0x75,
|
||||
0x63, 0x74, 0x6f, 0x72, 0x20, 0x71, 0x75, 0x69, 0x73, 0x20, 0x6f, 0x64, 0x69, 0x6f,
|
||||
0x20, 0x6e, 0x6f, 0x6e, 0x2c, 0x20, 0x6d, 0x6f, 0x6c, 0x6c, 0x69, 0x73, 0x20, 0x74,
|
||||
0x65, 0x6d, 0x70, 0x6f, 0x72, 0x20, 0x6d, 0x61, 0x73, 0x73, 0x61, 0x2e, 0x20, 0x50,
|
||||
0x68, 0x61, 0x73, 0x65, 0x6c, 0x6c, 0x75, 0x73, 0x20, 0x66, 0x65, 0x75, 0x67, 0x69,
|
||||
0x61, 0x74, 0x20, 0x69, 0x70, 0x73, 0x75, 0x6d, 0x20, 0x61, 0x74, 0x20, 0x69, 0x6d,
|
||||
0x70, 0x65, 0x72, 0x64, 0x69, 0x65, 0x74, 0x20, 0x66, 0x61, 0x63, 0x69, 0x6c, 0x69,
|
||||
0x73, 0x69, 0x73, 0x2e, 0x20, 0x50, 0x72, 0x61, 0x65, 0x73, 0x65, 0x6e, 0x74, 0x20,
|
||||
0x70, 0x68, 0x61, 0x72, 0x65, 0x74, 0x72, 0x61, 0x20, 0x61, 0x75, 0x67, 0x75, 0x65,
|
||||
0x20, 0x6e, 0x6f, 0x6e, 0x20, 0x6f, 0x64, 0x69, 0x6f, 0x20, 0x63, 0x6f, 0x6e, 0x67,
|
||||
0x75, 0x65, 0x20, 0x74, 0x72, 0x69, 0x73, 0x74, 0x69, 0x71, 0x75, 0x65, 0x2e, 0x20,
|
||||
0x50, 0x72, 0x6f, 0x69, 0x6e, 0x20, 0x73, 0x61, 0x67, 0x69, 0x74, 0x74, 0x69, 0x73,
|
||||
0x20, 0x66, 0x65, 0x72, 0x6d, 0x65, 0x6e, 0x74, 0x75, 0x6d, 0x20, 0x6c, 0x61, 0x63,
|
||||
0x75, 0x73, 0x2c, 0x20, 0x73, 0x69, 0x74, 0x20, 0x61, 0x6d, 0x65, 0x74, 0x20, 0x76,
|
||||
0x69, 0x76, 0x65, 0x72, 0x72, 0x61, 0x20, 0x61, 0x72, 0x63, 0x75, 0x20, 0x63, 0x6f,
|
||||
0x6e, 0x73, 0x65, 0x63, 0x74, 0x65, 0x74, 0x75, 0x72, 0x20, 0x61, 0x2e, 0x20, 0x43,
|
||||
0x75, 0x72, 0x61, 0x62, 0x69, 0x74, 0x75, 0x72, 0x20, 0x74, 0x69, 0x6e, 0x63, 0x69,
|
||||
0x64, 0x75, 0x6e, 0x74, 0x20, 0x6e, 0x6f, 0x6e, 0x20, 0x6c, 0x6f, 0x72, 0x65, 0x6d,
|
||||
0x20, 0x76, 0x69, 0x74, 0x61, 0x65, 0x20, 0x6c, 0x61, 0x6f, 0x72, 0x65, 0x65, 0x74,
|
||||
0x2e, 0x20, 0x49, 0x6e, 0x20, 0x64, 0x69, 0x63, 0x74, 0x75, 0x6d, 0x20, 0x74, 0x65,
|
||||
0x6d, 0x70, 0x75, 0x73, 0x20, 0x74, 0x69, 0x6e, 0x63, 0x69, 0x64, 0x75, 0x6e, 0x74,
|
||||
0x2e, 0x20, 0x46, 0x75, 0x73, 0x63, 0x65, 0x20, 0x71, 0x75, 0x69, 0x73, 0x20, 0x6d,
|
||||
0x69, 0x20, 0x73, 0x65, 0x64, 0x20, 0x65, 0x72, 0x6f, 0x73, 0x20, 0x63, 0x6f, 0x6d,
|
||||
0x6d, 0x6f, 0x64, 0x6f, 0x20, 0x76, 0x65, 0x6e, 0x65, 0x6e, 0x61, 0x74, 0x69, 0x73,
|
||||
0x2e, 0x20, 0x51, 0x75, 0x69, 0x73, 0x71, 0x75, 0x65, 0x20, 0x65, 0x67, 0x65, 0x73,
|
||||
0x74, 0x61, 0x73, 0x20, 0x64, 0x6f, 0x6c, 0x6f, 0x72, 0x20, 0x65, 0x74, 0x20, 0x6e,
|
||||
0x75, 0x6e, 0x63, 0x20, 0x64, 0x69, 0x63, 0x74, 0x75, 0x6d, 0x20, 0x62, 0x6c, 0x61,
|
||||
0x6e, 0x64, 0x69, 0x74, 0x2e, 0x20, 0x56, 0x65, 0x73, 0x74, 0x69, 0x62, 0x75, 0x6c,
|
||||
0x75, 0x6d, 0x20, 0x65, 0x75, 0x20, 0x6c, 0x69, 0x62, 0x65, 0x72, 0x6f, 0x20, 0x65,
|
||||
0x67, 0x65, 0x74, 0x20, 0x61, 0x6e, 0x74, 0x65, 0x20, 0x76, 0x61, 0x72, 0x69, 0x75,
|
||||
0x73, 0x20, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x72, 0x61, 0x74, 0x20, 0x65, 0x67, 0x65,
|
||||
0x74, 0x20, 0x75, 0x74, 0x20, 0x6e, 0x69, 0x62, 0x68, 0x2e, 0x00, 0x00,
|
||||
];
|
||||
|
||||
let encrypted = [
|
||||
0x3f, 0x41, 0xde, 0x72, 0x6a, 0x7b, 0x71, 0x63, 0x59, 0x9e, 0x0f, 0x81, 0x31, 0x16,
|
||||
0x6d, 0xe7, 0x73, 0x5f, 0x1a, 0xe1, 0xa1, 0xec, 0x78, 0x1d, 0xde, 0x0a, 0xf7, 0xcf,
|
||||
0x1d, 0xbd, 0x21, 0xe9, 0xcd, 0xd3, 0xb6, 0xc1, 0xf0, 0xb7, 0x65, 0x43, 0xd4, 0xcb,
|
||||
0xa0, 0xf5, 0x38, 0x0f, 0x19, 0xcc, 0x09, 0x4f, 0x60, 0x29, 0x37, 0x05, 0x6d, 0x9e,
|
||||
0x05, 0xd8, 0xdc, 0x8b, 0x51, 0x20, 0x94, 0x5b, 0x15, 0xe7, 0x99, 0x14, 0x6b, 0x7b,
|
||||
0x18, 0x3d, 0x4a, 0xdb, 0xcc, 0xfd, 0xe7, 0xdc, 0x8a, 0x3b, 0x1d, 0xf4, 0x4e, 0x8c,
|
||||
0xfb, 0xfa, 0xec, 0x01, 0x10, 0x0e, 0x1d, 0x26, 0xad, 0x0c, 0xc6, 0x39, 0xb9, 0x58,
|
||||
0x9f, 0x1e, 0x51, 0xc5, 0xb1, 0x4b, 0x86, 0x72, 0xec, 0x09, 0xb3, 0x8c, 0x70, 0xd6,
|
||||
0xbc, 0xbd, 0x83, 0xfd, 0x53, 0xe3, 0x5d, 0xc8, 0xc7, 0xfe, 0xb4, 0xe0, 0x7f, 0x90,
|
||||
0x62, 0xf4, 0x3a, 0x74, 0xd1, 0x5f, 0xa8, 0x00, 0xb7, 0xe7, 0x2b, 0xd0, 0x43, 0x43,
|
||||
0xbc, 0xee, 0x7d, 0x50, 0x74, 0xe6, 0x10, 0x16, 0xbf, 0xb0, 0xf4, 0x0a, 0x82, 0x87,
|
||||
0x39, 0x63, 0x30, 0x4b, 0x5a, 0xd7, 0xb4, 0x2c, 0xa0, 0x87, 0xae, 0x00, 0x11, 0x73,
|
||||
0xc0, 0x10, 0x1b, 0x64, 0xb3, 0x69, 0xdc, 0x18, 0xd0, 0x43, 0x0f, 0xfb, 0xb5, 0x85,
|
||||
0xd7, 0x1b, 0x7b, 0xeb, 0x06, 0xb4, 0x17, 0x9e, 0x42, 0x37, 0xc8, 0xe4, 0x59, 0x64,
|
||||
0x99, 0x18, 0xd4, 0x4f, 0xef, 0x16, 0x97, 0x32, 0xb3, 0x8a, 0x5b, 0x31, 0x7a, 0xbc,
|
||||
0x36, 0x1c, 0x0e, 0xd9, 0x80, 0x57, 0x61, 0x7b, 0x81, 0x34, 0x2f, 0xcc, 0x48, 0xcf,
|
||||
0x81, 0x65, 0x99, 0xfb, 0xd1, 0x8e, 0x78, 0x90, 0xe6, 0x2e, 0x7a, 0xc2, 0x46, 0x61,
|
||||
0x94, 0x57, 0x57, 0x55, 0xc8, 0xf1, 0x06, 0x0e, 0x7c, 0xa0, 0x25, 0xb8, 0x1c, 0x41,
|
||||
0x9d, 0x65, 0x5f, 0xee, 0xd6, 0x21, 0x15, 0xf8, 0xa7, 0xd8, 0x1d, 0x8c, 0xc6, 0x78,
|
||||
0x36, 0x75, 0x2e, 0x04, 0x04, 0xba, 0x43, 0x36, 0x87, 0x5c, 0x05, 0x1e, 0x83, 0xdc,
|
||||
0x0d, 0xb6, 0x2b, 0x7d, 0x87, 0xf2, 0xc4, 0xf5, 0x64, 0xd0, 0x3d, 0xa0, 0x11, 0x74,
|
||||
0xc1, 0x22, 0x23, 0x98, 0x07, 0xed, 0x3e, 0x24, 0xbd, 0xbc, 0xe2, 0x3e, 0x65, 0xaf,
|
||||
0x98, 0x63, 0x09, 0x31, 0xb5, 0x5f, 0x07, 0x9a, 0x43, 0xb1, 0xcc, 0x15, 0xf0, 0x45,
|
||||
0xcb, 0xe7, 0xa2, 0xf0, 0x35, 0x75, 0x0c, 0x50, 0xf9, 0xfa, 0xba, 0xf8, 0x59, 0xb8,
|
||||
0x14, 0x1d, 0x15, 0x02, 0x1c, 0xa7, 0x56, 0x1a, 0x7f, 0xd5, 0xdd, 0x6e, 0x45, 0x3c,
|
||||
0x97, 0x1d, 0xca, 0x20, 0x53, 0x44, 0xc6, 0xe7, 0xb4, 0xcb, 0x0a, 0xd8, 0x37, 0x7a,
|
||||
0x2a, 0x3d, 0x17, 0x52, 0x34, 0x38, 0x2f, 0x7f, 0x6f, 0x99, 0x0f, 0x55, 0x31, 0x2c,
|
||||
0xf2, 0xed, 0xe2, 0xf4, 0x53, 0xa7, 0x71, 0x45, 0x18, 0x3b, 0xa9, 0x80, 0xb8, 0x7f,
|
||||
0x26, 0xca, 0x6c, 0x5e, 0xb0, 0xcf, 0x8b, 0xa1, 0x4d, 0x5e, 0x2a, 0x47, 0x9f, 0x90,
|
||||
0x82, 0x79, 0xd9, 0x9c, 0xb7, 0xd2, 0x7c, 0xf0, 0xcc, 0xc1, 0xd9, 0xe9, 0x0d, 0xf7,
|
||||
0xc6, 0x2a, 0xc1, 0x20, 0x90, 0x83, 0x43, 0x7e, 0x7b, 0x57, 0xd9, 0xcf, 0x5f, 0x3d,
|
||||
0x46, 0x60, 0xed, 0x93, 0x7b, 0xce, 0x9e, 0xd2, 0xa4, 0xf1, 0xd0, 0xde, 0x82, 0x0c,
|
||||
0xc7, 0xf6, 0xee, 0x0d, 0x8d, 0xce, 0x79, 0x87, 0x55, 0x88, 0x46, 0x77, 0x94, 0xee,
|
||||
0xf2, 0xcf, 0x0e, 0x48, 0xab, 0x04, 0x5d, 0xb7, 0xbb, 0x98, 0x64, 0xc6, 0x7c, 0xe3,
|
||||
0x79, 0xaf, 0x61, 0xfc, 0x38, 0x89, 0x70, 0x7b, 0x08, 0xda, 0x46, 0x79, 0xbf, 0x52,
|
||||
0x41, 0x08, 0xc1, 0x0b, 0x0a, 0x47, 0x29, 0x81, 0x08, 0xcf, 0x8f, 0x44, 0xca, 0xdb,
|
||||
0x47, 0x71, 0x0c, 0x31, 0x00, 0x31, 0x8b, 0x51, 0xc4, 0x34, 0x2c, 0xd8, 0xe1, 0x52,
|
||||
0xcb, 0xcf, 0xd0, 0xea, 0x56, 0xb8, 0x8a, 0x13, 0x4b, 0x21, 0x8b, 0xec, 0xb8, 0xc2,
|
||||
0xe7, 0x10, 0x4f, 0xea, 0x76, 0x05, 0x8e, 0x69, 0x98, 0x21, 0xe0, 0x57, 0xad, 0xa2,
|
||||
0x82, 0x55, 0x7a, 0x2a, 0x45, 0x63, 0x21, 0x26, 0xfa, 0xdd, 0x4f, 0x13, 0x6d, 0x89,
|
||||
0x63, 0x6b, 0x17, 0x41, 0xdf, 0x7b, 0xed, 0x48, 0x0a, 0x5e, 0xc0, 0x76, 0xa0, 0x31,
|
||||
0x52, 0x32, 0x2b, 0x01, 0x64, 0x0c, 0x3f, 0xc3, 0x79, 0x4a, 0xf6, 0xc7, 0x92, 0xb7,
|
||||
0x0c, 0x27, 0x10, 0xee, 0x90, 0x89, 0xe6, 0x44, 0x20, 0x8f, 0xa5, 0x66, 0xc4, 0x67,
|
||||
0x26, 0xf4, 0x31, 0xa8, 0x39, 0x5f, 0x40, 0x6e, 0x23, 0xd0, 0x2a, 0x6d, 0x20, 0x2a,
|
||||
0xc2, 0x2c, 0xf2, 0x21, 0xb4, 0xb1, 0x0d, 0x20, 0xd4, 0x06, 0x9e, 0x24, 0xd7, 0xd9,
|
||||
0x44, 0xd6, 0x7a, 0x89, 0xa8, 0x7a, 0xf0, 0x96, 0x85, 0x96, 0xdd, 0xa6, 0xb9, 0xaf,
|
||||
0x9a, 0x2d, 0xbe, 0xd3, 0xd3, 0xdd, 0xc0, 0x37, 0xc6, 0x39, 0x84, 0x65, 0x61, 0x36,
|
||||
0xa6, 0xee, 0x5f, 0x9e, 0x3d, 0x98, 0xda, 0xed, 0xc6, 0xb4, 0x7f, 0x55, 0xe0, 0xca,
|
||||
0x3f, 0xf5, 0xb4, 0x7e, 0xf8, 0x16, 0x28, 0x7d, 0x84, 0x09, 0x30, 0x7f, 0xe1, 0x25,
|
||||
0x8b, 0xa7, 0x00, 0x53, 0xa3, 0x20, 0x19, 0x6a, 0x4f, 0x3d, 0xf9, 0x8c, 0x09, 0x62,
|
||||
0x9d, 0xf6, 0x86, 0x32, 0xfb, 0x93, 0x68, 0xb7, 0x1c, 0x6d, 0x04, 0x7c, 0x0c, 0x38,
|
||||
0x3a, 0x59, 0x99, 0xd1, 0xa0, 0x41, 0x20, 0xa6, 0xe0, 0x8c, 0xba, 0x1d, 0x1e, 0xbd,
|
||||
0x22, 0x81, 0x60, 0xb2, 0x6d, 0x09, 0xa9, 0x78, 0xed, 0x27, 0x45, 0xb5,
|
||||
];
|
||||
|
||||
let mut buffer = decrypted.clone();
|
||||
|
||||
let mut encrypter = GCCrypter::new(seed);
|
||||
encrypter.crypt(&mut buffer);
|
||||
assert_eq!(encrypted, buffer);
|
||||
|
||||
let mut decrypter = GCCrypter::new(seed);
|
||||
decrypter.crypt(&mut buffer);
|
||||
assert_eq!(decrypted, buffer);
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
pub mod bytes;
|
||||
pub mod compression;
|
||||
pub mod encryption;
|
||||
pub mod packets;
|
||||
pub mod quest;
|
||||
pub mod text;
|
||||
mod utils;
|
|
@ -1,102 +0,0 @@
|
|||
use crate::text::{Language, LanguageError};
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod init;
|
||||
pub mod quest;
|
||||
|
||||
pub const PACKET_DEFAULT_LANGUAGE: Language = Language::English;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PacketError {
|
||||
#[error("Packet ID {0} is wrong for this packet type")]
|
||||
WrongId(u8),
|
||||
|
||||
#[error("Packet size {0} is wrong for this packet type")]
|
||||
WrongSize(u16),
|
||||
|
||||
#[error("I/O error while processing packet data")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("String field encoding error")]
|
||||
LanguageError(#[from] LanguageError),
|
||||
|
||||
#[error("Packet data format error: {0}")]
|
||||
DataFormatError(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[repr(C, packed)]
|
||||
pub struct PacketHeader {
|
||||
pub id: u8,
|
||||
pub flags: u8,
|
||||
pub size: u16,
|
||||
}
|
||||
|
||||
impl PacketHeader {
|
||||
pub const fn header_size() -> usize {
|
||||
std::mem::size_of::<Self>()
|
||||
}
|
||||
|
||||
pub fn from_bytes<T: ReadBytesExt>(reader: &mut T) -> Result<PacketHeader, PacketError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let id = reader.read_u8()?;
|
||||
let flags = reader.read_u8()?;
|
||||
let size = reader.read_u16::<LittleEndian>()?;
|
||||
Ok(PacketHeader { id, flags, size })
|
||||
}
|
||||
|
||||
pub fn write_bytes<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), PacketError> {
|
||||
writer.write_u8(self.id)?;
|
||||
writer.write_u8(self.flags)?;
|
||||
writer.write_u16::<LittleEndian>(self.size)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn id(&self) -> u8 {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn flags(&self) -> u8 {
|
||||
self.flags
|
||||
}
|
||||
|
||||
pub fn size(&self) -> u16 {
|
||||
self.size
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GenericPacket {
|
||||
pub header: PacketHeader,
|
||||
pub body: Box<[u8]>,
|
||||
}
|
||||
|
||||
impl GenericPacket {
|
||||
pub fn new(header: PacketHeader, body: Box<[u8]>) -> GenericPacket {
|
||||
GenericPacket { header, body }
|
||||
}
|
||||
|
||||
pub fn from_bytes<T: ReadBytesExt>(reader: &mut T) -> Result<GenericPacket, PacketError> {
|
||||
let header = PacketHeader::from_bytes(reader)?;
|
||||
let data_length = header.size as usize - PacketHeader::header_size();
|
||||
let mut body = vec![0u8; data_length];
|
||||
reader.read_exact(&mut body)?;
|
||||
Ok(GenericPacket {
|
||||
header,
|
||||
body: body.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write_bytes<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), PacketError> {
|
||||
self.header.write_bytes(writer)?;
|
||||
writer.write_all(self.body.as_ref())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn size(&self) -> usize {
|
||||
self.header.size as usize + self.body.len()
|
||||
}
|
||||
}
|
|
@ -1,282 +0,0 @@
|
|||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::io::Cursor;
|
||||
|
||||
use crate::bytes::ReadFixedLengthByteArray;
|
||||
use crate::packets::{GenericPacket, PacketError, PacketHeader};
|
||||
|
||||
pub const COPYRIGHT_MESSAGE_SIZE: usize = 64;
|
||||
|
||||
pub const LOGIN_SERVER_COPYRIGHT_MESSAGE: &[u8; COPYRIGHT_MESSAGE_SIZE] =
|
||||
b"DreamCast Port Map. Copyright SEGA Enterprises. 1999\0\0\0\0\0\0\0\0\0\0\0\0";
|
||||
pub const SHIP_SERVER_COPYRIGHT_MESSAGE: &[u8; COPYRIGHT_MESSAGE_SIZE] =
|
||||
b"DreamCast Lobby Server. Copyright SEGA Enterprises. 1999\0\0\0\0\0\0\0\0";
|
||||
|
||||
pub const PACKET_ID_INIT_ENCRYPTION_LOGIN_SERVER: u8 = 0x17;
|
||||
pub const PACKET_ID_INIT_ENCRYPTION_SHIP_SERVER: u8 = 0x02;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[repr(C, packed)]
|
||||
pub struct InitEncryptionPacket {
|
||||
pub header: PacketHeader,
|
||||
pub copyright_message: [u8; COPYRIGHT_MESSAGE_SIZE],
|
||||
pub server_key: u32,
|
||||
pub client_key: u32,
|
||||
}
|
||||
|
||||
impl InitEncryptionPacket {
|
||||
pub const fn packet_size() -> usize {
|
||||
std::mem::size_of::<Self>()
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
is_login_server: bool,
|
||||
server_key: u32,
|
||||
client_key: u32,
|
||||
) -> Result<InitEncryptionPacket, PacketError> {
|
||||
Ok(InitEncryptionPacket {
|
||||
header: PacketHeader {
|
||||
id: if is_login_server {
|
||||
PACKET_ID_INIT_ENCRYPTION_LOGIN_SERVER
|
||||
} else {
|
||||
PACKET_ID_INIT_ENCRYPTION_SHIP_SERVER
|
||||
},
|
||||
flags: 0,
|
||||
size: Self::packet_size() as u16,
|
||||
},
|
||||
copyright_message: if is_login_server {
|
||||
LOGIN_SERVER_COPYRIGHT_MESSAGE.clone()
|
||||
} else {
|
||||
SHIP_SERVER_COPYRIGHT_MESSAGE.clone()
|
||||
},
|
||||
server_key,
|
||||
client_key,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_bytes<T: ReadBytesExt>(reader: &mut T) -> Result<InitEncryptionPacket, PacketError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let header = PacketHeader::from_bytes(reader)?;
|
||||
Self::from_header_and_bytes(header, reader)
|
||||
}
|
||||
|
||||
pub fn from_header_and_bytes<T: ReadBytesExt>(
|
||||
header: PacketHeader,
|
||||
reader: &mut T,
|
||||
) -> Result<InitEncryptionPacket, PacketError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
if header.id != PACKET_ID_INIT_ENCRYPTION_LOGIN_SERVER
|
||||
&& header.id != PACKET_ID_INIT_ENCRYPTION_SHIP_SERVER
|
||||
{
|
||||
return Err(PacketError::WrongId(header.id));
|
||||
}
|
||||
if header.size < Self::packet_size() as u16 {
|
||||
return Err(PacketError::WrongSize(header.size));
|
||||
}
|
||||
|
||||
let copyright_message: [u8; COPYRIGHT_MESSAGE_SIZE] = reader.read_bytes()?;
|
||||
if copyright_message.ne(LOGIN_SERVER_COPYRIGHT_MESSAGE)
|
||||
&& copyright_message.ne(SHIP_SERVER_COPYRIGHT_MESSAGE)
|
||||
{
|
||||
return Err(PacketError::DataFormatError(String::from(
|
||||
"Unexpected copyright message string",
|
||||
)));
|
||||
}
|
||||
|
||||
let server_key = reader.read_u32::<LittleEndian>()?;
|
||||
let client_key = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
// if the packet contained extra bytes we need to read them from the buffer.
|
||||
// but we don't actually care about these extra bytes. we're not going to keep them ...
|
||||
if header.size > Self::packet_size() as u16 {
|
||||
let remaining_length = header.size as usize - Self::packet_size();
|
||||
let mut _throw_away = vec![0u8; remaining_length];
|
||||
reader.read_exact(&mut _throw_away)?;
|
||||
}
|
||||
|
||||
Ok(InitEncryptionPacket {
|
||||
header,
|
||||
copyright_message,
|
||||
server_key,
|
||||
client_key,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write_body_bytes<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), PacketError> {
|
||||
writer.write_all(&self.copyright_message)?;
|
||||
writer.write_u32::<LittleEndian>(self.server_key)?;
|
||||
writer.write_u32::<LittleEndian>(self.client_key)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_bytes<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), PacketError> {
|
||||
self.header.write_bytes(writer)?;
|
||||
self.write_body_bytes(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn server_key(&self) -> u32 {
|
||||
self.server_key
|
||||
}
|
||||
|
||||
pub fn client_key(&self) -> u32 {
|
||||
self.client_key
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<GenericPacket> for InitEncryptionPacket {
|
||||
type Error = PacketError;
|
||||
|
||||
fn try_from(value: GenericPacket) -> Result<Self, Self::Error> {
|
||||
let mut reader = Cursor::new(value.body);
|
||||
Ok(InitEncryptionPacket::from_header_and_bytes(
|
||||
value.header,
|
||||
&mut reader,
|
||||
)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<GenericPacket> for InitEncryptionPacket {
|
||||
type Error = PacketError;
|
||||
|
||||
fn try_into(self) -> Result<GenericPacket, Self::Error> {
|
||||
let header = self.header;
|
||||
let mut body = Vec::new();
|
||||
self.write_body_bytes(&mut body)?;
|
||||
Ok(GenericPacket {
|
||||
header,
|
||||
body: body.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use claim::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
pub fn create_init_encryption_packet_from_bytes() -> Result<(), PacketError> {
|
||||
// login server packet
|
||||
|
||||
let mut bytes: &[u8] = &[
|
||||
0x17, 0x00, 0x4c, 0x00, 0x44, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x61, 0x73, 0x74, 0x20,
|
||||
0x50, 0x6f, 0x72, 0x74, 0x20, 0x4d, 0x61, 0x70, 0x2e, 0x20, 0x43, 0x6f, 0x70, 0x79,
|
||||
0x72, 0x69, 0x67, 0x68, 0x74, 0x20, 0x53, 0x45, 0x47, 0x41, 0x20, 0x45, 0x6e, 0x74,
|
||||
0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x73, 0x2e, 0x20, 0x31, 0x39, 0x39, 0x39,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x50, 0x5f,
|
||||
0x48, 0x1e, 0x50, 0x5f, 0x48, 0x1e,
|
||||
];
|
||||
|
||||
let packet = InitEncryptionPacket::from_bytes(&mut bytes)?;
|
||||
assert_eq!(packet.header.id(), PACKET_ID_INIT_ENCRYPTION_LOGIN_SERVER);
|
||||
assert_eq!(
|
||||
packet.header.size(),
|
||||
InitEncryptionPacket::packet_size() as u16
|
||||
);
|
||||
assert_eq!(packet.copyright_message, *LOGIN_SERVER_COPYRIGHT_MESSAGE);
|
||||
assert_eq!(packet.server_key(), 0x1e485f50);
|
||||
assert_eq!(packet.client_key(), 0x1e485f50);
|
||||
|
||||
// ship server packet
|
||||
|
||||
let mut bytes: &[u8] = &[
|
||||
0x02, 0x00, 0x4c, 0x00, 0x44, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x61, 0x73, 0x74, 0x20,
|
||||
0x4c, 0x6f, 0x62, 0x62, 0x79, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x20,
|
||||
0x43, 0x6f, 0x70, 0x79, 0x72, 0x69, 0x67, 0x68, 0x74, 0x20, 0x53, 0x45, 0x47, 0x41,
|
||||
0x20, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x73, 0x2e, 0x20,
|
||||
0x31, 0x39, 0x39, 0x39, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0xf7,
|
||||
0x50, 0x90, 0x23, 0xe5, 0x6e, 0x1c,
|
||||
];
|
||||
|
||||
let packet = InitEncryptionPacket::from_bytes(&mut bytes)?;
|
||||
assert_eq!(packet.header.id(), PACKET_ID_INIT_ENCRYPTION_SHIP_SERVER);
|
||||
assert_eq!(
|
||||
packet.header.size(),
|
||||
InitEncryptionPacket::packet_size() as u16
|
||||
);
|
||||
assert_eq!(packet.copyright_message, *SHIP_SERVER_COPYRIGHT_MESSAGE);
|
||||
assert_eq!(packet.server_key(), 0x9050f737);
|
||||
assert_eq!(packet.client_key(), 0x1c6ee523);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn create_init_encryption_packet_via_new() -> Result<(), PacketError> {
|
||||
// login server packet
|
||||
|
||||
let packet = InitEncryptionPacket::new(true, 0x11223344, 0x55667788)?;
|
||||
assert_eq!(packet.header.id(), PACKET_ID_INIT_ENCRYPTION_LOGIN_SERVER);
|
||||
assert_eq!(
|
||||
packet.header.size(),
|
||||
InitEncryptionPacket::packet_size() as u16
|
||||
);
|
||||
assert_eq!(packet.copyright_message, *LOGIN_SERVER_COPYRIGHT_MESSAGE);
|
||||
assert_eq!(packet.server_key(), 0x11223344);
|
||||
assert_eq!(packet.client_key(), 0x55667788);
|
||||
|
||||
// ship server packet
|
||||
|
||||
let packet = InitEncryptionPacket::new(false, 0x44332211, 0x88776655)?;
|
||||
assert_eq!(packet.header.id(), PACKET_ID_INIT_ENCRYPTION_SHIP_SERVER);
|
||||
assert_eq!(
|
||||
packet.header.size(),
|
||||
InitEncryptionPacket::packet_size() as u16
|
||||
);
|
||||
assert_eq!(packet.copyright_message, *SHIP_SERVER_COPYRIGHT_MESSAGE);
|
||||
assert_eq!(packet.server_key(), 0x44332211);
|
||||
assert_eq!(packet.client_key(), 0x88776655);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn can_create_init_encryption_packet_from_bytes_with_extra_bytes_included(
|
||||
) -> Result<(), PacketError> {
|
||||
// the extra bytes that can be included in this packet are basically always ignored
|
||||
// and we don't even provide any way to access them once the packet struct is created
|
||||
// (they are skipped over).
|
||||
// i don't think this is a big deal, but it is worth mentioning ...
|
||||
|
||||
let mut bytes: &[u8] = &[
|
||||
0x17, 0x00, 0x10, 0x01, 0x44, 0x72, 0x65, 0x61, 0x6d, 0x43, 0x61, 0x73, 0x74, 0x20,
|
||||
0x50, 0x6f, 0x72, 0x74, 0x20, 0x4d, 0x61, 0x70, 0x2e, 0x20, 0x43, 0x6f, 0x70, 0x79,
|
||||
0x72, 0x69, 0x67, 0x68, 0x74, 0x20, 0x53, 0x45, 0x47, 0x41, 0x20, 0x45, 0x6e, 0x74,
|
||||
0x65, 0x72, 0x70, 0x72, 0x69, 0x73, 0x65, 0x73, 0x2e, 0x20, 0x31, 0x39, 0x39, 0x39,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x94, 0x39,
|
||||
0x25, 0x4b, 0x1a, 0x23, 0x2f, 0x72, 0x54, 0x68, 0x69, 0x73, 0x20, 0x73, 0x65, 0x72,
|
||||
0x76, 0x65, 0x72, 0x20, 0x69, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x6e, 0x6f, 0x20, 0x77,
|
||||
0x61, 0x79, 0x20, 0x61, 0x66, 0x66, 0x69, 0x6c, 0x69, 0x61, 0x74, 0x65, 0x64, 0x2c,
|
||||
0x20, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x6f, 0x72, 0x65, 0x64, 0x2c, 0x20, 0x6f, 0x72,
|
||||
0x20, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x20, 0x62, 0x79, 0x20,
|
||||
0x53, 0x45, 0x47, 0x41, 0x20, 0x45, 0x6e, 0x74, 0x65, 0x72, 0x70, 0x72, 0x69, 0x73,
|
||||
0x65, 0x73, 0x20, 0x6f, 0x72, 0x20, 0x53, 0x4f, 0x4e, 0x49, 0x43, 0x54, 0x45, 0x41,
|
||||
0x4d, 0x2e, 0x20, 0x54, 0x68, 0x65, 0x20, 0x70, 0x72, 0x65, 0x63, 0x65, 0x64, 0x69,
|
||||
0x6e, 0x67, 0x20, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x20, 0x65, 0x78, 0x69,
|
||||
0x73, 0x74, 0x73, 0x20, 0x6f, 0x6e, 0x6c, 0x79, 0x20, 0x69, 0x6e, 0x20, 0x6f, 0x72,
|
||||
0x64, 0x65, 0x72, 0x20, 0x74, 0x6f, 0x20, 0x72, 0x65, 0x6d, 0x61, 0x69, 0x6e, 0x20,
|
||||
0x63, 0x6f, 0x6d, 0x70, 0x61, 0x74, 0x69, 0x62, 0x6c, 0x65, 0x20, 0x77, 0x69, 0x74,
|
||||
0x68, 0x20, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x61, 0x6d, 0x73, 0x20, 0x74, 0x68, 0x61,
|
||||
0x74, 0x20, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x20, 0x69, 0x74, 0x2e, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
];
|
||||
|
||||
let packet = InitEncryptionPacket::from_bytes(&mut bytes)?;
|
||||
assert_eq!(packet.header.id(), PACKET_ID_INIT_ENCRYPTION_LOGIN_SERVER);
|
||||
assert_ge!(
|
||||
packet.header.size(),
|
||||
InitEncryptionPacket::packet_size() as u16
|
||||
);
|
||||
assert_eq!(packet.copyright_message, *LOGIN_SERVER_COPYRIGHT_MESSAGE);
|
||||
assert_eq!(packet.server_key(), 0x4b253994);
|
||||
assert_eq!(packet.client_key(), 0x722f231a);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,721 +0,0 @@
|
|||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
|
||||
use crate::bytes::{FixedLengthByteArrays, ReadFixedLengthByteArray};
|
||||
use crate::packets::{PacketError, PacketHeader};
|
||||
use crate::text::{Language, LanguageError};
|
||||
|
||||
// TODO: think a bit more about this? is this the best way to do this? it's probably ok ...
|
||||
pub const PACKET_FILENAME_LANGUAGE: Language = Language::English;
|
||||
|
||||
pub const QUEST_HEADER_NAME_SIZE: usize = 32;
|
||||
pub const QUEST_HEADER_FILENAME_SIZE: usize = 16;
|
||||
|
||||
pub const PACKET_ID_QUEST_HEADER_ONLINE: u8 = 0x44;
|
||||
pub const PACKET_ID_QUEST_HEADER_OFFLINE: u8 = 0xa6;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum QuestPacketFileType {
|
||||
Bin,
|
||||
Dat,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl QuestPacketFileType {
|
||||
pub fn from(source: &[u8]) -> QuestPacketFileType {
|
||||
let source = source.as_unpadded_slice();
|
||||
if source.ends_with(b".bin") {
|
||||
QuestPacketFileType::Bin
|
||||
} else if source.ends_with(b".dat") {
|
||||
QuestPacketFileType::Dat
|
||||
} else {
|
||||
QuestPacketFileType::Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[repr(C, packed)]
|
||||
pub struct QuestHeaderPacket {
|
||||
pub header: PacketHeader,
|
||||
pub name: [u8; QUEST_HEADER_NAME_SIZE],
|
||||
pub unused: u16,
|
||||
pub flags: u16,
|
||||
pub filename: [u8; QUEST_HEADER_FILENAME_SIZE],
|
||||
pub size: u32,
|
||||
}
|
||||
|
||||
impl QuestHeaderPacket {
|
||||
pub const fn packet_size() -> usize {
|
||||
std::mem::size_of::<Self>()
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
name: &str,
|
||||
language: Language,
|
||||
filename: &str,
|
||||
data_size: usize,
|
||||
is_online: bool,
|
||||
) -> Result<QuestHeaderPacket, PacketError> {
|
||||
if name.len() > QUEST_HEADER_NAME_SIZE {
|
||||
return Err(PacketError::DataFormatError(format!(
|
||||
"name is too large ({} characters, should be {} or less)",
|
||||
name.len(),
|
||||
QUEST_HEADER_NAME_SIZE
|
||||
)));
|
||||
}
|
||||
if filename.len() > QUEST_HEADER_FILENAME_SIZE {
|
||||
return Err(PacketError::DataFormatError(format!(
|
||||
"filename is too large ({} characters, should be {} or less)",
|
||||
filename.len(),
|
||||
QUEST_HEADER_FILENAME_SIZE
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(QuestHeaderPacket {
|
||||
header: PacketHeader {
|
||||
id: if is_online {
|
||||
PACKET_ID_QUEST_HEADER_ONLINE
|
||||
} else {
|
||||
PACKET_ID_QUEST_HEADER_OFFLINE
|
||||
},
|
||||
flags: 0,
|
||||
size: Self::packet_size() as u16,
|
||||
},
|
||||
name: language.encode_text(name)?.to_array(),
|
||||
unused: 0,
|
||||
flags: 0,
|
||||
filename: PACKET_FILENAME_LANGUAGE.encode_text(filename)?.to_array(),
|
||||
size: data_size as u32,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_bytes<T: ReadBytesExt>(reader: &mut T) -> Result<QuestHeaderPacket, PacketError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let header = PacketHeader::from_bytes(reader)?;
|
||||
Self::from_header_and_bytes(header, reader)
|
||||
}
|
||||
|
||||
pub fn from_header_and_bytes<T: ReadBytesExt>(
|
||||
header: PacketHeader,
|
||||
reader: &mut T,
|
||||
) -> Result<Self, PacketError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
if header.id != PACKET_ID_QUEST_HEADER_ONLINE && header.id != PACKET_ID_QUEST_HEADER_OFFLINE
|
||||
{
|
||||
return Err(PacketError::WrongId(header.id));
|
||||
}
|
||||
if header.size != Self::packet_size() as u16 {
|
||||
return Err(PacketError::WrongSize(header.size));
|
||||
}
|
||||
|
||||
let name: [u8; QUEST_HEADER_NAME_SIZE] = reader.read_bytes()?;
|
||||
let unused = reader.read_u16::<LittleEndian>()?;
|
||||
let flags = reader.read_u16::<LittleEndian>()?;
|
||||
let filename: [u8; QUEST_HEADER_FILENAME_SIZE] = reader.read_bytes()?;
|
||||
let size = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
Ok(QuestHeaderPacket {
|
||||
header,
|
||||
name,
|
||||
unused,
|
||||
flags,
|
||||
filename,
|
||||
size,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write_bytes<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), PacketError> {
|
||||
self.header.write_bytes(writer)?;
|
||||
writer.write_all(&self.name)?;
|
||||
writer.write_u16::<LittleEndian>(self.unused)?;
|
||||
writer.write_u16::<LittleEndian>(self.flags)?;
|
||||
writer.write_all(&self.filename)?;
|
||||
writer.write_u32::<LittleEndian>(self.size)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn name_str(&self) -> Result<String, LanguageError> {
|
||||
PACKET_FILENAME_LANGUAGE.decode_text(self.name.as_unpadded_slice())
|
||||
}
|
||||
|
||||
pub fn filename_str(&self) -> Result<String, LanguageError> {
|
||||
PACKET_FILENAME_LANGUAGE.decode_text(self.filename.as_unpadded_slice())
|
||||
}
|
||||
|
||||
pub fn file_type(&self) -> QuestPacketFileType {
|
||||
QuestPacketFileType::from(&self.filename)
|
||||
}
|
||||
}
|
||||
|
||||
pub const PACKET_ID_QUEST_DATA_ONLINE: u8 = 0x13;
|
||||
pub const PACKET_ID_QUEST_DATA_OFFLINE: u8 = 0xa7;
|
||||
pub const QUEST_DATA_PACKET_DATA_SIZE: usize = 1024;
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[repr(C, packed)]
|
||||
pub struct QuestDataPacket {
|
||||
pub header: PacketHeader,
|
||||
pub filename: [u8; QUEST_HEADER_FILENAME_SIZE],
|
||||
pub data: [u8; QUEST_DATA_PACKET_DATA_SIZE],
|
||||
pub size: u32,
|
||||
}
|
||||
|
||||
impl QuestDataPacket {
|
||||
pub const fn packet_size() -> usize {
|
||||
std::mem::size_of::<Self>()
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
filename: &str,
|
||||
src: &[u8],
|
||||
is_online: bool,
|
||||
) -> Result<QuestDataPacket, PacketError> {
|
||||
if filename.len() > QUEST_HEADER_FILENAME_SIZE {
|
||||
return Err(PacketError::DataFormatError(format!(
|
||||
"filename is too large ({} characters, should be {} or less)",
|
||||
filename.len(),
|
||||
QUEST_HEADER_FILENAME_SIZE
|
||||
)));
|
||||
}
|
||||
if src.len() > QUEST_DATA_PACKET_DATA_SIZE {
|
||||
return Err(PacketError::DataFormatError(format!(
|
||||
"Data buffer is too large ({} bytes, should be {} or less)",
|
||||
src.len(),
|
||||
QUEST_DATA_PACKET_DATA_SIZE
|
||||
)));
|
||||
}
|
||||
|
||||
// remaining bytes (if any) will be zeros
|
||||
let mut data = [0u8; QUEST_DATA_PACKET_DATA_SIZE];
|
||||
data[0..src.len()].copy_from_slice(src);
|
||||
|
||||
Ok(QuestDataPacket {
|
||||
header: PacketHeader {
|
||||
id: if is_online {
|
||||
PACKET_ID_QUEST_DATA_ONLINE
|
||||
} else {
|
||||
PACKET_ID_QUEST_DATA_OFFLINE
|
||||
},
|
||||
flags: 0,
|
||||
size: Self::packet_size() as u16,
|
||||
},
|
||||
filename: PACKET_FILENAME_LANGUAGE.encode_text(filename)?.to_array(),
|
||||
data,
|
||||
size: src.len() as u32,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_bytes<T: ReadBytesExt>(reader: &mut T) -> Result<QuestDataPacket, PacketError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let header = PacketHeader::from_bytes(reader)?;
|
||||
Self::from_header_and_bytes(header, reader)
|
||||
}
|
||||
|
||||
pub fn from_header_and_bytes<T: ReadBytesExt>(
|
||||
header: PacketHeader,
|
||||
reader: &mut T,
|
||||
) -> Result<QuestDataPacket, PacketError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
if header.id != PACKET_ID_QUEST_DATA_ONLINE && header.id != PACKET_ID_QUEST_DATA_OFFLINE {
|
||||
return Err(PacketError::WrongId(header.id));
|
||||
}
|
||||
if header.size != Self::packet_size() as u16 {
|
||||
return Err(PacketError::WrongSize(header.size));
|
||||
}
|
||||
|
||||
let filename: [u8; QUEST_HEADER_FILENAME_SIZE] = reader.read_bytes()?;
|
||||
let data: [u8; QUEST_DATA_PACKET_DATA_SIZE] = reader.read_bytes()?;
|
||||
let size = reader.read_u32::<LittleEndian>()?;
|
||||
if size > QUEST_DATA_PACKET_DATA_SIZE as u32 {
|
||||
return Err(PacketError::DataFormatError(format!(
|
||||
"Data chunk size field value is too large: {}",
|
||||
size
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(QuestDataPacket {
|
||||
header,
|
||||
filename,
|
||||
data,
|
||||
size,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write_bytes<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), PacketError> {
|
||||
self.header.write_bytes(writer)?;
|
||||
writer.write_all(&self.filename)?;
|
||||
writer.write_all(&self.data)?;
|
||||
writer.write_u32::<LittleEndian>(self.size)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn filename_str(&self) -> Result<String, LanguageError> {
|
||||
PACKET_FILENAME_LANGUAGE.decode_text(self.filename.as_unpadded_slice())
|
||||
}
|
||||
|
||||
pub fn file_type(&self) -> QuestPacketFileType {
|
||||
QuestPacketFileType::from(&self.filename)
|
||||
}
|
||||
|
||||
pub fn data_size(&self) -> u32 {
|
||||
self.size
|
||||
}
|
||||
|
||||
pub fn data(&self) -> &[u8] {
|
||||
&self.data[0..self.size as usize]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use claim::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
pub fn create_quest_header_packet_from_bytes() -> Result<(), PacketError> {
|
||||
// dat
|
||||
let mut bytes: &[u8] = &[
|
||||
0xA6, 0xC9, 0x3C, 0x00, 0x4C, 0x6F, 0x73, 0x74, 0x20, 0x48, 0x45, 0x41, 0x54, 0x20,
|
||||
0x53, 0x57, 0x4F, 0x52, 0x44, 0x3A, 0x32, 0x2D, 0x31, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x64, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x35, 0x38, 0x2E, 0x64, 0x61, 0x74, 0x00, 0x00, 0x00, 0x00,
|
||||
0x01, 0x3B, 0x00, 0x00,
|
||||
];
|
||||
|
||||
let packet = QuestHeaderPacket::from_bytes(&mut bytes)?;
|
||||
assert_eq!(packet.header.id(), PACKET_ID_QUEST_HEADER_OFFLINE);
|
||||
assert_eq!(
|
||||
packet.header.size(),
|
||||
QuestHeaderPacket::packet_size() as u16
|
||||
);
|
||||
assert_eq!(packet.name_str()?, "Lost HEAT SWORD:2-1");
|
||||
assert_eq!(packet.filename_str()?, "dquest58.dat");
|
||||
assert_eq!(packet.file_type(), QuestPacketFileType::Dat);
|
||||
|
||||
// bin
|
||||
let mut bytes: &[u8] = &[
|
||||
0xA6, 0x88, 0x3C, 0x00, 0x4C, 0x6F, 0x73, 0x74, 0x20, 0x48, 0x45, 0x41, 0x54, 0x20,
|
||||
0x53, 0x57, 0x4F, 0x52, 0x44, 0x3A, 0x32, 0x2D, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x71,
|
||||
0x75, 0x65, 0x73, 0x74, 0x35, 0x38, 0x2E, 0x62, 0x69, 0x6E, 0x00, 0x00, 0x00, 0x00,
|
||||
0x23, 0x06, 0x00, 0x00,
|
||||
];
|
||||
|
||||
let packet = QuestHeaderPacket::from_bytes(&mut bytes)?;
|
||||
assert_eq!(packet.header.id(), PACKET_ID_QUEST_HEADER_OFFLINE);
|
||||
assert_eq!(
|
||||
packet.header.size(),
|
||||
QuestHeaderPacket::packet_size() as u16
|
||||
);
|
||||
assert_eq!(packet.name_str()?, "Lost HEAT SWORD:2-2");
|
||||
assert_eq!(packet.filename_str()?, "dquest58.bin");
|
||||
assert_eq!(packet.file_type(), QuestPacketFileType::Bin);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn create_quest_data_packet_from_bytes() -> Result<(), PacketError> {
|
||||
// dat
|
||||
let mut bytes: &[u8] = &[
|
||||
0xA7, 0x00, 0x18, 0x04, 0x64, 0x71, 0x75, 0x65, 0x73, 0x74, 0x35, 0x38, 0x2E, 0x64,
|
||||
0x61, 0x74, 0x00, 0x00, 0x00, 0x00, 0xDC, 0x8F, 0x00, 0x00, 0x1B, 0x09, 0x38, 0x63,
|
||||
0x7B, 0xC2, 0x03, 0xBE, 0x96, 0x53, 0xE7, 0xA5, 0x12, 0x9B, 0x0B, 0xC8, 0x22, 0x17,
|
||||
0x7E, 0x12, 0xC8, 0xF4, 0x81, 0x2A, 0x3D, 0x22, 0x77, 0x03, 0x73, 0x96, 0x62, 0xDE,
|
||||
0x99, 0xC7, 0xD6, 0xB3, 0x04, 0x0C, 0x7F, 0x53, 0x5E, 0x28, 0xEF, 0xB4, 0x4D, 0x08,
|
||||
0xA7, 0xA2, 0xD0, 0x86, 0x6A, 0x5D, 0xA8, 0xB9, 0x9A, 0x4E, 0x60, 0x37, 0x3D, 0x56,
|
||||
0x9D, 0xF4, 0x5C, 0xF2, 0xCC, 0x7A, 0xF3, 0x81, 0x60, 0x8F, 0x71, 0xB4, 0xD3, 0x1E,
|
||||
0xE0, 0xAE, 0xE2, 0x0B, 0xF5, 0xF6, 0x69, 0x0D, 0x91, 0x62, 0x2E, 0xE4, 0x31, 0x84,
|
||||
0xF6, 0x8F, 0xD0, 0xEB, 0x72, 0x14, 0x70, 0x6B, 0xD2, 0xF7, 0x95, 0x7C, 0xA0, 0x5C,
|
||||
0xA1, 0x11, 0xC2, 0x5B, 0xD9, 0x1A, 0x30, 0x12, 0xE1, 0x74, 0x20, 0x8A, 0xA6, 0x58,
|
||||
0xDF, 0xF1, 0x6A, 0xA3, 0xA0, 0xC7, 0xAB, 0x93, 0x39, 0x76, 0xE7, 0xD2, 0x6A, 0x7B,
|
||||
0xB5, 0x69, 0x84, 0x64, 0x15, 0x9A, 0x7C, 0x3C, 0xDD, 0x4F, 0x4D, 0xB8, 0x67, 0x85,
|
||||
0xC8, 0x1D, 0xC7, 0x59, 0x73, 0x54, 0x7C, 0xEE, 0xA9, 0x72, 0xED, 0xCF, 0x5B, 0x0E,
|
||||
0x05, 0x4C, 0x3C, 0x67, 0xEC, 0x3E, 0xF4, 0x00, 0xF1, 0x67, 0x1B, 0xEB, 0xCC, 0x78,
|
||||
0x2D, 0x68, 0x26, 0xB2, 0x5E, 0x60, 0x69, 0x2B, 0x42, 0xB3, 0x91, 0xF8, 0xBF, 0xD1,
|
||||
0x85, 0xEA, 0x2E, 0x41, 0xDD, 0xD3, 0x09, 0x5F, 0x2A, 0xCF, 0xD3, 0x10, 0x8D, 0xA2,
|
||||
0x8F, 0x8A, 0x7B, 0x4B, 0xF3, 0x6A, 0x61, 0xD5, 0x2A, 0x32, 0x3C, 0x28, 0x74, 0x7E,
|
||||
0xD6, 0x7B, 0x11, 0x46, 0xC9, 0x36, 0x5B, 0xA3, 0x27, 0x9A, 0xE1, 0xEC, 0x40, 0x83,
|
||||
0x24, 0x57, 0xA8, 0x09, 0xB1, 0x63, 0xDC, 0x33, 0x69, 0x7A, 0x17, 0x4E, 0x51, 0x7B,
|
||||
0x7F, 0x16, 0xFD, 0x63, 0x50, 0x3A, 0x34, 0x5E, 0x64, 0x20, 0x3A, 0xEB, 0x71, 0x42,
|
||||
0x53, 0x83, 0xE9, 0xDF, 0x73, 0xF6, 0x1C, 0xBB, 0x83, 0x1B, 0x28, 0x03, 0x20, 0x48,
|
||||
0xB9, 0x73, 0xF4, 0x13, 0x0D, 0x6D, 0x38, 0x02, 0x44, 0x2D, 0xEB, 0x3A, 0x0D, 0x9A,
|
||||
0xFC, 0x59, 0x5B, 0x56, 0x47, 0x13, 0xB3, 0x00, 0xE8, 0x22, 0x22, 0x6F, 0xAF, 0x8D,
|
||||
0x60, 0x69, 0xB4, 0x25, 0xD2, 0x92, 0xE9, 0xD9, 0xBE, 0xB3, 0x50, 0x53, 0xBB, 0xF8,
|
||||
0x3F, 0xE9, 0x07, 0xD3, 0x7B, 0xAD, 0x46, 0x67, 0xCF, 0xA8, 0xDD, 0x14, 0xFD, 0x42,
|
||||
0x36, 0x27, 0x3A, 0x34, 0x75, 0x8E, 0xC8, 0xEB, 0xFD, 0x72, 0x84, 0x84, 0xF5, 0x88,
|
||||
0xF6, 0x86, 0xB6, 0x8F, 0x71, 0x0F, 0x73, 0xC8, 0x95, 0x79, 0x2A, 0x16, 0x04, 0x1A,
|
||||
0x05, 0x83, 0x30, 0xE9, 0xC9, 0x6A, 0xDF, 0xCA, 0x42, 0x24, 0xE4, 0xE9, 0x97, 0x81,
|
||||
0xC0, 0x0B, 0xD7, 0x3A, 0x84, 0xAD, 0xCE, 0x06, 0xCE, 0xAD, 0xA7, 0x50, 0xA1, 0xD4,
|
||||
0xE9, 0x7B, 0x0D, 0x77, 0xF7, 0x1F, 0xCF, 0x0F, 0xBD, 0x3D, 0x97, 0x11, 0x47, 0xCE,
|
||||
0x46, 0x83, 0x8E, 0x98, 0x1B, 0xDB, 0xFA, 0xEC, 0xB1, 0x50, 0xD7, 0x3A, 0x2F, 0xC6,
|
||||
0xB8, 0x4B, 0xE9, 0xF8, 0xE5, 0x01, 0x5C, 0xC7, 0x6B, 0x10, 0x9B, 0xE5, 0x6E, 0xEE,
|
||||
0x08, 0x12, 0xE5, 0xE6, 0x9B, 0xE6, 0x75, 0x26, 0xD8, 0x41, 0xB4, 0x31, 0xCE, 0x54,
|
||||
0xD1, 0x71, 0x92, 0x67, 0x4E, 0xE5, 0x7C, 0x3F, 0x70, 0x02, 0x1B, 0xFC, 0x7B, 0x8F,
|
||||
0x98, 0x0A, 0xF7, 0x09, 0x2D, 0x8D, 0xC7, 0x88, 0x65, 0x05, 0x60, 0x91, 0x77, 0x9E,
|
||||
0x16, 0x02, 0x32, 0xF7, 0x78, 0x52, 0xD2, 0x0D, 0x91, 0x23, 0xD9, 0xCC, 0x23, 0x21,
|
||||
0x77, 0xD4, 0x19, 0x55, 0x19, 0xB6, 0x68, 0xB7, 0xE8, 0xF7, 0x6B, 0x2C, 0x5F, 0x92,
|
||||
0x59, 0x8E, 0x9D, 0x79, 0xAE, 0x48, 0xE0, 0xDD, 0x3D, 0x7E, 0x09, 0x52, 0x06, 0xEC,
|
||||
0x70, 0x3D, 0x5F, 0xB3, 0x19, 0xB0, 0xD2, 0x7C, 0xF7, 0xA1, 0xFA, 0xAD, 0x78, 0x24,
|
||||
0x23, 0xB6, 0x89, 0xAC, 0x69, 0x9B, 0xAB, 0xF1, 0xD6, 0xA7, 0x78, 0x45, 0x63, 0xDD,
|
||||
0x00, 0x3A, 0x3C, 0xD0, 0x54, 0x77, 0xEF, 0xE5, 0xD7, 0x0B, 0xA7, 0x72, 0x92, 0x7F,
|
||||
0xAF, 0xD2, 0xBD, 0x7D, 0xFA, 0x24, 0x69, 0x6F, 0xAE, 0x77, 0x76, 0x78, 0x44, 0x0D,
|
||||
0x28, 0x6F, 0xB8, 0xA5, 0x1A, 0xE6, 0x2F, 0x23, 0xD3, 0x7A, 0xB4, 0x18, 0x55, 0x02,
|
||||
0x33, 0xD6, 0x00, 0xD1, 0x8F, 0xAB, 0x6D, 0x64, 0x62, 0x12, 0x5B, 0x8C, 0x79, 0xE6,
|
||||
0xFF, 0xEE, 0x64, 0x5F, 0xBC, 0xE5, 0xF2, 0xC2, 0x0A, 0xB0, 0x9B, 0x8E, 0x54, 0x75,
|
||||
0x7D, 0x06, 0x6B, 0xDE, 0x42, 0x30, 0x34, 0xA1, 0xA1, 0x93, 0xC9, 0xFB, 0x37, 0x08,
|
||||
0xE3, 0x28, 0x23, 0x6F, 0x6A, 0x5D, 0x98, 0x6F, 0x63, 0x23, 0x60, 0x4E, 0xCB, 0xF2,
|
||||
0xB4, 0x0C, 0x4B, 0x95, 0x3D, 0xCE, 0xBC, 0xF7, 0x91, 0x2A, 0xAE, 0x71, 0x9D, 0x8C,
|
||||
0x86, 0x28, 0x19, 0x40, 0x85, 0xE2, 0x94, 0x5C, 0xA5, 0xB8, 0xDA, 0x0B, 0x96, 0x67,
|
||||
0x72, 0xA5, 0xED, 0xAC, 0xAD, 0x4C, 0x7D, 0x2E, 0x84, 0xDE, 0x33, 0x9A, 0x36, 0xF9,
|
||||
0x3D, 0xA0, 0x54, 0x35, 0x2E, 0xA6, 0x8F, 0xD6, 0x02, 0x23, 0x97, 0x0B, 0xE4, 0x31,
|
||||
0xBD, 0x90, 0xAA, 0x61, 0xC5, 0x42, 0x14, 0xF9, 0x82, 0xF8, 0x88, 0x44, 0xF3, 0xA8,
|
||||
0xAA, 0x91, 0xAE, 0xA3, 0xB3, 0x59, 0xF5, 0x79, 0x3D, 0xDB, 0xFC, 0x60, 0xA0, 0xEB,
|
||||
0x5B, 0x55, 0xAF, 0xF1, 0xD7, 0x37, 0xF1, 0x68, 0xFC, 0x1C, 0x49, 0x5E, 0xF5, 0x89,
|
||||
0x03, 0x46, 0x71, 0xB1, 0x3F, 0x04, 0x33, 0x3D, 0x17, 0x10, 0x30, 0x31, 0xE9, 0x0B,
|
||||
0xDD, 0x7E, 0x4B, 0x84, 0x9B, 0x95, 0x5F, 0x18, 0x2A, 0xC9, 0x5F, 0xFE, 0xF8, 0x7E,
|
||||
0x70, 0xA7, 0x50, 0xC3, 0x93, 0x59, 0x61, 0x2E, 0x05, 0x06, 0x2B, 0x65, 0x1F, 0xB2,
|
||||
0xD5, 0x88, 0x8C, 0x42, 0x01, 0x42, 0xDF, 0x93, 0x63, 0x15, 0x33, 0x71, 0x92, 0x09,
|
||||
0x0A, 0xEA, 0x1B, 0x1F, 0x08, 0x39, 0xCC, 0x69, 0x0A, 0x13, 0xE2, 0x52, 0x01, 0x64,
|
||||
0x7E, 0xAF, 0xC7, 0x2B, 0x93, 0x10, 0xD0, 0xC1, 0x46, 0xF4, 0x13, 0xE3, 0xF6, 0x89,
|
||||
0x82, 0xFF, 0x73, 0x0F, 0xEA, 0x60, 0x3A, 0xFD, 0x7E, 0xD0, 0xDF, 0x7F, 0x3A, 0x13,
|
||||
0x20, 0x5D, 0x68, 0x43, 0xA7, 0x2B, 0x22, 0x03, 0x43, 0xD3, 0x47, 0xE6, 0x7E, 0xDE,
|
||||
0x2E, 0xE4, 0xC2, 0xAA, 0x4E, 0x4F, 0x5D, 0xF3, 0x9E, 0x50, 0xBD, 0xFF, 0xD9, 0x79,
|
||||
0x88, 0x3D, 0x0F, 0xF4, 0x0A, 0xA1, 0xB3, 0x40, 0x44, 0x7C, 0x04, 0xFF, 0xC7, 0x55,
|
||||
0xE5, 0x46, 0xEF, 0x11, 0x10, 0x73, 0x9F, 0x77, 0x35, 0x41, 0x2A, 0x08, 0x18, 0x23,
|
||||
0xE5, 0xB6, 0xDB, 0x09, 0x01, 0x05, 0x21, 0xAA, 0x61, 0x21, 0xD1, 0xB0, 0x12, 0xE3,
|
||||
0x92, 0x4A, 0xAD, 0x24, 0xA2, 0x53, 0x4A, 0xBA, 0x20, 0x34, 0x85, 0x7D, 0xB9, 0xF9,
|
||||
0x97, 0xF0, 0xF9, 0x13, 0xA7, 0x58, 0xDE, 0x9C, 0xA5, 0xA9, 0x09, 0xE7, 0x8A, 0xAD,
|
||||
0x7D, 0xCA, 0x61, 0x2E, 0xFD, 0x78, 0x86, 0xE7, 0xDF, 0x6B, 0x61, 0x39, 0x2C, 0xDE,
|
||||
0x9F, 0x1D, 0x4F, 0x87, 0x1F, 0x76, 0xAE, 0xA6, 0x5B, 0x0D, 0xD5, 0x3D, 0x15, 0x25,
|
||||
0x84, 0xD3, 0xA0, 0xE1, 0x1B, 0xAC, 0xC1, 0x8B, 0x9A, 0x9A, 0x7C, 0x82, 0x64, 0x29,
|
||||
0xAD, 0x4A, 0x93, 0x76, 0x24, 0x64, 0x15, 0x31, 0x1D, 0xAB, 0x8F, 0xD2, 0xDB, 0x63,
|
||||
0x88, 0x3A, 0x39, 0x69, 0x80, 0x78, 0x5D, 0x32, 0x8B, 0xAE, 0xB3, 0xD4, 0x12, 0xCD,
|
||||
0xBF, 0x7A, 0x3B, 0x02, 0xA0, 0x89, 0x66, 0xC7, 0x27, 0x60, 0x42, 0x07, 0x13, 0x5A,
|
||||
0x95, 0x4C, 0x1C, 0x07, 0x15, 0x91, 0x8D, 0x0D, 0x00, 0x04, 0x00, 0x00,
|
||||
];
|
||||
|
||||
let packet = QuestDataPacket::from_bytes(&mut bytes)?;
|
||||
assert_eq!(packet.header.id(), PACKET_ID_QUEST_DATA_OFFLINE);
|
||||
assert_eq!(packet.header.size(), QuestDataPacket::packet_size() as u16);
|
||||
assert_eq!(packet.filename_str()?, "dquest58.dat");
|
||||
assert_eq!(packet.file_type(), QuestPacketFileType::Dat);
|
||||
assert_eq!(packet.data_size(), 1024);
|
||||
|
||||
//bin
|
||||
let mut bytes: &[u8] = &[
|
||||
0xA7, 0x01, 0x18, 0x04, 0x64, 0x71, 0x75, 0x65, 0x73, 0x74, 0x35, 0x38, 0x2E, 0x62,
|
||||
0x69, 0x6E, 0x00, 0x00, 0x00, 0x00, 0xDB, 0xCF, 0x73, 0x74, 0x09, 0x46, 0x72, 0x65,
|
||||
0xB6, 0x44, 0xE8, 0xE9, 0xB3, 0xD6, 0xE6, 0x88, 0xF7, 0x3C, 0xF9, 0xC0, 0x05, 0xED,
|
||||
0x58, 0xE1, 0xAA, 0x17, 0x14, 0x70, 0xB4, 0xD0, 0x5A, 0x25, 0xA6, 0x85, 0x26, 0x24,
|
||||
0x19, 0xDF, 0x0C, 0xC4, 0x1F, 0x4B, 0xA6, 0xC1, 0xF1, 0x4E, 0x1C, 0x35, 0x56, 0x0C,
|
||||
0x31, 0xF7, 0x2F, 0xC8, 0x0B, 0xDC, 0xA0, 0xCD, 0x5F, 0xBE, 0x2C, 0x29, 0xF5, 0xE2,
|
||||
0x40, 0xA4, 0x9D, 0xD7, 0xC4, 0xFA, 0x74, 0x9B, 0x7A, 0xF2, 0x8A, 0xB6, 0x42, 0x3B,
|
||||
0xF8, 0x3A, 0x76, 0x78, 0xAA, 0x1B, 0xC4, 0xAA, 0xD3, 0x5E, 0x8A, 0x37, 0x94, 0xC0,
|
||||
0xA5, 0x21, 0x7F, 0x01, 0x8E, 0x68, 0x1A, 0xF4, 0xBC, 0xCA, 0x82, 0x02, 0xC1, 0x07,
|
||||
0x5C, 0xDB, 0xEE, 0x28, 0x92, 0x91, 0xF2, 0x6C, 0x79, 0x05, 0x8E, 0xC4, 0xB5, 0xAC,
|
||||
0xC7, 0x13, 0xD7, 0x5F, 0x8C, 0x0C, 0x21, 0x84, 0x29, 0xC1, 0xFF, 0x0B, 0x78, 0xCF,
|
||||
0x35, 0x87, 0xEB, 0xF8, 0x6E, 0x11, 0x41, 0x6A, 0xE4, 0xDD, 0x93, 0x3F, 0x1D, 0x63,
|
||||
0x3C, 0xA3, 0x3E, 0xAA, 0x5C, 0x62, 0x4A, 0x26, 0xFB, 0xBC, 0x55, 0xC9, 0x2A, 0x28,
|
||||
0xF7, 0xD2, 0x7A, 0x1B, 0x53, 0xCD, 0xF3, 0x67, 0xCD, 0x02, 0x0E, 0x26, 0xBA, 0x0B,
|
||||
0xCE, 0x44, 0x4B, 0x78, 0x6B, 0xE6, 0xDE, 0xC6, 0x06, 0x52, 0xD7, 0xCB, 0x97, 0x17,
|
||||
0xCB, 0x8A, 0x9A, 0x2C, 0x89, 0x68, 0x0A, 0x1D, 0x5E, 0xBA, 0xD1, 0x3B, 0xF5, 0x63,
|
||||
0x99, 0x70, 0x71, 0x2F, 0x37, 0x8A, 0x07, 0xFF, 0x59, 0x1F, 0x10, 0x45, 0xC0, 0x02,
|
||||
0x7E, 0xF6, 0xFF, 0x32, 0xB7, 0xAA, 0xD8, 0x0A, 0xF8, 0x43, 0x86, 0x30, 0x61, 0x48,
|
||||
0xEE, 0x8E, 0x0A, 0xB4, 0x2F, 0x85, 0x8F, 0x6D, 0x16, 0x96, 0x99, 0x2A, 0x5B, 0xE5,
|
||||
0x93, 0x47, 0x61, 0x30, 0xF3, 0x1F, 0x3E, 0x48, 0xFF, 0x6D, 0xE9, 0x64, 0xB8, 0xA3,
|
||||
0x6F, 0x33, 0xCD, 0x6D, 0x55, 0x40, 0xC2, 0x10, 0x1F, 0x0B, 0xBB, 0xBA, 0x77, 0xC6,
|
||||
0x7E, 0x82, 0x71, 0xA5, 0xEF, 0x25, 0x87, 0x06, 0x6B, 0x6B, 0x3D, 0x48, 0xBF, 0x1B,
|
||||
0x1A, 0x1C, 0x6C, 0x80, 0x6D, 0xC2, 0xCB, 0x15, 0x6F, 0x28, 0x66, 0xAA, 0xD9, 0xF0,
|
||||
0x53, 0xB5, 0xD0, 0x14, 0xCD, 0x19, 0xC9, 0xB0, 0xBC, 0x5A, 0x31, 0xF8, 0x5E, 0x96,
|
||||
0x54, 0xA0, 0xDF, 0x76, 0xE1, 0xEB, 0x0A, 0xC2, 0xF3, 0x96, 0x2D, 0x95, 0xD2, 0x49,
|
||||
0xCA, 0x10, 0x1D, 0xD1, 0x7A, 0xAF, 0x5B, 0x73, 0x71, 0xB6, 0x0A, 0xD8, 0x8F, 0xF1,
|
||||
0x4E, 0x5E, 0x3B, 0xA0, 0xD0, 0xB7, 0x3B, 0x96, 0xC0, 0x22, 0xDB, 0x2D, 0x6A, 0x95,
|
||||
0x4E, 0xC2, 0x95, 0x76, 0xB9, 0x14, 0xEC, 0x8E, 0x8D, 0x48, 0xD8, 0x41, 0x66, 0x74,
|
||||
0xD2, 0x81, 0xE8, 0xDA, 0x4B, 0xB6, 0x77, 0x92, 0x6E, 0xA3, 0x85, 0xF4, 0x67, 0x38,
|
||||
0x60, 0xD3, 0x5A, 0x38, 0x37, 0x8D, 0xE3, 0x7B, 0x20, 0xBA, 0x3E, 0x0E, 0x34, 0x15,
|
||||
0xBB, 0x17, 0x9A, 0xA9, 0xE0, 0xF0, 0x20, 0x28, 0x14, 0xD5, 0x94, 0x7B, 0xEF, 0xEF,
|
||||
0xBA, 0x7E, 0x41, 0x98, 0x41, 0x88, 0xA4, 0x8B, 0xC8, 0x5D, 0x2C, 0xBD, 0xE2, 0x13,
|
||||
0xC4, 0x00, 0xC9, 0x6A, 0x1A, 0x43, 0x75, 0xDA, 0x0C, 0xCE, 0x3D, 0x1E, 0x8E, 0xA4,
|
||||
0x0F, 0xD8, 0x4C, 0xAE, 0x60, 0x46, 0xA3, 0xF0, 0x98, 0x35, 0x90, 0x86, 0xE9, 0x04,
|
||||
0xF9, 0xA1, 0x05, 0xBC, 0xA9, 0x11, 0x7B, 0xE9, 0xA6, 0x3D, 0x80, 0x37, 0x94, 0xAE,
|
||||
0xCC, 0x44, 0x67, 0xD1, 0x8B, 0x7D, 0xDC, 0x25, 0xED, 0x55, 0xB2, 0x50, 0x30, 0xAA,
|
||||
0x8B, 0x4D, 0x50, 0xC8, 0x19, 0xEA, 0x7F, 0x80, 0x5E, 0xDA, 0xF2, 0x2B, 0xED, 0x70,
|
||||
0xBE, 0xD7, 0x2A, 0xE5, 0x66, 0x09, 0x7D, 0x05, 0x2E, 0xE5, 0x19, 0x49, 0xA9, 0x0D,
|
||||
0xB5, 0xA9, 0x55, 0x52, 0xCE, 0x06, 0x31, 0x65, 0x3E, 0x09, 0x7F, 0x07, 0x5D, 0x51,
|
||||
0xCD, 0x96, 0xB6, 0xA3, 0x65, 0x38, 0x52, 0x37, 0x69, 0xC3, 0xA0, 0x4E, 0xEE, 0x65,
|
||||
0xBA, 0x3C, 0x0B, 0x5C, 0x2B, 0x23, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x23, 0x02, 0x00, 0x00,
|
||||
];
|
||||
|
||||
let packet = QuestDataPacket::from_bytes(&mut bytes)?;
|
||||
assert_eq!(packet.header.id(), PACKET_ID_QUEST_DATA_OFFLINE);
|
||||
assert_eq!(packet.header.size(), QuestDataPacket::packet_size() as u16);
|
||||
assert_eq!(packet.filename_str()?, "dquest58.bin");
|
||||
assert_eq!(packet.file_type(), QuestPacketFileType::Bin);
|
||||
assert_eq!(packet.data_size(), 547);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn create_quest_header_packet_via_new() -> Result<(), PacketError> {
|
||||
// bin
|
||||
let packet =
|
||||
QuestHeaderPacket::new("My Quest", Language::English, "myquest.bin", 424242, true)?;
|
||||
assert_eq!(packet.header.id(), PACKET_ID_QUEST_HEADER_ONLINE);
|
||||
assert_eq!(
|
||||
packet.header.size(),
|
||||
QuestHeaderPacket::packet_size() as u16
|
||||
);
|
||||
assert_eq!(packet.name_str()?, "My Quest");
|
||||
assert_eq!(packet.filename_str()?, "myquest.bin");
|
||||
assert_eq!(packet.file_type(), QuestPacketFileType::Bin);
|
||||
|
||||
// dat
|
||||
let packet = QuestHeaderPacket::new(
|
||||
"Some Kind Of Quest",
|
||||
Language::English,
|
||||
"somequest.dat",
|
||||
123456,
|
||||
true,
|
||||
)?;
|
||||
assert_eq!(packet.header.id(), PACKET_ID_QUEST_HEADER_ONLINE);
|
||||
assert_eq!(
|
||||
packet.header.size(),
|
||||
QuestHeaderPacket::packet_size() as u16
|
||||
);
|
||||
assert_eq!(packet.name_str()?, "Some Kind Of Quest");
|
||||
assert_eq!(packet.filename_str()?, "somequest.dat");
|
||||
assert_eq!(packet.file_type(), QuestPacketFileType::Dat);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn create_quest_data_packet_via_new() -> Result<(), PacketError> {
|
||||
// bin
|
||||
let data = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
|
||||
let packet = QuestDataPacket::new("myquest.bin", &data, true)?;
|
||||
assert_eq!(packet.header.id(), PACKET_ID_QUEST_DATA_ONLINE);
|
||||
assert_eq!(packet.header.size(), QuestDataPacket::packet_size() as u16);
|
||||
assert_eq!(packet.filename_str()?, "myquest.bin");
|
||||
assert_eq!(packet.file_type(), QuestPacketFileType::Bin);
|
||||
assert_eq!(packet.data_size(), data.len() as u32);
|
||||
assert_eq!(packet.data(), data);
|
||||
|
||||
// dat
|
||||
let data = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
|
||||
let packet = QuestDataPacket::new("thequest.dat", &data, true)?;
|
||||
assert_eq!(packet.header.id(), PACKET_ID_QUEST_DATA_ONLINE);
|
||||
assert_eq!(packet.header.size(), QuestDataPacket::packet_size() as u16);
|
||||
assert_eq!(packet.filename_str()?, "thequest.dat");
|
||||
assert_eq!(packet.file_type(), QuestPacketFileType::Dat);
|
||||
assert_eq!(packet.data_size(), data.len() as u32);
|
||||
assert_eq!(packet.data(), data);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn quest_header_packet_from_bytes_errors() -> Result<(), PacketError> {
|
||||
let mut bytes: &[u8] = &[0x42, 0x00, 0x3C, 0x00, 0x01, 0x02, 0x03, 0x04];
|
||||
assert_matches!(
|
||||
QuestHeaderPacket::from_bytes(&mut bytes),
|
||||
Err(PacketError::WrongId(0x42))
|
||||
);
|
||||
|
||||
let mut bytes: &[u8] = &[0x44, 0x00, 0x10, 0x00, 0x01, 0x02, 0x03];
|
||||
assert_matches!(
|
||||
QuestHeaderPacket::from_bytes(&mut bytes),
|
||||
Err(PacketError::WrongSize(16))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn quest_data_packet_from_bytes_errors() -> Result<(), PacketError> {
|
||||
let mut bytes: &[u8] = &[0xBB, 0x42, 0x18, 0x04, 0x11, 0x22, 0x33, 0x44];
|
||||
assert_matches!(
|
||||
QuestDataPacket::from_bytes(&mut bytes),
|
||||
Err(PacketError::WrongId(0xBB))
|
||||
);
|
||||
|
||||
let mut bytes: &[u8] = &[0x13, 0x42, 0xAA, 0xBB, 0x11, 0x22, 0x33, 0x44];
|
||||
assert_matches!(
|
||||
QuestDataPacket::from_bytes(&mut bytes),
|
||||
Err(PacketError::WrongSize(0xBBAA))
|
||||
);
|
||||
|
||||
// bad "size" value at the very end of the packet. too large, must be <= 1024
|
||||
let mut bytes: &[u8] = &[
|
||||
0xA7, 0x01, 0x18, 0x04, 0x64, 0x71, 0x75, 0x65, 0x73, 0x74, 0x35, 0x38, 0x2E, 0x62,
|
||||
0x69, 0x6E, 0x00, 0x00, 0x00, 0x00, 0xDB, 0xCF, 0x73, 0x74, 0x09, 0x46, 0x72, 0x65,
|
||||
0xB6, 0x44, 0xE8, 0xE9, 0xB3, 0xD6, 0xE6, 0x88, 0xF7, 0x3C, 0xF9, 0xC0, 0x05, 0xED,
|
||||
0x58, 0xE1, 0xAA, 0x17, 0x14, 0x70, 0xB4, 0xD0, 0x5A, 0x25, 0xA6, 0x85, 0x26, 0x24,
|
||||
0x19, 0xDF, 0x0C, 0xC4, 0x1F, 0x4B, 0xA6, 0xC1, 0xF1, 0x4E, 0x1C, 0x35, 0x56, 0x0C,
|
||||
0x31, 0xF7, 0x2F, 0xC8, 0x0B, 0xDC, 0xA0, 0xCD, 0x5F, 0xBE, 0x2C, 0x29, 0xF5, 0xE2,
|
||||
0x40, 0xA4, 0x9D, 0xD7, 0xC4, 0xFA, 0x74, 0x9B, 0x7A, 0xF2, 0x8A, 0xB6, 0x42, 0x3B,
|
||||
0xF8, 0x3A, 0x76, 0x78, 0xAA, 0x1B, 0xC4, 0xAA, 0xD3, 0x5E, 0x8A, 0x37, 0x94, 0xC0,
|
||||
0xA5, 0x21, 0x7F, 0x01, 0x8E, 0x68, 0x1A, 0xF4, 0xBC, 0xCA, 0x82, 0x02, 0xC1, 0x07,
|
||||
0x5C, 0xDB, 0xEE, 0x28, 0x92, 0x91, 0xF2, 0x6C, 0x79, 0x05, 0x8E, 0xC4, 0xB5, 0xAC,
|
||||
0xC7, 0x13, 0xD7, 0x5F, 0x8C, 0x0C, 0x21, 0x84, 0x29, 0xC1, 0xFF, 0x0B, 0x78, 0xCF,
|
||||
0x35, 0x87, 0xEB, 0xF8, 0x6E, 0x11, 0x41, 0x6A, 0xE4, 0xDD, 0x93, 0x3F, 0x1D, 0x63,
|
||||
0x3C, 0xA3, 0x3E, 0xAA, 0x5C, 0x62, 0x4A, 0x26, 0xFB, 0xBC, 0x55, 0xC9, 0x2A, 0x28,
|
||||
0xF7, 0xD2, 0x7A, 0x1B, 0x53, 0xCD, 0xF3, 0x67, 0xCD, 0x02, 0x0E, 0x26, 0xBA, 0x0B,
|
||||
0xCE, 0x44, 0x4B, 0x78, 0x6B, 0xE6, 0xDE, 0xC6, 0x06, 0x52, 0xD7, 0xCB, 0x97, 0x17,
|
||||
0xCB, 0x8A, 0x9A, 0x2C, 0x89, 0x68, 0x0A, 0x1D, 0x5E, 0xBA, 0xD1, 0x3B, 0xF5, 0x63,
|
||||
0x99, 0x70, 0x71, 0x2F, 0x37, 0x8A, 0x07, 0xFF, 0x59, 0x1F, 0x10, 0x45, 0xC0, 0x02,
|
||||
0x7E, 0xF6, 0xFF, 0x32, 0xB7, 0xAA, 0xD8, 0x0A, 0xF8, 0x43, 0x86, 0x30, 0x61, 0x48,
|
||||
0xEE, 0x8E, 0x0A, 0xB4, 0x2F, 0x85, 0x8F, 0x6D, 0x16, 0x96, 0x99, 0x2A, 0x5B, 0xE5,
|
||||
0x93, 0x47, 0x61, 0x30, 0xF3, 0x1F, 0x3E, 0x48, 0xFF, 0x6D, 0xE9, 0x64, 0xB8, 0xA3,
|
||||
0x6F, 0x33, 0xCD, 0x6D, 0x55, 0x40, 0xC2, 0x10, 0x1F, 0x0B, 0xBB, 0xBA, 0x77, 0xC6,
|
||||
0x7E, 0x82, 0x71, 0xA5, 0xEF, 0x25, 0x87, 0x06, 0x6B, 0x6B, 0x3D, 0x48, 0xBF, 0x1B,
|
||||
0x1A, 0x1C, 0x6C, 0x80, 0x6D, 0xC2, 0xCB, 0x15, 0x6F, 0x28, 0x66, 0xAA, 0xD9, 0xF0,
|
||||
0x53, 0xB5, 0xD0, 0x14, 0xCD, 0x19, 0xC9, 0xB0, 0xBC, 0x5A, 0x31, 0xF8, 0x5E, 0x96,
|
||||
0x54, 0xA0, 0xDF, 0x76, 0xE1, 0xEB, 0x0A, 0xC2, 0xF3, 0x96, 0x2D, 0x95, 0xD2, 0x49,
|
||||
0xCA, 0x10, 0x1D, 0xD1, 0x7A, 0xAF, 0x5B, 0x73, 0x71, 0xB6, 0x0A, 0xD8, 0x8F, 0xF1,
|
||||
0x4E, 0x5E, 0x3B, 0xA0, 0xD0, 0xB7, 0x3B, 0x96, 0xC0, 0x22, 0xDB, 0x2D, 0x6A, 0x95,
|
||||
0x4E, 0xC2, 0x95, 0x76, 0xB9, 0x14, 0xEC, 0x8E, 0x8D, 0x48, 0xD8, 0x41, 0x66, 0x74,
|
||||
0xD2, 0x81, 0xE8, 0xDA, 0x4B, 0xB6, 0x77, 0x92, 0x6E, 0xA3, 0x85, 0xF4, 0x67, 0x38,
|
||||
0x60, 0xD3, 0x5A, 0x38, 0x37, 0x8D, 0xE3, 0x7B, 0x20, 0xBA, 0x3E, 0x0E, 0x34, 0x15,
|
||||
0xBB, 0x17, 0x9A, 0xA9, 0xE0, 0xF0, 0x20, 0x28, 0x14, 0xD5, 0x94, 0x7B, 0xEF, 0xEF,
|
||||
0xBA, 0x7E, 0x41, 0x98, 0x41, 0x88, 0xA4, 0x8B, 0xC8, 0x5D, 0x2C, 0xBD, 0xE2, 0x13,
|
||||
0xC4, 0x00, 0xC9, 0x6A, 0x1A, 0x43, 0x75, 0xDA, 0x0C, 0xCE, 0x3D, 0x1E, 0x8E, 0xA4,
|
||||
0x0F, 0xD8, 0x4C, 0xAE, 0x60, 0x46, 0xA3, 0xF0, 0x98, 0x35, 0x90, 0x86, 0xE9, 0x04,
|
||||
0xF9, 0xA1, 0x05, 0xBC, 0xA9, 0x11, 0x7B, 0xE9, 0xA6, 0x3D, 0x80, 0x37, 0x94, 0xAE,
|
||||
0xCC, 0x44, 0x67, 0xD1, 0x8B, 0x7D, 0xDC, 0x25, 0xED, 0x55, 0xB2, 0x50, 0x30, 0xAA,
|
||||
0x8B, 0x4D, 0x50, 0xC8, 0x19, 0xEA, 0x7F, 0x80, 0x5E, 0xDA, 0xF2, 0x2B, 0xED, 0x70,
|
||||
0xBE, 0xD7, 0x2A, 0xE5, 0x66, 0x09, 0x7D, 0x05, 0x2E, 0xE5, 0x19, 0x49, 0xA9, 0x0D,
|
||||
0xB5, 0xA9, 0x55, 0x52, 0xCE, 0x06, 0x31, 0x65, 0x3E, 0x09, 0x7F, 0x07, 0x5D, 0x51,
|
||||
0xCD, 0x96, 0xB6, 0xA3, 0x65, 0x38, 0x52, 0x37, 0x69, 0xC3, 0xA0, 0x4E, 0xEE, 0x65,
|
||||
0xBA, 0x3C, 0x0B, 0x5C, 0x2B, 0x23, 0x3C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78,
|
||||
];
|
||||
assert_matches!(
|
||||
QuestDataPacket::from_bytes(&mut bytes),
|
||||
Err(PacketError::DataFormatError(..))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn quest_header_packet_via_new_errors() -> Result<(), PacketError> {
|
||||
assert_matches!(
|
||||
QuestHeaderPacket::new(
|
||||
"this string is too long and will not fit in the space available for a quest name",
|
||||
Language::English,
|
||||
"quest.bin",
|
||||
12345,
|
||||
true
|
||||
),
|
||||
Err(PacketError::DataFormatError(..))
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
QuestHeaderPacket::new(
|
||||
"A Quest!",
|
||||
Language::English,
|
||||
"thisfilenameistoolong.bin",
|
||||
42,
|
||||
false
|
||||
),
|
||||
Err(PacketError::DataFormatError(..))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn quest_data_packet_via_new_errors() -> Result<(), PacketError> {
|
||||
assert_matches!(
|
||||
QuestDataPacket::new("areallylongfilename.dat", &[0x01, 0x02, 0x03], true),
|
||||
Err(PacketError::DataFormatError(..))
|
||||
);
|
||||
|
||||
let lots_of_data = [0x42u8].repeat(2048);
|
||||
assert_matches!(
|
||||
QuestDataPacket::new("thequest.bin", &lots_of_data, false),
|
||||
Err(PacketError::DataFormatError(..))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,419 +0,0 @@
|
|||
use std::fmt::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use byteorder::WriteBytesExt;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::quest::bin::{QuestBin, QuestBinError};
|
||||
use crate::quest::dat::{QuestDat, QuestDatError, QuestDatTableType};
|
||||
use crate::quest::qst::{QuestQst, QuestQstError};
|
||||
use crate::text::Language;
|
||||
use crate::utils::crc32;
|
||||
|
||||
pub mod bin;
|
||||
pub mod dat;
|
||||
pub mod qst;
|
||||
|
||||
fn format_description_field(description: &String) -> String {
|
||||
description
|
||||
.trim()
|
||||
.replace("\n", "\n ")
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum QuestError {
|
||||
#[error("I/O error reading quest")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("Error processing quest bin")]
|
||||
QuestBinError(#[from] QuestBinError),
|
||||
|
||||
#[error("Error processing quest dat")]
|
||||
QuestDatError(#[from] QuestDatError),
|
||||
|
||||
#[error("Error processing quest qst")]
|
||||
QuestQstError(#[from] QuestQstError),
|
||||
}
|
||||
|
||||
pub struct Quest {
|
||||
pub bin: QuestBin,
|
||||
pub dat: QuestDat,
|
||||
}
|
||||
|
||||
impl Quest {
|
||||
pub fn from_bindat_files(bin_path: &Path, dat_path: &Path) -> Result<Quest, QuestError> {
|
||||
// try to load bin and dat files each as compressed files first as that is the normal
|
||||
// format that these are stored as. if that fails, then try one more time for each one
|
||||
// to load as an uncompressed file. if that fails too, return the error
|
||||
|
||||
let bin = match QuestBin::from_compressed_file(bin_path) {
|
||||
Err(QuestBinError::PrsCompressionError(_)) => {
|
||||
QuestBin::from_uncompressed_file(bin_path)?
|
||||
}
|
||||
Err(e) => return Err(QuestError::QuestBinError(e)),
|
||||
Ok(bin) => bin,
|
||||
};
|
||||
|
||||
let dat = match QuestDat::from_compressed_file(dat_path) {
|
||||
Err(QuestDatError::PrsCompressionError(_)) => {
|
||||
QuestDat::from_uncompressed_file(dat_path)?
|
||||
}
|
||||
Err(e) => return Err(QuestError::QuestDatError(e)),
|
||||
Ok(dat) => dat,
|
||||
};
|
||||
|
||||
Ok(Quest { bin, dat })
|
||||
}
|
||||
|
||||
pub fn from_qst_file(path: &Path) -> Result<Quest, QuestError> {
|
||||
let qst = QuestQst::from_file(path)?;
|
||||
Self::from_qst(qst)
|
||||
}
|
||||
|
||||
pub fn from_qst(qst: QuestQst) -> Result<Quest, QuestError> {
|
||||
let bin = qst.extract_bin()?;
|
||||
let dat = qst.extract_dat()?;
|
||||
|
||||
Ok(Quest { bin, dat })
|
||||
}
|
||||
|
||||
pub fn as_qst(&self) -> Result<QuestQst, QuestError> {
|
||||
Ok(QuestQst::from_bindat(&self.bin, &self.dat)?)
|
||||
}
|
||||
|
||||
pub fn write_as_qst_bytes<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), QuestError> {
|
||||
let qst = self.as_qst()?;
|
||||
Ok(qst.write_bytes(writer)?)
|
||||
}
|
||||
|
||||
pub fn to_qst_file(&self, path: &Path) -> Result<(), QuestError> {
|
||||
let qst = QuestQst::from_bindat(&self.bin, &self.dat)?;
|
||||
Ok(qst.to_file(path)?)
|
||||
}
|
||||
|
||||
pub fn to_compressed_bindat_files(
|
||||
&self,
|
||||
bin_path: &Path,
|
||||
dat_path: &Path,
|
||||
) -> Result<(), QuestError> {
|
||||
self.bin.to_compressed_file(bin_path)?;
|
||||
self.dat.to_compressed_file(dat_path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_uncompressed_bindat_files(
|
||||
&self,
|
||||
bin_path: &Path,
|
||||
dat_path: &Path,
|
||||
) -> Result<(), QuestError> {
|
||||
self.bin.to_uncompressed_file(bin_path)?;
|
||||
self.dat.to_uncompressed_file(dat_path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &String {
|
||||
&self.bin.header.name
|
||||
}
|
||||
|
||||
pub fn short_description(&self) -> &String {
|
||||
&self.bin.header.short_description
|
||||
}
|
||||
|
||||
pub fn long_description(&self) -> &String {
|
||||
&self.bin.header.long_description
|
||||
}
|
||||
|
||||
pub fn language(&self) -> Language {
|
||||
self.bin.header.language
|
||||
}
|
||||
|
||||
pub fn is_download(&self) -> bool {
|
||||
self.bin.header.is_download
|
||||
}
|
||||
|
||||
pub fn set_is_download(&mut self, value: bool) {
|
||||
self.bin.header.is_download = value
|
||||
}
|
||||
|
||||
pub fn quest_number(&self) -> u8 {
|
||||
self.bin.header.quest_number()
|
||||
}
|
||||
|
||||
pub fn quest_number_u16(&self) -> u16 {
|
||||
self.bin.header.quest_number_u16()
|
||||
}
|
||||
|
||||
pub fn episode(&self) -> u8 {
|
||||
self.bin.header.episode()
|
||||
}
|
||||
|
||||
pub fn display_bin_info(&self) -> String {
|
||||
let object_code_crc32 = crc32(self.bin.object_code.as_ref());
|
||||
let function_offset_table_crc32 = crc32(self.bin.function_offset_table.as_ref());
|
||||
|
||||
let mut s = String::new();
|
||||
|
||||
// HACK: i'm just directly calling .unwrap() for all of these because we're writing into
|
||||
// a string buffer that we own here, so this should really never fail and i didn't
|
||||
// want to have this method return a Result<>
|
||||
|
||||
writeln!(s, "QUEST .BIN FILE").unwrap();
|
||||
writeln!(
|
||||
s,
|
||||
"======================================================================"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
s,
|
||||
"Decompressed Size: {}",
|
||||
self.bin.calculate_size()
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(s, "Name: {}", self.bin.header.name).unwrap();
|
||||
writeln!(
|
||||
s,
|
||||
"object_code: size: {}, crc32: {:08x}",
|
||||
self.bin.object_code.len(),
|
||||
object_code_crc32
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
s,
|
||||
"function_offset_table: size: {}, crc32: {:08x}",
|
||||
self.bin.function_offset_table.len(),
|
||||
function_offset_table_crc32
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
s,
|
||||
"Is Download? {}",
|
||||
self.bin.header.is_download
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
s,
|
||||
"Quest Number/ID: {0} (8-bit) {1}, 0x{1:04x} (16-bit)",
|
||||
self.bin.header.quest_number(),
|
||||
self.bin.header.quest_number_u16()
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
s,
|
||||
"Episode: {} (0x{:02x})",
|
||||
self.bin.header.episode() + 1,
|
||||
self.bin.header.episode()
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
s,
|
||||
"Language: {:?}, encoding: {}",
|
||||
self.bin.header.language,
|
||||
self.bin.header.language.get_encoding().name()
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
s,
|
||||
"Short Description: {}\n",
|
||||
format_description_field(&self.bin.header.short_description)
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
s,
|
||||
"Long Description: {}\n",
|
||||
format_description_field(&self.bin.header.long_description)
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
s
|
||||
}
|
||||
|
||||
pub fn display_dat_info(&self) -> String {
|
||||
let mut s = String::new();
|
||||
|
||||
let episode = self.bin.header.episode() as u32;
|
||||
|
||||
// HACK: i'm just directly calling .unwrap() for all of these because we're writing into
|
||||
// a string buffer that we own here, so this should really never fail and i didn't
|
||||
// want to have this method return a Result<>
|
||||
|
||||
writeln!(s, "QUEST .DAT FILE").unwrap();
|
||||
writeln!(
|
||||
s,
|
||||
"================================================================================"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(s, "Decompressed size: {}\n", self.dat.calculate_size()).unwrap();
|
||||
writeln!(
|
||||
s,
|
||||
"(Using episode {} to lookup table area names)",
|
||||
episode + 1
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
writeln!(
|
||||
s,
|
||||
"Idx Size Table Type Area Count CRC32"
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for (index, table) in self.dat.tables.iter().enumerate() {
|
||||
let body_size = table.bytes.len();
|
||||
let body_crc32 = crc32(table.bytes.as_ref());
|
||||
|
||||
match table.table_type() {
|
||||
QuestDatTableType::Object => {
|
||||
let num_entities = body_size / 68;
|
||||
writeln!(
|
||||
s,
|
||||
"{:3} {:5} {:<21} {:30} {:5} {:08x}",
|
||||
index,
|
||||
body_size,
|
||||
table.table_type().to_string(),
|
||||
table.area_name(episode).to_string(),
|
||||
num_entities,
|
||||
body_crc32
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
QuestDatTableType::NPC => {
|
||||
let num_entities = body_size / 72;
|
||||
writeln!(
|
||||
s,
|
||||
"{:3} {:5} {:<21} {:30} {:5} {:08x}",
|
||||
index,
|
||||
body_size,
|
||||
table.table_type().to_string(),
|
||||
table.area_name(episode).to_string(),
|
||||
num_entities,
|
||||
body_crc32
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
QuestDatTableType::Wave => {
|
||||
writeln!(
|
||||
s,
|
||||
"{:3} {:5} {:<21} {:30} {:08x}",
|
||||
index,
|
||||
body_size,
|
||||
table.table_type().to_string(),
|
||||
table.area_name(episode).to_string(),
|
||||
body_crc32
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
QuestDatTableType::ChallengeModeSpawns => {
|
||||
writeln!(
|
||||
s,
|
||||
"{:3} {:5} {:<21} {:30} {:08x}",
|
||||
index,
|
||||
body_size,
|
||||
table.table_type().to_string(),
|
||||
table.area_name(episode).to_string(),
|
||||
body_crc32
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
QuestDatTableType::ChallengeModeUnknown => {
|
||||
writeln!(
|
||||
s,
|
||||
"{:3} {:5} {:<21} {:30} {:08x}",
|
||||
index,
|
||||
body_size,
|
||||
table.table_type().to_string(),
|
||||
table.area_name(episode).to_string(),
|
||||
body_crc32
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
QuestDatTableType::Unknown(n) => {
|
||||
writeln!(s, "{:3} {:5} Unknown: {}", index, body_size, n).unwrap();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use claim::*;
|
||||
use tempfile::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
pub fn can_load_from_compressed_bindat_files() {
|
||||
let bin_path = Path::new("../test-assets/q058-ret-gc.bin");
|
||||
let dat_path = Path::new("../test-assets/q058-ret-gc.dat");
|
||||
assert_ok!(Quest::from_bindat_files(bin_path, dat_path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn can_load_from_uncompressed_bindat_files() {
|
||||
let bin_path = Path::new("../test-assets/q058-ret-gc.uncompressed.bin");
|
||||
let dat_path = Path::new("../test-assets/q058-ret-gc.uncompressed.dat");
|
||||
assert_ok!(Quest::from_bindat_files(bin_path, dat_path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn can_load_from_offline_qst_file() {
|
||||
let path = Path::new("../test-assets/q058-ret-gc.offline.qst");
|
||||
assert_ok!(Quest::from_qst_file(path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn can_load_from_online_qst_file() {
|
||||
let path = Path::new("../test-assets/q058-ret-gc.online.qst");
|
||||
assert_ok!(Quest::from_qst_file(path));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn can_create_from_qst_struct() {
|
||||
let qst = QuestQst::from_file(Path::new("../test-assets/q058-ret-gc.online.qst")).unwrap();
|
||||
assert_ok!(Quest::from_qst(qst));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn can_save_to_compressed_bindat_files() -> Result<(), QuestError> {
|
||||
let quest = Quest::from_bindat_files(
|
||||
Path::new("../test-assets/q058-ret-gc.bin"),
|
||||
Path::new("../test-assets/q058-ret-gc.dat"),
|
||||
)?;
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let bin_save_path = tmp_dir.path().join("quest58.bin");
|
||||
let dat_save_path = tmp_dir.path().join("quest58.dat");
|
||||
assert_ok!(quest.to_compressed_bindat_files(&bin_save_path, &dat_save_path));
|
||||
assert_ok!(QuestBin::from_compressed_file(&bin_save_path));
|
||||
assert_ok!(QuestDat::from_compressed_file(&dat_save_path));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn can_save_to_uncompressed_bindat_files() -> Result<(), QuestError> {
|
||||
let quest = Quest::from_bindat_files(
|
||||
Path::new("../test-assets/q058-ret-gc.bin"),
|
||||
Path::new("../test-assets/q058-ret-gc.dat"),
|
||||
)?;
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let bin_save_path = tmp_dir.path().join("quest58.bin");
|
||||
let dat_save_path = tmp_dir.path().join("quest58.dat");
|
||||
assert_ok!(quest.to_uncompressed_bindat_files(&bin_save_path, &dat_save_path));
|
||||
assert_ok!(QuestBin::from_uncompressed_file(&bin_save_path));
|
||||
assert_ok!(QuestDat::from_uncompressed_file(&dat_save_path));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn can_save_to_qst_file() -> Result<(), QuestError> {
|
||||
let quest = Quest::from_bindat_files(
|
||||
Path::new("../test-assets/q058-ret-gc.bin"),
|
||||
Path::new("../test-assets/q058-ret-gc.dat"),
|
||||
)?;
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let qst_save_path = tmp_dir.path().join("quest58.qst");
|
||||
assert_ok!(quest.to_qst_file(&qst_save_path));
|
||||
assert_ok!(QuestQst::from_file(&qst_save_path));
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,643 +0,0 @@
|
|||
use std::fmt::{Debug, Formatter};
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, Cursor, Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::bytes::*;
|
||||
use crate::compression::{prs_compress, prs_decompress, PrsCompressionError};
|
||||
use crate::text::Language;
|
||||
|
||||
pub const QUEST_BIN_NAME_LENGTH: usize = 32;
|
||||
pub const QUEST_BIN_SHORT_DESCRIPTION_LENGTH: usize = 128;
|
||||
pub const QUEST_BIN_LONG_DESCRIPTION_LENGTH: usize = 288;
|
||||
|
||||
pub const QUEST_BIN_HEADER_SIZE: usize = 20
|
||||
+ QUEST_BIN_NAME_LENGTH
|
||||
+ QUEST_BIN_SHORT_DESCRIPTION_LENGTH
|
||||
+ QUEST_BIN_LONG_DESCRIPTION_LENGTH;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum QuestBinError {
|
||||
#[error("I/O error while processing quest bin")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("PRS compression failed")]
|
||||
PrsCompressionError(#[from] PrsCompressionError),
|
||||
|
||||
#[error("Bad quest bin data format: {0}")]
|
||||
DataFormatError(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct QuestNumberAndEpisode {
|
||||
pub number: u8,
|
||||
pub episode: u8,
|
||||
}
|
||||
|
||||
pub union QuestNumber {
|
||||
pub number_and_episode: QuestNumberAndEpisode,
|
||||
pub number: u16,
|
||||
}
|
||||
|
||||
impl Debug for QuestNumber {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"QuestNumber {{ number: {}, number_and_episode: {:?} }}",
|
||||
unsafe { self.number },
|
||||
unsafe { self.number_and_episode },
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct QuestBinHeader {
|
||||
pub is_download: bool,
|
||||
pub language: Language,
|
||||
pub quest_number: QuestNumber,
|
||||
pub name: String,
|
||||
pub short_description: String,
|
||||
pub long_description: String,
|
||||
}
|
||||
|
||||
impl QuestBinHeader {
|
||||
// the reality is that i kind of have to support access to the quest_number/episode as u8's as
|
||||
// well as the quest_number as a u16 simultaneously. it appears that all of sega's quests (at
|
||||
// least, all of the ones i've looked at in detail) used the quest_number and episode fields as
|
||||
// individual u8's, but there are quite a bunch of custom quests that stored quest_number
|
||||
// values as a u16 (i believe this is Qedit's fault?)
|
||||
|
||||
pub fn quest_number(&self) -> u8 {
|
||||
unsafe { self.quest_number.number_and_episode.number }
|
||||
}
|
||||
|
||||
pub fn quest_number_u16(&self) -> u16 {
|
||||
unsafe { self.quest_number.number }
|
||||
}
|
||||
|
||||
pub fn episode(&self) -> u8 {
|
||||
unsafe { self.quest_number.number_and_episode.episode }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct QuestBin {
|
||||
pub header: QuestBinHeader,
|
||||
pub object_code: Box<[u8]>,
|
||||
pub function_offset_table: Box<[u8]>,
|
||||
}
|
||||
|
||||
impl QuestBin {
|
||||
pub fn from_compressed_bytes(bytes: &[u8]) -> Result<QuestBin, QuestBinError> {
|
||||
let decompressed = prs_decompress(&bytes)?;
|
||||
let mut reader = Cursor::new(decompressed);
|
||||
Ok(QuestBin::from_uncompressed_bytes(&mut reader)?)
|
||||
}
|
||||
|
||||
pub fn from_uncompressed_bytes<T: ReadBytesExt>(
|
||||
reader: &mut T,
|
||||
) -> Result<QuestBin, QuestBinError> {
|
||||
let object_code_offset = reader.read_u32::<LittleEndian>()?;
|
||||
if object_code_offset != QUEST_BIN_HEADER_SIZE as u32 {
|
||||
return Err(QuestBinError::DataFormatError(format!(
|
||||
"Invalid object_code_offset found: {}",
|
||||
object_code_offset
|
||||
)));
|
||||
}
|
||||
|
||||
let function_offset_table_offset = reader.read_u32::<LittleEndian>()?;
|
||||
if function_offset_table_offset <= object_code_offset {
|
||||
return Err(QuestBinError::DataFormatError(format!(
|
||||
"function_offset_table_offset points to a location that occurs before the object_code"
|
||||
)));
|
||||
}
|
||||
|
||||
let bin_size = reader.read_u32::<LittleEndian>()?;
|
||||
let _xfffffff = reader.read_u32::<LittleEndian>()?; // always expected to be 0xffffffff
|
||||
let is_download = reader.read_u8()?;
|
||||
let is_download = is_download != 0;
|
||||
|
||||
let language = reader.read_u8()?;
|
||||
let language = match Language::from_number(language) {
|
||||
Err(e) => {
|
||||
return Err(QuestBinError::DataFormatError(format!(
|
||||
"Unsupported language value found in quest header: {}",
|
||||
e
|
||||
)))
|
||||
}
|
||||
Ok(encoding) => encoding,
|
||||
};
|
||||
|
||||
let quest_number_and_episode = reader.read_u16::<LittleEndian>()?;
|
||||
let quest_number = QuestNumber {
|
||||
number: quest_number_and_episode,
|
||||
};
|
||||
|
||||
let name_bytes: [u8; QUEST_BIN_NAME_LENGTH] = reader.read_bytes()?;
|
||||
let name = match language.decode_text(name_bytes.as_unpadded_slice()) {
|
||||
Err(e) => {
|
||||
return Err(QuestBinError::DataFormatError(format!(
|
||||
"Error decoding string in quest 'name' field: {}",
|
||||
e
|
||||
)))
|
||||
}
|
||||
Ok(value) => value,
|
||||
};
|
||||
|
||||
let short_description_bytes: [u8; QUEST_BIN_SHORT_DESCRIPTION_LENGTH] =
|
||||
reader.read_bytes()?;
|
||||
let short_description =
|
||||
match language.decode_text(short_description_bytes.as_unpadded_slice()) {
|
||||
Err(e) => {
|
||||
return Err(QuestBinError::DataFormatError(format!(
|
||||
"Error decoding string in quest 'short_description' field: {}",
|
||||
e
|
||||
)))
|
||||
}
|
||||
Ok(value) => value,
|
||||
};
|
||||
|
||||
let long_description_bytes: [u8; QUEST_BIN_LONG_DESCRIPTION_LENGTH] =
|
||||
reader.read_bytes()?;
|
||||
let long_description =
|
||||
match language.decode_text(long_description_bytes.as_unpadded_slice()) {
|
||||
Err(e) => {
|
||||
return Err(QuestBinError::DataFormatError(format!(
|
||||
"Error decoding string in quest 'long_description' field: {}",
|
||||
e
|
||||
)))
|
||||
}
|
||||
Ok(value) => value,
|
||||
};
|
||||
|
||||
let mut object_code =
|
||||
vec![0u8; (function_offset_table_offset - object_code_offset) as usize];
|
||||
reader.read_exact(&mut object_code)?;
|
||||
|
||||
let function_offset_table_size = bin_size - function_offset_table_offset;
|
||||
if function_offset_table_size % 4 != 0 {
|
||||
return Err(QuestBinError::DataFormatError(
|
||||
format!(
|
||||
"Non-dword-sized data segment found in quest bin where function offset table is expected. Function offset table data size: {}",
|
||||
function_offset_table_size
|
||||
)
|
||||
));
|
||||
}
|
||||
let mut function_offset_table = vec![0u8; function_offset_table_size as usize];
|
||||
reader.read_exact(&mut function_offset_table)?;
|
||||
|
||||
let bin = QuestBin {
|
||||
header: QuestBinHeader {
|
||||
is_download,
|
||||
language,
|
||||
quest_number,
|
||||
name,
|
||||
short_description,
|
||||
long_description,
|
||||
},
|
||||
object_code: object_code.into_boxed_slice(),
|
||||
function_offset_table: function_offset_table.into_boxed_slice(),
|
||||
};
|
||||
|
||||
let our_bin_size = bin.calculate_size();
|
||||
if our_bin_size != bin_size as usize {
|
||||
return Err(QuestBinError::DataFormatError(format!(
|
||||
"bin_size value {} found in header does not match size of data actually read {}",
|
||||
bin_size, our_bin_size
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(bin)
|
||||
}
|
||||
|
||||
pub fn from_compressed_file(path: &Path) -> Result<QuestBin, QuestBinError> {
|
||||
let mut file = File::open(path)?;
|
||||
let mut buffer = Vec::new();
|
||||
file.read_to_end(&mut buffer)?;
|
||||
QuestBin::from_compressed_bytes(&buffer)
|
||||
}
|
||||
|
||||
pub fn from_uncompressed_file(path: &Path) -> Result<QuestBin, QuestBinError> {
|
||||
let file = File::open(path)?;
|
||||
let mut reader = BufReader::new(file);
|
||||
Ok(QuestBin::from_uncompressed_bytes(&mut reader)?)
|
||||
}
|
||||
|
||||
pub fn write_uncompressed_bytes<T: WriteBytesExt>(
|
||||
&self,
|
||||
writer: &mut T,
|
||||
) -> Result<(), QuestBinError> {
|
||||
let bin_size = self.calculate_size();
|
||||
let object_code_offset = QUEST_BIN_HEADER_SIZE;
|
||||
let function_offset_table_offset = QUEST_BIN_HEADER_SIZE + self.object_code.len();
|
||||
|
||||
writer.write_u32::<LittleEndian>(object_code_offset as u32)?;
|
||||
writer.write_u32::<LittleEndian>(function_offset_table_offset as u32)?;
|
||||
writer.write_u32::<LittleEndian>(bin_size as u32)?;
|
||||
writer.write_u32::<LittleEndian>(0xfffffff)?; // always 0xffffffff
|
||||
writer.write_u8(self.header.is_download as u8)?;
|
||||
writer.write_u8(self.header.language as u8)?;
|
||||
writer.write_u16::<LittleEndian>(unsafe { self.header.quest_number.number })?;
|
||||
|
||||
let language = self.header.language;
|
||||
|
||||
let name_bytes = match language.encode_text(&self.header.name) {
|
||||
Err(e) => {
|
||||
return Err(QuestBinError::DataFormatError(format!(
|
||||
"Error encoding string for quest 'name' field: {}",
|
||||
e
|
||||
)))
|
||||
}
|
||||
Ok(value) => value,
|
||||
};
|
||||
writer.write_all(&name_bytes.to_array::<QUEST_BIN_NAME_LENGTH>())?;
|
||||
|
||||
let short_description_bytes = match language.encode_text(&self.header.short_description) {
|
||||
Err(e) => {
|
||||
return Err(QuestBinError::DataFormatError(format!(
|
||||
"Error encoding string for quest 'short_description_bytes' field: {}",
|
||||
e
|
||||
)))
|
||||
}
|
||||
Ok(value) => value,
|
||||
};
|
||||
writer
|
||||
.write_all(&short_description_bytes.to_array::<QUEST_BIN_SHORT_DESCRIPTION_LENGTH>())?;
|
||||
|
||||
let long_description_bytes = match language.encode_text(&self.header.long_description) {
|
||||
Err(e) => {
|
||||
return Err(QuestBinError::DataFormatError(format!(
|
||||
"Error encoding string for quest 'long_description_bytes' field: {}",
|
||||
e
|
||||
)))
|
||||
}
|
||||
Ok(value) => value,
|
||||
};
|
||||
writer
|
||||
.write_all(&long_description_bytes.to_array::<QUEST_BIN_LONG_DESCRIPTION_LENGTH>())?;
|
||||
|
||||
writer.write_all(self.object_code.as_ref())?;
|
||||
writer.write_all(self.function_offset_table.as_ref())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_compressed_file(&self, path: &Path) -> Result<(), QuestBinError> {
|
||||
let compressed_bytes = self.to_compressed_bytes()?;
|
||||
let mut file = File::create(path)?;
|
||||
file.write_all(compressed_bytes.as_ref())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_uncompressed_file(&self, path: &Path) -> Result<(), QuestBinError> {
|
||||
let mut file = File::create(path)?;
|
||||
self.write_uncompressed_bytes(&mut file)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_uncompressed_bytes(&self) -> Result<Box<[u8]>, QuestBinError> {
|
||||
let mut buffer = Cursor::new(Vec::<u8>::new());
|
||||
self.write_uncompressed_bytes(&mut buffer)?;
|
||||
Ok(buffer.into_inner().into_boxed_slice())
|
||||
}
|
||||
|
||||
pub fn to_compressed_bytes(&self) -> Result<Box<[u8]>, QuestBinError> {
|
||||
let uncompressed = self.to_uncompressed_bytes()?;
|
||||
Ok(prs_compress(uncompressed.as_ref())?)
|
||||
}
|
||||
|
||||
pub fn calculate_size(&self) -> usize {
|
||||
QUEST_BIN_HEADER_SIZE
|
||||
+ self.object_code.as_ref().len()
|
||||
+ self.function_offset_table.as_ref().len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use claim::*;
|
||||
use rand::prelude::StdRng;
|
||||
use rand::{Fill, SeedableRng};
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn validate_quest_58_bin(bin: &QuestBin) {
|
||||
assert_eq!(2000, bin.object_code.len());
|
||||
assert_eq!(4008, bin.function_offset_table.len());
|
||||
assert_eq!(6476, bin.calculate_size());
|
||||
|
||||
assert_eq!(58, bin.header.quest_number());
|
||||
assert_eq!(0, bin.header.episode());
|
||||
assert_eq!(58, bin.header.quest_number_u16());
|
||||
|
||||
assert_eq!(false, bin.header.is_download);
|
||||
assert_eq!(Language::Japanese, bin.header.language);
|
||||
|
||||
assert_eq!("Lost HEAT SWORD", bin.header.name);
|
||||
assert_eq!(
|
||||
"Retrieve a\nweapon from\na Dragon!",
|
||||
bin.header.short_description
|
||||
);
|
||||
assert_eq!(
|
||||
"Client: Hopkins, hunter\nQuest:\n My weapon was taken\n from me when I was\n fighting a Dragon.\nReward: ??? Meseta\n\n\n",
|
||||
bin.header.long_description
|
||||
);
|
||||
}
|
||||
|
||||
pub fn validate_quest_118_bin(bin: &QuestBin) {
|
||||
assert_eq!(32860, bin.object_code.len());
|
||||
assert_eq!(22004, bin.function_offset_table.len());
|
||||
assert_eq!(55332, bin.calculate_size());
|
||||
|
||||
assert_eq!(118, bin.header.quest_number());
|
||||
assert_eq!(0, bin.header.episode());
|
||||
assert_eq!(118, bin.header.quest_number_u16());
|
||||
|
||||
assert_eq!(false, bin.header.is_download);
|
||||
assert_eq!(Language::Japanese, bin.header.language);
|
||||
|
||||
assert_eq!("Towards the Future", bin.header.name);
|
||||
assert_eq!(
|
||||
"Challenge the\nnew simulator.",
|
||||
bin.header.short_description
|
||||
);
|
||||
assert_eq!(
|
||||
"Client: Principal\nQuest: Wishes to have\nhunters challenge the\nnew simulator\nReward: ??? Meseta",
|
||||
bin.header.long_description
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn read_compressed_quest_58_bin() -> Result<(), QuestBinError> {
|
||||
let path = Path::new("../test-assets/q058-ret-gc.bin");
|
||||
let bin = QuestBin::from_compressed_file(&path)?;
|
||||
validate_quest_58_bin(&bin);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn write_compressed_quest_58_bin() -> Result<(), QuestBinError> {
|
||||
let data = include_bytes!("../../../test-assets/q058-ret-gc.bin");
|
||||
let bin = QuestBin::from_compressed_bytes(data)?;
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let bin_path = tmp_dir.path().join("quest58.bin");
|
||||
bin.to_compressed_file(&bin_path)?;
|
||||
let bin = QuestBin::from_compressed_file(&bin_path)?;
|
||||
validate_quest_58_bin(&bin);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn read_uncompressed_quest_58_bin() -> Result<(), QuestBinError> {
|
||||
let path = Path::new("../test-assets/q058-ret-gc.uncompressed.bin");
|
||||
let bin = QuestBin::from_uncompressed_file(&path)?;
|
||||
validate_quest_58_bin(&bin);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn write_uncompressed_quest_58_bin() -> Result<(), QuestBinError> {
|
||||
let data = include_bytes!("../../../test-assets/q058-ret-gc.bin");
|
||||
let bin = QuestBin::from_compressed_bytes(data)?;
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let bin_path = tmp_dir.path().join("quest58.bin");
|
||||
bin.to_uncompressed_file(&bin_path)?;
|
||||
let bin = QuestBin::from_uncompressed_file(&bin_path)?;
|
||||
validate_quest_58_bin(&bin);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn read_compressed_quest_118_bin() -> Result<(), QuestBinError> {
|
||||
let path = Path::new("../test-assets/q118-vr-gc.bin");
|
||||
let bin = QuestBin::from_compressed_file(&path)?;
|
||||
validate_quest_118_bin(&bin);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn write_compressed_quest_118_bin() -> Result<(), QuestBinError> {
|
||||
let data = include_bytes!("../../../test-assets/q118-vr-gc.bin");
|
||||
let bin = QuestBin::from_compressed_bytes(data)?;
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let bin_path = tmp_dir.path().join("quest118.bin");
|
||||
bin.to_compressed_file(&bin_path)?;
|
||||
let bin = QuestBin::from_compressed_file(&bin_path)?;
|
||||
validate_quest_118_bin(&bin);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn read_uncompressed_quest_118_bin() -> Result<(), QuestBinError> {
|
||||
let path = Path::new("../test-assets/q118-vr-gc.uncompressed.bin");
|
||||
let bin = QuestBin::from_uncompressed_file(&path)?;
|
||||
validate_quest_118_bin(&bin);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn write_uncompressed_quest_118_bin() -> Result<(), QuestBinError> {
|
||||
let data = include_bytes!("../../../test-assets/q118-vr-gc.bin");
|
||||
let bin = QuestBin::from_compressed_bytes(data)?;
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let bin_path = tmp_dir.path().join("quest118.bin");
|
||||
bin.to_uncompressed_file(&bin_path)?;
|
||||
let bin = QuestBin::from_uncompressed_file(&bin_path)?;
|
||||
validate_quest_118_bin(&bin);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_on_load_from_zero_bytes() {
|
||||
let mut data: &[u8] = &[];
|
||||
assert_matches!(
|
||||
QuestBin::from_uncompressed_bytes(&mut data),
|
||||
Err(QuestBinError::IoError(..))
|
||||
);
|
||||
assert_matches!(
|
||||
QuestBin::from_compressed_bytes(&mut data),
|
||||
Err(QuestBinError::PrsCompressionError(..))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_on_load_from_garbage_bytes() {
|
||||
let mut data: &[u8] = b"This is definitely not a quest";
|
||||
assert_matches!(
|
||||
QuestBin::from_uncompressed_bytes(&mut data),
|
||||
Err(QuestBinError::DataFormatError(..))
|
||||
);
|
||||
assert_matches!(
|
||||
QuestBin::from_compressed_bytes(&mut data),
|
||||
Err(QuestBinError::PrsCompressionError(..))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_on_mostly_junk_header() {
|
||||
// the only correct part of this header is the object_code_offset
|
||||
let mut data = Vec::<u8>::new();
|
||||
data.write_u32::<LittleEndian>(QUEST_BIN_HEADER_SIZE as u32)
|
||||
.unwrap();
|
||||
data.write_all(b"This is also definitely not a quest")
|
||||
.unwrap();
|
||||
assert_matches!(
|
||||
QuestBin::from_uncompressed_bytes(&mut data.as_slice()),
|
||||
Err(QuestBinError::DataFormatError(..))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_on_header_with_bad_language_value() {
|
||||
// otherwise valid looking bin_header, but bad language value
|
||||
let mut data: &[u8] = &[
|
||||
0xD4, 0x01, 0x00, 0x00, // object_code_offset
|
||||
0xA4, 0x09, 0x00, 0x00, // function_offset_table_offset
|
||||
0x4C, 0x19, 0x00, 0x00, // bin_size
|
||||
0xFF, 0xFF, 0xFF, 0xFF, // xfffffff
|
||||
0x00, // is_download
|
||||
0x42, // language
|
||||
0x3A, 0x00, // quest_number_and_episode
|
||||
];
|
||||
assert_matches!(
|
||||
QuestBin::from_uncompressed_bytes(&mut data),
|
||||
Err(QuestBinError::DataFormatError(..))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_on_header_with_garbage_text_fields() {
|
||||
// valid bin_header (so far) ...
|
||||
let header: &[u8] = &[
|
||||
0xD4, 0x01, 0x00, 0x00, // object_code_offset
|
||||
0xA4, 0x09, 0x00, 0x00, // function_offset_table_offset
|
||||
0x4C, 0x19, 0x00, 0x00, // bin_size
|
||||
0xFF, 0xFF, 0xFF, 0xFF, // xfffffff
|
||||
0x00, // is_download
|
||||
0x00, // language
|
||||
0x3A, 0x00, // quest_number_and_episode
|
||||
];
|
||||
// ... and lets append random garbage to it.
|
||||
// this garbage will read as text fields (name, etc) and be decoded from shift jis which
|
||||
// will fail ...
|
||||
let mut random_garbage = [0u8; 4096];
|
||||
let mut rng = StdRng::seed_from_u64(123456);
|
||||
random_garbage.try_fill(&mut rng).unwrap();
|
||||
let data = [header, &random_garbage].concat();
|
||||
assert_matches!(
|
||||
QuestBin::from_uncompressed_bytes(&mut data.as_slice()),
|
||||
Err(QuestBinError::DataFormatError(..))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_on_bin_that_is_too_small() {
|
||||
// a complete valid bin_header ...
|
||||
let header: &[u8] = &[
|
||||
0xD4, 0x01, 0x00, 0x00, 0xA4, 0x09, 0x00, 0x00, 0x4C, 0x19, 0x00, 0x00, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0x00, 0x00, 0x3A, 0x00, 0x4C, 0x6F, 0x73, 0x74, 0x20, 0x48, 0x45, 0x41,
|
||||
0x54, 0x20, 0x53, 0x57, 0x4F, 0x52, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0x65, 0x74, 0x72,
|
||||
0x69, 0x65, 0x76, 0x65, 0x20, 0x61, 0x0A, 0x77, 0x65, 0x61, 0x70, 0x6F, 0x6E, 0x20,
|
||||
0x66, 0x72, 0x6F, 0x6D, 0x0A, 0x61, 0x20, 0x44, 0x72, 0x61, 0x67, 0x6F, 0x6E, 0x21,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x43, 0x6C,
|
||||
0x69, 0x65, 0x6E, 0x74, 0x3A, 0x20, 0x20, 0x48, 0x6F, 0x70, 0x6B, 0x69, 0x6E, 0x73,
|
||||
0x2C, 0x20, 0x68, 0x75, 0x6E, 0x74, 0x65, 0x72, 0x0A, 0x51, 0x75, 0x65, 0x73, 0x74,
|
||||
0x3A, 0x0A, 0x20, 0x4D, 0x79, 0x20, 0x77, 0x65, 0x61, 0x70, 0x6F, 0x6E, 0x20, 0x77,
|
||||
0x61, 0x73, 0x20, 0x74, 0x61, 0x6B, 0x65, 0x6E, 0x0A, 0x20, 0x66, 0x72, 0x6F, 0x6D,
|
||||
0x20, 0x6D, 0x65, 0x20, 0x77, 0x68, 0x65, 0x6E, 0x20, 0x49, 0x20, 0x77, 0x61, 0x73,
|
||||
0x0A, 0x20, 0x66, 0x69, 0x67, 0x68, 0x74, 0x69, 0x6E, 0x67, 0x20, 0x61, 0x20, 0x44,
|
||||
0x72, 0x61, 0x67, 0x6F, 0x6E, 0x2E, 0x0A, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x3A,
|
||||
0x20, 0x20, 0x3F, 0x3F, 0x3F, 0x20, 0x4D, 0x65, 0x73, 0x65, 0x74, 0x61, 0x0A, 0x0A,
|
||||
0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
];
|
||||
// ... and lets again append random garbage to it
|
||||
let mut random_garbage = [0u8; 4096];
|
||||
let mut rng = StdRng::seed_from_u64(35645128);
|
||||
random_garbage.try_fill(&mut rng).unwrap();
|
||||
let data = [header, &random_garbage].concat();
|
||||
// note that the only reason this fails is because the buffer we've provided is not large
|
||||
// enough. this call would return successfully if random_garbage was large enough (based on
|
||||
// the sizes loaded from the bin_header) since we don't parse/validate either the
|
||||
// object_code or function_offset_table data
|
||||
assert_matches!(
|
||||
QuestBin::from_uncompressed_bytes(&mut data.as_slice()),
|
||||
Err(QuestBinError::IoError(..))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_on_non_dword_sized_function_offset_table() {
|
||||
// i don't think this scenario being tested is really important ... it would mean something
|
||||
// is generating garbage function_offset_tables to begin with ...
|
||||
|
||||
// a complete valid bin_header ...
|
||||
let header: &[u8] = &[
|
||||
0xD4, 0x01, 0x00, 0x00, 0xA4, 0x09, 0x00, 0x00, 0x4A, 0x19, 0x00, 0x00, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0x00, 0x00, 0x3A, 0x00, 0x4C, 0x6F, 0x73, 0x74, 0x20, 0x48, 0x45, 0x41,
|
||||
0x54, 0x20, 0x53, 0x57, 0x4F, 0x52, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x52, 0x65, 0x74, 0x72,
|
||||
0x69, 0x65, 0x76, 0x65, 0x20, 0x61, 0x0A, 0x77, 0x65, 0x61, 0x70, 0x6F, 0x6E, 0x20,
|
||||
0x66, 0x72, 0x6F, 0x6D, 0x0A, 0x61, 0x20, 0x44, 0x72, 0x61, 0x67, 0x6F, 0x6E, 0x21,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x43, 0x6C,
|
||||
0x69, 0x65, 0x6E, 0x74, 0x3A, 0x20, 0x20, 0x48, 0x6F, 0x70, 0x6B, 0x69, 0x6E, 0x73,
|
||||
0x2C, 0x20, 0x68, 0x75, 0x6E, 0x74, 0x65, 0x72, 0x0A, 0x51, 0x75, 0x65, 0x73, 0x74,
|
||||
0x3A, 0x0A, 0x20, 0x4D, 0x79, 0x20, 0x77, 0x65, 0x61, 0x70, 0x6F, 0x6E, 0x20, 0x77,
|
||||
0x61, 0x73, 0x20, 0x74, 0x61, 0x6B, 0x65, 0x6E, 0x0A, 0x20, 0x66, 0x72, 0x6F, 0x6D,
|
||||
0x20, 0x6D, 0x65, 0x20, 0x77, 0x68, 0x65, 0x6E, 0x20, 0x49, 0x20, 0x77, 0x61, 0x73,
|
||||
0x0A, 0x20, 0x66, 0x69, 0x67, 0x68, 0x74, 0x69, 0x6E, 0x67, 0x20, 0x61, 0x20, 0x44,
|
||||
0x72, 0x61, 0x67, 0x6F, 0x6E, 0x2E, 0x0A, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x3A,
|
||||
0x20, 0x20, 0x3F, 0x3F, 0x3F, 0x20, 0x4D, 0x65, 0x73, 0x65, 0x74, 0x61, 0x0A, 0x0A,
|
||||
0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
];
|
||||
let object_code_data = [0u8; 2000];
|
||||
let function_offset_table_data = [0u8; 4006]; // size here matches header above, but it is still a bad size
|
||||
let data = [header, &object_code_data, &function_offset_table_data].concat();
|
||||
|
||||
assert_matches!(
|
||||
QuestBin::from_uncompressed_bytes(&mut data.as_slice()),
|
||||
Err(QuestBinError::DataFormatError(..))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,728 +0,0 @@
|
|||
use std::fmt::{Display, Formatter};
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, Cursor, Read, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::compression::{prs_compress, prs_decompress, PrsCompressionError};
|
||||
|
||||
pub const QUEST_DAT_TABLE_HEADER_SIZE: usize = 16;
|
||||
|
||||
pub const QUEST_DAT_AREAS: [[&str; 18]; 2] = [
|
||||
[
|
||||
"Pioneer 2",
|
||||
"Forest 1",
|
||||
"Forest 2",
|
||||
"Caves 1",
|
||||
"Caves 2",
|
||||
"Caves 3",
|
||||
"Mines 1",
|
||||
"Mines 2",
|
||||
"Ruins 1",
|
||||
"Ruins 2",
|
||||
"Ruins 3",
|
||||
"Under the Dome",
|
||||
"Underground Channel",
|
||||
"Monitor Room",
|
||||
"????",
|
||||
"Visual Lobby",
|
||||
"VR Spaceship Alpha",
|
||||
"VR Temple Alpha",
|
||||
],
|
||||
[
|
||||
"Lab",
|
||||
"VR Temple Alpha",
|
||||
"VR Temple Beta",
|
||||
"VR Spaceship Alpha",
|
||||
"VR Spaceship Beta",
|
||||
"Central Control Area",
|
||||
"Jungle North",
|
||||
"Jungle East",
|
||||
"Mountain",
|
||||
"Seaside",
|
||||
"Seabed Upper",
|
||||
"Seabed Lower",
|
||||
"Cliffs of Gal Da Val",
|
||||
"Test Subject Disposal Area",
|
||||
"VR Temple Final",
|
||||
"VR Spaceship Final",
|
||||
"Seaside Night",
|
||||
"Control Tower",
|
||||
],
|
||||
];
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum QuestDatError {
|
||||
#[error("I/O error while processing quest dat")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("PRS compression failed")]
|
||||
PrsCompressionError(#[from] PrsCompressionError),
|
||||
|
||||
#[error("Bad quest dat data format: {0}")]
|
||||
DataFormatError(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||
pub enum QuestDatTableType {
|
||||
Object,
|
||||
NPC,
|
||||
Wave,
|
||||
ChallengeModeSpawns,
|
||||
ChallengeModeUnknown,
|
||||
Unknown(u32),
|
||||
}
|
||||
|
||||
impl Display for QuestDatTableType {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
use QuestDatTableType::*;
|
||||
match self {
|
||||
Object => write!(f, "Object"),
|
||||
NPC => write!(f, "NPC"),
|
||||
Wave => write!(f, "Wave"),
|
||||
ChallengeModeSpawns => write!(f, "Challenge Mode Spawns"),
|
||||
ChallengeModeUnknown => write!(f, "Challenge Mode Unknown"),
|
||||
Unknown(n) => write!(f, "Unknown value ({})", n),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for QuestDatTableType {
|
||||
fn from(value: u32) -> Self {
|
||||
// TODO: is there some way to cast an int back to an enum?
|
||||
use QuestDatTableType::*;
|
||||
match value {
|
||||
1 => Object,
|
||||
2 => NPC,
|
||||
3 => Wave,
|
||||
4 => ChallengeModeSpawns,
|
||||
5 => ChallengeModeUnknown,
|
||||
n => Unknown(n),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&QuestDatTableType> for u32 {
|
||||
fn from(value: &QuestDatTableType) -> Self {
|
||||
use QuestDatTableType::*;
|
||||
match *value {
|
||||
Object => 1,
|
||||
NPC => 2,
|
||||
Wave => 3,
|
||||
ChallengeModeSpawns => 4,
|
||||
ChallengeModeUnknown => 5,
|
||||
Unknown(n) => n,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct QuestDatTableHeader {
|
||||
pub table_type: QuestDatTableType,
|
||||
pub area: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct QuestDatTable {
|
||||
pub header: QuestDatTableHeader,
|
||||
pub bytes: Box<[u8]>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum QuestArea {
|
||||
Area(&'static str),
|
||||
InvalidArea(u32),
|
||||
InvalidEpisode(u32),
|
||||
}
|
||||
|
||||
impl Display for QuestArea {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
use QuestArea::*;
|
||||
match self {
|
||||
Area(s) => write!(f, "{}", s),
|
||||
InvalidArea(n) => write!(f, "Invalid Area: {}", n),
|
||||
InvalidEpisode(n) => write!(f, "Invalid Episode: {}", n),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl QuestDatTable {
|
||||
pub fn table_type(&self) -> QuestDatTableType {
|
||||
self.header.table_type
|
||||
}
|
||||
|
||||
pub fn area_name(&self, episode: u32) -> QuestArea {
|
||||
use QuestArea::*;
|
||||
match QUEST_DAT_AREAS.get(episode as usize) {
|
||||
Some(list) => match list.get(self.header.area as usize) {
|
||||
Some(area) => Area(area),
|
||||
None => InvalidArea(self.header.area),
|
||||
},
|
||||
None => InvalidEpisode(episode),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_size(&self) -> usize {
|
||||
QUEST_DAT_TABLE_HEADER_SIZE + self.bytes.as_ref().len()
|
||||
}
|
||||
|
||||
fn body_size(&self) -> usize {
|
||||
self.bytes.as_ref().len()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct QuestDat {
|
||||
pub tables: Box<[QuestDatTable]>,
|
||||
}
|
||||
|
||||
impl QuestDat {
|
||||
pub fn from_compressed_bytes(bytes: &[u8]) -> Result<QuestDat, QuestDatError> {
|
||||
let decompressed = prs_decompress(&bytes)?;
|
||||
let mut reader = Cursor::new(decompressed);
|
||||
Ok(QuestDat::from_uncompressed_bytes(&mut reader)?)
|
||||
}
|
||||
|
||||
pub fn from_uncompressed_bytes<T: ReadBytesExt>(
|
||||
reader: &mut T,
|
||||
) -> Result<QuestDat, QuestDatError> {
|
||||
let mut tables = Vec::new();
|
||||
let mut index = 0;
|
||||
loop {
|
||||
let table_type = reader.read_u32::<LittleEndian>()?;
|
||||
let table_size = reader.read_u32::<LittleEndian>()?;
|
||||
let area = reader.read_u32::<LittleEndian>()?;
|
||||
let table_body_size = reader.read_u32::<LittleEndian>()?;
|
||||
|
||||
// quest .dat files appear to always use a "zero-table" to mark the end of the file
|
||||
if table_type == 0 && table_size == 0 && area == 0 && table_body_size == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if table_size != table_body_size.wrapping_add(QUEST_DAT_TABLE_HEADER_SIZE as u32) {
|
||||
return Err(QuestDatError::DataFormatError(format!(
|
||||
"Malformed table at index {}. table_size != table_body_size + 16",
|
||||
index
|
||||
)));
|
||||
}
|
||||
|
||||
let table_type: QuestDatTableType = match table_type.into() {
|
||||
QuestDatTableType::Unknown(n) => {
|
||||
return Err(QuestDatError::DataFormatError(format!(
|
||||
"Invalid table_type {} for table at index {}",
|
||||
n, index
|
||||
)))
|
||||
}
|
||||
otherwise => otherwise,
|
||||
};
|
||||
|
||||
// note: both episode area lists are the same size
|
||||
if area >= QUEST_DAT_AREAS[0].len() as u32 {
|
||||
return Err(QuestDatError::DataFormatError(format!(
|
||||
"Invalid area {} for table at index {}",
|
||||
area, index
|
||||
)));
|
||||
}
|
||||
|
||||
let mut body_bytes = vec![0u8; table_body_size as usize];
|
||||
reader.read_exact(&mut body_bytes)?;
|
||||
|
||||
tables.push(QuestDatTable {
|
||||
header: QuestDatTableHeader { table_type, area },
|
||||
bytes: body_bytes.into_boxed_slice(),
|
||||
});
|
||||
|
||||
index += 1;
|
||||
}
|
||||
|
||||
// i wrote this check thinking that an empty .dat file is the most useless thing ever,
|
||||
// but maybe it is possible to exist in a legitimate quest for a totally script-driven
|
||||
// "quest" ... e.g. some sort of "utility quest" that exists just for the purpose of letting
|
||||
// a user interact with the script stored in the .bin? dunno really, but i guess i might
|
||||
// as well disable this check ...
|
||||
//
|
||||
//if tables.len() == 0 {
|
||||
// return Err(QuestDatError::DataFormatError(String::from(
|
||||
// "no tables found, probably not a .dat file?",
|
||||
// )));
|
||||
//}
|
||||
|
||||
Ok(QuestDat {
|
||||
tables: tables.into_boxed_slice(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_compressed_file(path: &Path) -> Result<QuestDat, QuestDatError> {
|
||||
let mut file = File::open(path)?;
|
||||
let mut buffer = Vec::new();
|
||||
file.read_to_end(&mut buffer)?;
|
||||
QuestDat::from_compressed_bytes(&buffer)
|
||||
}
|
||||
|
||||
pub fn from_uncompressed_file(path: &Path) -> Result<QuestDat, QuestDatError> {
|
||||
let file = File::open(path)?;
|
||||
let mut reader = BufReader::new(file);
|
||||
Ok(QuestDat::from_uncompressed_bytes(&mut reader)?)
|
||||
}
|
||||
|
||||
pub fn write_uncompressed_bytes<T: WriteBytesExt>(
|
||||
&self,
|
||||
writer: &mut T,
|
||||
) -> Result<(), QuestDatError> {
|
||||
for table in self.tables.iter() {
|
||||
let table_size = table.calculate_size() as u32;
|
||||
let table_body_size = table.body_size() as u32;
|
||||
|
||||
writer.write_u32::<LittleEndian>((&table.header.table_type).into())?;
|
||||
writer.write_u32::<LittleEndian>(table_size)?;
|
||||
writer.write_u32::<LittleEndian>(table.header.area)?;
|
||||
writer.write_u32::<LittleEndian>(table_body_size)?;
|
||||
|
||||
writer.write_all(table.bytes.as_ref())?;
|
||||
}
|
||||
|
||||
// write "zero table" at eof. this seems to be a convention used everywhere for quest .dat
|
||||
writer.write_u32::<LittleEndian>(0)?; // table_type
|
||||
writer.write_u32::<LittleEndian>(0)?; // table_size
|
||||
writer.write_u32::<LittleEndian>(0)?; // area
|
||||
writer.write_u32::<LittleEndian>(0)?; // table_body_size
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_compressed_file(&self, path: &Path) -> Result<(), QuestDatError> {
|
||||
let compressed_bytes = self.to_compressed_bytes()?;
|
||||
let mut file = File::create(path)?;
|
||||
file.write_all(compressed_bytes.as_ref())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_uncompressed_file(&self, path: &Path) -> Result<(), QuestDatError> {
|
||||
let mut file = File::create(path)?;
|
||||
self.write_uncompressed_bytes(&mut file)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_uncompressed_bytes(&self) -> Result<Box<[u8]>, QuestDatError> {
|
||||
let mut buffer = Cursor::new(Vec::<u8>::new());
|
||||
self.write_uncompressed_bytes(&mut buffer)?;
|
||||
Ok(buffer.into_inner().into_boxed_slice())
|
||||
}
|
||||
|
||||
pub fn to_compressed_bytes(&self) -> Result<Box<[u8]>, QuestDatError> {
|
||||
let uncompressed = self.to_uncompressed_bytes()?;
|
||||
Ok(prs_compress(uncompressed.as_ref())?)
|
||||
}
|
||||
|
||||
pub fn calculate_size(&self) -> usize {
|
||||
self.tables
|
||||
.iter()
|
||||
.map(|table| QUEST_DAT_TABLE_HEADER_SIZE + table.body_size() as usize)
|
||||
.sum()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use claim::*;
|
||||
use rand::prelude::StdRng;
|
||||
use rand::{Fill, SeedableRng};
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn validate_quest_58_dat(dat: &QuestDat) {
|
||||
let episode = 0;
|
||||
|
||||
assert_eq!(11, dat.tables.len());
|
||||
|
||||
let table = &dat.tables[0];
|
||||
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||
assert_eq!(2260, table.calculate_size());
|
||||
assert_eq!(2244, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Pioneer 2"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[1];
|
||||
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||
assert_eq!(7020, table.calculate_size());
|
||||
assert_eq!(7004, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Forest 1"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[2];
|
||||
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||
assert_eq!(9536, table.calculate_size());
|
||||
assert_eq!(9520, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Forest 2"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[3];
|
||||
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||
assert_eq!(1376, table.calculate_size());
|
||||
assert_eq!(1360, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Under the Dome"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[4];
|
||||
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||
assert_eq!(1672, table.calculate_size());
|
||||
assert_eq!(1656, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Pioneer 2"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[5];
|
||||
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||
assert_eq!(6064, table.calculate_size());
|
||||
assert_eq!(6048, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Forest 1"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[6];
|
||||
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||
assert_eq!(7432, table.calculate_size());
|
||||
assert_eq!(7416, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Forest 2"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[7];
|
||||
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||
assert_eq!(88, table.calculate_size());
|
||||
assert_eq!(72, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Under the Dome"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[8];
|
||||
assert_eq!(QuestDatTableType::Wave, table.table_type());
|
||||
assert_eq!(560, table.calculate_size());
|
||||
assert_eq!(544, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Forest 1"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[9];
|
||||
assert_eq!(QuestDatTableType::Wave, table.table_type());
|
||||
assert_eq!(736, table.calculate_size());
|
||||
assert_eq!(720, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Forest 2"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[10];
|
||||
assert_eq!(QuestDatTableType::Wave, table.table_type());
|
||||
assert_eq!(60, table.calculate_size());
|
||||
assert_eq!(44, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Under the Dome"), table.area_name(episode));
|
||||
}
|
||||
|
||||
pub fn validate_quest_118_dat(dat: &QuestDat) {
|
||||
let episode = 0;
|
||||
|
||||
assert_eq!(25, dat.tables.len());
|
||||
|
||||
let table = &dat.tables[0];
|
||||
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||
assert_eq!(1988, table.calculate_size());
|
||||
assert_eq!(1972, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Forest 2"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[1];
|
||||
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||
assert_eq!(2872, table.calculate_size());
|
||||
assert_eq!(2856, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Caves 3"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[2];
|
||||
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||
assert_eq!(2532, table.calculate_size());
|
||||
assert_eq!(2516, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Mines 2"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[3];
|
||||
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||
assert_eq!(2668, table.calculate_size());
|
||||
assert_eq!(2652, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Ruins 3"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[4];
|
||||
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||
assert_eq!(1580, table.calculate_size());
|
||||
assert_eq!(1564, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Under the Dome"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[5];
|
||||
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||
assert_eq!(1104, table.calculate_size());
|
||||
assert_eq!(1088, table.body_size());
|
||||
assert_eq!(
|
||||
QuestArea::Area("Underground Channel"),
|
||||
table.area_name(episode)
|
||||
);
|
||||
|
||||
let table = &dat.tables[6];
|
||||
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||
assert_eq!(2056, table.calculate_size());
|
||||
assert_eq!(2040, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Monitor Room"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[7];
|
||||
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||
assert_eq!(2396, table.calculate_size());
|
||||
assert_eq!(2380, table.body_size());
|
||||
assert_eq!(QuestArea::Area("????"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[8];
|
||||
assert_eq!(QuestDatTableType::Object, table.table_type());
|
||||
assert_eq!(1784, table.calculate_size());
|
||||
assert_eq!(1768, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Pioneer 2"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[9];
|
||||
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||
assert_eq!(1528, table.calculate_size());
|
||||
assert_eq!(1512, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Pioneer 2"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[10];
|
||||
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||
assert_eq!(2392, table.calculate_size());
|
||||
assert_eq!(2376, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Forest 2"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[11];
|
||||
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||
assert_eq!(3760, table.calculate_size());
|
||||
assert_eq!(3744, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Caves 3"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[12];
|
||||
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||
assert_eq!(4480, table.calculate_size());
|
||||
assert_eq!(4464, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Mines 2"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[13];
|
||||
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||
assert_eq!(3256, table.calculate_size());
|
||||
assert_eq!(3240, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Ruins 3"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[14];
|
||||
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||
assert_eq!(88, table.calculate_size());
|
||||
assert_eq!(72, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Under the Dome"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[15];
|
||||
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||
assert_eq!(88, table.calculate_size());
|
||||
assert_eq!(72, table.body_size());
|
||||
assert_eq!(
|
||||
QuestArea::Area("Underground Channel"),
|
||||
table.area_name(episode)
|
||||
);
|
||||
|
||||
let table = &dat.tables[16];
|
||||
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||
assert_eq!(160, table.calculate_size());
|
||||
assert_eq!(144, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Monitor Room"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[17];
|
||||
assert_eq!(QuestDatTableType::NPC, table.table_type());
|
||||
assert_eq!(88, table.calculate_size());
|
||||
assert_eq!(72, table.body_size());
|
||||
assert_eq!(QuestArea::Area("????"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[18];
|
||||
assert_eq!(QuestDatTableType::Wave, table.table_type());
|
||||
assert_eq!(232, table.calculate_size());
|
||||
assert_eq!(216, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Forest 2"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[19];
|
||||
assert_eq!(QuestDatTableType::Wave, table.table_type());
|
||||
assert_eq!(532, table.calculate_size());
|
||||
assert_eq!(516, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Caves 3"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[20];
|
||||
assert_eq!(QuestDatTableType::Wave, table.table_type());
|
||||
assert_eq!(768, table.calculate_size());
|
||||
assert_eq!(752, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Mines 2"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[21];
|
||||
assert_eq!(QuestDatTableType::Wave, table.table_type());
|
||||
assert_eq!(368, table.calculate_size());
|
||||
assert_eq!(352, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Ruins 3"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[22];
|
||||
assert_eq!(QuestDatTableType::Wave, table.table_type());
|
||||
assert_eq!(60, table.calculate_size());
|
||||
assert_eq!(44, table.body_size());
|
||||
assert_eq!(QuestArea::Area("Under the Dome"), table.area_name(episode));
|
||||
|
||||
let table = &dat.tables[23];
|
||||
assert_eq!(QuestDatTableType::Wave, table.table_type());
|
||||
assert_eq!(60, table.calculate_size());
|
||||
assert_eq!(44, table.body_size());
|
||||
assert_eq!(
|
||||
QuestArea::Area("Underground Channel"),
|
||||
table.area_name(episode)
|
||||
);
|
||||
|
||||
let table = &dat.tables[24];
|
||||
assert_eq!(QuestDatTableType::Wave, table.table_type());
|
||||
assert_eq!(68, table.calculate_size());
|
||||
assert_eq!(52, table.body_size());
|
||||
assert_eq!(QuestArea::Area("????"), table.area_name(episode));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn read_compressed_quest_58_dat() -> Result<(), QuestDatError> {
|
||||
let path = Path::new("../test-assets/q058-ret-gc.dat");
|
||||
let dat = QuestDat::from_compressed_file(&path)?;
|
||||
validate_quest_58_dat(&dat);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn write_compressed_quest_58_dat() -> Result<(), QuestDatError> {
|
||||
let data = include_bytes!("../../../test-assets/q058-ret-gc.dat");
|
||||
let dat = QuestDat::from_compressed_bytes(data)?;
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let dat_path = tmp_dir.path().join("quest58.dat");
|
||||
dat.to_compressed_file(&dat_path)?;
|
||||
let dat = QuestDat::from_compressed_file(&dat_path)?;
|
||||
validate_quest_58_dat(&dat);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn read_uncompressed_quest_58_dat() -> Result<(), QuestDatError> {
|
||||
let path = Path::new("../test-assets/q058-ret-gc.uncompressed.dat");
|
||||
let dat = QuestDat::from_uncompressed_file(&path)?;
|
||||
validate_quest_58_dat(&dat);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn write_uncompressed_quest_58_dat() -> Result<(), QuestDatError> {
|
||||
let data = include_bytes!("../../../test-assets/q058-ret-gc.dat");
|
||||
let dat = QuestDat::from_compressed_bytes(data)?;
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let dat_path = tmp_dir.path().join("quest58.dat");
|
||||
dat.to_uncompressed_file(&dat_path)?;
|
||||
let dat = QuestDat::from_uncompressed_file(&dat_path)?;
|
||||
validate_quest_58_dat(&dat);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn read_compressed_quest_118_dat() -> Result<(), QuestDatError> {
|
||||
let path = Path::new("../test-assets/q118-vr-gc.dat");
|
||||
let dat = QuestDat::from_compressed_file(&path)?;
|
||||
validate_quest_118_dat(&dat);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn write_compressed_quest_118_dat() -> Result<(), QuestDatError> {
|
||||
let data = include_bytes!("../../../test-assets/q118-vr-gc.dat");
|
||||
let dat = QuestDat::from_compressed_bytes(data)?;
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let dat_path = tmp_dir.path().join("quest118.dat");
|
||||
dat.to_compressed_file(&dat_path)?;
|
||||
let dat = QuestDat::from_compressed_file(&dat_path)?;
|
||||
validate_quest_118_dat(&dat);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn read_uncompressed_quest_118_dat() -> Result<(), QuestDatError> {
|
||||
let path = Path::new("../test-assets/q118-vr-gc.uncompressed.dat");
|
||||
let dat = QuestDat::from_uncompressed_file(&path)?;
|
||||
validate_quest_118_dat(&dat);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn write_uncompressed_quest_118_dat() -> Result<(), QuestDatError> {
|
||||
let data = include_bytes!("../../../test-assets/q118-vr-gc.dat");
|
||||
let dat = QuestDat::from_compressed_bytes(data)?;
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let dat_path = tmp_dir.path().join("quest118.dat");
|
||||
dat.to_uncompressed_file(&dat_path)?;
|
||||
let dat = QuestDat::from_uncompressed_file(&dat_path)?;
|
||||
validate_quest_118_dat(&dat);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_on_load_from_zero_bytes() {
|
||||
let mut data: &[u8] = &[];
|
||||
assert_matches!(
|
||||
QuestDat::from_uncompressed_bytes(&mut data),
|
||||
Err(QuestDatError::IoError(..))
|
||||
);
|
||||
assert_matches!(
|
||||
QuestDat::from_compressed_bytes(&mut data),
|
||||
Err(QuestDatError::PrsCompressionError(..))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_on_load_from_garbage_bytes() {
|
||||
let mut data: &[u8] = b"This is definitely not a quest";
|
||||
assert_matches!(
|
||||
QuestDat::from_uncompressed_bytes(&mut data),
|
||||
Err(QuestDatError::DataFormatError(..))
|
||||
);
|
||||
assert_matches!(
|
||||
QuestDat::from_compressed_bytes(&mut data),
|
||||
Err(QuestDatError::PrsCompressionError(..))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn errpr_on_table_header_with_bad_table_size() {
|
||||
// dat table header with a table_size issue (table_size != table_body_size + 16)
|
||||
let mut header: &[u8] = &[
|
||||
0x01, 0x00, 0x00, 0x00, // table_type
|
||||
0xD3, 0x08, 0x00, 0x00, // table_size
|
||||
0x00, 0x00, 0x00, 0x00, // area
|
||||
0xC4, 0x08, 0x00, 0x00, // table_body_size
|
||||
];
|
||||
assert_matches!(
|
||||
QuestDat::from_uncompressed_bytes(&mut header),
|
||||
Err(QuestDatError::DataFormatError(..))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_on_table_header_with_bad_table_type() {
|
||||
// dat table header with a table_type issue
|
||||
let mut header: &[u8] = &[
|
||||
0x11, 0x00, 0x00, 0x00, // table_type
|
||||
0xD4, 0x08, 0x00, 0x00, // table_size
|
||||
0x00, 0x00, 0x00, 0x00, // area
|
||||
0xC4, 0x08, 0x00, 0x00, // table_body_size
|
||||
];
|
||||
assert_matches!(
|
||||
QuestDat::from_uncompressed_bytes(&mut header),
|
||||
Err(QuestDatError::DataFormatError(..))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_on_table_with_body_data_that_is_too_small() {
|
||||
// a valid dat table header ...
|
||||
let header: &[u8] = &[
|
||||
0x01, 0x00, 0x00, 0x00, // table_type
|
||||
0xD4, 0x08, 0x00, 0x00, // table_size
|
||||
0x00, 0x00, 0x00, 0x00, // area
|
||||
0xC4, 0x08, 0x00, 0x00, // table_body_size
|
||||
];
|
||||
// ... with not enough random garbage in the table body area
|
||||
let mut random_garbage = [0u8; 256];
|
||||
let mut rng = StdRng::seed_from_u64(76478964);
|
||||
random_garbage.try_fill(&mut rng).unwrap();
|
||||
let data = [header, &random_garbage].concat();
|
||||
assert_matches!(
|
||||
QuestDat::from_uncompressed_bytes(&mut data.as_slice()),
|
||||
Err(QuestDatError::IoError(..))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,983 +0,0 @@
|
|||
use std::fs::File;
|
||||
use std::io::{BufReader, Cursor, Write};
|
||||
use std::path::Path;
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use itertools::Itertools;
|
||||
use rand::random;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::bytes::FixedLengthByteArrays;
|
||||
use crate::encryption::{Crypter, PCCrypter};
|
||||
use crate::packets::quest::*;
|
||||
use crate::packets::{PacketError, PacketHeader};
|
||||
use crate::quest::bin::{QuestBin, QuestBinError};
|
||||
use crate::quest::dat::{QuestDat, QuestDatError};
|
||||
use crate::text::LanguageError;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum QuestQstError {
|
||||
#[error("I/O error while processing quest qst")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
#[error("String encoding error during processing of quest qst string data")]
|
||||
StringEncodingError(#[from] LanguageError),
|
||||
|
||||
#[error("Error reading quest qst data packet")]
|
||||
DataPacketError(#[from] PacketError),
|
||||
|
||||
#[error("Bad quest qst data format: {0}")]
|
||||
DataFormatError(String),
|
||||
|
||||
#[error("Error processing quest bin")]
|
||||
QuestBinError(#[from] QuestBinError),
|
||||
|
||||
#[error("Error processing quest dat")]
|
||||
QuestDatError(#[from] QuestDatError),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct QuestQst {
|
||||
bin_header: QuestHeaderPacket,
|
||||
dat_header: QuestHeaderPacket,
|
||||
bin_chunks: Box<[QuestDataPacket]>,
|
||||
dat_chunks: Box<[QuestDataPacket]>,
|
||||
}
|
||||
|
||||
fn encrypt_quest_data(
|
||||
quest_data: &mut [u8],
|
||||
decompressed_size: usize,
|
||||
) -> Result<Box<[u8]>, QuestQstError> {
|
||||
let crypt_key = random::<u32>();
|
||||
|
||||
// yes, PC encryption is used even for gamecube qst files
|
||||
let mut crypter = PCCrypter::new(crypt_key);
|
||||
crypter.crypt(quest_data);
|
||||
|
||||
let mut result = Vec::<u8>::with_capacity(8 + quest_data.len());
|
||||
result.write_u32::<LittleEndian>(decompressed_size as u32)?;
|
||||
result.write_u32::<LittleEndian>(crypt_key)?;
|
||||
result.write_all(quest_data)?;
|
||||
Ok(result.into_boxed_slice())
|
||||
}
|
||||
|
||||
fn decrypt_quest_data(quest_data: &mut [u8]) -> Result<&[u8], QuestQstError> {
|
||||
let mut prefix = &quest_data[0..8];
|
||||
let _decompressed_size = prefix.read_u32::<LittleEndian>()?;
|
||||
let crypt_key = prefix.read_u32::<LittleEndian>()?;
|
||||
|
||||
// yes, PC encryption is used even for gamecube qst files
|
||||
let mut crypter = PCCrypter::new(crypt_key);
|
||||
let mut result = &mut quest_data[8..];
|
||||
crypter.crypt(&mut result);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn create_quest_data_chunks(
|
||||
quest_data: &[u8],
|
||||
filename: &str,
|
||||
is_online_quest: bool,
|
||||
) -> Result<Box<[QuestDataPacket]>, QuestQstError> {
|
||||
let mut chunks = Vec::<QuestDataPacket>::new();
|
||||
for (index, chunk) in quest_data.chunks(QUEST_DATA_PACKET_DATA_SIZE).enumerate() {
|
||||
let mut chunk = QuestDataPacket::new(&filename, chunk, is_online_quest)?;
|
||||
chunk.header.flags = index as u8;
|
||||
chunks.push(chunk);
|
||||
}
|
||||
Ok(chunks.into_boxed_slice())
|
||||
}
|
||||
|
||||
fn extract_quest_chunk_data(
|
||||
chunks: &[QuestDataPacket],
|
||||
is_online_quest: bool,
|
||||
) -> Result<Vec<u8>, QuestQstError> {
|
||||
// TODO: rewrite this function, it is kinda sloppy ...
|
||||
|
||||
let mut data = Vec::<u8>::new();
|
||||
for chunk in chunks.iter() {
|
||||
data.write_all(&chunk.data[0..(chunk.size as usize)])?;
|
||||
}
|
||||
|
||||
let actual_data = if is_online_quest {
|
||||
data
|
||||
} else {
|
||||
decrypt_quest_data(&mut data)?.into()
|
||||
};
|
||||
|
||||
Ok(actual_data)
|
||||
}
|
||||
|
||||
impl QuestQst {
|
||||
pub fn from_bindat(bin: &QuestBin, dat: &QuestDat) -> Result<QuestQst, QuestQstError> {
|
||||
let is_online = !bin.header.is_download; // "download quest" = "offline quest" (because it is played from a memory card ...)
|
||||
let quest_name = &bin.header.name;
|
||||
let quest_number = bin.header.quest_number_u16(); // i hate the quest .bin quest_number u8/u16 confusion amongst PSO tools ...
|
||||
let bin_filename = format!("quest{}.bin", quest_number);
|
||||
let dat_filename = format!("quest{}.dat", quest_number);
|
||||
|
||||
let mut bin_bytes = bin.to_compressed_bytes()?;
|
||||
let mut dat_bytes = dat.to_compressed_bytes()?;
|
||||
if !is_online {
|
||||
// offline quests are encrypted with some extra bits added before the encrypted data
|
||||
bin_bytes = encrypt_quest_data(bin_bytes.as_mut(), bin.calculate_size())?;
|
||||
dat_bytes = encrypt_quest_data(dat_bytes.as_mut(), dat.calculate_size())?;
|
||||
}
|
||||
|
||||
let bin_header = QuestHeaderPacket::new(
|
||||
quest_name,
|
||||
bin.header.language,
|
||||
&bin_filename,
|
||||
bin_bytes.len(),
|
||||
is_online,
|
||||
)?;
|
||||
|
||||
let dat_header = QuestHeaderPacket::new(
|
||||
quest_name,
|
||||
bin.header.language,
|
||||
&dat_filename,
|
||||
dat_bytes.len(),
|
||||
is_online,
|
||||
)?;
|
||||
|
||||
let bin_chunks = create_quest_data_chunks(bin_bytes.as_ref(), &bin_filename, is_online)?;
|
||||
let dat_chunks = create_quest_data_chunks(dat_bytes.as_ref(), &dat_filename, is_online)?;
|
||||
|
||||
Ok(QuestQst {
|
||||
bin_header,
|
||||
dat_header,
|
||||
bin_chunks,
|
||||
dat_chunks,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn from_file(path: &Path) -> Result<QuestQst, QuestQstError> {
|
||||
let file = File::open(path)?;
|
||||
let mut reader = BufReader::new(file);
|
||||
Ok(Self::from_bytes(&mut reader)?)
|
||||
}
|
||||
|
||||
pub fn from_bytes<T: ReadBytesExt>(reader: &mut T) -> Result<QuestQst, QuestQstError> {
|
||||
let mut bin_header: Option<QuestHeaderPacket> = None;
|
||||
let mut dat_header: Option<QuestHeaderPacket> = None;
|
||||
let mut bin_chunks = Vec::<QuestDataPacket>::new();
|
||||
let mut dat_chunks = Vec::<QuestDataPacket>::new();
|
||||
let mut bin_data_counter: usize = 0;
|
||||
let mut dat_data_counter: usize = 0;
|
||||
|
||||
// loop, continuing to read packets until we have ALL of the following:
|
||||
// - a bin header
|
||||
// - a dat header
|
||||
// - bin data chunks that contain the exact number of bytes specified by the bin header
|
||||
// - dat data chunks that contain the exact number of bytes specified by the dat header
|
||||
//
|
||||
// the way this reading works should allow for the maximum amount of flexibility of the qst
|
||||
// file layout. though, most (all?) things that create qst files will follow this ordering:
|
||||
// - bin and dat header (either bin+dat or dat+bin)
|
||||
// - interleaved bin and dat chunks
|
||||
//
|
||||
// however, i have observed that fuzziqer servers (newserv, khyller) generally sends out
|
||||
// quest packets un-interleaved. that is, these servers send out bin header + bin data, and
|
||||
// then dat header + dat data (actually, i think the ordering might be dat first ...? meh)
|
||||
//
|
||||
// thus, i decided that even if there is only a very small chance that someone out there
|
||||
// saved a qst file in such a "non-standard" format, that we could easily account for any
|
||||
// of those variations here
|
||||
while (bin_header.is_none()
|
||||
|| (bin_header.is_some()
|
||||
&& bin_data_counter < bin_header.as_ref().unwrap().size as usize))
|
||||
|| (dat_header.is_none()
|
||||
|| (dat_header.is_some()
|
||||
&& dat_data_counter < dat_header.as_ref().unwrap().size as usize))
|
||||
{
|
||||
// what type of packet is this?
|
||||
let packet_header = PacketHeader::from_bytes(reader)?;
|
||||
match packet_header.id {
|
||||
PACKET_ID_QUEST_HEADER_ONLINE | PACKET_ID_QUEST_HEADER_OFFLINE => {
|
||||
// there can only be one bin and dat header per qst file
|
||||
if bin_header.is_some() && dat_header.is_some() {
|
||||
return Err(QuestQstError::DataFormatError(String::from(
|
||||
"Encountered more than two header packets",
|
||||
)));
|
||||
}
|
||||
|
||||
let header = QuestHeaderPacket::from_header_and_bytes(packet_header, reader)?;
|
||||
|
||||
// the header packet must include a filename, as this is used to determine
|
||||
// whether it is for a .bin or .dat file
|
||||
if header.filename.as_unpadded_slice().len() == 0 {
|
||||
return Err(QuestQstError::DataFormatError(String::from(
|
||||
"Encountered header packet with blank filename",
|
||||
)));
|
||||
}
|
||||
|
||||
match header.file_type() {
|
||||
QuestPacketFileType::Bin => {
|
||||
if bin_header.is_some() {
|
||||
return Err(QuestQstError::DataFormatError(String::from(
|
||||
"Encountered duplicate bin file header packet",
|
||||
)));
|
||||
} else {
|
||||
bin_header = Some(header);
|
||||
}
|
||||
}
|
||||
QuestPacketFileType::Dat => {
|
||||
if dat_header.is_some() {
|
||||
return Err(QuestQstError::DataFormatError(String::from(
|
||||
"Encountered duplicate dat file header packet",
|
||||
)));
|
||||
} else {
|
||||
dat_header = Some(header);
|
||||
}
|
||||
}
|
||||
QuestPacketFileType::Unknown => {
|
||||
return Err(QuestQstError::DataFormatError(String::from(
|
||||
"Unable to determine file type from filename in header packet",
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
PACKET_ID_QUEST_DATA_ONLINE | PACKET_ID_QUEST_DATA_OFFLINE => {
|
||||
// data chunk packets must come after its associated header packet
|
||||
// (e.g. .bin data chunks must follow the .bin header, same for .dat ...)
|
||||
if bin_header.is_none() && dat_header.is_none() {
|
||||
return Err(QuestQstError::DataFormatError(String::from(
|
||||
"Encountered data chunk packet before any header packets",
|
||||
)));
|
||||
}
|
||||
|
||||
let chunk = QuestDataPacket::from_header_and_bytes(packet_header, reader)?;
|
||||
|
||||
// the data chunk packet must include a filename, as this is used to determine
|
||||
// whether it is for a .bin or .dat file
|
||||
if chunk.filename.as_unpadded_slice().len() == 0 {
|
||||
return Err(QuestQstError::DataFormatError(String::from(
|
||||
"Encountered data chunk packet with blank filename",
|
||||
)));
|
||||
}
|
||||
|
||||
// small sanity check, technically would not be a problem, but there shouldn't
|
||||
// be any "blank" data chunk packets
|
||||
if chunk.size == 0 {
|
||||
return Err(QuestQstError::DataFormatError(String::from(
|
||||
"Encountered data chunk packet with zero-length data",
|
||||
)));
|
||||
}
|
||||
|
||||
match chunk.file_type() {
|
||||
QuestPacketFileType::Bin => {
|
||||
if bin_header.is_none() {
|
||||
return Err(QuestQstError::DataFormatError(String::from("Encountered data chunk packet for bin file before its header packet")));
|
||||
} else {
|
||||
bin_data_counter += chunk.size as usize;
|
||||
bin_chunks.push(chunk);
|
||||
}
|
||||
}
|
||||
QuestPacketFileType::Dat => {
|
||||
if dat_header.is_none() {
|
||||
return Err(QuestQstError::DataFormatError(String::from("Encountered data chunk packet for dat file before its header packet")));
|
||||
} else {
|
||||
dat_data_counter += chunk.size as usize;
|
||||
dat_chunks.push(chunk);
|
||||
}
|
||||
}
|
||||
QuestPacketFileType::Unknown => {
|
||||
return Err(QuestQstError::DataFormatError(String::from(
|
||||
"Unable to determine file type from filename in data chunk packet",
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
other_id => {
|
||||
return Err(QuestQstError::DataFormatError(format!(
|
||||
"Unexpected packet id found in quest qst data: {}",
|
||||
other_id
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let bin_header = bin_header.unwrap();
|
||||
let dat_header = dat_header.unwrap();
|
||||
|
||||
// validate that the file bin/dat data chunk byte counts matched what was specified in the
|
||||
// bin/dat headers
|
||||
|
||||
if bin_data_counter as u32 != bin_header.size {
|
||||
let size = bin_header.size;
|
||||
return Err(QuestQstError::DataFormatError(format!(
|
||||
"Read {} bytes of bin data, but the bin header specified {} bytes would be present",
|
||||
bin_data_counter, size
|
||||
)));
|
||||
}
|
||||
if dat_data_counter as u32 != dat_header.size {
|
||||
let size = dat_header.size;
|
||||
return Err(QuestQstError::DataFormatError(format!(
|
||||
"Read {} bytes of dat data, but the dat header specified {} bytes would be present",
|
||||
dat_data_counter, size
|
||||
)));
|
||||
}
|
||||
|
||||
// validate that all packets encountered (header and data chunk) were of the same category
|
||||
// the entire qst file should have only contained packet IDs:
|
||||
// - PACKET_ID_QUEST_HEADER_ONLINE and PACKET_ID_QUEST_DATA_ONLINE, or
|
||||
// - PACKET_ID_QUEST_HEADER_OFFLINE and PACKET_ID_QUEST_DATA_OFFLINE
|
||||
|
||||
if bin_header.header.id != dat_header.header.id {
|
||||
return Err(QuestQstError::DataFormatError(String::from(
|
||||
"Packet header ID mismatch between bin and dat headers",
|
||||
)));
|
||||
}
|
||||
let expected_chunk_packets_id = if bin_header.header.id == PACKET_ID_QUEST_HEADER_ONLINE {
|
||||
PACKET_ID_QUEST_DATA_ONLINE
|
||||
} else {
|
||||
PACKET_ID_QUEST_DATA_OFFLINE
|
||||
};
|
||||
|
||||
if bin_chunks
|
||||
.iter()
|
||||
.filter(|chunk| chunk.header.id != expected_chunk_packets_id)
|
||||
.count()
|
||||
!= 0
|
||||
{
|
||||
return Err(QuestQstError::DataFormatError(format!(
|
||||
"One or more bin data chunk packets were not of the expected type: {}",
|
||||
expected_chunk_packets_id
|
||||
)));
|
||||
}
|
||||
if dat_chunks
|
||||
.iter()
|
||||
.filter(|chunk| chunk.header.id != expected_chunk_packets_id)
|
||||
.count()
|
||||
!= 0
|
||||
{
|
||||
return Err(QuestQstError::DataFormatError(format!(
|
||||
"One or more dat data chunk packets were not of the expected type: {}",
|
||||
expected_chunk_packets_id
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(QuestQst {
|
||||
bin_header,
|
||||
dat_header,
|
||||
bin_chunks: bin_chunks.into_boxed_slice(),
|
||||
dat_chunks: dat_chunks.into_boxed_slice(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write_bytes<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), QuestQstError> {
|
||||
self.bin_header.write_bytes(writer)?;
|
||||
self.dat_header.write_bytes(writer)?;
|
||||
for chunk in self.bin_chunks.iter().interleave(self.dat_chunks.iter()) {
|
||||
chunk.write_bytes(writer)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_file(&self, path: &Path) -> Result<(), QuestQstError> {
|
||||
let mut file = File::create(path)?;
|
||||
self.write_bytes(&mut file)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Result<Box<[u8]>, QuestQstError> {
|
||||
let mut buffer = Cursor::new(Vec::<u8>::new());
|
||||
self.write_bytes(&mut buffer)?;
|
||||
Ok(buffer.into_inner().into_boxed_slice())
|
||||
}
|
||||
|
||||
pub fn is_online(&self) -> bool {
|
||||
// assumes that a QuestQst could never be created with bin/dat headers containing
|
||||
// different packet IDs ...
|
||||
self.bin_header.header.id == PACKET_ID_QUEST_HEADER_ONLINE
|
||||
}
|
||||
|
||||
pub fn extract_bin_bytes(&self) -> Result<Box<[u8]>, QuestQstError> {
|
||||
Ok(extract_quest_chunk_data(&self.bin_chunks, self.is_online())?.into_boxed_slice())
|
||||
}
|
||||
|
||||
pub fn extract_bin(&self) -> Result<QuestBin, QuestQstError> {
|
||||
let data = self.extract_bin_bytes()?;
|
||||
Ok(QuestBin::from_compressed_bytes(data.as_ref())?)
|
||||
}
|
||||
|
||||
pub fn extract_dat_bytes(&self) -> Result<Box<[u8]>, QuestQstError> {
|
||||
Ok(extract_quest_chunk_data(&self.dat_chunks, self.is_online())?.into_boxed_slice())
|
||||
}
|
||||
|
||||
pub fn extract_dat(&self) -> Result<QuestDat, QuestQstError> {
|
||||
let data = self.extract_dat_bytes()?;
|
||||
Ok(QuestDat::from_compressed_bytes(data.as_ref())?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::ops::Deref;
|
||||
|
||||
use claim::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use crate::quest::bin::tests::{validate_quest_118_bin, validate_quest_58_bin};
|
||||
use crate::quest::dat::tests::{validate_quest_118_dat, validate_quest_58_dat};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn get_num_chunks_for_size(size: usize) -> usize {
|
||||
((size as f32) / (QUEST_DATA_PACKET_DATA_SIZE as f32)).ceil() as usize
|
||||
}
|
||||
|
||||
fn validate_quest_58_qst(
|
||||
qst: &QuestQst,
|
||||
bin_size: usize,
|
||||
dat_size: usize,
|
||||
is_online: bool,
|
||||
) -> Result<(), QuestQstError> {
|
||||
let (expected_header_id, expected_chunk_id) = if is_online {
|
||||
(PACKET_ID_QUEST_HEADER_ONLINE, PACKET_ID_QUEST_DATA_ONLINE)
|
||||
} else {
|
||||
(PACKET_ID_QUEST_HEADER_OFFLINE, PACKET_ID_QUEST_DATA_OFFLINE)
|
||||
};
|
||||
|
||||
assert_eq!(qst.is_online(), is_online);
|
||||
|
||||
assert_eq!(qst.bin_header.header.id, expected_header_id);
|
||||
assert_eq!(qst.bin_header.name_str()?, "Lost HEAT SWORD");
|
||||
assert_eq!(qst.bin_header.filename_str()?, "quest58.bin");
|
||||
assert_eq!(qst.bin_header.file_type(), QuestPacketFileType::Bin);
|
||||
let size = qst.bin_header.size as usize;
|
||||
assert_eq!(size, bin_size);
|
||||
|
||||
let num_chunks = get_num_chunks_for_size(bin_size);
|
||||
assert_eq!(qst.bin_chunks.len(), num_chunks);
|
||||
for chunk in qst.bin_chunks.iter() {
|
||||
assert_eq!(chunk.header.id, expected_chunk_id);
|
||||
assert_eq!(chunk.filename_str()?, "quest58.bin");
|
||||
assert_eq!(chunk.file_type(), QuestPacketFileType::Bin);
|
||||
assert!(chunk.data().len() > 0);
|
||||
}
|
||||
|
||||
assert_eq!(qst.dat_header.header.id, expected_header_id);
|
||||
assert_eq!(qst.dat_header.name_str()?, "Lost HEAT SWORD");
|
||||
assert_eq!(qst.dat_header.filename_str()?, "quest58.dat");
|
||||
assert_eq!(qst.dat_header.file_type(), QuestPacketFileType::Dat);
|
||||
let size = qst.dat_header.size as usize;
|
||||
assert_eq!(size, dat_size);
|
||||
|
||||
let num_chunks = get_num_chunks_for_size(dat_size);
|
||||
assert_eq!(qst.dat_chunks.len(), num_chunks);
|
||||
for chunk in qst.dat_chunks.iter() {
|
||||
assert_eq!(chunk.header.id, expected_chunk_id);
|
||||
assert_eq!(chunk.filename_str()?, "quest58.dat");
|
||||
assert_eq!(chunk.file_type(), QuestPacketFileType::Dat);
|
||||
assert!(chunk.data().len() > 0);
|
||||
}
|
||||
|
||||
let mut bin = qst.extract_bin()?;
|
||||
if !is_online {
|
||||
assert_eq!(true, bin.header.is_download);
|
||||
bin.header.is_download = false;
|
||||
}
|
||||
validate_quest_58_bin(&bin);
|
||||
|
||||
let dat = qst.extract_dat()?;
|
||||
validate_quest_58_dat(&dat);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_quest_118_qst(
|
||||
qst: &QuestQst,
|
||||
bin_size: usize,
|
||||
dat_size: usize,
|
||||
is_online: bool,
|
||||
) -> Result<(), QuestQstError> {
|
||||
let (expected_header_id, expected_chunk_id) = if is_online {
|
||||
(PACKET_ID_QUEST_HEADER_ONLINE, PACKET_ID_QUEST_DATA_ONLINE)
|
||||
} else {
|
||||
(PACKET_ID_QUEST_HEADER_OFFLINE, PACKET_ID_QUEST_DATA_OFFLINE)
|
||||
};
|
||||
|
||||
assert_eq!(qst.is_online(), is_online);
|
||||
|
||||
assert_eq!(qst.bin_header.header.id, expected_header_id);
|
||||
assert_eq!(qst.bin_header.name_str()?, "Towards the Future");
|
||||
assert_eq!(qst.bin_header.filename_str()?, "quest118.bin");
|
||||
assert_eq!(qst.bin_header.file_type(), QuestPacketFileType::Bin);
|
||||
let size = qst.bin_header.size as usize;
|
||||
assert_eq!(size, bin_size);
|
||||
|
||||
let num_chunks = get_num_chunks_for_size(bin_size);
|
||||
assert_eq!(qst.bin_chunks.len(), num_chunks);
|
||||
for chunk in qst.bin_chunks.iter() {
|
||||
assert_eq!(chunk.header.id, expected_chunk_id);
|
||||
assert_eq!(chunk.filename_str()?, "quest118.bin");
|
||||
assert_eq!(chunk.file_type(), QuestPacketFileType::Bin);
|
||||
assert!(chunk.data().len() > 0);
|
||||
}
|
||||
|
||||
assert_eq!(qst.dat_header.header.id, expected_header_id);
|
||||
assert_eq!(qst.dat_header.name_str()?, "Towards the Future");
|
||||
assert_eq!(qst.dat_header.filename_str()?, "quest118.dat");
|
||||
assert_eq!(qst.dat_header.file_type(), QuestPacketFileType::Dat);
|
||||
let size = qst.dat_header.size as usize;
|
||||
assert_eq!(size, dat_size);
|
||||
|
||||
let num_chunks = get_num_chunks_for_size(dat_size);
|
||||
assert_eq!(qst.dat_chunks.len(), num_chunks);
|
||||
for chunk in qst.dat_chunks.iter() {
|
||||
assert_eq!(chunk.header.id, expected_chunk_id);
|
||||
assert_eq!(chunk.filename_str()?, "quest118.dat");
|
||||
assert_eq!(chunk.file_type(), QuestPacketFileType::Dat);
|
||||
assert!(chunk.data().len() > 0);
|
||||
}
|
||||
|
||||
let mut bin = qst.extract_bin()?;
|
||||
if !is_online {
|
||||
assert_eq!(true, bin.header.is_download);
|
||||
bin.header.is_download = false;
|
||||
}
|
||||
validate_quest_118_bin(&bin);
|
||||
|
||||
let dat = qst.extract_dat()?;
|
||||
validate_quest_118_dat(&dat);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn read_quest_58_qst_from_file() -> Result<(), QuestQstError> {
|
||||
let qst = QuestQst::from_file(Path::new("../test-assets/q058-ret-gc.online.qst"))?;
|
||||
validate_quest_58_qst(&qst, 1438, 15097, true)?;
|
||||
|
||||
let qst = QuestQst::from_file(Path::new("../test-assets/q058-ret-gc.offline.qst"))?;
|
||||
validate_quest_58_qst(&qst, 1571, 15105, false)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn write_quest_58_qst_to_file() -> Result<(), QuestQstError> {
|
||||
let online_data = include_bytes!("../../../test-assets/q058-ret-gc.online.qst");
|
||||
let offline_data = include_bytes!("../../../test-assets/q058-ret-gc.offline.qst");
|
||||
|
||||
let tmp_dir = TempDir::new()?;
|
||||
|
||||
let mut reader = Cursor::new(online_data);
|
||||
let qst = QuestQst::from_bytes(&mut reader)?;
|
||||
let qst_path = tmp_dir.path().join("quest58.online.qst");
|
||||
qst.to_file(&qst_path)?;
|
||||
let qst = QuestQst::from_file(&qst_path)?;
|
||||
validate_quest_58_qst(&qst, 1438, 15097, true)?;
|
||||
|
||||
let mut reader = Cursor::new(offline_data);
|
||||
let qst = QuestQst::from_bytes(&mut reader)?;
|
||||
let qst_path = tmp_dir.path().join("quest58.offline.qst");
|
||||
qst.to_file(&qst_path)?;
|
||||
let qst = QuestQst::from_file(&qst_path)?;
|
||||
validate_quest_58_qst(&qst, 1571, 15105, false)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn read_quest_118_qst_from_file() -> Result<(), QuestQstError> {
|
||||
let qst = QuestQst::from_file(Path::new("../test-assets/q118-vr-gc.online.qst"))?;
|
||||
validate_quest_118_qst(&qst, 14208, 11802, true)?;
|
||||
|
||||
let qst = QuestQst::from_file(Path::new("../test-assets/q118-vr-gc.offline.qst"))?;
|
||||
validate_quest_118_qst(&qst, 14801, 11810, false)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn write_quest_118_qst_to_file() -> Result<(), QuestQstError> {
|
||||
let online_data = include_bytes!("../../../test-assets/q118-vr-gc.online.qst");
|
||||
let offline_data = include_bytes!("../../../test-assets/q118-vr-gc.offline.qst");
|
||||
|
||||
let tmp_dir = TempDir::new()?;
|
||||
|
||||
let mut reader = Cursor::new(online_data);
|
||||
let qst = QuestQst::from_bytes(&mut reader)?;
|
||||
let qst_path = tmp_dir.path().join("quest118.online.qst");
|
||||
qst.to_file(&qst_path)?;
|
||||
let qst = QuestQst::from_file(&qst_path)?;
|
||||
validate_quest_118_qst(&qst, 14208, 11802, true)?;
|
||||
|
||||
let mut reader = Cursor::new(offline_data);
|
||||
let qst = QuestQst::from_bytes(&mut reader)?;
|
||||
let qst_path = tmp_dir.path().join("quest118.offline.qst");
|
||||
qst.to_file(&qst_path)?;
|
||||
let qst = QuestQst::from_file(&qst_path)?;
|
||||
validate_quest_118_qst(&qst, 14801, 11810, false)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn create_qst_from_quest_58_bindat_files() -> Result<(), QuestQstError> {
|
||||
let mut bin = QuestBin::from_compressed_file(Path::new("../test-assets/q058-ret-gc.bin"))?;
|
||||
let dat = QuestDat::from_compressed_file(Path::new("../test-assets/q058-ret-gc.dat"))?;
|
||||
|
||||
let qst = QuestQst::from_bindat(&bin, &dat)?;
|
||||
validate_quest_58_qst(&qst, 1565, 15507, true)?;
|
||||
|
||||
bin.header.is_download = true;
|
||||
let qst = QuestQst::from_bindat(&bin, &dat)?;
|
||||
validate_quest_58_qst(&qst, 1573, 15515, false)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn create_qst_from_quest_118_bindat_files() -> Result<(), QuestQstError> {
|
||||
let mut bin = QuestBin::from_compressed_file(Path::new("../test-assets/q118-vr-gc.bin"))?;
|
||||
let dat = QuestDat::from_compressed_file(Path::new("../test-assets/q118-vr-gc.dat"))?;
|
||||
|
||||
let qst = QuestQst::from_bindat(&bin, &dat)?;
|
||||
validate_quest_118_qst(&qst, 14794, 12277, true)?;
|
||||
|
||||
bin.header.is_download = true;
|
||||
let qst = QuestQst::from_bindat(&bin, &dat)?;
|
||||
validate_quest_118_qst(&qst, 14803, 12285, false)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_on_load_from_zero_bytes() {
|
||||
let mut data: &[u8] = &[];
|
||||
assert_matches!(
|
||||
QuestQst::from_bytes(&mut data),
|
||||
Err(QuestQstError::DataPacketError(..))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_on_load_from_garbage_bytes() {
|
||||
let mut data: &[u8] = b"This is definitely not a quest";
|
||||
assert_matches!(
|
||||
QuestQst::from_bytes(&mut data),
|
||||
Err(QuestQstError::DataFormatError(..))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_on_bad_packet_id_encountered() {
|
||||
// packet header bytes with bad packet id
|
||||
let mut data: &[u8] = &[0x42, 0x00, 0x3C, 0x00];
|
||||
assert_matches!(
|
||||
QuestQst::from_bytes(&mut data),
|
||||
Err(QuestQstError::DataFormatError(..))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_on_bad_packet_size_encountered() {
|
||||
// packet header bytes for a QuestHeaderPacket, but with the wrong packet size
|
||||
let mut data: &[u8] = &[0x44, 0x00, 0x3C, 0x11];
|
||||
assert_matches!(
|
||||
QuestQst::from_bytes(&mut data),
|
||||
Err(QuestQstError::DataPacketError(PacketError::WrongSize(..)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_on_header_packet_with_non_bin_or_dat_filename() {
|
||||
// an otherwise valid QuestHeaderPacket, but with a filename that cannot be recognized
|
||||
// as a .bin or .dat file type
|
||||
let mut data: &[u8] = &[
|
||||
0xA6, 0x00, 0x3C, 0x00, 0x4C, 0x6F, 0x73, 0x74, 0x20, 0x48, 0x45, 0x41, 0x54, 0x20,
|
||||
0x53, 0x57, 0x4F, 0x52, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x35, 0x38, 0x2E, 0x78, 0x79, 0x7A, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x23, 0x06, 0x00, 0x00,
|
||||
];
|
||||
assert_matches!(
|
||||
QuestQst::from_bytes(&mut data),
|
||||
Err(QuestQstError::DataFormatError(..))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_on_header_packet_with_missing_filename() {
|
||||
// an otherwise valid QuestHeaderPacket, but with a missing filename (all null bytes)
|
||||
let mut data: &[u8] = &[
|
||||
0xA6, 0x00, 0x3C, 0x00, 0x4C, 0x6F, 0x73, 0x74, 0x20, 0x48, 0x45, 0x41, 0x54, 0x20,
|
||||
0x53, 0x57, 0x4F, 0x52, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x23, 0x06, 0x00, 0x00,
|
||||
];
|
||||
assert_matches!(
|
||||
QuestQst::from_bytes(&mut data),
|
||||
Err(QuestQstError::DataFormatError(..))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_on_duplicate_header_packet() {
|
||||
// two QuestHeaderPackets in a row, both for the .dat file
|
||||
let mut data: &[u8] = &[
|
||||
0xA6, 0x00, 0x3C, 0x00, 0x4C, 0x6F, 0x73, 0x74, 0x20, 0x48, 0x45, 0x41, 0x54, 0x20,
|
||||
0x53, 0x57, 0x4F, 0x52, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x35, 0x38, 0x2E, 0x64, 0x61, 0x74, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x01, 0x3B, 0x00, 0x00, 0xA6, 0x00, 0x3C, 0x00, 0x4C, 0x6F, 0x73, 0x74, 0x20, 0x48,
|
||||
0x45, 0x41, 0x54, 0x20, 0x53, 0x57, 0x4F, 0x52, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x71, 0x75, 0x65, 0x73, 0x74, 0x35, 0x38, 0x2E, 0x64, 0x61, 0x74, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x3B, 0x00, 0x00,
|
||||
];
|
||||
assert_matches!(
|
||||
QuestQst::from_bytes(&mut data),
|
||||
Err(QuestQstError::DataFormatError(..))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_on_data_packet_before_header_packet() {
|
||||
// packet header bytes for a QuestDataPacket, fails because we expect to find a
|
||||
// QuestHeaderPacket before any data packets
|
||||
let mut data: &[u8] = &[0xA7, 0x00, 0x12, 0x34];
|
||||
assert_matches!(
|
||||
QuestQst::from_bytes(&mut data),
|
||||
Err(QuestQstError::DataFormatError(..))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_on_data_packet_with_non_bin_or_dat_filename() {
|
||||
// header is fine
|
||||
let header: &[u8] = &[
|
||||
0xA6, 0x00, 0x3C, 0x00, 0x4C, 0x6F, 0x73, 0x74, 0x20, 0x48, 0x45, 0x41, 0x54, 0x20,
|
||||
0x53, 0x57, 0x4F, 0x52, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x35, 0x38, 0x2E, 0x62, 0x69, 0x6E, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x23, 0x06, 0x00, 0x00,
|
||||
];
|
||||
// data chunk packet has non .bin or .dat filename (but is otherwise fine)
|
||||
let data_chunk: &[u8] = &[
|
||||
0xA7, 0x00, 0x18, 0x04, 0x71, 0x75, 0x65, 0x73, 0x74, 0x35, 0x38, 0x2E, 0x66, 0x6F,
|
||||
0x6F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x19, 0x00, 0x00, 0x86, 0xF9, 0xD3, 0x12,
|
||||
0xF5, 0xDA, 0x71, 0xBA, 0x23, 0x5A, 0x97, 0x42, 0x6A, 0xFC, 0xB0, 0x9E, 0x73, 0x0F,
|
||||
0xFC, 0xCF, 0x61, 0x21, 0xF0, 0x92, 0x6F, 0xDF, 0x25, 0x33, 0x17, 0x9E, 0x7C, 0x75,
|
||||
0x7B, 0x21, 0xC3, 0x5F, 0xD2, 0x8C, 0x03, 0x9F, 0x67, 0x60, 0x94, 0x6E, 0x25, 0xE5,
|
||||
0x26, 0x25, 0x6C, 0xB4, 0x39, 0xF4, 0xBF, 0xDF, 0x76, 0x6A, 0x07, 0x6B, 0x81, 0xD0,
|
||||
0x18, 0x4D, 0xF9, 0xE8, 0xAE, 0x59, 0x0E, 0x56, 0x5B, 0x4A, 0x4D, 0x0F, 0x11, 0x27,
|
||||
0xD7, 0xBD, 0xA0, 0xAE, 0xCE, 0x14, 0x9E, 0xB2, 0x88, 0x57, 0x3B, 0xCB, 0xA1, 0xB5,
|
||||
0x0D, 0xEB, 0x44, 0x4D, 0x13, 0x8B, 0x89, 0x9C, 0x80, 0x6F, 0xF0, 0x25, 0x2C, 0xAC,
|
||||
0x20, 0xDA, 0x65, 0x78, 0x93, 0x02, 0x75, 0x41, 0xFC, 0xDD, 0xFD, 0xAB, 0x5B, 0xD9,
|
||||
0x78, 0xA9, 0x09, 0xA6, 0xFE, 0xD2, 0x52, 0xFC, 0x4A, 0x66, 0x90, 0xB4, 0xDF, 0xD7,
|
||||
0x34, 0x4A, 0xD4, 0x3C, 0xE8, 0xA3, 0x0B, 0xB4, 0xFD, 0xE3, 0x34, 0x6C, 0x49, 0x85,
|
||||
0xF4, 0xAE, 0xB9, 0x55, 0x6B, 0x3E, 0x4B, 0x82, 0xCF, 0x5B, 0x22, 0x3A, 0x92, 0xA3,
|
||||
0x0F, 0xB7, 0xBE, 0x71, 0x59, 0xCC, 0xE7, 0xDA, 0xDB, 0x69, 0x77, 0xC7, 0x63, 0x44,
|
||||
0x8E, 0xC4, 0xF7, 0xE2, 0x5F, 0xA4, 0xAB, 0x5D, 0x7A, 0x21, 0xD7, 0x1F, 0xAD, 0x58,
|
||||
0xAD, 0x66, 0x3E, 0x4A, 0x00, 0x93, 0xA2, 0x0A, 0xCD, 0xB4, 0x96, 0x1A, 0xF0, 0x72,
|
||||
0xD0, 0xEB, 0xF1, 0x89, 0x16, 0xFF, 0x11, 0x66, 0x5A, 0xF0, 0x63, 0xB8, 0x56, 0x09,
|
||||
0x99, 0xCE, 0x66, 0x6B, 0x7C, 0xE5, 0xE7, 0xE1, 0x2E, 0xC4, 0xEA, 0xA5, 0x43, 0x82,
|
||||
0x82, 0xAE, 0x49, 0x3E, 0xF1, 0x5F, 0x1C, 0xBB, 0xD1, 0x5B, 0x79, 0xBA, 0xE1, 0xF8,
|
||||
0x68, 0x6C, 0xB1, 0x39, 0x01, 0xAA, 0xCE, 0x4F, 0xE8, 0x90, 0x84, 0xD9, 0xEE, 0x99,
|
||||
0xE1, 0x60, 0xD8, 0x1D, 0xE8, 0x80, 0xF5, 0x34, 0x75, 0x07, 0xAB, 0x60, 0x94, 0x9B,
|
||||
0x91, 0x45, 0xF5, 0xD6, 0xF5, 0x32, 0x29, 0xD8, 0xAE, 0xD7, 0xE6, 0x58, 0x02, 0x87,
|
||||
0x44, 0xF7, 0x46, 0xCC, 0x28, 0x38, 0xBF, 0x1C, 0x9C, 0x5B, 0xF7, 0x88, 0x46, 0x85,
|
||||
0x1D, 0x0A, 0x9E, 0xEC, 0xBE, 0x34, 0x20, 0xD6, 0xCD, 0x80, 0xF5, 0x72, 0xDD, 0x17,
|
||||
0x96, 0x40, 0x5D, 0x8A, 0x52, 0xD3, 0x2E, 0x9B, 0x8B, 0xDD, 0x59, 0xC4, 0x90, 0x3C,
|
||||
0x1A, 0x44, 0x33, 0x58, 0x5C, 0xE1, 0x6F, 0xDF, 0x9D, 0xFF, 0xFC, 0x6E, 0xF3, 0x0C,
|
||||
0x7F, 0xBE, 0x3F, 0x3F, 0x41, 0xBA, 0xD9, 0xFB, 0x93, 0x08, 0x4B, 0xB0, 0x29, 0x58,
|
||||
0x95, 0xB5, 0x28, 0xE4, 0xED, 0x5A, 0x97, 0xBE, 0xDD, 0x15, 0xCB, 0xCE, 0x41, 0x11,
|
||||
0xBE, 0x86, 0x52, 0xED, 0x90, 0x0F, 0x7C, 0x62, 0xE8, 0x24, 0x63, 0x5B, 0x48, 0x6B,
|
||||
0x37, 0xEA, 0x59, 0xE9, 0xD2, 0x18, 0xD4, 0x3D, 0xA7, 0x5F, 0x96, 0x36, 0xED, 0x6F,
|
||||
0xC6, 0x30, 0x06, 0x4E, 0x96, 0xB8, 0x5D, 0x3A, 0x14, 0xBC, 0xD5, 0x4A, 0x8D, 0x0C,
|
||||
0x6C, 0xB2, 0x7B, 0x54, 0xA8, 0x81, 0xB1, 0x2E, 0x05, 0x46, 0xE7, 0x45, 0x54, 0x7E,
|
||||
0x3C, 0x0D, 0x30, 0x79, 0x3E, 0xE9, 0xB1, 0xD4, 0xB5, 0xD7, 0x83, 0x27, 0xE6, 0x83,
|
||||
0x08, 0xDD, 0x43, 0xA1, 0xA5, 0xDE, 0xD7, 0xC7, 0xDC, 0xB2, 0x99, 0xC4, 0x76, 0x20,
|
||||
0xB6, 0xDB, 0xA9, 0x4C, 0x96, 0x22, 0x2D, 0xEC, 0xBE, 0x01, 0x33, 0xAB, 0xC4, 0x63,
|
||||
0xE6, 0x54, 0xDD, 0x55, 0x66, 0xEB, 0xAF, 0xAD, 0x1D, 0xA4, 0x77, 0x18, 0x03, 0xD4,
|
||||
0xCF, 0x16, 0x8F, 0x0F, 0xD7, 0x5A, 0x19, 0xDB, 0xE6, 0xB5, 0xE1, 0x8E, 0x76, 0xF8,
|
||||
0x86, 0x1C, 0x24, 0x17, 0xD4, 0x4B, 0xDA, 0x63, 0x87, 0x94, 0x4E, 0x8E, 0x85, 0x7A,
|
||||
0x60, 0xC0, 0xF4, 0xBA, 0x04, 0x27, 0x3A, 0x31, 0x20, 0xDB, 0x8C, 0x54, 0x92, 0xFE,
|
||||
0x38, 0x92, 0xFA, 0x6D, 0xD9, 0x4B, 0xBA, 0x1E, 0xED, 0x8A, 0xEC, 0x92, 0xD7, 0x76,
|
||||
0x0B, 0x7C, 0x6B, 0x75, 0xFF, 0xFA, 0xE1, 0x53, 0x0F, 0xA6, 0x98, 0x3E, 0x74, 0xD9,
|
||||
0x04, 0xEF, 0x5C, 0xD7, 0xF9, 0xBF, 0x5C, 0x87, 0xD8, 0x7E, 0xF4, 0xDB, 0xFB, 0xAC,
|
||||
0xD2, 0xA1, 0xEA, 0x9F, 0x00, 0xB3, 0xD1, 0x28, 0xE7, 0xEC, 0xD7, 0xDA, 0x42, 0x43,
|
||||
0x70, 0x0F, 0x49, 0xC5, 0xF0, 0xFE, 0xD2, 0xE4, 0xEF, 0xA6, 0xA7, 0xE0, 0x36, 0x38,
|
||||
0x8B, 0x27, 0x1D, 0x42, 0x83, 0xC5, 0x3A, 0xD2, 0x85, 0x1B, 0xB1, 0xBD, 0xE1, 0x9C,
|
||||
0x20, 0x35, 0x65, 0x40, 0x41, 0x9E, 0xF7, 0x01, 0x34, 0x0F, 0xC9, 0xE0, 0xCC, 0x5D,
|
||||
0x94, 0xD9, 0xD4, 0x17, 0x45, 0xFC, 0x10, 0x77, 0x56, 0x48, 0x2A, 0xA5, 0x91, 0x6A,
|
||||
0x2E, 0x3B, 0xED, 0xCF, 0x2C, 0xB0, 0xE5, 0x46, 0x71, 0x7E, 0x87, 0xB6, 0x5F, 0xA4,
|
||||
0x6F, 0xE9, 0x68, 0xA7, 0x9E, 0x83, 0x22, 0x25, 0x18, 0x7D, 0x13, 0xE7, 0xB1, 0x60,
|
||||
0x0A, 0xAB, 0x2D, 0x24, 0xA7, 0x2F, 0xB3, 0xFE, 0x09, 0xDC, 0x46, 0x9F, 0xAA, 0x00,
|
||||
0x0A, 0xB1, 0x3D, 0x6E, 0x5C, 0x6D, 0x61, 0xED, 0x35, 0x96, 0xB8, 0x0F, 0xE3, 0xCF,
|
||||
0x33, 0x99, 0xA1, 0x6B, 0x8B, 0x92, 0x66, 0x5F, 0xEC, 0xFB, 0xD0, 0x22, 0x24, 0x01,
|
||||
0x10, 0x1E, 0xAF, 0x16, 0x38, 0x0A, 0x5C, 0xB9, 0xCC, 0x06, 0xCE, 0xCE, 0x5C, 0x20,
|
||||
0xF4, 0xC0, 0xCB, 0x34, 0x09, 0x23, 0xE9, 0x14, 0x2F, 0x19, 0x98, 0x4A, 0xC1, 0xE0,
|
||||
0x55, 0xDF, 0xD7, 0xBF, 0x68, 0xEE, 0x14, 0xB0, 0xFE, 0x88, 0x97, 0xFC, 0x79, 0x36,
|
||||
0x23, 0xA2, 0x2D, 0xB1, 0xC9, 0x0F, 0x2C, 0x32, 0x4E, 0x8C, 0x39, 0x47, 0x51, 0xD5,
|
||||
0xCD, 0x8D, 0x23, 0x73, 0xFC, 0x56, 0xAB, 0x81, 0x5D, 0x23, 0xEF, 0xF4, 0x9B, 0x19,
|
||||
0x08, 0xD4, 0xB9, 0x1C, 0x12, 0x44, 0x8A, 0xBD, 0x4F, 0x4B, 0x9B, 0xD7, 0x4C, 0xBA,
|
||||
0x1B, 0x8A, 0x08, 0x05, 0xA1, 0xF5, 0xEE, 0x55, 0x03, 0x2B, 0xAA, 0x71, 0x4D, 0x51,
|
||||
0x6F, 0xF8, 0xC6, 0xBC, 0x28, 0x07, 0x53, 0xB5, 0x07, 0xC3, 0x00, 0x12, 0x64, 0x05,
|
||||
0x63, 0x6E, 0xC1, 0x13, 0xDD, 0x1E, 0xDB, 0x04, 0x29, 0x1D, 0xC8, 0xBB, 0xE3, 0x11,
|
||||
0x05, 0x21, 0xF0, 0xD8, 0x50, 0x12, 0x04, 0xC0, 0x92, 0x6B, 0x0B, 0x35, 0x0D, 0xEC,
|
||||
0xD3, 0x38, 0x09, 0xC7, 0x8A, 0x25, 0xFB, 0x66, 0xCC, 0xD9, 0x89, 0xDA, 0x9E, 0x48,
|
||||
0x08, 0x3A, 0xF4, 0xDD, 0x48, 0x1F, 0x46, 0x0E, 0x64, 0xCE, 0x84, 0x21, 0x62, 0xD4,
|
||||
0x3F, 0x32, 0xEC, 0x41, 0xF7, 0x29, 0xC3, 0xB4, 0xEF, 0x90, 0xFD, 0x80, 0x42, 0x1A,
|
||||
0x8E, 0xF9, 0xEE, 0x64, 0xE5, 0x18, 0xD1, 0xA8, 0x39, 0xBF, 0x59, 0x9D, 0xD7, 0x4B,
|
||||
0xA2, 0x96, 0xED, 0x7A, 0xF2, 0x23, 0x3B, 0xA7, 0xD6, 0xEA, 0xEF, 0x08, 0xAD, 0x61,
|
||||
0x67, 0x8A, 0x55, 0x51, 0x03, 0x2D, 0xBE, 0x1A, 0x61, 0xBE, 0xC8, 0x8F, 0xEE, 0x99,
|
||||
0x8B, 0x7B, 0xD6, 0x80, 0xFB, 0xBE, 0x1C, 0xC2, 0xE5, 0xC0, 0x4E, 0x5C, 0xC9, 0x95,
|
||||
0x20, 0xEA, 0x68, 0xF1, 0xF8, 0xFA, 0x1E, 0x4F, 0x2B, 0xC8, 0x11, 0x79, 0x18, 0xDD,
|
||||
0x9A, 0x9E, 0x23, 0x58, 0x05, 0xE1, 0xC3, 0x12, 0xF3, 0xB3, 0x41, 0xD0, 0xAD, 0xED,
|
||||
0x4C, 0xE0, 0x9E, 0xBC, 0xB9, 0x70, 0xF2, 0xAB, 0xA4, 0x18, 0xD5, 0xAB, 0xAB, 0x8C,
|
||||
0x65, 0xBE, 0x86, 0x62, 0x7C, 0xB2, 0xFF, 0xDD, 0x17, 0xDA, 0x3C, 0xD1, 0x91, 0x7D,
|
||||
0x01, 0x8A, 0x6A, 0xA3, 0x45, 0x06, 0x11, 0xE8, 0xA4, 0x01, 0x48, 0xF7, 0xC1, 0xB2,
|
||||
0x4A, 0xE5, 0x98, 0xCC, 0xCC, 0xB2, 0xFF, 0x9A, 0x00, 0x04, 0x00, 0x00,
|
||||
];
|
||||
let data = [header, data_chunk].concat();
|
||||
assert_matches!(
|
||||
QuestQst::from_bytes(&mut data.as_slice()),
|
||||
Err(QuestQstError::DataFormatError(..))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_on_data_packet_with_missing_filename() {
|
||||
// header is fine
|
||||
let header: &[u8] = &[
|
||||
0xA6, 0x00, 0x3C, 0x00, 0x4C, 0x6F, 0x73, 0x74, 0x20, 0x48, 0x45, 0x41, 0x54, 0x20,
|
||||
0x53, 0x57, 0x4F, 0x52, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x35, 0x38, 0x2E, 0x62, 0x69, 0x6E, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x23, 0x06, 0x00, 0x00,
|
||||
];
|
||||
// data chunk packet has no filename (all null bytes)
|
||||
let data_chunk: &[u8] = &[
|
||||
0xA7, 0x00, 0x18, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x19, 0x00, 0x00, 0x86, 0xF9, 0xD3, 0x12,
|
||||
0xF5, 0xDA, 0x71, 0xBA, 0x23, 0x5A, 0x97, 0x42, 0x6A, 0xFC, 0xB0, 0x9E, 0x73, 0x0F,
|
||||
0xFC, 0xCF, 0x61, 0x21, 0xF0, 0x92, 0x6F, 0xDF, 0x25, 0x33, 0x17, 0x9E, 0x7C, 0x75,
|
||||
0x7B, 0x21, 0xC3, 0x5F, 0xD2, 0x8C, 0x03, 0x9F, 0x67, 0x60, 0x94, 0x6E, 0x25, 0xE5,
|
||||
0x26, 0x25, 0x6C, 0xB4, 0x39, 0xF4, 0xBF, 0xDF, 0x76, 0x6A, 0x07, 0x6B, 0x81, 0xD0,
|
||||
0x18, 0x4D, 0xF9, 0xE8, 0xAE, 0x59, 0x0E, 0x56, 0x5B, 0x4A, 0x4D, 0x0F, 0x11, 0x27,
|
||||
0xD7, 0xBD, 0xA0, 0xAE, 0xCE, 0x14, 0x9E, 0xB2, 0x88, 0x57, 0x3B, 0xCB, 0xA1, 0xB5,
|
||||
0x0D, 0xEB, 0x44, 0x4D, 0x13, 0x8B, 0x89, 0x9C, 0x80, 0x6F, 0xF0, 0x25, 0x2C, 0xAC,
|
||||
0x20, 0xDA, 0x65, 0x78, 0x93, 0x02, 0x75, 0x41, 0xFC, 0xDD, 0xFD, 0xAB, 0x5B, 0xD9,
|
||||
0x78, 0xA9, 0x09, 0xA6, 0xFE, 0xD2, 0x52, 0xFC, 0x4A, 0x66, 0x90, 0xB4, 0xDF, 0xD7,
|
||||
0x34, 0x4A, 0xD4, 0x3C, 0xE8, 0xA3, 0x0B, 0xB4, 0xFD, 0xE3, 0x34, 0x6C, 0x49, 0x85,
|
||||
0xF4, 0xAE, 0xB9, 0x55, 0x6B, 0x3E, 0x4B, 0x82, 0xCF, 0x5B, 0x22, 0x3A, 0x92, 0xA3,
|
||||
0x0F, 0xB7, 0xBE, 0x71, 0x59, 0xCC, 0xE7, 0xDA, 0xDB, 0x69, 0x77, 0xC7, 0x63, 0x44,
|
||||
0x8E, 0xC4, 0xF7, 0xE2, 0x5F, 0xA4, 0xAB, 0x5D, 0x7A, 0x21, 0xD7, 0x1F, 0xAD, 0x58,
|
||||
0xAD, 0x66, 0x3E, 0x4A, 0x00, 0x93, 0xA2, 0x0A, 0xCD, 0xB4, 0x96, 0x1A, 0xF0, 0x72,
|
||||
0xD0, 0xEB, 0xF1, 0x89, 0x16, 0xFF, 0x11, 0x66, 0x5A, 0xF0, 0x63, 0xB8, 0x56, 0x09,
|
||||
0x99, 0xCE, 0x66, 0x6B, 0x7C, 0xE5, 0xE7, 0xE1, 0x2E, 0xC4, 0xEA, 0xA5, 0x43, 0x82,
|
||||
0x82, 0xAE, 0x49, 0x3E, 0xF1, 0x5F, 0x1C, 0xBB, 0xD1, 0x5B, 0x79, 0xBA, 0xE1, 0xF8,
|
||||
0x68, 0x6C, 0xB1, 0x39, 0x01, 0xAA, 0xCE, 0x4F, 0xE8, 0x90, 0x84, 0xD9, 0xEE, 0x99,
|
||||
0xE1, 0x60, 0xD8, 0x1D, 0xE8, 0x80, 0xF5, 0x34, 0x75, 0x07, 0xAB, 0x60, 0x94, 0x9B,
|
||||
0x91, 0x45, 0xF5, 0xD6, 0xF5, 0x32, 0x29, 0xD8, 0xAE, 0xD7, 0xE6, 0x58, 0x02, 0x87,
|
||||
0x44, 0xF7, 0x46, 0xCC, 0x28, 0x38, 0xBF, 0x1C, 0x9C, 0x5B, 0xF7, 0x88, 0x46, 0x85,
|
||||
0x1D, 0x0A, 0x9E, 0xEC, 0xBE, 0x34, 0x20, 0xD6, 0xCD, 0x80, 0xF5, 0x72, 0xDD, 0x17,
|
||||
0x96, 0x40, 0x5D, 0x8A, 0x52, 0xD3, 0x2E, 0x9B, 0x8B, 0xDD, 0x59, 0xC4, 0x90, 0x3C,
|
||||
0x1A, 0x44, 0x33, 0x58, 0x5C, 0xE1, 0x6F, 0xDF, 0x9D, 0xFF, 0xFC, 0x6E, 0xF3, 0x0C,
|
||||
0x7F, 0xBE, 0x3F, 0x3F, 0x41, 0xBA, 0xD9, 0xFB, 0x93, 0x08, 0x4B, 0xB0, 0x29, 0x58,
|
||||
0x95, 0xB5, 0x28, 0xE4, 0xED, 0x5A, 0x97, 0xBE, 0xDD, 0x15, 0xCB, 0xCE, 0x41, 0x11,
|
||||
0xBE, 0x86, 0x52, 0xED, 0x90, 0x0F, 0x7C, 0x62, 0xE8, 0x24, 0x63, 0x5B, 0x48, 0x6B,
|
||||
0x37, 0xEA, 0x59, 0xE9, 0xD2, 0x18, 0xD4, 0x3D, 0xA7, 0x5F, 0x96, 0x36, 0xED, 0x6F,
|
||||
0xC6, 0x30, 0x06, 0x4E, 0x96, 0xB8, 0x5D, 0x3A, 0x14, 0xBC, 0xD5, 0x4A, 0x8D, 0x0C,
|
||||
0x6C, 0xB2, 0x7B, 0x54, 0xA8, 0x81, 0xB1, 0x2E, 0x05, 0x46, 0xE7, 0x45, 0x54, 0x7E,
|
||||
0x3C, 0x0D, 0x30, 0x79, 0x3E, 0xE9, 0xB1, 0xD4, 0xB5, 0xD7, 0x83, 0x27, 0xE6, 0x83,
|
||||
0x08, 0xDD, 0x43, 0xA1, 0xA5, 0xDE, 0xD7, 0xC7, 0xDC, 0xB2, 0x99, 0xC4, 0x76, 0x20,
|
||||
0xB6, 0xDB, 0xA9, 0x4C, 0x96, 0x22, 0x2D, 0xEC, 0xBE, 0x01, 0x33, 0xAB, 0xC4, 0x63,
|
||||
0xE6, 0x54, 0xDD, 0x55, 0x66, 0xEB, 0xAF, 0xAD, 0x1D, 0xA4, 0x77, 0x18, 0x03, 0xD4,
|
||||
0xCF, 0x16, 0x8F, 0x0F, 0xD7, 0x5A, 0x19, 0xDB, 0xE6, 0xB5, 0xE1, 0x8E, 0x76, 0xF8,
|
||||
0x86, 0x1C, 0x24, 0x17, 0xD4, 0x4B, 0xDA, 0x63, 0x87, 0x94, 0x4E, 0x8E, 0x85, 0x7A,
|
||||
0x60, 0xC0, 0xF4, 0xBA, 0x04, 0x27, 0x3A, 0x31, 0x20, 0xDB, 0x8C, 0x54, 0x92, 0xFE,
|
||||
0x38, 0x92, 0xFA, 0x6D, 0xD9, 0x4B, 0xBA, 0x1E, 0xED, 0x8A, 0xEC, 0x92, 0xD7, 0x76,
|
||||
0x0B, 0x7C, 0x6B, 0x75, 0xFF, 0xFA, 0xE1, 0x53, 0x0F, 0xA6, 0x98, 0x3E, 0x74, 0xD9,
|
||||
0x04, 0xEF, 0x5C, 0xD7, 0xF9, 0xBF, 0x5C, 0x87, 0xD8, 0x7E, 0xF4, 0xDB, 0xFB, 0xAC,
|
||||
0xD2, 0xA1, 0xEA, 0x9F, 0x00, 0xB3, 0xD1, 0x28, 0xE7, 0xEC, 0xD7, 0xDA, 0x42, 0x43,
|
||||
0x70, 0x0F, 0x49, 0xC5, 0xF0, 0xFE, 0xD2, 0xE4, 0xEF, 0xA6, 0xA7, 0xE0, 0x36, 0x38,
|
||||
0x8B, 0x27, 0x1D, 0x42, 0x83, 0xC5, 0x3A, 0xD2, 0x85, 0x1B, 0xB1, 0xBD, 0xE1, 0x9C,
|
||||
0x20, 0x35, 0x65, 0x40, 0x41, 0x9E, 0xF7, 0x01, 0x34, 0x0F, 0xC9, 0xE0, 0xCC, 0x5D,
|
||||
0x94, 0xD9, 0xD4, 0x17, 0x45, 0xFC, 0x10, 0x77, 0x56, 0x48, 0x2A, 0xA5, 0x91, 0x6A,
|
||||
0x2E, 0x3B, 0xED, 0xCF, 0x2C, 0xB0, 0xE5, 0x46, 0x71, 0x7E, 0x87, 0xB6, 0x5F, 0xA4,
|
||||
0x6F, 0xE9, 0x68, 0xA7, 0x9E, 0x83, 0x22, 0x25, 0x18, 0x7D, 0x13, 0xE7, 0xB1, 0x60,
|
||||
0x0A, 0xAB, 0x2D, 0x24, 0xA7, 0x2F, 0xB3, 0xFE, 0x09, 0xDC, 0x46, 0x9F, 0xAA, 0x00,
|
||||
0x0A, 0xB1, 0x3D, 0x6E, 0x5C, 0x6D, 0x61, 0xED, 0x35, 0x96, 0xB8, 0x0F, 0xE3, 0xCF,
|
||||
0x33, 0x99, 0xA1, 0x6B, 0x8B, 0x92, 0x66, 0x5F, 0xEC, 0xFB, 0xD0, 0x22, 0x24, 0x01,
|
||||
0x10, 0x1E, 0xAF, 0x16, 0x38, 0x0A, 0x5C, 0xB9, 0xCC, 0x06, 0xCE, 0xCE, 0x5C, 0x20,
|
||||
0xF4, 0xC0, 0xCB, 0x34, 0x09, 0x23, 0xE9, 0x14, 0x2F, 0x19, 0x98, 0x4A, 0xC1, 0xE0,
|
||||
0x55, 0xDF, 0xD7, 0xBF, 0x68, 0xEE, 0x14, 0xB0, 0xFE, 0x88, 0x97, 0xFC, 0x79, 0x36,
|
||||
0x23, 0xA2, 0x2D, 0xB1, 0xC9, 0x0F, 0x2C, 0x32, 0x4E, 0x8C, 0x39, 0x47, 0x51, 0xD5,
|
||||
0xCD, 0x8D, 0x23, 0x73, 0xFC, 0x56, 0xAB, 0x81, 0x5D, 0x23, 0xEF, 0xF4, 0x9B, 0x19,
|
||||
0x08, 0xD4, 0xB9, 0x1C, 0x12, 0x44, 0x8A, 0xBD, 0x4F, 0x4B, 0x9B, 0xD7, 0x4C, 0xBA,
|
||||
0x1B, 0x8A, 0x08, 0x05, 0xA1, 0xF5, 0xEE, 0x55, 0x03, 0x2B, 0xAA, 0x71, 0x4D, 0x51,
|
||||
0x6F, 0xF8, 0xC6, 0xBC, 0x28, 0x07, 0x53, 0xB5, 0x07, 0xC3, 0x00, 0x12, 0x64, 0x05,
|
||||
0x63, 0x6E, 0xC1, 0x13, 0xDD, 0x1E, 0xDB, 0x04, 0x29, 0x1D, 0xC8, 0xBB, 0xE3, 0x11,
|
||||
0x05, 0x21, 0xF0, 0xD8, 0x50, 0x12, 0x04, 0xC0, 0x92, 0x6B, 0x0B, 0x35, 0x0D, 0xEC,
|
||||
0xD3, 0x38, 0x09, 0xC7, 0x8A, 0x25, 0xFB, 0x66, 0xCC, 0xD9, 0x89, 0xDA, 0x9E, 0x48,
|
||||
0x08, 0x3A, 0xF4, 0xDD, 0x48, 0x1F, 0x46, 0x0E, 0x64, 0xCE, 0x84, 0x21, 0x62, 0xD4,
|
||||
0x3F, 0x32, 0xEC, 0x41, 0xF7, 0x29, 0xC3, 0xB4, 0xEF, 0x90, 0xFD, 0x80, 0x42, 0x1A,
|
||||
0x8E, 0xF9, 0xEE, 0x64, 0xE5, 0x18, 0xD1, 0xA8, 0x39, 0xBF, 0x59, 0x9D, 0xD7, 0x4B,
|
||||
0xA2, 0x96, 0xED, 0x7A, 0xF2, 0x23, 0x3B, 0xA7, 0xD6, 0xEA, 0xEF, 0x08, 0xAD, 0x61,
|
||||
0x67, 0x8A, 0x55, 0x51, 0x03, 0x2D, 0xBE, 0x1A, 0x61, 0xBE, 0xC8, 0x8F, 0xEE, 0x99,
|
||||
0x8B, 0x7B, 0xD6, 0x80, 0xFB, 0xBE, 0x1C, 0xC2, 0xE5, 0xC0, 0x4E, 0x5C, 0xC9, 0x95,
|
||||
0x20, 0xEA, 0x68, 0xF1, 0xF8, 0xFA, 0x1E, 0x4F, 0x2B, 0xC8, 0x11, 0x79, 0x18, 0xDD,
|
||||
0x9A, 0x9E, 0x23, 0x58, 0x05, 0xE1, 0xC3, 0x12, 0xF3, 0xB3, 0x41, 0xD0, 0xAD, 0xED,
|
||||
0x4C, 0xE0, 0x9E, 0xBC, 0xB9, 0x70, 0xF2, 0xAB, 0xA4, 0x18, 0xD5, 0xAB, 0xAB, 0x8C,
|
||||
0x65, 0xBE, 0x86, 0x62, 0x7C, 0xB2, 0xFF, 0xDD, 0x17, 0xDA, 0x3C, 0xD1, 0x91, 0x7D,
|
||||
0x01, 0x8A, 0x6A, 0xA3, 0x45, 0x06, 0x11, 0xE8, 0xA4, 0x01, 0x48, 0xF7, 0xC1, 0xB2,
|
||||
0x4A, 0xE5, 0x98, 0xCC, 0xCC, 0xB2, 0xFF, 0x9A, 0x00, 0x04, 0x00, 0x00,
|
||||
];
|
||||
let data = [header, data_chunk].concat();
|
||||
assert_matches!(
|
||||
QuestQst::from_bytes(&mut data.as_slice()),
|
||||
Err(QuestQstError::DataFormatError(..))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_when_qst_does_not_contain_both_bin_and_dat_packets() {
|
||||
let qst = QuestQst::from_file(Path::new("../test-assets/q058-ret-gc.online.qst")).unwrap();
|
||||
// write only the bin header+data packets into a buffer which we will then read a qst from
|
||||
let mut bytes = Cursor::new(Vec::new());
|
||||
qst.bin_header.write_bytes(&mut bytes).unwrap();
|
||||
for chunk in qst.bin_chunks.iter() {
|
||||
chunk.write_bytes(&mut bytes).unwrap();
|
||||
}
|
||||
// this will fail because from_bytes loops infinitely until both a bin and dat are fully
|
||||
// read. it will reach eof on the buffer once all the bin data is read
|
||||
assert_matches!(
|
||||
QuestQst::from_bytes(&mut bytes),
|
||||
Err(QuestQstError::DataPacketError(PacketError::IoError(..)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_when_qst_header_packet_ids_do_not_all_match() {
|
||||
let qst = QuestQst::from_file(Path::new("../test-assets/q058-ret-gc.online.qst")).unwrap();
|
||||
let mut bytes = qst.to_bytes().unwrap();
|
||||
|
||||
// packet id is the very first byte. switch it to the wrong type
|
||||
bytes[0] = 0xA6;
|
||||
|
||||
assert_matches!(
|
||||
QuestQst::from_bytes(&mut bytes.deref()),
|
||||
Err(QuestQstError::DataFormatError(..))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn error_when_qst_data_packet_ids_do_not_all_match() {
|
||||
let qst = QuestQst::from_file(Path::new("../test-assets/q058-ret-gc.online.qst")).unwrap();
|
||||
let mut bytes = qst.to_bytes().unwrap();
|
||||
|
||||
// switch packet id of the first data packet to the wrong type
|
||||
bytes[QuestHeaderPacket::packet_size() * 2] = 0xA7;
|
||||
|
||||
assert_matches!(
|
||||
QuestQst::from_bytes(&mut bytes.deref()),
|
||||
Err(QuestQstError::DataFormatError(..))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
use encoding_rs::{Encoding, SHIFT_JIS, WINDOWS_1252};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum LanguageError {
|
||||
#[error("Error encoding string to {0} bytes")]
|
||||
EncodeError(String),
|
||||
|
||||
#[error("Error decoding as {0} bytes")]
|
||||
DecodeError(String),
|
||||
|
||||
#[error("The number {0} does not correspond to any supported language")]
|
||||
InvalidLanguageValue(u8),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||
pub enum Language {
|
||||
English = 1,
|
||||
French = 3,
|
||||
German = 2,
|
||||
Japanese = 0,
|
||||
Spanish = 4,
|
||||
}
|
||||
|
||||
impl Language {
|
||||
pub fn from_number(number: u8) -> Result<Language, LanguageError> {
|
||||
use Language::*;
|
||||
let language = match number {
|
||||
0 => Japanese,
|
||||
1 => English,
|
||||
2 => German,
|
||||
3 => French,
|
||||
4 => Spanish,
|
||||
n => return Err(LanguageError::InvalidLanguageValue(n)),
|
||||
};
|
||||
Ok(language)
|
||||
}
|
||||
|
||||
pub fn get_encoding(&self) -> &'static Encoding {
|
||||
use Language::*;
|
||||
match self {
|
||||
// we should technically be using ISO-8859-1, but encoding_rs does not have it ???
|
||||
// this is probably close enough at any rate ...
|
||||
English | French | German | Spanish => WINDOWS_1252,
|
||||
Japanese => SHIFT_JIS,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode_text(&self, bytes: &[u8]) -> Result<String, LanguageError> {
|
||||
let encoding = self.get_encoding();
|
||||
let (cow, encoding_used, had_errors) = encoding.decode(bytes);
|
||||
if had_errors {
|
||||
Err(LanguageError::DecodeError(encoding_used.name().to_string()))
|
||||
} else {
|
||||
Ok(cow.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode_text(&self, s: &str) -> Result<Vec<u8>, LanguageError> {
|
||||
let encoding = self.get_encoding();
|
||||
let (cow, encoding_used, had_errors) = encoding.encode(s);
|
||||
if had_errors {
|
||||
Err(LanguageError::EncodeError(encoding_used.name().to_string()))
|
||||
} else {
|
||||
Ok(cow.to_vec())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use claim::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
pub fn language_encode_decode() {
|
||||
assert_eq!(
|
||||
"The East Tower",
|
||||
Language::English
|
||||
.decode_text(&[
|
||||
0x54, 0x68, 0x65, 0x20, 0x45, 0x61, 0x73, 0x74, 0x20, 0x54, 0x6f, 0x77, 0x65,
|
||||
0x72
|
||||
])
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
vec![
|
||||
0x54, 0x68, 0x65, 0x20, 0x45, 0x61, 0x73, 0x74, 0x20, 0x54, 0x6f, 0x77, 0x65, 0x72
|
||||
],
|
||||
Language::English.encode_text("The East Tower").unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
"東天の塔",
|
||||
Language::Japanese
|
||||
.decode_text(&[0x93, 0x8c, 0x93, 0x56, 0x82, 0xcc, 0x93, 0x83])
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
vec![0x93, 0x8c, 0x93, 0x56, 0x82, 0xcc, 0x93, 0x83],
|
||||
Language::Japanese.encode_text("東天の塔").unwrap()
|
||||
);
|
||||
|
||||
assert_matches!(
|
||||
Language::English.encode_text("東天の塔"),
|
||||
Err(LanguageError::EncodeError(_))
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
use crc::{crc32, Hasher32};
|
||||
|
||||
pub fn crc32(bytes: &[u8]) -> u32 {
|
||||
let mut digest = crc32::Digest::new(crc32::IEEE);
|
||||
digest.write(bytes);
|
||||
digest.sum32()
|
||||
}
|
455
quest_info.c
Normal file
455
quest_info.c
Normal file
|
@ -0,0 +1,455 @@
|
|||
/*
|
||||
* PSO EP1&2 (Gamecube) Quest Info Tool
|
||||
*
|
||||
* This tool can load Gamecube quest data from any of the following types of files:
|
||||
*
|
||||
* - Compressed .bin + .dat file combo
|
||||
* - Online-play, unencrypted (0x44 / 0x13) .qst file (interleaved or not)
|
||||
* - Download/Offline-play, encrypted (0xA6 / 0xA7) .qst file (interleaved or not)
|
||||
*
|
||||
* And display basic information about the quest and perform some basic validations on the data.
|
||||
*
|
||||
* Gered King, March 2021
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <malloc.h>
|
||||
|
||||
#include <sylverant/encryption.h>
|
||||
#include "fuzziqer_prs.h"
|
||||
|
||||
#include "retvals.h"
|
||||
#include "utils.h"
|
||||
#include "quests.h"
|
||||
|
||||
typedef struct _PACKED_ {
|
||||
uint8_t pkt_id;
|
||||
uint8_t pkt_flags;
|
||||
uint16_t pkt_size;
|
||||
} PACKET_HEADER;
|
||||
|
||||
#define PACKET_TYPE_ERROR 0
|
||||
#define PACKET_TYPE_HEADER 1
|
||||
#define PACKET_TYPE_DATA 2
|
||||
#define PACKET_TYPE_EOF 4 // not really a packet type, lol
|
||||
|
||||
#define QST_TYPE_NONE 0
|
||||
#define QST_TYPE_ONLINE 1
|
||||
#define QST_TYPE_DOWNLOAD 2
|
||||
|
||||
const char* get_area_string(int area, int episode) {
|
||||
if (episode == 0) {
|
||||
switch (area) {
|
||||
case 0: return "Pioneer 2";
|
||||
case 1: return "Forest 1";
|
||||
case 2: return "Forest 2";
|
||||
case 3: return "Caves 1";
|
||||
case 4: return "Caves 2";
|
||||
case 5: return "Caves 3";
|
||||
case 6: return "Mines 1";
|
||||
case 7: return "Mines 2";
|
||||
case 8: return "Ruins 1";
|
||||
case 9: return "Ruins 2";
|
||||
case 10: return "Ruins 3";
|
||||
case 11: return "Under the Dome";
|
||||
case 12: return "Underground Channel";
|
||||
case 13: return "Monitor Room";
|
||||
case 14: return "????";
|
||||
case 15: return "Visual Lobby";
|
||||
case 16: return "VR Spaceship Alpha";
|
||||
case 17: return "VR Temple Alpha";
|
||||
default: return "Invalid Area";
|
||||
}
|
||||
} else if (episode == 1) {
|
||||
switch (area) {
|
||||
case 0: return "Lab";
|
||||
case 1: return "VR Temple Alpha";
|
||||
case 2: return "VR Temple Beta";
|
||||
case 3: return "VR Spaceship Alpha";
|
||||
case 4: return "VR Spaceship Beta";
|
||||
case 5: return "Central Control Area";
|
||||
case 6: return "Jungle North";
|
||||
case 7: return "Jungle East";
|
||||
case 8: return "Mountain";
|
||||
case 9: return "Seaside";
|
||||
case 10: return "Seabed Upper";
|
||||
case 11: return "Seabed Lower";
|
||||
case 12: return "Cliffs of Gal Da Val";
|
||||
case 13: return "Test Subject Disposal Area";
|
||||
case 14: return "VR Temple Final";
|
||||
case 15: return "VR Spaceship Final";
|
||||
case 16: return "Seaside Night";
|
||||
case 17: return "Control Tower";
|
||||
default: return "Invalid Area";
|
||||
}
|
||||
} else {
|
||||
return "Invalid Episode";
|
||||
}
|
||||
}
|
||||
|
||||
void display_info(uint8_t *bin_data, size_t bin_length, uint8_t *dat_data, size_t dat_length, int qst_type) {
|
||||
int validation_result;
|
||||
int32_t result;
|
||||
uint8_t *decompressed_bin_data = NULL;
|
||||
uint8_t *decompressed_dat_data = NULL;
|
||||
size_t decompressed_bin_length, decompressed_dat_length;
|
||||
|
||||
printf("Decompressing .bin data ...\n");
|
||||
result = fuzziqer_prs_decompress_buf(bin_data, &decompressed_bin_data, bin_length);
|
||||
if (result < 0) {
|
||||
printf("Error code %d decompressing .bin data.\n", result);
|
||||
goto error;
|
||||
}
|
||||
decompressed_bin_length = result;
|
||||
|
||||
printf("Decompressing .dat data ...\n");
|
||||
result = fuzziqer_prs_decompress_buf(dat_data, &decompressed_dat_data, dat_length);
|
||||
if (result < 0) {
|
||||
printf("Error code %d decompressing .dat data.\n", result);
|
||||
goto error;
|
||||
}
|
||||
decompressed_dat_length = result;
|
||||
|
||||
|
||||
printf("Validating .bin data ...\n");
|
||||
QUEST_BIN_HEADER *bin_header = (QUEST_BIN_HEADER*)decompressed_bin_data;
|
||||
validation_result = validate_quest_bin(bin_header, decompressed_bin_length, true);
|
||||
validation_result = handle_quest_bin_validation_issues(validation_result, bin_header, &decompressed_bin_data,
|
||||
&decompressed_bin_length);
|
||||
if (validation_result) {
|
||||
printf("Aborting due to invalid quest .bin data.\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
|
||||
printf("Validating .dat data ...\n");
|
||||
validation_result = validate_quest_dat(decompressed_dat_data, decompressed_dat_length, true);
|
||||
validation_result = handle_quest_dat_validation_issues(validation_result, &decompressed_dat_data, &decompressed_dat_length);
|
||||
if (validation_result) {
|
||||
printf("Aborting due to invalid quest .dat data.\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
printf("\n\n");
|
||||
|
||||
printf("QUEST FILE FORMAT: ");
|
||||
switch (qst_type) {
|
||||
case QST_TYPE_NONE: printf("PRS-compressed .bin/.dat\n"); break;
|
||||
case QST_TYPE_DOWNLOAD: printf("download/offline .qst with encrypted PRS-compressed .bin/.dat (0x%02X)\n", PACKET_ID_QUEST_INFO_DOWNLOAD); break;
|
||||
case QST_TYPE_ONLINE: printf("online .qst with PRS-compressed .bin/.dat (0x%02X)\n", PACKET_ID_QUEST_INFO_ONLINE); break;
|
||||
default: printf("unknown\n");
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
|
||||
printf("QUEST .BIN FILE\n");
|
||||
printf("======================================================================\n");
|
||||
printf("name: %s\n", bin_header->name);
|
||||
printf("download flag: %d\n", bin_header->download);
|
||||
printf("quest_number: %d (8-bit) %d, 0x%04x (16-bit)\n", bin_header->quest_number_byte, bin_header->quest_number_word, bin_header->quest_number_word);
|
||||
printf("episode: %d (0x%02x)\n", bin_header->episode + 1, bin_header->episode);
|
||||
printf("xffffffff: 0x%08x\n", bin_header->xffffffff);
|
||||
printf("unknown: 0x%02x\n", bin_header->unknown);
|
||||
printf("\n");
|
||||
printf("short_description:\n%s\n\n", bin_header->short_description);
|
||||
printf("long_description:\n%s\n", bin_header->long_description);
|
||||
printf("object_code_offset: %d\n", bin_header->object_code_offset);
|
||||
printf("function_offset_table_offset: %d\n", bin_header->function_offset_table_offset);
|
||||
printf("object_code_size: %d\n", (bin_header->function_offset_table_offset - bin_header->object_code_offset));
|
||||
printf("function_offset_table_size: %d\n", (bin_header->bin_size - bin_header->function_offset_table_offset));
|
||||
|
||||
|
||||
printf("\n\n");
|
||||
printf("QUEST .DAT FILE\n");
|
||||
printf("======================================================================\n");
|
||||
|
||||
int table_index = 0;
|
||||
uint32_t offset = 0;
|
||||
printf("Idx Size Table Type Area Count\n");
|
||||
while (offset < decompressed_dat_length) {
|
||||
QUEST_DAT_TABLE_HEADER *table_header = (QUEST_DAT_TABLE_HEADER*)(decompressed_dat_data + offset);
|
||||
|
||||
switch (table_header->type) {
|
||||
case 1:
|
||||
printf("%3d %5d %-21s %-30s %5d",
|
||||
table_index,
|
||||
table_header->table_body_size,
|
||||
"Object",
|
||||
get_area_string(table_header->area, bin_header->episode),
|
||||
table_header->table_body_size / 68);
|
||||
break;
|
||||
case 2:
|
||||
printf("%3d %5d %-21s %-30s %5d",
|
||||
table_index,
|
||||
table_header->table_body_size,
|
||||
"NPC",
|
||||
get_area_string(table_header->area, bin_header->episode),
|
||||
table_header->table_body_size / 72);
|
||||
break;
|
||||
case 3:
|
||||
printf("%3d %5d %-21s %-30s",
|
||||
table_index,
|
||||
table_header->table_body_size,
|
||||
"Wave",
|
||||
get_area_string(table_header->area, bin_header->episode));
|
||||
break;
|
||||
case 4:
|
||||
printf("%3d %5d %-21s %-30s",
|
||||
table_index,
|
||||
table_header->table_body_size,
|
||||
"Challenge Mode Spawns",
|
||||
get_area_string(table_header->area, bin_header->episode));
|
||||
break;
|
||||
case 5:
|
||||
printf("%3d %5d %21s %30s",
|
||||
table_index,
|
||||
table_header->table_body_size,
|
||||
"Challenge Mode (?)",
|
||||
get_area_string(table_header->area, bin_header->episode));
|
||||
break;
|
||||
default:
|
||||
if (table_header->type == 0 && table_header->table_size == 0 && table_header->area == 0 && table_header->table_body_size == 0)
|
||||
printf("%3d %5d %-21s", table_index, 0, "EOF marker");
|
||||
else
|
||||
printf("%3d %5d Unknown (%d)",
|
||||
table_index,
|
||||
table_header->table_body_size,
|
||||
table_header->type);
|
||||
break;
|
||||
}
|
||||
printf("\n");
|
||||
|
||||
offset += sizeof(QUEST_DAT_TABLE_HEADER);
|
||||
offset += table_header->table_body_size;
|
||||
++table_index;
|
||||
}
|
||||
|
||||
error:
|
||||
free(decompressed_bin_data);
|
||||
free(decompressed_dat_data);
|
||||
}
|
||||
|
||||
int read_next_qst_packet(FILE *fp, QST_HEADER *out_header_packet, QST_DATA_CHUNK *out_data_packet) {
|
||||
size_t bytes_read;
|
||||
PACKET_HEADER packet_header;
|
||||
|
||||
bytes_read = fread(&packet_header, 1, sizeof(PACKET_HEADER), fp);
|
||||
if (bytes_read == 0 && feof(fp))
|
||||
return PACKET_TYPE_EOF;
|
||||
if (bytes_read != sizeof(PACKET_HEADER))
|
||||
return PACKET_TYPE_ERROR;
|
||||
|
||||
if (packet_header.pkt_size == sizeof(QST_HEADER) &&
|
||||
(packet_header.pkt_id == PACKET_ID_QUEST_INFO_ONLINE ||
|
||||
packet_header.pkt_id == PACKET_ID_QUEST_INFO_DOWNLOAD)) {
|
||||
memcpy(out_header_packet, &packet_header, sizeof(PACKET_HEADER));
|
||||
size_t remaining_bytes = sizeof(QST_HEADER) - sizeof(PACKET_HEADER);
|
||||
bytes_read = fread((uint8_t*)out_header_packet + sizeof(PACKET_HEADER), 1, remaining_bytes, fp);
|
||||
if (bytes_read != remaining_bytes)
|
||||
return PACKET_TYPE_ERROR;
|
||||
else
|
||||
return PACKET_TYPE_HEADER;
|
||||
|
||||
} else if (packet_header.pkt_size == sizeof(QST_DATA_CHUNK) &&
|
||||
(packet_header.pkt_id == PACKET_ID_QUEST_CHUNK_ONLINE ||
|
||||
packet_header.pkt_id == PACKET_ID_QUEST_CHUNK_DOWNLOAD)) {
|
||||
memcpy(out_data_packet, &packet_header, sizeof(PACKET_HEADER));
|
||||
size_t remaining_bytes = sizeof(QST_DATA_CHUNK) - sizeof(PACKET_HEADER);
|
||||
bytes_read = fread((uint8_t*)out_data_packet + sizeof(PACKET_HEADER), 1, remaining_bytes, fp);
|
||||
if (bytes_read != remaining_bytes)
|
||||
return PACKET_TYPE_ERROR;
|
||||
else
|
||||
return PACKET_TYPE_DATA;
|
||||
|
||||
} else
|
||||
return PACKET_TYPE_ERROR;
|
||||
}
|
||||
|
||||
int load_quest_from_qst(const char *filename, uint8_t **out_bin_data, size_t *out_bin_length, uint8_t **out_dat_data, size_t *out_dat_length, int *out_qst_type) {
|
||||
int returncode;
|
||||
FILE *fp = NULL;
|
||||
uint8_t *bin_data = NULL;
|
||||
uint8_t *dat_data = NULL;
|
||||
int qst_type;
|
||||
|
||||
fp = fopen(filename, "rb");
|
||||
if (!fp) {
|
||||
returncode = ERROR_FILE_NOT_FOUND;
|
||||
goto error;
|
||||
}
|
||||
|
||||
char bin_filename[QUEST_FILENAME_MAX_LENGTH] = "";
|
||||
char dat_filename[QUEST_FILENAME_MAX_LENGTH] = "";
|
||||
size_t bin_data_length, dat_data_length;
|
||||
size_t bin_data_pos, dat_data_pos;
|
||||
|
||||
while (!feof(fp)) {
|
||||
QST_HEADER header;
|
||||
QST_DATA_CHUNK data;
|
||||
int type = read_next_qst_packet(fp, &header, &data);
|
||||
|
||||
if (type == PACKET_TYPE_EOF && bin_data && dat_data)
|
||||
break;
|
||||
|
||||
if (type == PACKET_TYPE_ERROR) {
|
||||
returncode = ERROR_BAD_DATA;
|
||||
goto error;
|
||||
|
||||
} else if (type == PACKET_TYPE_HEADER) {
|
||||
//CRYPT_PrintData(&header, sizeof(QST_HEADER));
|
||||
if (string_ends_with(header.filename, ".bin")) {
|
||||
strncpy(bin_filename, header.filename, QUEST_FILENAME_MAX_LENGTH);
|
||||
bin_data_length = header.size;
|
||||
bin_data_pos = 0;
|
||||
bin_data = malloc(bin_data_length);
|
||||
} else if (string_ends_with(header.filename, ".dat")) {
|
||||
strncpy(dat_filename, header.filename, QUEST_FILENAME_MAX_LENGTH);
|
||||
dat_data_length = header.size;
|
||||
dat_data_pos = 0;
|
||||
dat_data = malloc(dat_data_length);
|
||||
} else {
|
||||
returncode = ERROR_BAD_DATA;
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (header.pkt_id == PACKET_ID_QUEST_INFO_ONLINE)
|
||||
qst_type = QST_TYPE_ONLINE;
|
||||
else
|
||||
qst_type = QST_TYPE_DOWNLOAD;
|
||||
|
||||
} else if (type == PACKET_TYPE_DATA) {
|
||||
//CRYPT_PrintData(&data, sizeof(QST_DATA_CHUNK));
|
||||
if (strncmp(data.filename, bin_filename, QUEST_FILENAME_MAX_LENGTH) == 0) {
|
||||
memcpy(bin_data + bin_data_pos, data.data, data.size);
|
||||
bin_data_pos += data.size;
|
||||
|
||||
} else if (strncmp(data.filename, dat_filename, QUEST_FILENAME_MAX_LENGTH) == 0) {
|
||||
memcpy(dat_data + dat_data_pos, data.data, data.size);
|
||||
dat_data_pos += data.size;
|
||||
|
||||
} else {
|
||||
returncode = ERROR_BAD_DATA;
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
|
||||
*out_bin_length = bin_data_length;
|
||||
*out_dat_length = dat_data_length;
|
||||
*out_bin_data = bin_data;
|
||||
*out_dat_data = dat_data;
|
||||
*out_qst_type = qst_type;
|
||||
|
||||
return SUCCESS;
|
||||
|
||||
error:
|
||||
fclose(fp);
|
||||
free(bin_data);
|
||||
free(dat_data);
|
||||
return returncode;
|
||||
}
|
||||
|
||||
int decrypt_qst_bindat(uint8_t *bin_data, size_t *bin_length, uint8_t *dat_data, size_t *dat_length) {
|
||||
DOWNLOAD_QUEST_CHUNKS_HEADER *bin_dl_header = (DOWNLOAD_QUEST_CHUNKS_HEADER*)bin_data;
|
||||
DOWNLOAD_QUEST_CHUNKS_HEADER *dat_dl_header = (DOWNLOAD_QUEST_CHUNKS_HEADER*)dat_data;
|
||||
|
||||
CRYPT_SETUP bin_cs, dat_cs;
|
||||
CRYPT_CreateKeys(&bin_cs, &bin_dl_header->crypt_key, CRYPT_PC);
|
||||
CRYPT_CreateKeys(&dat_cs, &dat_dl_header->crypt_key, CRYPT_PC);
|
||||
|
||||
uint8_t *actual_bin_data = bin_data + sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER);
|
||||
uint8_t *actual_dat_data = dat_data + sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER);
|
||||
size_t decrypted_bin_length = *bin_length - sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER);
|
||||
size_t decrypted_dat_length = *dat_length - sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER);
|
||||
CRYPT_CryptData(&bin_cs, bin_data + sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER), decrypted_bin_length, 0);
|
||||
CRYPT_CryptData(&dat_cs, dat_data + sizeof(DOWNLOAD_QUEST_CHUNKS_HEADER), decrypted_dat_length, 0);
|
||||
|
||||
memmove(bin_data, actual_bin_data, decrypted_bin_length);
|
||||
memmove(dat_data, actual_dat_data, decrypted_dat_length);
|
||||
|
||||
*bin_length = decrypted_bin_length;
|
||||
*dat_length = decrypted_dat_length;
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int load_quest_from_bindat(const char *bin_filename, const char *dat_filename, uint8_t **out_bin_data, size_t *out_bin_length, uint8_t **out_dat_data, size_t *out_dat_length) {
|
||||
int returncode;
|
||||
uint8_t *bin_data = NULL;
|
||||
uint8_t *dat_data = NULL;
|
||||
uint32_t bin_data_length, dat_data_length;
|
||||
|
||||
returncode = read_file(bin_filename, &bin_data, &bin_data_length);
|
||||
if (returncode)
|
||||
goto error;
|
||||
|
||||
returncode = read_file(dat_filename, &dat_data, &dat_data_length);
|
||||
if (returncode)
|
||||
goto error;
|
||||
|
||||
*out_bin_length = bin_data_length;
|
||||
*out_dat_length = dat_data_length;
|
||||
*out_bin_data = bin_data;
|
||||
*out_dat_data = dat_data;
|
||||
|
||||
return SUCCESS;
|
||||
|
||||
error:
|
||||
free(bin_data);
|
||||
free(dat_data);
|
||||
return returncode;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int returncode;
|
||||
|
||||
if (argc != 2 && argc != 3) {
|
||||
printf("Usage: quest_info quest.bin quest.dat\n");
|
||||
printf(" quest_info quest.qst\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
uint8_t *bin_data = NULL;
|
||||
uint8_t *dat_data = NULL;
|
||||
size_t bin_data_size, dat_data_size;
|
||||
int qst_type = QST_TYPE_NONE;
|
||||
|
||||
if (argc == 2) {
|
||||
printf("Reading .qst file: %s\n", argv[1]);
|
||||
returncode = load_quest_from_qst(argv[1], &bin_data, &bin_data_size, &dat_data, &dat_data_size, &qst_type);
|
||||
if (returncode) {
|
||||
printf("Error code %d (%s) loading quest: %s\n", returncode, get_error_message(returncode), argv[1]);
|
||||
goto error;
|
||||
}
|
||||
if (qst_type == QST_TYPE_DOWNLOAD) {
|
||||
printf("Decrypting download .qst data ...\n");
|
||||
returncode = decrypt_qst_bindat(bin_data, &bin_data_size, dat_data, &dat_data_size);
|
||||
if (returncode) {
|
||||
printf("Error code %d (%s) while decrypting .qst contents", returncode, get_error_message(returncode));
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
printf("Reading .bin file %s and .dat file %s ... \n", argv[1], argv[2]);
|
||||
returncode = load_quest_from_bindat(argv[1], argv[2], &bin_data, &bin_data_size, &dat_data, &dat_data_size);
|
||||
if (returncode) {
|
||||
printf("Error code %d (%s) loading quest files %s and %s\n", returncode, get_error_message(returncode), argv[1], argv[2]);
|
||||
goto error;
|
||||
}
|
||||
}
|
||||
|
||||
display_info(bin_data, bin_data_size, dat_data, dat_data_size, qst_type);
|
||||
|
||||
returncode = 0;
|
||||
goto quit;
|
||||
error:
|
||||
returncode = 1;
|
||||
quit:
|
||||
free(bin_data);
|
||||
free(dat_data);
|
||||
return returncode;
|
||||
}
|
23
quest_info.md
Normal file
23
quest_info.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
# PSO EP1&2 (Gamecube) Quest Info Tool
|
||||
|
||||
This tool can load Gamecube quest data from any of the following types of files:
|
||||
|
||||
- Compressed .bin + .dat file combo
|
||||
- Online-play, unencrypted (0x44 / 0x13) .qst file (interleaved or not)
|
||||
- Download/Offline-play, encrypted (0xA6 / 0xA7) .qst file (interleaved or not)
|
||||
|
||||
And display basic information about the quest and perform some basic validations on the data.
|
||||
|
||||
This tool was primarily written for my own benefit, as I needed something to help me quickly run through a series of
|
||||
.bin/.dat files freshly extracted from a set of GCI files to validate them to make sure I had not done something silly
|
||||
during the process.
|
||||
|
||||
## Usage
|
||||
|
||||
Simply pass either a `.bin` and `.dat` file (in that order) as arguments, or pass a single `.qst` file.
|
||||
|
||||
```text
|
||||
quest_info quest.bin quest.dat
|
||||
|
||||
quest_info quest.qst
|
||||
```
|
181
quests.c
Normal file
181
quests.c
Normal file
|
@ -0,0 +1,181 @@
|
|||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <malloc.h>
|
||||
|
||||
#include "retvals.h"
|
||||
#include "quests.h"
|
||||
|
||||
int generate_qst_header(const char *src_file, size_t src_file_size, const QUEST_BIN_HEADER *bin_header, QST_HEADER *out_header) {
|
||||
if (!src_file || !bin_header || !out_header)
|
||||
return ERROR_INVALID_PARAMS;
|
||||
|
||||
memset(out_header, 0, sizeof(QST_HEADER));
|
||||
|
||||
out_header->pkt_id = PACKET_ID_QUEST_INFO_DOWNLOAD;
|
||||
out_header->pkt_size = sizeof(QST_HEADER);
|
||||
out_header->pkt_flags = 0;
|
||||
out_header->flags = 0;
|
||||
out_header->size = src_file_size;
|
||||
|
||||
strncpy(out_header->name, bin_header->name, strlen(bin_header->name));
|
||||
strncpy(out_header->filename, src_file, strlen(src_file));
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int generate_qst_data_chunk(const char *base_filename, uint8_t counter, const uint8_t *src, uint32_t size, QST_DATA_CHUNK *out_chunk) {
|
||||
if (!base_filename || !src || !out_chunk)
|
||||
return ERROR_INVALID_PARAMS;
|
||||
|
||||
memset(out_chunk, 0, sizeof(QST_DATA_CHUNK));
|
||||
|
||||
out_chunk->pkt_id = PACKET_ID_QUEST_CHUNK_DOWNLOAD;
|
||||
out_chunk->pkt_flags = counter;
|
||||
out_chunk->pkt_size = sizeof(QST_DATA_CHUNK);
|
||||
strncpy(out_chunk->filename, base_filename, sizeof(out_chunk->filename));
|
||||
memcpy(out_chunk->data, src, size);
|
||||
out_chunk->size = size;
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int validate_quest_bin(const QUEST_BIN_HEADER *header, uint32_t length, bool print_errors) {
|
||||
int result = 0;
|
||||
|
||||
// TODO: validations might need tweaking ...
|
||||
if (header->object_code_offset != 468) {
|
||||
if (print_errors)
|
||||
printf("Quest bin file issue: unexpected object_code_offset = %d\n", header->object_code_offset);
|
||||
result |= QUESTBIN_ERROR_OBJECT_CODE_OFFSET;
|
||||
}
|
||||
if (header->bin_size < length) {
|
||||
if (print_errors)
|
||||
printf("Quest bin file issue: bin_size %d is smaller than the actual decompressed bin size %d\n", header->bin_size, length);
|
||||
result |= QUESTBIN_ERROR_SMALLER_BIN_SIZE;
|
||||
} else if (header->bin_size > length) {
|
||||
if (print_errors)
|
||||
printf("Quest bin file issue: bin_size %d is larger than the actual decompressed bin size %d\n", header->bin_size, length);
|
||||
result |= QUESTBIN_ERROR_LARGER_BIN_SIZE;
|
||||
}
|
||||
if (strlen(header->name) == 0) {
|
||||
if (print_errors)
|
||||
printf("Quest bin file issue: blank quest name\n");
|
||||
result |= QUESTBIN_ERROR_NAME;
|
||||
}
|
||||
if (header->episode > 1) {
|
||||
if (print_errors)
|
||||
printf("Quest bin file issue: unexpected episode value %d, quest was probably created using a 16-bit quest_number\n", header->episode);
|
||||
result |= QUESTBIN_ERROR_EPISODE;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int validate_quest_dat(const uint8_t *data, uint32_t length, bool print_errors) {
|
||||
int result = 0;
|
||||
int table_index = 0;
|
||||
|
||||
// TODO: validations might need tweaking ...
|
||||
uint32_t offset = 0;
|
||||
while (offset < length) {
|
||||
QUEST_DAT_TABLE_HEADER *table_header = (QUEST_DAT_TABLE_HEADER*)(data + offset);
|
||||
|
||||
if (table_header->type > 5) {
|
||||
if (print_errors)
|
||||
printf("Quest dat file issue: invalid table type value %d found in table index %d\n", table_header->type, table_index);
|
||||
result |= QUESTDAT_ERROR_TYPE;
|
||||
}
|
||||
if (table_header->type == 0 &&
|
||||
table_header->table_size == 0 &&
|
||||
table_header->area == 0 &&
|
||||
table_header->table_body_size == 0) {
|
||||
if ((offset + sizeof(QUEST_DAT_TABLE_HEADER)) == length) {
|
||||
// ignore this case ... this empty table is used to mark EOF apparently
|
||||
} else {
|
||||
if (print_errors)
|
||||
printf("Quest dat file issue: empty table encountered at table index %d with %d bytes left in file. treating this as early EOF\n", table_index, length - offset);
|
||||
result |= QUESTDAT_ERROR_PREMATURE_EOF;
|
||||
break;
|
||||
}
|
||||
|
||||
} else if (table_header->table_size == (table_header->table_body_size - sizeof(QUEST_DAT_TABLE_HEADER))) {
|
||||
if (print_errors)
|
||||
printf("Quest dat file issue: mismatching table_size (%d) and table_body_size (%d) found in table index %d\n",
|
||||
table_header->table_size,
|
||||
table_header->table_body_size,
|
||||
table_index);
|
||||
result |= QUESTDAT_ERROR_TABLE_BODY_SIZE;
|
||||
}
|
||||
|
||||
offset += sizeof(QUEST_DAT_TABLE_HEADER);
|
||||
offset += table_header->table_body_size;
|
||||
++table_index;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// HACK: this function applies some arguably shitty hack-fixes under certain circumstances.
|
||||
int handle_quest_bin_validation_issues(int bin_validation_result, QUEST_BIN_HEADER *bin_header, uint8_t **decompressed_bin_data, size_t *decompressed_bin_length) {
|
||||
// this hacky fix _probably_ isn't so bad. in these cases, the extra data sitting in the decompressed memory seems
|
||||
// to just be repeated subsets of the previous "good" data. almost as if the PRS decompression was stuck in a loop
|
||||
// that it eventually worked itself out of. just a wild guess though ...
|
||||
if (bin_validation_result & QUESTBIN_ERROR_SMALLER_BIN_SIZE) {
|
||||
bin_validation_result &= ~QUESTBIN_ERROR_SMALLER_BIN_SIZE;
|
||||
printf("WARNING: Decompressed .bin data is larger than expected. Proceeding using the smaller .bin header bin_size value ...\n");
|
||||
*decompressed_bin_length = bin_header->bin_size;
|
||||
}
|
||||
|
||||
// this hacky fix is _probably_ not too bad either, but might have more potential for breaking things than the
|
||||
// above hack fix. maybe. i also think this is a result of some PRS decompression bug (or maybe a PRS compression
|
||||
// bug? since i believe the decompression implementation is based on game code disassembly, but most (all?) of the
|
||||
// PRS-compression implementations are based on the fuzziqer implementation which he coded himself instead of it
|
||||
// being based on game code disassembly?) ... who knows!
|
||||
if (bin_validation_result & QUESTBIN_ERROR_LARGER_BIN_SIZE) {
|
||||
bin_validation_result &= ~QUESTBIN_ERROR_LARGER_BIN_SIZE;
|
||||
if ((*decompressed_bin_length + 1) == bin_header->bin_size) {
|
||||
printf("WARNING: Decompressed .bin data is 1 byte smaller than the .bin header bin_size specifies. Correcting by adding a null byte ...\n");
|
||||
size_t length = *decompressed_bin_length + 1;
|
||||
uint8_t *new_bin_data;
|
||||
new_bin_data = realloc(*decompressed_bin_data, length);
|
||||
new_bin_data[length - 1] = 0;
|
||||
*decompressed_bin_data = new_bin_data;
|
||||
*decompressed_bin_length = length;
|
||||
}
|
||||
}
|
||||
if (bin_validation_result & QUESTBIN_ERROR_EPISODE) {
|
||||
bin_validation_result &= ~QUESTBIN_ERROR_EPISODE;
|
||||
printf("WARNING: .bin header episode value should be ignored due to apparent 16-bit quest_number value\n");
|
||||
}
|
||||
|
||||
return bin_validation_result;
|
||||
}
|
||||
|
||||
int handle_quest_dat_validation_issues(int dat_validation_result, uint8_t **decompressed_dat_data, size_t *decompressed_dat_length) {
|
||||
// this one is a bit more annoying. the quest .dat format does not have any explicit value anywhere that tells you
|
||||
// how large the entire data should be. so we have to guess. from what i can piece together, .dat files normally
|
||||
// have a table with all zeros located at the end of the file (therefore, the last 16 bytes of an uncompressed .dat
|
||||
// file should all be zero). in the cases where i have seen what looks like an early zero table in a .dat file, if
|
||||
// i let the process of walking through the file continue, the subsequent tables all look like garbage with random
|
||||
// values. so i am guessing that this is also a result of PRS compression/decompression issues ...
|
||||
if (dat_validation_result & QUESTDAT_ERROR_PREMATURE_EOF) {
|
||||
dat_validation_result &= ~QUESTDAT_ERROR_PREMATURE_EOF;
|
||||
printf("WARNING: .dat file appeared to end early (found zero-length table before end of file was reached). Decompressed .dat data might be too large? Ignoring.\n");
|
||||
}
|
||||
|
||||
return dat_validation_result;
|
||||
}
|
||||
|
||||
void print_quick_quest_info(QUEST_BIN_HEADER *bin_header, size_t compressed_bin_size, size_t compressed_dat_size) {
|
||||
printf("Quest: id=%d (%d, 0x%04x), episode=%d (0x%02x), download=%d, unknown=0x%02x, name=\"%s\"\n",
|
||||
bin_header->quest_number_byte,
|
||||
bin_header->quest_number_word,
|
||||
bin_header->quest_number_word,
|
||||
bin_header->episode + 1,
|
||||
bin_header->episode,
|
||||
bin_header->download,
|
||||
bin_header->unknown,
|
||||
bin_header->name);
|
||||
printf(" compressed_bin_size=%ld, compressed_dat_size=%ld\n", compressed_bin_size, compressed_dat_size);
|
||||
}
|
122
quests.h
Normal file
122
quests.h
Normal file
|
@ -0,0 +1,122 @@
|
|||
#ifndef QUESTS_H_INCLUDED
|
||||
#define QUESTS_H_INCLUDED
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "defs.h"
|
||||
|
||||
#define QUESTBIN_ERROR_OBJECT_CODE_OFFSET 1
|
||||
#define QUESTBIN_ERROR_LARGER_BIN_SIZE 2
|
||||
#define QUESTBIN_ERROR_SMALLER_BIN_SIZE 4
|
||||
#define QUESTBIN_ERROR_NAME 8
|
||||
#define QUESTBIN_ERROR_EPISODE 16
|
||||
|
||||
#define QUESTDAT_ERROR_TYPE 1
|
||||
#define QUESTDAT_ERROR_TABLE_BODY_SIZE 2
|
||||
#define QUESTDAT_ERROR_PREMATURE_EOF 4
|
||||
|
||||
#define PACKET_ID_QUEST_INFO_ONLINE 0x44
|
||||
#define PACKET_ID_QUEST_INFO_DOWNLOAD 0xa6
|
||||
#define PACKET_ID_QUEST_CHUNK_ONLINE 0x13
|
||||
#define PACKET_ID_QUEST_CHUNK_DOWNLOAD 0xa7
|
||||
|
||||
#define QUEST_FILENAME_MAX_LENGTH 16
|
||||
|
||||
// decompressed quest .bin file header
|
||||
typedef struct _PACKED_ {
|
||||
uint32_t object_code_offset;
|
||||
uint32_t function_offset_table_offset;
|
||||
uint32_t bin_size;
|
||||
uint32_t xffffffff; // always 0xffffffff ?
|
||||
uint8_t download; // must be '1' to be usable as an offline quest (played from memory card)
|
||||
|
||||
// have seen some projects define this field as language. "newserv" just calls it unknown? i've seen multiple
|
||||
// values present for english language quests ...
|
||||
uint8_t unknown;
|
||||
|
||||
// "newserv" has these like this here, as quest_number and episode separately. most other projects that parse
|
||||
// .bin files treat quest_number as a 16-bit number. in general, i think the "episode" field as a separate byte
|
||||
// is *probably* better when dealing with non-custom quests. however, some custom quests (which are mostly of
|
||||
// dubious quality anyway) clearly were created using a tool which had quest_number as a 16-bit value ...
|
||||
// ... so .... i dunno! i guess i'll just leave it like this ...
|
||||
union {
|
||||
struct {
|
||||
uint8_t quest_number_byte;
|
||||
uint8_t episode;
|
||||
};
|
||||
struct {
|
||||
uint16_t quest_number_word;
|
||||
};
|
||||
};
|
||||
|
||||
// some sources say these strings are all UTF-16LE, but i'm not sure that is really the case for gamecube data?
|
||||
// for gamecube-format quest .bin files, it instead looks like SHIFT-JIS probably ... ?
|
||||
|
||||
char name[32];
|
||||
char short_description[128];
|
||||
char long_description[288];
|
||||
} QUEST_BIN_HEADER;
|
||||
|
||||
// decompressed quest .dat file table header
|
||||
typedef struct _PACKED_ {
|
||||
uint32_t type;
|
||||
uint32_t table_size;
|
||||
uint32_t area;
|
||||
uint32_t table_body_size;
|
||||
} QUEST_DAT_TABLE_HEADER;
|
||||
|
||||
// .qst file header, for either the embedded bin or dat quest data (there should be two of these per .qst file).
|
||||
typedef struct _PACKED_ {
|
||||
// 0xA6 = download to memcard, 0x44 = download for online play
|
||||
// (quest file data chunks must then be encoded accordingly. 0xA6 = use 0xA7, and 0x44 = use 0x13)
|
||||
uint8_t pkt_id;
|
||||
|
||||
// khyller sets .dat header value to 0xC9, .bin header value to 0x88
|
||||
// newserv sets both to 0x00
|
||||
// sylverant appears to set it differently per quest, the logic/reasoning behind it is unknown to me
|
||||
// ... so, this value is probably unimportant?
|
||||
uint8_t pkt_flags;
|
||||
|
||||
uint16_t pkt_size;
|
||||
char name[32];
|
||||
uint16_t unused;
|
||||
|
||||
// khyller sets .dat header value to 0x02, .bin header value to 0x00
|
||||
// newserv sets both to 0x02
|
||||
// sylverant sets both to 0x00
|
||||
// ... and so, this value is also probably unimportant?
|
||||
uint16_t flags;
|
||||
|
||||
char filename[QUEST_FILENAME_MAX_LENGTH];
|
||||
uint32_t size;
|
||||
} QST_HEADER;
|
||||
|
||||
// .qst raw .bin/.dat file data packet. the original .bin/.dat file data is broken down into as many of these structs
|
||||
// as is necessary to fit into the resulting .qst file
|
||||
typedef struct _PACKED_ {
|
||||
uint8_t pkt_id;
|
||||
uint8_t pkt_flags;
|
||||
uint16_t pkt_size;
|
||||
char filename[QUEST_FILENAME_MAX_LENGTH];
|
||||
uint8_t data[1024];
|
||||
uint32_t size;
|
||||
} QST_DATA_CHUNK;
|
||||
|
||||
// for download/offline .qst files only. the raw .bin/.dat file data needs to be prefixed with one of these structs
|
||||
// before being turned into QST_DATA_CHUNKs. only one of these is needed per each .bin/.dat file.
|
||||
typedef struct _PACKED_ {
|
||||
uint32_t decompressed_size;
|
||||
uint32_t crypt_key;
|
||||
} DOWNLOAD_QUEST_CHUNKS_HEADER;
|
||||
|
||||
int generate_qst_header(const char *src_file, size_t src_file_size, const QUEST_BIN_HEADER *bin_header, QST_HEADER *out_header);
|
||||
int generate_qst_data_chunk(const char *base_filename, uint8_t counter, const uint8_t *src, uint32_t size, QST_DATA_CHUNK *out_chunk);
|
||||
int validate_quest_bin(const QUEST_BIN_HEADER *header, uint32_t length, bool print_errors);
|
||||
int validate_quest_dat(const uint8_t *data, uint32_t length, bool print_errors);
|
||||
int handle_quest_bin_validation_issues(int bin_validation_result, QUEST_BIN_HEADER *bin_header, uint8_t **decompressed_bin_data, size_t *decompressed_bin_length);
|
||||
int handle_quest_dat_validation_issues(int dat_validation_result, uint8_t **decompressed_dat_data, size_t *decompressed_dat_length);
|
||||
void print_quick_quest_info(QUEST_BIN_HEADER *bin_header, size_t compressed_bin_size, size_t compressed_dat_size);
|
||||
|
||||
#endif
|
11
retvals.h
Normal file
11
retvals.h
Normal file
|
@ -0,0 +1,11 @@
|
|||
#ifndef RETVALS_H_INCLUDED
|
||||
#define RETVALS_H_INCLUDED
|
||||
|
||||
#define SUCCESS 0
|
||||
#define ERROR_INVALID_PARAMS 1
|
||||
#define ERROR_FILE_NOT_FOUND 2
|
||||
#define ERROR_CREATING_FILE 3
|
||||
#define ERROR_BAD_DATA 4
|
||||
#define ERROR_IO 5
|
||||
|
||||
#endif
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
32
textconv.c
Normal file
32
textconv.c
Normal file
|
@ -0,0 +1,32 @@
|
|||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <malloc.h>
|
||||
|
||||
#include <iconv.h>
|
||||
|
||||
#include "textconv.h"
|
||||
#include "retvals.h"
|
||||
|
||||
int sjis_to_utf8(char *s, size_t length) {
|
||||
if (!s)
|
||||
return ERROR_INVALID_PARAMS;
|
||||
|
||||
iconv_t conv;
|
||||
size_t in_size, out_size;
|
||||
|
||||
char *outbuf = malloc(length);
|
||||
|
||||
in_size = length;
|
||||
out_size = length;
|
||||
char *in = s;
|
||||
char *out = outbuf;
|
||||
conv = iconv_open("SHIFT_JIS", "UTF-8");
|
||||
iconv(conv, &in, &in_size, &out, &out_size);
|
||||
iconv_close(conv);
|
||||
|
||||
memset(s, 0, length);
|
||||
memcpy(s, outbuf, length);
|
||||
free(outbuf);
|
||||
|
||||
return SUCCESS;
|
||||
}
|
11
textconv.h
Normal file
11
textconv.h
Normal file
|
@ -0,0 +1,11 @@
|
|||
#ifndef TEXTCONV_H_INCLUDED
|
||||
#define TEXTCONV_H_INCLUDED
|
||||
|
||||
#include <stdio.h>
|
||||
#include <iconv.h>
|
||||
|
||||
#include "retvals.h"
|
||||
|
||||
int sjis_to_utf8(char *s, size_t length);
|
||||
|
||||
#endif
|
129
utils.c
Normal file
129
utils.c
Normal file
|
@ -0,0 +1,129 @@
|
|||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
#include <malloc.h>
|
||||
|
||||
#include "utils.h"
|
||||
#include "retvals.h"
|
||||
|
||||
// from error codes defined in retvals.h
|
||||
static const char *error_messages[] = {
|
||||
"No error", // SUCCESS
|
||||
"Invalid parameter(s)", // ERROR_INVALID_PARAMS
|
||||
"File not found", // ERROR_FILE_NOT_FOUND
|
||||
"Cannot create file", // ERROR_CREATING_FILE
|
||||
"Bad data", // ERROR_BAD_DATA
|
||||
"I/O error", // ERROR_IO
|
||||
NULL
|
||||
};
|
||||
|
||||
int read_file(const char *filename, uint8_t** out_file_data, uint32_t *out_file_size) {
|
||||
if (!filename || !out_file_size || !out_file_data)
|
||||
return ERROR_INVALID_PARAMS;
|
||||
|
||||
FILE *fp = fopen(filename, "rb");
|
||||
if (!fp)
|
||||
return ERROR_FILE_NOT_FOUND;
|
||||
|
||||
fseek(fp, 0, SEEK_END);
|
||||
*out_file_size = ftell(fp);
|
||||
fseek(fp, 0, SEEK_SET);
|
||||
|
||||
uint8_t *result = malloc(*out_file_size);
|
||||
|
||||
uint32_t read, next;
|
||||
uint8_t buffer[1024];
|
||||
|
||||
next = 0;
|
||||
|
||||
do {
|
||||
read = fread(buffer, 1, 1024, fp);
|
||||
if (read) {
|
||||
memcpy(&result[next], buffer, read);
|
||||
next += read;
|
||||
}
|
||||
} while (read);
|
||||
|
||||
*out_file_data = result;
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int write_file(const char *filename, const void *data, size_t size) {
|
||||
if (!filename || !data || size == 0)
|
||||
return ERROR_INVALID_PARAMS;
|
||||
|
||||
FILE *fp = fopen(filename, "wb");
|
||||
if (!fp)
|
||||
return ERROR_CREATING_FILE;
|
||||
|
||||
int bytes_written = fwrite(data, 1, size, fp);
|
||||
if (bytes_written != size) {
|
||||
fclose(fp);
|
||||
return ERROR_IO;
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
int get_filesize(const char *filename, size_t *out_size) {
|
||||
if (!filename || !out_size)
|
||||
return ERROR_INVALID_PARAMS;
|
||||
|
||||
FILE *fp = fopen(filename, "rb");
|
||||
if (!fp)
|
||||
return ERROR_FILE_NOT_FOUND;
|
||||
|
||||
fseek(fp, 0, SEEK_END);
|
||||
*out_size = ftell(fp);
|
||||
fclose(fp);
|
||||
|
||||
return SUCCESS;
|
||||
}
|
||||
|
||||
const char* path_to_filename(const char *path) {
|
||||
const char *pos = strrchr(path, '/');
|
||||
if (pos) {
|
||||
return pos+1;
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
char* append_string(const char *a, const char *b) {
|
||||
if (!a)
|
||||
return NULL;
|
||||
|
||||
char *result = malloc(strlen(a) + strlen(b));
|
||||
strcpy(result, a);
|
||||
strcat(result, b);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool string_ends_with(const char *s, const char *suffix) {
|
||||
if (!s || !suffix)
|
||||
return false;
|
||||
|
||||
size_t s_len = strlen(s);
|
||||
size_t suffix_len = strlen(suffix);
|
||||
if (suffix_len > s_len)
|
||||
return false;
|
||||
else {
|
||||
const char *end_of_s = s + (s_len - suffix_len);
|
||||
return strncmp(end_of_s, suffix, suffix_len) == 0;
|
||||
}
|
||||
}
|
||||
|
||||
const char* get_error_message(int retvals_error_code) {
|
||||
retvals_error_code = abs(retvals_error_code);
|
||||
|
||||
int max_error_index;
|
||||
for (max_error_index = 0; error_messages[max_error_index]; ++max_error_index) {}
|
||||
max_error_index = max_error_index;
|
||||
|
||||
if (retvals_error_code >= max_error_index)
|
||||
return "Unknown error";
|
||||
else
|
||||
return error_messages[retvals_error_code];
|
||||
}
|
18
utils.h
Normal file
18
utils.h
Normal file
|
@ -0,0 +1,18 @@
|
|||
#ifndef UTILS_H_INCLUDED
|
||||
#define UTILS_H_INCLUDED
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "retvals.h"
|
||||
|
||||
int read_file(const char *filename, uint8_t** out_file_data, uint32_t *out_file_size);
|
||||
int write_file(const char *filename, const void *data, size_t size);
|
||||
int get_filesize(const char *filename, size_t *out_size);
|
||||
const char* path_to_filename(const char *path);
|
||||
char* append_string(const char *a, const char *b);
|
||||
bool string_ends_with(const char *s, const char *suffix);
|
||||
|
||||
const char* get_error_message(int retvals_error_code);
|
||||
|
||||
#endif
|
Loading…
Reference in a new issue