1use std::io::Read;
2use std::path::{Path, PathBuf};
3
4use serde::Deserialize;
5
6use crate::error::LaunchError;
7use crate::launcher::options::LaunchOptions;
8use crate::models::minecraft::{ArtifactInfo, AssetItem, Library, MinecraftVersionJson};
9use crate::net::http::fetch_json;
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.path.join("instances").join(inst).join(&asset.path),
130 None => options.path.join(&asset.path),
131 };
132
133 items.push(AssetItem::Asset {
134 path: full_path.to_string_lossy().into_owned(),
135 sha1: asset.hash,
136 size: asset.size,
137 url: asset.url,
138 });
139 }
140
141 Ok(items)
142}
143
144pub fn natives_base_dir(options: &LaunchOptions, version_json: &MinecraftVersionJson) -> PathBuf {
149 options
150 .path
151 .join("versions")
152 .join(&version_json.id)
153 .join("natives")
154}
155
156pub fn natives_dir_for(options: &LaunchOptions, version_json: &MinecraftVersionJson) -> PathBuf {
168 let base = natives_base_dir(options, version_json);
169 match natives_library_subdir(version_json) {
170 Some(sub) => base.join(sub),
171 None => base,
172 }
173}
174
175fn natives_library_subdir(version_json: &MinecraftVersionJson) -> Option<String> {
181 const PREFIX: &str = "-Djava.library.path=${natives_directory}";
182 let jvm = version_json.arguments.as_ref()?.jvm.as_ref()?;
183 for entry in jvm {
184 let Some(s) = entry.as_str() else { continue };
186 let Some(rest) = s.strip_prefix(PREFIX) else {
187 continue;
188 };
189 let rest = rest.trim_start_matches('/');
190 return if rest.is_empty() {
191 None
192 } else {
193 Some(rest.to_string())
194 };
195 }
196 None
197}
198
199pub async fn extract_natives(
207 options: &LaunchOptions,
208 version_json: &MinecraftVersionJson,
209 bundle: &[AssetItem],
210) -> Result<(), LaunchError> {
211 let native_paths: Vec<PathBuf> = bundle
212 .iter()
213 .filter_map(|item| match item {
214 AssetItem::NativeAsset { path, .. } => Some(PathBuf::from(path)),
215 _ => None,
216 })
217 .collect();
218
219 if native_paths.is_empty() {
220 return Ok(());
221 }
222
223 let natives_dir = natives_dir_for(options, version_json);
224 tokio::fs::create_dir_all(&natives_dir).await?;
225
226 for jar_path in native_paths {
227 let dest = natives_dir.clone();
228 tokio::task::spawn_blocking(move || extract_jar_to_dir(&jar_path, &dest))
229 .await
230 .map_err(|e| LaunchError::Archive(e.to_string()))??;
231 }
232
233 Ok(())
234}
235
236fn arch_suffix_for_natives() -> &'static str {
243 match std::env::consts::ARCH {
244 "x86" => "32",
245 "x86_64" => "64",
246 _ => "",
247 }
248}
249
250fn artifact_to_item(
253 base: &Path,
254 artifact: &ArtifactInfo,
255 lib_name: &str,
256 is_native: bool,
257) -> Option<AssetItem> {
258 let rel = artifact.path.clone().or_else(|| {
259 get_path_libraries(lib_name, None, None)
260 .ok()
261 .map(|lp| lp.path)
262 })?;
263
264 let full_path = base
265 .join("libraries")
266 .join(&rel)
267 .to_string_lossy()
268 .into_owned();
269
270 let sha1 = artifact.sha1.clone().unwrap_or_default();
271 let size = artifact.size.unwrap_or(0);
272 let url = artifact.url.clone();
273
274 if is_native {
275 Some(AssetItem::NativeAsset {
276 path: full_path,
277 sha1,
278 size,
279 url,
280 })
281 } else {
282 Some(AssetItem::Asset {
283 path: full_path,
284 sha1,
285 size,
286 url,
287 })
288 }
289}
290
291fn resolve_regular_library(base: &Path, lib: &Library) -> Option<AssetItem> {
298 let is_native = lib
306 .name
307 .split(':')
308 .nth(3)
309 .map(|c| c.starts_with("natives-"))
310 .unwrap_or(false);
311
312 if let Some(artifact) = lib.downloads.as_ref().and_then(|d| d.artifact.as_ref()) {
314 return artifact_to_item(base, artifact, &lib.name, is_native);
315 }
316
317 if let Some(repo) = &lib.url {
319 if let Ok(lp) = get_path_libraries(&lib.name, None, None) {
320 let url = format!("{}/{}", repo.trim_end_matches('/'), lp.path);
321 return Some(AssetItem::Asset {
322 path: base
323 .join("libraries")
324 .join(&lp.path)
325 .to_string_lossy()
326 .into_owned(),
327 sha1: String::new(),
328 size: 0,
329 url,
330 });
331 }
332 }
333
334 None
335}
336
337fn extract_jar_to_dir(jar_path: &Path, dest: &Path) -> Result<(), LaunchError> {
351 let file = std::fs::File::open(jar_path)?;
352 let mut archive =
353 zip::ZipArchive::new(file).map_err(|e| LaunchError::Archive(e.to_string()))?;
354
355 for i in 0..archive.len() {
356 let mut entry = archive
357 .by_index(i)
358 .map_err(|e| LaunchError::Archive(e.to_string()))?;
359
360 let name = entry.name().to_string();
361
362 if name.starts_with("META-INF") || entry.is_dir() {
365 continue;
366 }
367
368 let file_name = match Path::new(&name).file_name() {
370 Some(f) => f,
371 None => continue,
372 };
373 let out = dest.join(file_name);
374
375 let mut data = Vec::with_capacity(entry.size() as usize);
376 entry.read_to_end(&mut data)?;
377 std::fs::write(&out, &data)?;
378
379 #[cfg(unix)]
380 {
381 use std::os::unix::fs::PermissionsExt;
382 let mut perms = std::fs::metadata(&out)?.permissions();
383 perms.set_mode(0o755);
384 std::fs::set_permissions(&out, perms)?;
385 }
386 }
387
388 Ok(())
389}
390
391#[derive(Deserialize)]
394struct CustomAssetItem {
395 path: String,
396 hash: String,
397 size: u64,
398 url: String,
399}
400
401#[cfg(test)]
404mod tests {
405 use super::*;
406 use std::io::Write;
407 use tempfile::TempDir;
408
409 use crate::launcher::options::{JavaOptions, LoaderConfig, MemoryConfig, ScreenConfig};
410 use crate::models::minecraft::{
411 ArtifactInfo, Authenticator, DownloadArtifact, LibraryDownloads, VersionDownloads,
412 };
413
414 fn opts(path: PathBuf) -> LaunchOptions {
415 LaunchOptions {
416 path,
417 version: "1.20.4".into(),
418 authenticator: Authenticator {
419 access_token: "tok".into(),
420 name: "Player".into(),
421 uuid: "uuid".into(),
422 xbox_account: None,
423 user_properties: None,
424 client_id: None,
425 client_token: None,
426 },
427 timeout_secs: 10,
428 download_concurrency: 5,
429 verify_concurrency: 4,
430 memory: MemoryConfig::default(),
431 java: JavaOptions::default(),
432 loader: LoaderConfig::default(),
433 screen: ScreenConfig::default(),
434 verify: false,
435 game_args: vec![],
436 jvm_args: vec![],
437 instance: None,
438 url: None,
439 mcp: None,
440 intel_enabled_mac: false,
441 bypass_offline: false,
442 skip_bundle_check: false,
443 force_ipv4: false,
444 dns: None,
445 }
446 }
447
448 fn bare_version() -> MinecraftVersionJson {
449 MinecraftVersionJson {
450 id: "1.20.4".into(),
451 version_type: "release".into(),
452 assets: None,
453 asset_index: None,
454 downloads: None,
455 libraries: vec![],
456 arguments: None,
457 minecraft_arguments: None,
458 java_version: None,
459 main_class: None,
460 has_natives: false,
461 }
462 }
463
464 fn artifact(path: &str, url: &str) -> ArtifactInfo {
465 ArtifactInfo {
466 path: Some(path.into()),
467 sha1: Some("aabbcc".into()),
468 size: Some(1024),
469 url: url.into(),
470 }
471 }
472
473 fn lib_with_artifact(name: &str, path: &str, url: &str) -> Library {
474 Library {
475 name: name.into(),
476 rules: None,
477 natives: None,
478 downloads: Some(LibraryDownloads {
479 artifact: Some(artifact(path, url)),
480 classifiers: None,
481 }),
482 url: None,
483 loader: None,
484 }
485 }
486
487 #[test]
490 fn includes_client_jar_when_downloads_present() {
491 let dir = TempDir::new().unwrap();
492 let mut vj = bare_version();
493 vj.downloads = Some(VersionDownloads {
494 client: DownloadArtifact {
495 sha1: "abc".into(),
496 size: 42,
497 url: "https://example.com/client.jar".into(),
498 },
499 server: None,
500 client_mappings: None,
501 server_mappings: None,
502 });
503
504 let items = get_libraries(&opts(dir.path().to_path_buf()), &vj);
505 assert!(items
506 .iter()
507 .any(|i| matches!(i, AssetItem::Asset { path, .. } if path.ends_with("1.20.4.jar"))));
508 }
509
510 #[test]
511 fn includes_version_json_as_cfile() {
512 let dir = TempDir::new().unwrap();
513 let vj = bare_version();
514 let items = get_libraries(&opts(dir.path().to_path_buf()), &vj);
515 assert!(items
516 .iter()
517 .any(|i| matches!(i, AssetItem::CFile { path, .. } if path.ends_with("1.20.4.json"))));
518 }
519
520 #[test]
521 fn regular_library_becomes_asset() {
522 let dir = TempDir::new().unwrap();
523 let mut vj = bare_version();
524 vj.libraries = vec![lib_with_artifact(
525 "com.example:lib:1.0",
526 "com/example/lib/1.0/lib-1.0.jar",
527 "https://example.com/lib.jar",
528 )];
529
530 let items = get_libraries(&opts(dir.path().to_path_buf()), &vj);
531 assert!(items.iter().any(
532 |i| matches!(i, AssetItem::Asset { url, .. } if url == "https://example.com/lib.jar")
533 ));
534 }
535
536 #[test]
537 fn native_library_becomes_native_asset() {
538 let dir = TempDir::new().unwrap();
539 let mut vj = bare_version();
540
541 let current_os = mojang_os();
542 let classifier_key = format!("natives-{current_os}");
543
544 let mut classifiers = std::collections::HashMap::new();
545 classifiers.insert(
546 classifier_key.clone(),
547 artifact(
548 &format!("org/lwjgl/lwjgl/{classifier_key}/lwjgl-native.jar"),
549 "https://example.com/native.jar",
550 ),
551 );
552
553 let mut natives_map = std::collections::HashMap::new();
554 natives_map.insert(current_os.to_string(), classifier_key);
555
556 vj.libraries = vec![Library {
557 name: "org.lwjgl:lwjgl:3.3.1".into(),
558 rules: None,
559 natives: Some(natives_map),
560 downloads: Some(LibraryDownloads {
561 artifact: None,
562 classifiers: Some(classifiers),
563 }),
564 url: None,
565 loader: None,
566 }];
567
568 let items = get_libraries(&opts(dir.path().to_path_buf()), &vj);
569 assert!(items.iter().any(|i| matches!(i, AssetItem::NativeAsset { url, .. } if url == "https://example.com/native.jar")));
570 }
571
572 #[test]
573 fn modern_native_classifier_in_name_becomes_native_asset() {
574 let dir = TempDir::new().unwrap();
576 let mut vj = bare_version();
577
578 let current_os = mojang_os();
579 let classifier = format!("natives-{current_os}");
580 let lib_name = format!("org.lwjgl:lwjgl-glfw:3.3.2:{classifier}");
581 let jar_path = format!("org/lwjgl/lwjgl-glfw/3.3.2/lwjgl-glfw-3.3.2-{classifier}.jar");
582
583 vj.libraries = vec![Library {
584 name: lib_name,
585 rules: None,
586 natives: None,
587 downloads: Some(LibraryDownloads {
588 artifact: Some(artifact(
589 &jar_path,
590 "https://libraries.minecraft.net/native.jar",
591 )),
592 classifiers: None,
593 }),
594 url: None,
595 loader: None,
596 }];
597
598 let items = get_libraries(&opts(dir.path().to_path_buf()), &vj);
599 assert!(
600 items
601 .iter()
602 .any(|i| matches!(i, AssetItem::NativeAsset { .. })),
603 "expected NativeAsset for modern natives-<os> classifier, got: {items:?}"
604 );
605 }
606
607 #[test]
608 fn library_with_url_fallback_builds_url() {
609 let dir = TempDir::new().unwrap();
610 let mut vj = bare_version();
611 vj.libraries = vec![Library {
612 name: "net.fabricmc:fabric-loader:0.15.0".into(),
613 rules: None,
614 natives: None,
615 downloads: None,
616 url: Some("https://maven.fabricmc.net".into()),
617 loader: None,
618 }];
619
620 let items = get_libraries(&opts(dir.path().to_path_buf()), &vj);
621 assert!(items.iter().any(|i| match i {
622 AssetItem::Asset { url, .. } => url.starts_with("https://maven.fabricmc.net"),
623 _ => false,
624 }));
625 }
626
627 #[tokio::test]
630 async fn get_assets_others_none_url_returns_empty() {
631 let dir = TempDir::new().unwrap();
632 let client = reqwest::Client::new();
633 let result = get_assets_others(&opts(dir.path().to_path_buf()), None, &client)
634 .await
635 .unwrap();
636 assert!(result.is_empty());
637 }
638
639 #[tokio::test]
640 async fn get_assets_others_empty_string_returns_empty() {
641 let dir = TempDir::new().unwrap();
642 let client = reqwest::Client::new();
643 let result = get_assets_others(&opts(dir.path().to_path_buf()), Some(""), &client)
644 .await
645 .unwrap();
646 assert!(result.is_empty());
647 }
648
649 #[tokio::test]
652 async fn extract_natives_noop_with_empty_bundle() {
653 let dir = TempDir::new().unwrap();
654 let vj = bare_version();
655 extract_natives(&opts(dir.path().to_path_buf()), &vj, &[])
656 .await
657 .unwrap();
658 assert!(!dir.path().join("versions").exists());
659 }
660
661 #[tokio::test]
662 async fn extract_natives_extracts_to_natives_dir() {
663 let dir = TempDir::new().unwrap();
665 let jar_path = dir.path().join("native.jar");
666
667 {
668 use zip::write::SimpleFileOptions;
669 let mut w = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
670 let opts_zip = SimpleFileOptions::default();
671
672 w.start_file("META-INF/MANIFEST.MF", opts_zip).unwrap();
673 w.write_all(b"Manifest-Version: 1.0\n").unwrap();
674
675 w.start_file("META-INF/linux/x64/org/lwjgl/liblwjgl.so.sha1", opts_zip)
677 .unwrap();
678 w.write_all(b"deadbeef").unwrap();
679
680 w.start_file("liblwjgl.so", opts_zip).unwrap();
682 w.write_all(b"ELF root native").unwrap();
683
684 w.start_file("linux/x64/org/lwjgl/liblwjgl_opengl.so", opts_zip)
687 .unwrap();
688 w.write_all(b"ELF nested native").unwrap();
689
690 let finished = w.finish().unwrap();
691 std::fs::write(&jar_path, finished.get_ref()).unwrap();
692 }
693
694 let vj = bare_version();
695 let options = opts(dir.path().to_path_buf());
696
697 let bundle = vec![AssetItem::NativeAsset {
698 path: jar_path.to_string_lossy().into_owned(),
699 sha1: String::new(),
700 size: 0,
701 url: String::new(),
702 }];
703
704 extract_natives(&options, &vj, &bundle).await.unwrap();
705
706 let natives_dir = dir.path().join("versions").join("1.20.4").join("natives");
707
708 assert_eq!(
710 std::fs::read(natives_dir.join("liblwjgl.so")).unwrap(),
711 b"ELF root native"
712 );
713 assert_eq!(
715 std::fs::read(natives_dir.join("liblwjgl_opengl.so")).unwrap(),
716 b"ELF nested native"
717 );
718 assert!(!natives_dir.join("linux").exists());
720 assert!(!natives_dir.join("META-INF").exists());
721 assert!(!natives_dir.join("liblwjgl.so.sha1").exists());
722 }
723
724 fn version_with_java_subdir(id: &str) -> MinecraftVersionJson {
727 let mut vj = bare_version();
728 vj.id = id.to_string();
729 vj.arguments = Some(crate::models::minecraft::Arguments {
730 game: None,
731 jvm: Some(vec![
732 serde_json::Value::String("--enable-native-access=ALL-UNNAMED".into()),
733 serde_json::Value::String("-Djava.library.path=${natives_directory}/java".into()),
734 serde_json::Value::String(
735 "-Dorg.lwjgl.system.SharedLibraryExtractPath=${natives_directory}/lwjgl".into(),
736 ),
737 ]),
738 });
739 vj
740 }
741
742 #[test]
743 fn natives_subdir_detected_for_modern_scheme() {
744 let vj = version_with_java_subdir("26.2");
745 assert_eq!(natives_library_subdir(&vj), Some("java".to_string()));
746 assert_eq!(natives_library_subdir(&bare_version()), None);
748
749 let options = opts(PathBuf::from("/tmp/mc"));
750 let expected = options
751 .path
752 .join("versions")
753 .join("26.2")
754 .join("natives")
755 .join("java");
756 assert_eq!(natives_dir_for(&options, &vj), expected);
757 }
758
759 #[tokio::test]
760 async fn extract_natives_targets_java_subdir_on_modern_versions() {
761 let dir = TempDir::new().unwrap();
765 let jar_path = dir.path().join("native.jar");
766 {
767 use zip::write::SimpleFileOptions;
768 let mut w = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
769 let o = SimpleFileOptions::default();
770 w.start_file("linux/x64/org/lwjgl/liblwjgl.so", o).unwrap();
771 w.write_all(b"ELF").unwrap();
772 let finished = w.finish().unwrap();
773 std::fs::write(&jar_path, finished.get_ref()).unwrap();
774 }
775
776 let vj = version_with_java_subdir("26.2");
777 let options = opts(dir.path().to_path_buf());
778 let bundle = vec![AssetItem::NativeAsset {
779 path: jar_path.to_string_lossy().into_owned(),
780 sha1: String::new(),
781 size: 0,
782 url: String::new(),
783 }];
784
785 extract_natives(&options, &vj, &bundle).await.unwrap();
786
787 let natives_root = dir.path().join("versions").join("26.2").join("natives");
788 assert!(
789 natives_root.join("java").join("liblwjgl.so").exists(),
790 "liblwjgl.so must be in the java.library.path subdir"
791 );
792 assert!(
793 !natives_root.join("liblwjgl.so").exists(),
794 "must not be left at the natives root for modern versions"
795 );
796 }
797}