Compare commits
No commits in common. "original_c_version" and "master" have entirely different histories.
original_c
...
master
26
.gitignore
vendored
26
.gitignore
vendored
|
@ -1,25 +1,3 @@
|
|||
*.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
|
||||
|
|
|
@ -1,34 +0,0 @@
|
|||
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})
|
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
|
@ -0,0 +1,9 @@
|
|||
[workspace]
|
||||
|
||||
members = [
|
||||
"psoutils",
|
||||
"psogc_quest_tool",
|
||||
"gci_quest_extract",
|
||||
"decrypt_packets"
|
||||
]
|
||||
|
619
LICENSE
619
LICENSE
|
@ -1,619 +0,0 @@
|
|||
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,12 +1,19 @@
|
|||
# PSO Ep I & II Gamecube Tools
|
||||
# PSO Episode I & II Gamecube Tools
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Tools
|
||||
|
||||
* [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).
|
||||
* [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!)
|
||||
|
|
248
bindat_to_gcdl.c
248
bindat_to_gcdl.c
|
@ -1,248 +0,0 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
# 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
|
||||
```
|
|
@ -1,127 +0,0 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
# 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
|
||||
```
|
19
decrypt_packets/Cargo.toml
Normal file
19
decrypt_packets/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[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"
|
65
decrypt_packets/README.md
Normal file
65
decrypt_packets/README.md
Normal file
|
@ -0,0 +1,65 @@
|
|||
# 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
decrypt_packets/src/lib.rs
Normal file
1
decrypt_packets/src/lib.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod pcap;
|
31
decrypt_packets/src/main.rs
Normal file
31
decrypt_packets/src/main.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
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(())
|
||||
}
|
375
decrypt_packets/src/pcap.rs
Normal file
375
decrypt_packets/src/pcap.rs
Normal file
|
@ -0,0 +1,375 @@
|
|||
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
6
defs.h
|
@ -1,6 +0,0 @@
|
|||
#ifndef DEFS_H_INCLUDED
|
||||
#define DEFS_H_INCLUDED
|
||||
|
||||
#define _PACKED_ __attribute__((packed))
|
||||
|
||||
#endif
|
447
fuzziqer_prs.c
447
fuzziqer_prs.c
|
@ -1,447 +0,0 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
#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
267
gci_extract.c
|
@ -1,267 +0,0 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
# 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
|
||||
```
|
14
gci_quest_extract/Cargo.toml
Normal file
14
gci_quest_extract/Cargo.toml
Normal file
|
@ -0,0 +1,14 @@
|
|||
[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"
|
37
gci_quest_extract/README.md
Normal file
37
gci_quest_extract/README.md
Normal file
|
@ -0,0 +1,37 @@
|
|||
# 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!
|
121
gci_quest_extract/src/gci.rs
Normal file
121
gci_quest_extract/src/gci.rs
Normal file
|
@ -0,0 +1,121 @@
|
|||
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
gci_quest_extract/src/lib.rs
Normal file
1
gci_quest_extract/src/lib.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod gci;
|
35
gci_quest_extract/src/main.rs
Normal file
35
gci_quest_extract/src/main.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
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
121
gen_qst_header.c
|
@ -1,121 +0,0 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
# 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
|
||||
```
|
17
psogc_quest_tool/Cargo.toml
Normal file
17
psogc_quest_tool/Cargo.toml
Normal file
|
@ -0,0 +1,17 @@
|
|||
[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"
|
37
psogc_quest_tool/README.md
Normal file
37
psogc_quest_tool/README.md
Normal file
|
@ -0,0 +1,37 @@
|
|||
# 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.
|
388
psogc_quest_tool/src/convert.rs
Normal file
388
psogc_quest_tool/src/convert.rs
Normal file
|
@ -0,0 +1,388 @@
|
|||
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));
|
||||
}
|
||||
}
|
87
psogc_quest_tool/src/info.rs
Normal file
87
psogc_quest_tool/src/info.rs
Normal file
|
@ -0,0 +1,87 @@
|
|||
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(_));
|
||||
}
|
||||
}
|
2
psogc_quest_tool/src/lib.rs
Normal file
2
psogc_quest_tool/src/lib.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod convert;
|
||||
pub mod info;
|
54
psogc_quest_tool/src/main.rs
Normal file
54
psogc_quest_tool/src/main.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
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(())
|
||||
}
|
21
psoutils/Cargo.toml
Normal file
21
psoutils/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
|||
[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"
|
9
psoutils/README.md
Normal file
9
psoutils/README.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
# 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.
|
90
psoutils/src/bytes.rs
Normal file
90
psoutils/src/bytes.rs
Normal file
|
@ -0,0 +1,90 @@
|
|||
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>());
|
||||
}
|
||||
}
|
728
psoutils/src/compression.rs
Normal file
728
psoutils/src/compression.rs
Normal file
|
@ -0,0 +1,728 @@
|
|||
/*
|
||||
* 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(())
|
||||
}
|
||||
}
|
641
psoutils/src/encryption.rs
Normal file
641
psoutils/src/encryption.rs
Normal file
|
@ -0,0 +1,641 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
7
psoutils/src/lib.rs
Normal file
7
psoutils/src/lib.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
pub mod bytes;
|
||||
pub mod compression;
|
||||
pub mod encryption;
|
||||
pub mod packets;
|
||||
pub mod quest;
|
||||
pub mod text;
|
||||
mod utils;
|
102
psoutils/src/packets.rs
Normal file
102
psoutils/src/packets.rs
Normal file
|
@ -0,0 +1,102 @@
|
|||
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()
|
||||
}
|
||||
}
|
282
psoutils/src/packets/init.rs
Normal file
282
psoutils/src/packets/init.rs
Normal file
|
@ -0,0 +1,282 @@
|
|||
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(())
|
||||
}
|
||||
}
|
721
psoutils/src/packets/quest.rs
Normal file
721
psoutils/src/packets/quest.rs
Normal file
|
@ -0,0 +1,721 @@
|
|||
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(())
|
||||
}
|
||||
}
|
419
psoutils/src/quest.rs
Normal file
419
psoutils/src/quest.rs
Normal file
|
@ -0,0 +1,419 @@
|
|||
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(())
|
||||
}
|
||||
}
|
643
psoutils/src/quest/bin.rs
Normal file
643
psoutils/src/quest/bin.rs
Normal file
|
@ -0,0 +1,643 @@
|
|||
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(..))
|
||||
);
|
||||
}
|
||||
}
|
728
psoutils/src/quest/dat.rs
Normal file
728
psoutils/src/quest/dat.rs
Normal file
|
@ -0,0 +1,728 @@
|
|||
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(..))
|
||||
);
|
||||
}
|
||||
}
|
983
psoutils/src/quest/qst.rs
Normal file
983
psoutils/src/quest/qst.rs
Normal file
|
@ -0,0 +1,983 @@
|
|||
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(..))
|
||||
);
|
||||
}
|
||||
}
|
112
psoutils/src/text.rs
Normal file
112
psoutils/src/text.rs
Normal file
|
@ -0,0 +1,112 @@
|
|||
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(_))
|
||||
);
|
||||
}
|
||||
}
|
7
psoutils/src/utils.rs
Normal file
7
psoutils/src/utils.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
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
455
quest_info.c
|
@ -1,455 +0,0 @@
|
|||
/*
|
||||
* 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;
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
# 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
181
quests.c
|
@ -1,181 +0,0 @@
|
|||
#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
122
quests.h
|
@ -1,122 +0,0 @@
|
|||
#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
11
retvals.h
|
@ -1,11 +0,0 @@
|
|||
#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
|
BIN
test-assets/q058-ret-gc.bin
Executable file
BIN
test-assets/q058-ret-gc.bin
Executable file
Binary file not shown.
BIN
test-assets/q058-ret-gc.dat
Executable file
BIN
test-assets/q058-ret-gc.dat
Executable file
Binary file not shown.
BIN
test-assets/q058-ret-gc.offline.qst
Normal file
BIN
test-assets/q058-ret-gc.offline.qst
Normal file
Binary file not shown.
BIN
test-assets/q058-ret-gc.online.qst
Normal file
BIN
test-assets/q058-ret-gc.online.qst
Normal file
Binary file not shown.
BIN
test-assets/q058-ret-gc.uncompressed.bin
Normal file
BIN
test-assets/q058-ret-gc.uncompressed.bin
Normal file
Binary file not shown.
BIN
test-assets/q058-ret-gc.uncompressed.dat
Normal file
BIN
test-assets/q058-ret-gc.uncompressed.dat
Normal file
Binary file not shown.
BIN
test-assets/q118-vr-gc.bin
Executable file
BIN
test-assets/q118-vr-gc.bin
Executable file
Binary file not shown.
BIN
test-assets/q118-vr-gc.dat
Executable file
BIN
test-assets/q118-vr-gc.dat
Executable file
Binary file not shown.
BIN
test-assets/q118-vr-gc.offline.qst
Normal file
BIN
test-assets/q118-vr-gc.offline.qst
Normal file
Binary file not shown.
BIN
test-assets/q118-vr-gc.online.qst
Normal file
BIN
test-assets/q118-vr-gc.online.qst
Normal file
Binary file not shown.
BIN
test-assets/q118-vr-gc.uncompressed.bin
Normal file
BIN
test-assets/q118-vr-gc.uncompressed.bin
Normal file
Binary file not shown.
BIN
test-assets/q118-vr-gc.uncompressed.dat
Normal file
BIN
test-assets/q118-vr-gc.uncompressed.dat
Normal file
Binary file not shown.
32
textconv.c
32
textconv.c
|
@ -1,32 +0,0 @@
|
|||
#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
11
textconv.h
|
@ -1,11 +0,0 @@
|
|||
#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
129
utils.c
|
@ -1,129 +0,0 @@
|
|||
#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
18
utils.h
|
@ -1,18 +0,0 @@
|
|||
#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