mirror of
https://git.ely.my.id/Tenshi/Hi3-Patcher
synced 2026-11-04 21:46:48 +01:00
Initial Commit
This commit is contained in:
commit
737780597b
51 files changed
+8675
No files matched your search
+329
@@ -0,0 +1,329 @@
|
||||
//! Non-interactive command-line interface, intended for scripting and for
|
||||
//! driving the patcher from another application.
|
||||
//!
|
||||
//! Contract: the process exit code is authoritative — `0` success, `1` a
|
||||
//! patch/runtime error, `2` a usage error. The whole command line is
|
||||
//! validated before any patch work starts. With `--json`, the final line of
|
||||
//! stdout is a single JSON object (`{"ok":true,"command":...,"output":...}`
|
||||
//! or `{"ok":false,"error":...}`), usage errors included; human progress is
|
||||
//! printed before it.
|
||||
use crate::patch::{
|
||||
ace,
|
||||
dispatch::{Target, android, windows},
|
||||
hoyokprotect, protection, rsa,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use std::{collections::HashMap, path::PathBuf, process::ExitCode};
|
||||
|
||||
const USAGE: &str = "\
|
||||
Honkai Impact 3rd patcher
|
||||
|
||||
Usage:
|
||||
Hi3-Patcher Launch the interactive menu
|
||||
Hi3-Patcher <command> [options]
|
||||
|
||||
Commands:
|
||||
dispatch Redirect dispatch & SDK URLs to a server
|
||||
--platform <windows|android> --input <path>
|
||||
--server <host[:port]> [--https]
|
||||
rsa Replace the passport RSA public key
|
||||
--platform <windows|android> --input <path> [--key <base64>]
|
||||
ace Remove ACE from a Windows client (<= 8.9); requires a capture
|
||||
--input <BH3.exe|folder> [--yes]
|
||||
hoyok Disable HoYoKProtect in a Windows client (9.0+)
|
||||
--input <BH3.exe|folder>
|
||||
protection Remove Android protection and re-sign the APK
|
||||
--input <app.apk> [--variant <ano|tss|auto>]
|
||||
|
||||
Options:
|
||||
--json Emit a single JSON result object as the last stdout line
|
||||
-h, --help, -V, --version
|
||||
|
||||
Windows --input accepts the game folder or its BH3.exe.
|
||||
rsa --key defaults to the PemukulPaku public key when omitted.";
|
||||
|
||||
/// Options each command accepts: `(value options, switches)`. Every command
|
||||
/// also accepts `--json`.
|
||||
fn accepted(command: &str) -> Option<(&'static [&'static str], &'static [&'static str])> {
|
||||
Some(match command {
|
||||
"dispatch" => (&["platform", "input", "server"], &["https"]),
|
||||
"rsa" => (&["platform", "input", "key"], &[]),
|
||||
"ace" => (&["input"], &["yes"]),
|
||||
"hoyok" => (&["input"], &[]),
|
||||
"protection" => (&["input", "variant"], &[]),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
struct Parsed {
|
||||
values: HashMap<String, String>,
|
||||
switches: Vec<String>,
|
||||
}
|
||||
|
||||
/// Split `--name value`, `--name=value` and `--switch` tokens, rejecting
|
||||
/// anything the command does not accept. Errors are usage errors.
|
||||
fn parse(command: &str, rest: &[String]) -> Result<Parsed, String> {
|
||||
let (options, flags) =
|
||||
accepted(command).ok_or_else(|| format!("unknown command: {command}"))?;
|
||||
let mut values = HashMap::new();
|
||||
let mut switches: Vec<String> = Vec::new();
|
||||
let mut it = rest.iter();
|
||||
while let Some(token) = it.next() {
|
||||
let Some(name) = token.strip_prefix("--") else {
|
||||
return Err(format!("unexpected argument: {token}"));
|
||||
};
|
||||
let (name, inline) = match name.split_once('=') {
|
||||
Some((name, value)) => (name, Some(value)),
|
||||
None => (name, None),
|
||||
};
|
||||
if name == "json" || flags.contains(&name) {
|
||||
if inline.is_some() {
|
||||
return Err(format!("--{name} does not take a value"));
|
||||
}
|
||||
if switches.iter().any(|s| s == name) {
|
||||
return Err(format!("--{name} given more than once"));
|
||||
}
|
||||
switches.push(name.to_owned());
|
||||
} else if options.contains(&name) {
|
||||
let value = match inline {
|
||||
Some(value) => value.to_owned(),
|
||||
None => it
|
||||
.next()
|
||||
.filter(|value| !value.starts_with("--"))
|
||||
.ok_or_else(|| format!("--{name} needs a value"))?
|
||||
.clone(),
|
||||
};
|
||||
if values.insert(name.to_owned(), value).is_some() {
|
||||
return Err(format!("--{name} given more than once"));
|
||||
}
|
||||
} else {
|
||||
return Err(format!("{command} does not accept --{name}"));
|
||||
}
|
||||
}
|
||||
Ok(Parsed { values, switches })
|
||||
}
|
||||
|
||||
impl Parsed {
|
||||
fn required(&self, name: &str) -> Result<&str, String> {
|
||||
match self.values.get(name).map(String::as_str) {
|
||||
None | Some("") => Err(format!("--{name} is required")),
|
||||
Some(value) => Ok(value),
|
||||
}
|
||||
}
|
||||
fn has(&self, name: &str) -> bool {
|
||||
self.switches.iter().any(|s| s == name)
|
||||
}
|
||||
}
|
||||
|
||||
enum Platform {
|
||||
Windows,
|
||||
Android,
|
||||
}
|
||||
|
||||
enum Variant {
|
||||
Auto,
|
||||
Ano,
|
||||
Tss,
|
||||
}
|
||||
|
||||
/// A fully validated command, ready to run.
|
||||
enum Job {
|
||||
Dispatch { platform: Platform, input: PathBuf, target: Target },
|
||||
Rsa { platform: Platform, input: PathBuf, key: String },
|
||||
Ace { input: PathBuf, yes: bool },
|
||||
Hoyok { input: PathBuf },
|
||||
Protection { input: PathBuf, variant: Variant },
|
||||
}
|
||||
|
||||
fn parse_platform(args: &Parsed) -> Result<Platform, String> {
|
||||
match args.required("platform")? {
|
||||
"windows" | "win" => Ok(Platform::Windows),
|
||||
"android" => Ok(Platform::Android),
|
||||
other => Err(format!("unknown platform: {other}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve `--input`; a Windows input may be the game folder or its BH3.exe.
|
||||
fn input_path(args: &Parsed, platform: &Platform) -> Result<PathBuf, String> {
|
||||
let path = PathBuf::from(args.required("input")?);
|
||||
Ok(match platform {
|
||||
Platform::Windows if path.is_dir() => path.join("BH3.exe"),
|
||||
_ => path,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check a parsed command line per command. Errors are usage errors.
|
||||
fn job(command: &str, args: &Parsed) -> Result<Job, String> {
|
||||
Ok(match command {
|
||||
"dispatch" => {
|
||||
let platform = parse_platform(args)?;
|
||||
let input = input_path(args, &platform)?;
|
||||
let target = Target::new(args.required("server")?, args.has("https"))
|
||||
.map_err(|error| format!("--server: {error:#}"))?;
|
||||
Job::Dispatch { platform, input, target }
|
||||
}
|
||||
"rsa" => {
|
||||
let platform = parse_platform(args)?;
|
||||
let input = input_path(args, &platform)?;
|
||||
let key = args.values.get("key").cloned().unwrap_or_default();
|
||||
rsa::public_body(&key).map_err(|error| format!("--key: {error:#}"))?;
|
||||
Job::Rsa { platform, input, key }
|
||||
}
|
||||
"ace" => Job::Ace {
|
||||
input: input_path(args, &Platform::Windows)?,
|
||||
yes: args.has("yes"),
|
||||
},
|
||||
"hoyok" => Job::Hoyok {
|
||||
input: input_path(args, &Platform::Windows)?,
|
||||
},
|
||||
"protection" => Job::Protection {
|
||||
input: input_path(args, &Platform::Android)?,
|
||||
variant: match args.values.get("variant").map(String::as_str) {
|
||||
None | Some("auto") => Variant::Auto,
|
||||
Some("ano") => Variant::Ano,
|
||||
Some("tss") => Variant::Tss,
|
||||
Some(other) => return Err(format!("unknown variant: {other}")),
|
||||
},
|
||||
},
|
||||
other => return Err(format!("unknown command: {other}")),
|
||||
})
|
||||
}
|
||||
|
||||
/// Run a validated job. Returns the output path for Android patches.
|
||||
fn execute(job: Job) -> Result<Option<PathBuf>> {
|
||||
Ok(match job {
|
||||
Job::Dispatch { platform: Platform::Windows, input, target } => {
|
||||
windows::patch(&input, &target)?;
|
||||
None
|
||||
}
|
||||
Job::Dispatch { platform: Platform::Android, input, target } => {
|
||||
Some(android::patch(&input, &target)?)
|
||||
}
|
||||
Job::Rsa { platform: Platform::Windows, input, key } => {
|
||||
rsa::patch_windows(&input, &key)?;
|
||||
None
|
||||
}
|
||||
Job::Rsa { platform: Platform::Android, input, key } => {
|
||||
Some(rsa::patch_android(&input, &key)?)
|
||||
}
|
||||
Job::Ace { input, yes } => {
|
||||
ace::patch(&input, |_| Ok(yes))?;
|
||||
None
|
||||
}
|
||||
Job::Hoyok { input } => {
|
||||
hoyokprotect::patch(&input)?;
|
||||
None
|
||||
}
|
||||
Job::Protection { input, variant } => Some(match variant {
|
||||
Variant::Auto => protection::auto(&input)?,
|
||||
Variant::Ano => protection::anosdk(&input)?,
|
||||
Variant::Tss => protection::mobile_ace_tss(&input)?,
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
fn report(command: &str, result: Result<Option<PathBuf>>, json: bool) -> ExitCode {
|
||||
match result {
|
||||
Ok(output) => {
|
||||
let out = output.as_ref().map(|p| p.display().to_string());
|
||||
if json {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::json!({"ok": true, "command": command, "output": out})
|
||||
);
|
||||
} else if let Some(path) = out {
|
||||
println!("Output: {path}");
|
||||
}
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(error) => {
|
||||
let message = format!("{error:#}");
|
||||
if json {
|
||||
println!("{}", serde_json::json!({"ok": false, "error": message}));
|
||||
} else {
|
||||
eprintln!("error: {message}");
|
||||
}
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn usage_error(message: &str, json: bool) -> ExitCode {
|
||||
if json {
|
||||
println!("{}", serde_json::json!({"ok": false, "error": message}));
|
||||
} else {
|
||||
eprintln!("error: {message}\n\n{USAGE}");
|
||||
}
|
||||
ExitCode::from(2)
|
||||
}
|
||||
|
||||
/// Entry point for a non-empty argument list (the command and its options).
|
||||
pub fn run(argv: &[String]) -> ExitCode {
|
||||
let command = argv[0].as_str();
|
||||
let rest = &argv[1..];
|
||||
if matches!(command, "-h" | "--help" | "help")
|
||||
|| rest.iter().any(|a| a == "-h" || a == "--help")
|
||||
{
|
||||
println!("{USAGE}");
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
if matches!(command, "-V" | "--version" | "version") {
|
||||
println!("Hi3-Patcher {}", env!("CARGO_PKG_VERSION"));
|
||||
return ExitCode::SUCCESS;
|
||||
}
|
||||
let json = rest.iter().any(|a| a == "--json");
|
||||
match parse(command, rest).and_then(|args| job(command, &args)) {
|
||||
Ok(job) => report(command, execute(job), json),
|
||||
Err(message) => usage_error(&message, json),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Validate a command line the way `run` does, without executing it.
|
||||
fn check(line: &[&str]) -> Result<Job, String> {
|
||||
let argv: Vec<String> = line.iter().map(|s| s.to_string()).collect();
|
||||
parse(&argv[0], &argv[1..]).and_then(|args| job(&argv[0], &args))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accepts_valid_command_lines() {
|
||||
let cases: &[&[&str]] = &[
|
||||
&["dispatch", "--platform", "windows", "--input", "x", "--server", "1.1.1.1"],
|
||||
&["dispatch", "--platform=android", "--input=a.apk", "--server=h:3000", "--https", "--json"],
|
||||
&["rsa", "--platform", "win", "--input", "x"],
|
||||
&["rsa", "--platform", "windows", "--input", "x", "--key", ""],
|
||||
&["ace", "--input", "x", "--yes"],
|
||||
&["hoyok", "--input", "x"],
|
||||
&["protection", "--input", "a.apk", "--variant", "tss"],
|
||||
];
|
||||
for line in cases {
|
||||
assert!(check(line).is_ok(), "rejected: {line:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_usage_errors() {
|
||||
let cases: &[&[&str]] = &[
|
||||
&["bogus"],
|
||||
&["dispatch", "--platform", "windows", "--input", "x"],
|
||||
&["dispatch", "--platform", "plan9", "--input", "x", "--server", "h"],
|
||||
&["dispatch", "--platform", "windows", "--input", "x", "--server", "h/p"],
|
||||
&["dispatch", "stray"],
|
||||
&["dispatch", "--input"],
|
||||
&["dispatch", "--input", "--json"],
|
||||
&["dispatch", "--platform", "windows", "--input", "x", "--server", "h", "--sever", "h"],
|
||||
&["rsa", "--platform", "windows", "--input", "x", "--key", "abc"],
|
||||
&["ace", "--input", "x", "--https"],
|
||||
&["ace", "--input", "a", "--input", "b"],
|
||||
&["ace", "--input", "x", "--yes=1"],
|
||||
&["hoyok", "--input", ""],
|
||||
&["protection", "--input", "a.apk", "--variant", "x"],
|
||||
];
|
||||
for line in cases {
|
||||
assert!(check(line).is_err(), "accepted: {line:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user