Skip to content

Commit 6a61f44

Browse files
committed
fix(app): make instances with non-UTF8 text file encodings launcheable and importable
Previous to these changes, the app always assumed that Minecraft and other launchers always use UTF-8, which is not necessarily always true.
1 parent b66d99c commit 6a61f44

File tree

9 files changed

+131
-76
lines changed

9 files changed

+131
-76
lines changed

Cargo.lock

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ base64 = "0.22.1"
3636
bitflags = "2.9.0"
3737
bytes = "1.10.1"
3838
censor = "0.3.0"
39+
chardetng = "0.1.17"
3940
chrono = "0.4.41"
4041
clap = "4.5.38"
4142
clickhouse = "0.13.2"
@@ -50,6 +51,7 @@ dotenv-build = "0.1.1"
5051
dotenvy = "0.15.7"
5152
dunce = "1.0.5"
5253
either = "1.15.0"
54+
encoding_rs = "0.8.35"
5355
enumset = "1.1.6"
5456
flate2 = "1.1.1"
5557
fs4 = { version = "0.13.1", default-features = false }

packages/app-lib/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ tempfile.workspace = true
2020
dashmap = { workspace = true, features = ["serde"] }
2121
quick-xml = { workspace = true, features = ["async-tokio"] }
2222
enumset.workspace = true
23+
chardetng.workspace = true
24+
encoding_rs.workspace = true
2325

2426
chrono = { workspace = true, features = ["serde"] }
2527
daedalus.workspace = true

packages/app-lib/src/api/pack/import/atlauncher.rs

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,15 @@ pub struct ATLauncherMod {
9797

9898
// Check if folder has a instance.json that parses
9999
pub async fn is_valid_atlauncher(instance_folder: PathBuf) -> bool {
100-
let instance: String =
101-
io::read_to_string(&instance_folder.join("instance.json"))
102-
.await
103-
.unwrap_or("".to_string());
104-
let instance: Result<ATInstance, serde_json::Error> =
105-
serde_json::from_str::<ATInstance>(&instance);
100+
let instance = serde_json::from_str::<ATInstance>(
101+
&io::read_any_encoding_to_string(
102+
&instance_folder.join("instance.json"),
103+
)
104+
.await
105+
.unwrap_or(("".into(), encoding_rs::UTF_8))
106+
.0,
107+
);
108+
106109
if let Err(e) = instance {
107110
tracing::warn!(
108111
"Could not parse instance.json at {}: {}",
@@ -124,14 +127,17 @@ pub async fn import_atlauncher(
124127
) -> crate::Result<()> {
125128
let atlauncher_instance_path = atlauncher_base_path
126129
.join("instances")
127-
.join(instance_folder.clone());
130+
.join(&instance_folder);
128131

129132
// Load instance.json
130-
let atinstance: String =
131-
io::read_to_string(&atlauncher_instance_path.join("instance.json"))
132-
.await?;
133-
let atinstance: ATInstance =
134-
serde_json::from_str::<ATInstance>(&atinstance)?;
133+
let atinstance = serde_json::from_str::<ATInstance>(
134+
&io::read_any_encoding_to_string(
135+
&atlauncher_instance_path.join("instance.json"),
136+
)
137+
.await
138+
.unwrap_or(("".into(), encoding_rs::UTF_8))
139+
.0,
140+
)?;
135141

136142
// Icon path should be {instance_folder}/instance.png if it exists,
137143
// Second possibility is ATLauncher/configs/images/{safe_pack_name}.png (safe pack name is alphanumeric lowercase)

packages/app-lib/src/api/pack/import/curseforge.rs

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,27 +36,31 @@ pub struct InstalledModpack {
3636

3737
// Check if folder has a minecraftinstance.json that parses
3838
pub async fn is_valid_curseforge(instance_folder: PathBuf) -> bool {
39-
let minecraftinstance: String =
40-
io::read_to_string(&instance_folder.join("minecraftinstance.json"))
41-
.await
42-
.unwrap_or("".to_string());
43-
let minecraftinstance: Result<MinecraftInstance, serde_json::Error> =
44-
serde_json::from_str::<MinecraftInstance>(&minecraftinstance);
45-
minecraftinstance.is_ok()
39+
let minecraft_instance = serde_json::from_str::<MinecraftInstance>(
40+
&io::read_any_encoding_to_string(
41+
&instance_folder.join("minecraftinstance.json"),
42+
)
43+
.await
44+
.unwrap_or(("".into(), encoding_rs::UTF_8))
45+
.0,
46+
);
47+
minecraft_instance.is_ok()
4648
}
4749

4850
pub async fn import_curseforge(
4951
curseforge_instance_folder: PathBuf, // instance's folder
5052
profile_path: &str, // path to profile
5153
) -> crate::Result<()> {
5254
// Load minecraftinstance.json
53-
let minecraft_instance: String = io::read_to_string(
54-
&curseforge_instance_folder.join("minecraftinstance.json"),
55-
)
56-
.await?;
57-
let minecraft_instance: MinecraftInstance =
58-
serde_json::from_str::<MinecraftInstance>(&minecraft_instance)?;
59-
let override_title: Option<String> = minecraft_instance.name.clone();
55+
let minecraft_instance = serde_json::from_str::<MinecraftInstance>(
56+
&io::read_any_encoding_to_string(
57+
&curseforge_instance_folder.join("minecraftinstance.json"),
58+
)
59+
.await
60+
.unwrap_or(("".into(), encoding_rs::UTF_8))
61+
.0,
62+
)?;
63+
let override_title = minecraft_instance.name;
6064
let backup_name = format!(
6165
"Curseforge-{}",
6266
curseforge_instance_folder

packages/app-lib/src/api/pack/import/gdlauncher.rs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ pub struct GDLauncherLoader {
2525

2626
// Check if folder has a config.json that parses
2727
pub async fn is_valid_gdlauncher(instance_folder: PathBuf) -> bool {
28-
let config: String =
29-
io::read_to_string(&instance_folder.join("config.json"))
28+
let config = serde_json::from_str::<GDLauncherConfig>(
29+
&io::read_any_encoding_to_string(&instance_folder.join("config.json"))
3030
.await
31-
.unwrap_or("".to_string());
32-
let config: Result<GDLauncherConfig, serde_json::Error> =
33-
serde_json::from_str::<GDLauncherConfig>(&config);
31+
.unwrap_or(("".into(), encoding_rs::UTF_8))
32+
.0,
33+
);
3434
config.is_ok()
3535
}
3636

@@ -39,12 +39,15 @@ pub async fn import_gdlauncher(
3939
profile_path: &str, // path to profile
4040
) -> crate::Result<()> {
4141
// Load config.json
42-
let config: String =
43-
io::read_to_string(&gdlauncher_instance_folder.join("config.json"))
44-
.await?;
45-
let config: GDLauncherConfig =
46-
serde_json::from_str::<GDLauncherConfig>(&config)?;
47-
let override_title: Option<String> = config.loader.source_name.clone();
42+
let config = serde_json::from_str::<GDLauncherConfig>(
43+
&io::read_any_encoding_to_string(
44+
&gdlauncher_instance_folder.join("config.json"),
45+
)
46+
.await
47+
.unwrap_or(("".into(), encoding_rs::UTF_8))
48+
.0,
49+
)?;
50+
let override_title = config.loader.source_name;
4851
let backup_name = format!(
4952
"GDLauncher-{}",
5053
gdlauncher_instance_folder

packages/app-lib/src/api/pack/import/mmc.rs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,8 @@ pub async fn is_valid_mmc(instance_folder: PathBuf) -> bool {
144144
let instance_cfg = instance_folder.join("instance.cfg");
145145
let mmc_pack = instance_folder.join("mmc-pack.json");
146146

147-
let mmc_pack = match io::read_to_string(&mmc_pack).await {
148-
Ok(mmc_pack) => mmc_pack,
147+
let mmc_pack = match io::read_any_encoding_to_string(&mmc_pack).await {
148+
Ok((mmc_pack, _)) => mmc_pack,
149149
Err(_) => return false,
150150
};
151151

@@ -155,7 +155,7 @@ pub async fn is_valid_mmc(instance_folder: PathBuf) -> bool {
155155

156156
#[tracing::instrument]
157157
pub async fn get_instances_subpath(config: PathBuf) -> Option<String> {
158-
let launcher = io::read_to_string(&config).await.ok()?;
158+
let launcher = io::read_any_encoding_to_string(&config).await.ok()?.0;
159159
let launcher: MMCLauncherEnum = serde_ini::from_str(&launcher).ok()?;
160160
match launcher {
161161
MMCLauncherEnum::General(p) => Some(p.general.instance_dir),
@@ -165,10 +165,9 @@ pub async fn get_instances_subpath(config: PathBuf) -> Option<String> {
165165

166166
// Loading the INI (instance.cfg) file
167167
async fn load_instance_cfg(file_path: &Path) -> crate::Result<MMCInstance> {
168-
let instance_cfg: String = io::read_to_string(file_path).await?;
169-
let instance_cfg_enum: MMCInstanceEnum =
170-
serde_ini::from_str::<MMCInstanceEnum>(&instance_cfg)?;
171-
match instance_cfg_enum {
168+
match serde_ini::from_str::<MMCInstanceEnum>(
169+
&io::read_any_encoding_to_string(file_path).await?.0,
170+
)? {
172171
MMCInstanceEnum::General(instance_cfg) => Ok(instance_cfg.general),
173172
MMCInstanceEnum::Instance(instance_cfg) => Ok(instance_cfg),
174173
}
@@ -183,9 +182,13 @@ pub async fn import_mmc(
183182
let mmc_instance_path =
184183
mmc_base_path.join("instances").join(instance_folder);
185184

186-
let mmc_pack =
187-
io::read_to_string(&mmc_instance_path.join("mmc-pack.json")).await?;
188-
let mmc_pack: MMCPack = serde_json::from_str::<MMCPack>(&mmc_pack)?;
185+
let mmc_pack = serde_json::from_str::<MMCPack>(
186+
&io::read_any_encoding_to_string(
187+
&mmc_instance_path.join("mmc-pack.json"),
188+
)
189+
.await?
190+
.0,
191+
)?;
189192

190193
let instance_cfg =
191194
load_instance_cfg(&mmc_instance_path.join("instance.cfg")).await?;
@@ -243,7 +246,7 @@ pub async fn import_mmc(
243246
_ => return Err(crate::ErrorKind::InputError("Instance is managed, but managed pack type not specified in instance.cfg".to_string()).into())
244247
}
245248
} else {
246-
// Direclty import unmanaged pack
249+
// Directly import unmanaged pack
247250
import_mmc_unmanaged(
248251
profile_path,
249252
minecraft_folder,

packages/app-lib/src/launcher/mod.rs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use chrono::Utc;
1414
use daedalus as d;
1515
use daedalus::minecraft::{LoggingSide, RuleAction, VersionInfo};
1616
use daedalus::modded::LoaderVersion;
17+
use regex::Regex;
1718
use serde::Deserialize;
1819
use st::Profile;
1920
use std::collections::HashMap;
@@ -662,14 +663,29 @@ pub async fn launch_minecraft(
662663

663664
// Overwrites the minecraft options.txt file with the settings from the profile
664665
// Uses 'a:b' syntax which is not quite yaml
665-
use regex::Regex;
666-
667666
if !mc_set_options.is_empty() {
668667
let options_path = instance_path.join("options.txt");
669-
let mut options_string = String::new();
670-
if options_path.exists() {
671-
options_string = io::read_to_string(&options_path).await?;
668+
669+
let (mut options_string, input_encoding) = if options_path.exists() {
670+
io::read_any_encoding_to_string(&options_path).await?
671+
} else {
672+
(String::new(), encoding_rs::UTF_8)
673+
};
674+
675+
// UTF-16 encodings may be successfully detected and read, but we cannot encode
676+
// them back, and it's technically possible that the game client strongly expects
677+
// such encoding
678+
if input_encoding != input_encoding.output_encoding() {
679+
return Err(crate::ErrorKind::LauncherError(format!(
680+
"The instance options.txt file uses an unsupported encoding: {}. \
681+
Please either turn off instance options that need to modify this file, \
682+
or convert the file to an encoding that both the game and this app support, \
683+
such as UTF-8.",
684+
input_encoding.name()
685+
))
686+
.into());
672687
}
688+
673689
for (key, value) in mc_set_options {
674690
let re = Regex::new(&format!(r"(?m)^{}:.*$", regex::escape(key)))?;
675691
// check if the regex exists in the file
@@ -684,7 +700,8 @@ pub async fn launch_minecraft(
684700
}
685701
}
686702

687-
io::write(&options_path, options_string).await?;
703+
io::write(&options_path, input_encoding.encode(&options_string).0)
704+
.await?;
688705
}
689706

690707
crate::api::profile::edit(&profile.path, |prof| {

0 commit comments

Comments
 (0)