1use std::io::Read;
2use std::path::{Path, PathBuf};
3
4use serde::Deserialize;
5
6use crate::error::LaunchError;
7use crate::net::http::fetch_json;
8use crate::launcher::options::LaunchOptions;
9use crate::models::minecraft::{ArtifactInfo, AssetItem, Library, MinecraftVersionJson};
10use crate::utils::paths::get_path_libraries;
11use crate::utils::platform::{mojang_os, skip_library};
12
13pub fn get_libraries(
28 options: &LaunchOptions,
29 version_json: &MinecraftVersionJson,
30) -> Vec<AssetItem> {
31 let base = &options.path;
32 let current_os = mojang_os();
33 let arch_suffix = arch_suffix_for_natives();
34
35 let mut items: Vec<AssetItem> = Vec::new();
36
37 for lib in &version_json.libraries {
38 if let Some(natives_map) = &lib.natives {
39 let native_key = match natives_map.get(current_os) {
41 Some(k) => k.replace("${arch}", arch_suffix),
42 None => continue,
43 };
44
45 let artifact = lib
46 .downloads
47 .as_ref()
48 .and_then(|d| d.classifiers.as_ref())
49 .and_then(|c| c.get(&native_key));
50
51 if let Some(artifact) = artifact {
52 if let Some(item) = artifact_to_item(base, artifact, &lib.name, true) {
53 items.push(item);
54 }
55 }
56 } else {
57 if skip_library(lib.rules.as_deref().unwrap_or(&[])) {
59 continue;
60 }
61
62 if let Some(item) = resolve_regular_library(base, lib) {
63 items.push(item);
64 }
65 }
66 }
67
68 if let Some(dl) = &version_json.downloads {
70 items.push(AssetItem::Asset {
71 path: base
72 .join("versions")
73 .join(&version_json.id)
74 .join(format!("{}.jar", version_json.id))
75 .to_string_lossy()
76 .into_owned(),
77 sha1: dl.client.sha1.clone(),
78 size: dl.client.size,
79 url: dl.client.url.clone(),
80 });
81 }
82
83 if let Ok(content) = serde_json::to_string(version_json) {
85 items.push(AssetItem::CFile {
86 path: base
87 .join("versions")
88 .join(&version_json.id)
89 .join(format!("{}.json", version_json.id))
90 .to_string_lossy()
91 .into_owned(),
92 content,
93 });
94 }
95
96 items
97}
98
99pub async fn get_assets_others(
108 options: &LaunchOptions,
109 url: Option<&str>,
110 client: &reqwest::Client,
111) -> Result<Vec<AssetItem>, LaunchError> {
112 let url = match url {
113 Some(u) if !u.is_empty() => u,
114 _ => return Ok(vec![]),
115 };
116
117 let raw: Vec<CustomAssetItem> = fetch_json(client, url)
118 .await
119 .map_err(LaunchError::InvalidData)?;
120
121 let mut items = Vec::with_capacity(raw.len());
122
123 for asset in raw {
124 if asset.path.is_empty() {
125 continue;
126 }
127
128 let full_path = match &options.instance {
129 Some(inst) => options
130 .path
131 .join("instances")
132 .join(inst)
133 .join(&asset.path),
134 None => options.path.join(&asset.path),
135 };
136
137 items.push(AssetItem::Asset {
138 path: full_path.to_string_lossy().into_owned(),
139 sha1: asset.hash,
140 size: asset.size,
141 url: asset.url,
142 });
143 }
144
145 Ok(items)
146}
147
148pub async fn extract_natives(
155 options: &LaunchOptions,
156 version_json: &MinecraftVersionJson,
157 bundle: &[AssetItem],
158) -> Result<(), LaunchError> {
159 let native_paths: Vec<PathBuf> = bundle
160 .iter()
161 .filter_map(|item| match item {
162 AssetItem::NativeAsset { path, .. } => Some(PathBuf::from(path)),
163 _ => None,
164 })
165 .collect();
166
167 if native_paths.is_empty() {
168 return Ok(());
169 }
170
171 let natives_dir = options
172 .path
173 .join("versions")
174 .join(&version_json.id)
175 .join("natives");
176 tokio::fs::create_dir_all(&natives_dir).await?;
177
178 for jar_path in native_paths {
179 let dest = natives_dir.clone();
180 tokio::task::spawn_blocking(move || extract_jar_to_dir(&jar_path, &dest))
181 .await
182 .map_err(|e| LaunchError::Archive(e.to_string()))??;
183 }
184
185 Ok(())
186}
187
188fn arch_suffix_for_natives() -> &'static str {
195 match std::env::consts::ARCH {
196 "x86" => "32",
197 "x86_64" => "64",
198 _ => "",
199 }
200}
201
202fn artifact_to_item(
205 base: &Path,
206 artifact: &ArtifactInfo,
207 lib_name: &str,
208 is_native: bool,
209) -> Option<AssetItem> {
210 let rel = artifact.path.clone().or_else(|| {
211 get_path_libraries(lib_name, None, None)
212 .ok()
213 .map(|lp| lp.path)
214 })?;
215
216 let full_path = base
217 .join("libraries")
218 .join(&rel)
219 .to_string_lossy()
220 .into_owned();
221
222 let sha1 = artifact.sha1.clone().unwrap_or_default();
223 let size = artifact.size.unwrap_or(0);
224 let url = artifact.url.clone();
225
226 if is_native {
227 Some(AssetItem::NativeAsset { path: full_path, sha1, size, url })
228 } else {
229 Some(AssetItem::Asset { path: full_path, sha1, size, url })
230 }
231}
232
233fn resolve_regular_library(base: &Path, lib: &Library) -> Option<AssetItem> {
240 let is_native = lib.name.split(':').nth(3)
248 .map(|c| c.starts_with("natives-"))
249 .unwrap_or(false);
250
251 if let Some(artifact) = lib.downloads.as_ref().and_then(|d| d.artifact.as_ref()) {
253 return artifact_to_item(base, artifact, &lib.name, is_native);
254 }
255
256 if let Some(repo) = &lib.url {
258 if let Ok(lp) = get_path_libraries(&lib.name, None, None) {
259 let url = format!("{}/{}", repo.trim_end_matches('/'), lp.path);
260 return Some(AssetItem::Asset {
261 path: base
262 .join("libraries")
263 .join(&lp.path)
264 .to_string_lossy()
265 .into_owned(),
266 sha1: String::new(),
267 size: 0,
268 url,
269 });
270 }
271 }
272
273 None
274}
275
276fn extract_jar_to_dir(jar_path: &Path, dest: &Path) -> Result<(), LaunchError> {
280 let file = std::fs::File::open(jar_path)?;
281 let mut archive =
282 zip::ZipArchive::new(file).map_err(|e| LaunchError::Archive(e.to_string()))?;
283
284 for i in 0..archive.len() {
285 let mut entry = archive
286 .by_index(i)
287 .map_err(|e| LaunchError::Archive(e.to_string()))?;
288
289 let name = entry.name().to_string();
290
291 if name.starts_with("META-INF") {
292 continue;
293 }
294
295 let out = dest.join(&name);
296
297 if entry.is_dir() {
298 std::fs::create_dir_all(&out)?;
299 } else {
300 if let Some(parent) = out.parent() {
301 std::fs::create_dir_all(parent)?;
302 }
303 let mut data = Vec::with_capacity(entry.size() as usize);
304 entry.read_to_end(&mut data)?;
305 std::fs::write(&out, &data)?;
306
307 #[cfg(unix)]
308 {
309 use std::os::unix::fs::PermissionsExt;
310 let mut perms = std::fs::metadata(&out)?.permissions();
311 perms.set_mode(0o755);
312 std::fs::set_permissions(&out, perms)?;
313 }
314 }
315 }
316
317 Ok(())
318}
319
320#[derive(Deserialize)]
323struct CustomAssetItem {
324 path: String,
325 hash: String,
326 size: u64,
327 url: String,
328}
329
330#[cfg(test)]
333mod tests {
334 use super::*;
335 use std::io::Write;
336 use tempfile::TempDir;
337
338 use crate::launcher::options::{JavaOptions, LoaderConfig, MemoryConfig, ScreenConfig};
339 use crate::models::minecraft::{
340 ArtifactInfo, Authenticator, DownloadArtifact, LibraryDownloads, VersionDownloads,
341 };
342
343 fn opts(path: PathBuf) -> LaunchOptions {
344 LaunchOptions {
345 path,
346 version: "1.20.4".into(),
347 authenticator: Authenticator {
348 access_token: "tok".into(),
349 name: "Player".into(),
350 uuid: "uuid".into(),
351 xbox_account: None,
352 user_properties: None,
353 client_id: None,
354 client_token: None,
355 },
356 timeout_secs: 10,
357 download_concurrency: 5,
358 verify_concurrency: 4,
359 memory: MemoryConfig::default(),
360 java: JavaOptions::default(),
361 loader: LoaderConfig::default(),
362 screen: ScreenConfig::default(),
363 verify: false,
364 game_args: vec![],
365 jvm_args: vec![],
366 instance: None,
367 url: None,
368 mcp: None,
369 intel_enabled_mac: false,
370 bypass_offline: false,
371 skip_bundle_check: false,
372 }
373 }
374
375 fn bare_version() -> MinecraftVersionJson {
376 MinecraftVersionJson {
377 id: "1.20.4".into(),
378 version_type: "release".into(),
379 assets: None,
380 asset_index: None,
381 downloads: None,
382 libraries: vec![],
383 arguments: None,
384 minecraft_arguments: None,
385 java_version: None,
386 main_class: None,
387 has_natives: false,
388 }
389 }
390
391 fn artifact(path: &str, url: &str) -> ArtifactInfo {
392 ArtifactInfo {
393 path: Some(path.into()),
394 sha1: Some("aabbcc".into()),
395 size: Some(1024),
396 url: url.into(),
397 }
398 }
399
400 fn lib_with_artifact(name: &str, path: &str, url: &str) -> Library {
401 Library {
402 name: name.into(),
403 rules: None,
404 natives: None,
405 downloads: Some(LibraryDownloads {
406 artifact: Some(artifact(path, url)),
407 classifiers: None,
408 }),
409 url: None,
410 loader: None,
411 }
412 }
413
414 #[test]
417 fn includes_client_jar_when_downloads_present() {
418 let dir = TempDir::new().unwrap();
419 let mut vj = bare_version();
420 vj.downloads = Some(VersionDownloads {
421 client: DownloadArtifact {
422 sha1: "abc".into(),
423 size: 42,
424 url: "https://example.com/client.jar".into(),
425 },
426 server: None,
427 client_mappings: None,
428 server_mappings: None,
429 });
430
431 let items = get_libraries(&opts(dir.path().to_path_buf()), &vj);
432 assert!(items.iter().any(|i| matches!(i, AssetItem::Asset { path, .. } if path.ends_with("1.20.4.jar"))));
433 }
434
435 #[test]
436 fn includes_version_json_as_cfile() {
437 let dir = TempDir::new().unwrap();
438 let vj = bare_version();
439 let items = get_libraries(&opts(dir.path().to_path_buf()), &vj);
440 assert!(items.iter().any(|i| matches!(i, AssetItem::CFile { path, .. } if path.ends_with("1.20.4.json"))));
441 }
442
443 #[test]
444 fn regular_library_becomes_asset() {
445 let dir = TempDir::new().unwrap();
446 let mut vj = bare_version();
447 vj.libraries = vec![lib_with_artifact(
448 "com.example:lib:1.0",
449 "com/example/lib/1.0/lib-1.0.jar",
450 "https://example.com/lib.jar",
451 )];
452
453 let items = get_libraries(&opts(dir.path().to_path_buf()), &vj);
454 assert!(items.iter().any(|i| matches!(i, AssetItem::Asset { url, .. } if url == "https://example.com/lib.jar")));
455 }
456
457 #[test]
458 fn native_library_becomes_native_asset() {
459 let dir = TempDir::new().unwrap();
460 let mut vj = bare_version();
461
462 let current_os = mojang_os();
463 let classifier_key = format!("natives-{current_os}");
464
465 let mut classifiers = std::collections::HashMap::new();
466 classifiers.insert(
467 classifier_key.clone(),
468 artifact(
469 &format!("org/lwjgl/lwjgl/{classifier_key}/lwjgl-native.jar"),
470 "https://example.com/native.jar",
471 ),
472 );
473
474 let mut natives_map = std::collections::HashMap::new();
475 natives_map.insert(current_os.to_string(), classifier_key);
476
477 vj.libraries = vec![Library {
478 name: "org.lwjgl:lwjgl:3.3.1".into(),
479 rules: None,
480 natives: Some(natives_map),
481 downloads: Some(LibraryDownloads {
482 artifact: None,
483 classifiers: Some(classifiers),
484 }),
485 url: None,
486 loader: None,
487 }];
488
489 let items = get_libraries(&opts(dir.path().to_path_buf()), &vj);
490 assert!(items.iter().any(|i| matches!(i, AssetItem::NativeAsset { url, .. } if url == "https://example.com/native.jar")));
491 }
492
493 #[test]
494 fn modern_native_classifier_in_name_becomes_native_asset() {
495 let dir = TempDir::new().unwrap();
497 let mut vj = bare_version();
498
499 let current_os = mojang_os();
500 let classifier = format!("natives-{current_os}");
501 let lib_name = format!("org.lwjgl:lwjgl-glfw:3.3.2:{classifier}");
502 let jar_path = format!("org/lwjgl/lwjgl-glfw/3.3.2/lwjgl-glfw-3.3.2-{classifier}.jar");
503
504 vj.libraries = vec![Library {
505 name: lib_name,
506 rules: None,
507 natives: None,
508 downloads: Some(LibraryDownloads {
509 artifact: Some(artifact(&jar_path, "https://libraries.minecraft.net/native.jar")),
510 classifiers: None,
511 }),
512 url: None,
513 loader: None,
514 }];
515
516 let items = get_libraries(&opts(dir.path().to_path_buf()), &vj);
517 assert!(
518 items.iter().any(|i| matches!(i, AssetItem::NativeAsset { .. })),
519 "expected NativeAsset for modern natives-<os> classifier, got: {items:?}"
520 );
521 }
522
523 #[test]
524 fn library_with_url_fallback_builds_url() {
525 let dir = TempDir::new().unwrap();
526 let mut vj = bare_version();
527 vj.libraries = vec![Library {
528 name: "net.fabricmc:fabric-loader:0.15.0".into(),
529 rules: None,
530 natives: None,
531 downloads: None,
532 url: Some("https://maven.fabricmc.net".into()),
533 loader: None,
534 }];
535
536 let items = get_libraries(&opts(dir.path().to_path_buf()), &vj);
537 assert!(items.iter().any(|i| match i {
538 AssetItem::Asset { url, .. } => url.starts_with("https://maven.fabricmc.net"),
539 _ => false,
540 }));
541 }
542
543 #[tokio::test]
546 async fn get_assets_others_none_url_returns_empty() {
547 let dir = TempDir::new().unwrap();
548 let client = reqwest::Client::new();
549 let result = get_assets_others(&opts(dir.path().to_path_buf()), None, &client)
550 .await
551 .unwrap();
552 assert!(result.is_empty());
553 }
554
555 #[tokio::test]
556 async fn get_assets_others_empty_string_returns_empty() {
557 let dir = TempDir::new().unwrap();
558 let client = reqwest::Client::new();
559 let result = get_assets_others(&opts(dir.path().to_path_buf()), Some(""), &client)
560 .await
561 .unwrap();
562 assert!(result.is_empty());
563 }
564
565 #[tokio::test]
568 async fn extract_natives_noop_with_empty_bundle() {
569 let dir = TempDir::new().unwrap();
570 let vj = bare_version();
571 extract_natives(&opts(dir.path().to_path_buf()), &vj, &[])
572 .await
573 .unwrap();
574 assert!(!dir.path().join("versions").exists());
575 }
576
577 #[tokio::test]
578 async fn extract_natives_extracts_to_natives_dir() {
579 let dir = TempDir::new().unwrap();
581 let jar_path = dir.path().join("native.jar");
582
583 {
584 use zip::write::SimpleFileOptions;
585 let mut w = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
586 let opts_zip = SimpleFileOptions::default();
587
588 w.start_file("META-INF/MANIFEST.MF", opts_zip).unwrap();
589 w.write_all(b"Manifest-Version: 1.0\n").unwrap();
590
591 w.start_file("liblwjgl.so", opts_zip).unwrap();
592 w.write_all(b"ELF native library").unwrap();
593
594 let finished = w.finish().unwrap();
595 std::fs::write(&jar_path, finished.get_ref()).unwrap();
596 }
597
598 let vj = bare_version();
599 let options = opts(dir.path().to_path_buf());
600
601 let bundle = vec![AssetItem::NativeAsset {
602 path: jar_path.to_string_lossy().into_owned(),
603 sha1: String::new(),
604 size: 0,
605 url: String::new(),
606 }];
607
608 extract_natives(&options, &vj, &bundle).await.unwrap();
609
610 let natives_dir = dir.path().join("versions").join("1.20.4").join("natives");
611 assert!(natives_dir.join("liblwjgl.so").exists());
612 assert!(!natives_dir.join("META-INF").exists());
613
614 let content = std::fs::read(natives_dir.join("liblwjgl.so")).unwrap();
615 assert_eq!(content, b"ELF native library");
616 }
617}