allow specifying input and output filenames

This commit is contained in:
Martin Michelsen
2022-10-03 21:44:03 -07:00
parent f6987d6627
commit a16c207f4d
2 changed files with 171 additions and 92 deletions
+1 -1
View File
@@ -110,7 +110,7 @@ foreach(TestCase IN ITEMS ${TestCases})
add_test(
NAME ${TestCase}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${CMAKE_BINARY_DIR}/newserv --replay-log=${TestCase} --config=${CMAKE_SOURCE_DIR}/tests/config.json --require-password=password --require-access-key=111111111111)
COMMAND ${CMAKE_BINARY_DIR}/newserv --replay-log ${TestCase} --config=${CMAKE_SOURCE_DIR}/tests/config.json --require-password=password --require-access-key=111111111111)
endforeach()
+170 -91
View File
@@ -216,27 +216,38 @@ void print_usage() {
newserv - a Phantasy Star Online Swiss Army knife\n\
\n\
Usage:\n\
newserv [options]\n\
newserv [options] [input-filename [output-filename]]\n\
\n\
With no options, newserv runs in server mode. PSO clients can connect normally,\n\
join lobbies, play games, and use the proxy server. See README.md and\n\
system/config.json for more information.\n\
\n\
When options are given, newserv will do things other than running the server.\n\
Specifically:\n\
--compress-data\n\
--decompress-data\n\
Compress or decompress data using the PRS algorithm.\n\
--decrypt-data\n\
\n\
Some modes accept input and/or output filenames; see the descriptions below for\n\
details. If input-filename is missing or is '-', newserv reads from stdin;\n\
similarly, if output-filename is missing or is '-', newserv writes to stdout.\n\
\n\
The options are:\n\
--compress-prs\n\
--decompress-prs\n\
Compress or decompress data using the PRS algorithm. Both input-filename\n\
and output-filename may be specified.\n\
--decompress-gjs [input-filename [output-filename]]\n\
Decompress data using the GJS algorithm.Both input-filename and\n\
output-filename may be specified.\n\
--encrypt-data\n\
Read from stdin, encrypt or decrypt the data, and write the result to\n\
stdout. By default, PSO V3 encryption is used, but this can be overridden\n\
with --pc or --bb. The --seed option specifies the encryption seed (4 hex\n\
bytes for PC or GC, or 48 hex bytes for BB). For BB, the --key option is\n\
required as well, and refers to a .nsk file in system/blueburst/keys\n\
(without the directory or .nsk extension). For non-BB ciphers, the\n\
--big-endian option applies the cipher masks as big-endian instead of\n\
little-endian, which is necessary for some GameCube file formats.\n\
--decrypt-data\n\
Encrypt or decrypt data using PSO's standard network protocol encryption.\n\
Both input-filename and output-filename may be specified. By default, PSO\n\
V3 (GameCube/XBOX) encryption is used, but this can be overridden with\n\
the --pc or --bb options. The --seed= option specifies the encryption\n\
seed (4 hex bytes for PC or GC, or 48 hex bytes for BB). For BB, the\n\
--key option is required as well, and refers to a .nsk file in\n\
system/blueburst/keys (without the directory or .nsk extension). For\n\
non-BB ciphers, the --big-endian option applies the cipher masks as\n\
big-endian instead of little-endian, which is necessary for some GameCube\n\
file formats.\n\
--find-decryption-seed\n\
Perform a brute-force search for a decryption seed of the given data.\n\
The ciphertext is specified with the --encrypted= option and the expected\n\
@@ -249,17 +260,21 @@ Specifically:\n\
system, but this can be overridden with the --threads= option.\n\
--decode-sjis\n\
Apply newserv\'s text decoding algorithm to the data on stdin, producing\n\
little-endian UTF-16 data on stdout.\n\
--decode-gci=FILENAME\n\
--decode-dlq=FILENAME\n\
--decode-qst=FILENAME\n\
little-endian UTF-16 data on stdout. Both input-filename and\n\
output-filename may be specified.\n\
--decode-gci\n\
--decode-dlq\n\
--decode-qst\n\
Decode the given quest file into a compressed, unencrypted .bin or .dat\n\
file (or in the case of --decode-qst, both a .bin and a .dat file). The\n\
--decode-gci option can be used to decrypt encrypted GCI files. If you\n\
know the player\'s serial number who generated the GCI file, use the\n\
--seed= option and give the serial number (as a hex-encoded integer). If\n\
you don\'t know the serial number, newserv will find it via a brute-force\n\
search, but this will likely take a long time.\n\
file (or in the case of --decode-qst, both a .bin and a .dat file).\n\
input-filename must be specified, but output-filename msut not be; the\n\
output is written to <input-filename>.dec (or .bin, or .dat). DLQ and QST\n\
decoding is a relatively simple operation, but GCI decoding can be\n\
computationally expensive if the file is encrypted and doesn't contain an\n\
embedded seed. If you know the player\'s serial number who generated the\n\
GCI file, use the --seed= option and give the serial number (as a\n\
hex-encoded 32-bit integer). If you don\'t know the serial number, newserv\n\
will find it via a brute-force search, but this will take a long time.\n\
--cat-client=ADDR:PORT\n\
Connect to the given server and simulate a PSO client. newserv will then\n\
print all the received commands to stdout, and forward any commands typed\n\
@@ -268,17 +283,23 @@ Specifically:\n\
--gc, and --bb options can be used to select the command format and\n\
encryption. If --bb is used, the --key option is also required (as in\n\
--decrypt-data above).\n\
--replay-log=FILENAME\n\
Replay a terminal log as if it were a client session. This is used for\n\
regression testing, to make sure client sessions are repeatable and code\n\
changes don\'t affect existing (working) functionality.\n\
--extract-gsl=FILENAME\n\
--replay-log\n\
Replay a terminal log as if it were a client session. input-filename may\n\
be specified for this option. This is used for regression testing, to\n\
make sure client sessions are repeatable and code changes don\'t affect\n\
existing (working) functionality.\n\
--extract-gsl\n\
Extract all files from a GSL archive into the current directory.\n\
input-filename may be specified. If output-filename is specified, then it\n\
is treated as a prefix which is prepended to the filename of each file\n\
contained in the GSL archive. Importantly, if you want to put the files\n\
into a directory, you'll have to create the directory first, and include\n\
a trailing / on output-filename.\n\
\n\
A few options apply to multiple modes described above:\n\
--parse-data\n\
For modes that take input on stdin, parse the input as a hex string\n\
before encrypting/decoding/etc.\n\
For modes that take input (from a file or from stdin), parse the input as\n\
a hex string before encrypting/decoding/etc.\n\
--config=FILENAME\n\
Use this file instead of system/config.json.\n\
", stderr);
@@ -286,10 +307,11 @@ A few options apply to multiple modes described above:\n\
enum class Behavior {
RUN_SERVER = 0,
DECOMPRESS_DATA,
COMPRESS_DATA,
DECRYPT_DATA,
COMPRESS_PRS,
DECOMPRESS_PRS,
DECOMPRESS_GJS,
ENCRYPT_DATA,
DECRYPT_DATA,
FIND_DECRYPTION_SEED,
DECODE_QUEST_FILE,
DECODE_SJIS,
@@ -298,6 +320,27 @@ enum class Behavior {
CAT_CLIENT,
};
static bool behavior_takes_input_filename(Behavior b) {
return (b == Behavior::COMPRESS_PRS) ||
(b == Behavior::DECOMPRESS_PRS) ||
(b == Behavior::DECOMPRESS_GJS) ||
(b == Behavior::ENCRYPT_DATA) ||
(b == Behavior::DECRYPT_DATA) ||
(b == Behavior::DECODE_QUEST_FILE) ||
(b == Behavior::DECODE_SJIS) ||
(b == Behavior::EXTRACT_GSL) ||
(b == Behavior::REPLAY_LOG);
}
static bool behavior_takes_output_filename(Behavior b) {
return (b == Behavior::COMPRESS_PRS) ||
(b == Behavior::DECOMPRESS_PRS) ||
(b == Behavior::DECOMPRESS_GJS) ||
(b == Behavior::ENCRYPT_DATA) ||
(b == Behavior::DECRYPT_DATA) ||
(b == Behavior::DECODE_SJIS);
}
enum class QuestFileFormat {
GCI = 0,
DLQ,
@@ -308,7 +351,6 @@ int main(int argc, char** argv) {
Behavior behavior = Behavior::RUN_SERVER;
GameVersion cli_version = GameVersion::GC;
QuestFileFormat quest_file_type = QuestFileFormat::GCI;
string quest_filename;
string seed;
string key_file_name;
const char* config_filename = "system/config.json";
@@ -319,8 +361,8 @@ int main(int argc, char** argv) {
size_t num_threads = 0;
const char* find_decryption_seed_ciphertext = nullptr;
vector<const char*> find_decryption_seed_plaintexts;
const char* replay_log_filename = nullptr;
const char* extract_gsl_filename = nullptr;
const char* input_filename = nullptr;
const char* output_filename = nullptr;
const char* replay_required_access_key = "";
const char* replay_required_password = "";
struct sockaddr_storage cat_client_remote;
@@ -328,30 +370,29 @@ int main(int argc, char** argv) {
if (!strcmp(argv[x], "--help")) {
print_usage();
return 0;
} else if (!strcmp(argv[x], "--decompress-data")) {
behavior = Behavior::DECOMPRESS_DATA;
} else if (!strcmp(argv[x], "--compress-data")) {
behavior = Behavior::COMPRESS_DATA;
} else if (!strcmp(argv[x], "--decrypt-data")) {
behavior = Behavior::DECRYPT_DATA;
} else if (!strcmp(argv[x], "--compress-prs")) {
behavior = Behavior::COMPRESS_PRS;
} else if (!strcmp(argv[x], "--decompress-prs")) {
behavior = Behavior::DECOMPRESS_PRS;
} else if (!strcmp(argv[x], "--decompress-gjs")) {
behavior = Behavior::DECOMPRESS_PRS;
} else if (!strcmp(argv[x], "--encrypt-data")) {
behavior = Behavior::ENCRYPT_DATA;
} else if (!strcmp(argv[x], "--decrypt-data")) {
behavior = Behavior::DECRYPT_DATA;
} else if (!strcmp(argv[x], "--find-decryption-seed")) {
behavior = Behavior::FIND_DECRYPTION_SEED;
} else if (!strcmp(argv[x], "--decode-sjis")) {
behavior = Behavior::DECODE_SJIS;
} else if (!strncmp(argv[x], "--decode-gci=", 13)) {
} else if (!strcmp(argv[x], "--decode-gci")) {
behavior = Behavior::DECODE_QUEST_FILE;
quest_file_type = QuestFileFormat::GCI;
quest_filename = &argv[x][13];
} else if (!strncmp(argv[x], "--decode-dlq=", 13)) {
} else if (!strcmp(argv[x], "--decode-dlq")) {
behavior = Behavior::DECODE_QUEST_FILE;
quest_file_type = QuestFileFormat::DLQ;
quest_filename = &argv[x][13];
} else if (!strncmp(argv[x], "--decode-qst=", 13)) {
} else if (!strcmp(argv[x], "--decode-qst")) {
behavior = Behavior::DECODE_QUEST_FILE;
quest_file_type = QuestFileFormat::QST;
quest_filename = &argv[x][13];
} else if (!strncmp(argv[x], "--cat-client=", 13)) {
behavior = Behavior::CAT_CLIENT;
cat_client_remote = make_sockaddr_storage(parse_netloc(&argv[x][13])).first;
@@ -385,46 +426,70 @@ int main(int argc, char** argv) {
skip_little_endian = true;
} else if (!strcmp(argv[x], "--skip-big-endian")) {
skip_big_endian = true;
} else if (!strncmp(argv[x], "--replay-log=", 13)) {
} else if (!strcmp(argv[x], "--replay-log")) {
behavior = Behavior::REPLAY_LOG;
replay_log_filename = &argv[x][13];
} else if (!strncmp(argv[x], "--extract-gsl=", 14)) {
} else if (!strcmp(argv[x], "--extract-gsl")) {
behavior = Behavior::EXTRACT_GSL;
extract_gsl_filename = &argv[x][14];
} else if (!strncmp(argv[x], "--require-password=", 19)) {
replay_required_password = &argv[x][19];
} else if (!strncmp(argv[x], "--require-access-key=", 21)) {
replay_required_access_key = &argv[x][21];
} else if (!strncmp(argv[x], "--config=", 9)) {
config_filename = &argv[x][9];
} else if (!strcmp(argv[x], "-") || argv[x][0] != '-') {
if (!input_filename && behavior_takes_input_filename(behavior)) {
input_filename = argv[x];
} else if (!output_filename && behavior_takes_output_filename(behavior)) {
output_filename = argv[x];
} else {
throw invalid_argument(string_printf("unknown option: %s", argv[x]));
}
} else {
throw invalid_argument(string_printf("unknown option: %s", argv[x]));
}
}
auto read_input_data = [&]() -> string {
string data;
if (input_filename && strcmp(input_filename, "-")) {
return load_file(input_filename);
} else {
return read_all(stdin);
}
};
auto write_output_data = [&](const void* data, size_t size) {
if (output_filename && strcmp(output_filename, "-")) {
save_file(output_filename, data, size);
} else if (isatty(fileno(stdout))) {
print_data(stdout, data, size);
fflush(stdout);
} else {
fwritex(stdout, data, size);
fflush(stdout);
}
};
switch (behavior) {
case Behavior::DECOMPRESS_DATA:
case Behavior::COMPRESS_DATA: {
string data = read_all(stdin);
case Behavior::COMPRESS_PRS:
case Behavior::DECOMPRESS_PRS:
case Behavior::DECOMPRESS_GJS: {
string data = read_input_data();
if (parse_data) {
data = parse_data_string(data);
}
if (behavior == Behavior::DECOMPRESS_DATA) {
data = prs_decompress(data);
} else if (behavior == Behavior::COMPRESS_DATA) {
if (behavior == Behavior::COMPRESS_PRS) {
data = prs_compress(data);
} else if (behavior == Behavior::DECOMPRESS_PRS) {
data = prs_decompress(data);
} else if (behavior == Behavior::DECOMPRESS_GJS) {
data = gjs_decompress(data);
} else {
throw logic_error("invalid behavior");
}
if (isatty(fileno(stdout))) {
print_data(stdout, data);
} else {
fwritex(stdout, data);
}
fflush(stdout);
write_output_data(data.data(), data.size());
break;
}
@@ -452,7 +517,7 @@ int main(int argc, char** argv) {
throw logic_error("invalid game version");
}
string data = read_all(stdin);
string data = read_input_data();
if (parse_data) {
data = parse_data_string(data);
}
@@ -484,12 +549,7 @@ int main(int argc, char** argv) {
data.resize(original_size);
if (isatty(fileno(stdout))) {
print_data(stdout, data);
} else {
fwritex(stdout, data);
}
fflush(stdout);
write_output_data(data.data(), data.size());
break;
}
@@ -566,38 +626,53 @@ int main(int argc, char** argv) {
break;
}
case Behavior::DECODE_QUEST_FILE:
case Behavior::DECODE_QUEST_FILE: {
if (!input_filename || !strcmp(input_filename, "-")) {
throw invalid_argument("an input filename is required");
}
string output_filename_base = input_filename;
if (quest_file_type == QuestFileFormat::GCI) {
int64_t dec_seed = seed.empty() ? -1 : stoul(seed, nullptr, 16);
save_file(quest_filename + ".dec", Quest::decode_gci(quest_filename, num_threads, dec_seed));
save_file(output_filename_base + ".dec", Quest::decode_gci(
input_filename, num_threads, dec_seed));
} else if (quest_file_type == QuestFileFormat::DLQ) {
save_file(quest_filename + ".dec", Quest::decode_dlq(quest_filename));
save_file(output_filename_base + ".dec", Quest::decode_dlq(
input_filename));
} else if (quest_file_type == QuestFileFormat::QST) {
auto data = Quest::decode_qst(quest_filename);
save_file(quest_filename + ".bin", data.first);
save_file(quest_filename + ".dat", data.second);
auto data = Quest::decode_qst(input_filename);
save_file(output_filename_base + ".bin", data.first);
save_file(output_filename_base + ".dat", data.second);
} else {
throw logic_error("invalid quest file format");
}
break;
}
case Behavior::DECODE_SJIS: {
string data = read_all(stdin);
string data = read_input_data();
if (parse_data) {
data = parse_data_string(data);
}
auto decoded = decode_sjis(data);
print_data(stderr, decoded.data(), decoded.size() * sizeof(decoded[0]));
write_output_data(decoded.data(), decoded.size() * sizeof(decoded[0]));
break;
}
case Behavior::EXTRACT_GSL: {
shared_ptr<string> data(new string(load_file(extract_gsl_filename)));
GSLArchive gsl(data);
if (!output_filename) {
output_filename = "";
} else if (!strcmp(output_filename, "-")) {
throw invalid_argument("output prefix cannot be stdout");
}
string data = read_input_data();
shared_ptr<string> data_shared(new string(move(data)));
GSLArchive gsl(data_shared);
for (const auto& entry_it : gsl.all_entries()) {
auto e = gsl.get(entry_it.first);
save_file(entry_it.first, e.first, e.second);
save_file(output_filename + entry_it.first, e.first, e.second);
fprintf(stderr, "... %s\n", entry_it.first.c_str());
}
break;
@@ -640,7 +715,7 @@ int main(int argc, char** argv) {
auto config_json = JSONObject::parse(load_file(config_filename));
populate_state_from_config(state, config_json);
if (!replay_log_filename) {
if (behavior != Behavior::REPLAY_LOG) {
config_log.info("Loading license list");
state->license_manager.reset(new LicenseManager("system/licenses.nsi"));
} else {
@@ -690,7 +765,7 @@ int main(int argc, char** argv) {
config_log.info("Collecting quest metadata");
state->quest_index.reset(new QuestIndex("system/quests"));
if (!replay_log_filename) {
if (behavior != Behavior::REPLAY_LOG) {
config_log.info("Compiling client functions");
state->function_code_index.reset(new FunctionCodeIndex("system/ppc"));
config_log.info("Loading DOL files");
@@ -703,14 +778,14 @@ int main(int argc, char** argv) {
config_log.info("Creating menus");
state->create_menus(config_json);
if (replay_log_filename) {
if (behavior == Behavior::REPLAY_LOG) {
state->allow_saving = false;
state->license_manager->set_autosave(false);
config_log.info("Saving disabled because this is a replay session");
}
shared_ptr<DNSServer> dns_server;
if (state->dns_server_port && !replay_log_filename) {
if (state->dns_server_port && (behavior != Behavior::REPLAY_LOG)) {
config_log.info("Starting DNS server");
dns_server.reset(new DNSServer(base, state->local_address,
state->external_address));
@@ -728,9 +803,13 @@ int main(int argc, char** argv) {
config_log.info("Starting game server");
state->game_server.reset(new Server(base, state));
auto f = fopen_unique(replay_log_filename, "rt");
shared_ptr<FILE> log_f(stdin, +[](FILE*) { });
if (input_filename && strcmp(input_filename, "-")) {
log_f = fopen_shared(input_filename, "rt");
}
replay_session.reset(new ReplaySession(
base, f.get(), state, replay_required_access_key, replay_required_password));
base, log_f.get(), state, replay_required_access_key, replay_required_password));
replay_session->start();
} else if (behavior == Behavior::RUN_SERVER) {