Skip to main content

minecraft_java_rs_core/game/
version.rs

1use crate::error::LaunchError;
2use crate::launcher::options::LaunchOptions;
3use crate::models::minecraft::{MinecraftVersionJson, MojangVersionManifest};
4use crate::net::http::fetch_json;
5
6const MANIFEST_URL: &str = "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json";
7
8/// Fetch and return the `MinecraftVersionJson` for the version requested in
9/// `options`.
10///
11/// Steps:
12/// 1. Download the Mojang version manifest.
13/// 2. Resolve version aliases (`latest_release` / `r` / `lr`, etc.).
14/// 3. Locate the version entry and download its per-version JSON.
15/// 4. On Linux ARM, patch LWJGL/JInput libraries via `lwjgl_native::process_json`.
16pub async fn get_version_json(
17    options: &LaunchOptions,
18    client: &reqwest::Client,
19) -> Result<MinecraftVersionJson, LaunchError> {
20    fetch_version_json(MANIFEST_URL, options, client).await
21}
22
23/// Inner implementation — accepts an explicit manifest base URL so that tests
24/// can point at a local mock server instead of the real Mojang CDN.
25pub(crate) async fn fetch_version_json(
26    manifest_base: &str,
27    options: &LaunchOptions,
28    client: &reqwest::Client,
29) -> Result<MinecraftVersionJson, LaunchError> {
30    // Cache-buster keeps CDN proxies from returning stale manifests.
31    let ts = std::time::SystemTime::now()
32        .duration_since(std::time::UNIX_EPOCH)
33        .map(|d| d.as_millis())
34        .unwrap_or(0);
35    let manifest_url = format!("{manifest_base}?_t={ts}");
36
37    let manifest: MojangVersionManifest = fetch_json(client, &manifest_url)
38        .await
39        .map_err(LaunchError::InvalidData)?;
40
41    let version = resolve_alias(&options.version, &manifest);
42
43    let entry = manifest
44        .versions
45        .iter()
46        .find(|v| v.id == version)
47        .ok_or_else(|| LaunchError::VersionNotFound(version.clone()))?;
48
49    let mut version_json: MinecraftVersionJson = fetch_json(client, &entry.url)
50        .await
51        .map_err(LaunchError::InvalidData)?;
52
53    if is_linux_arm() {
54        crate::game::lwjgl_native::process_json(&mut version_json)?;
55    }
56
57    Ok(version_json)
58}
59
60/// Map symbolic aliases to the concrete version ID from the manifest.
61fn resolve_alias(version: &str, manifest: &MojangVersionManifest) -> String {
62    match version {
63        "latest_release" | "r" | "lr" => manifest.latest.release.clone(),
64        "latest_snapshot" | "s" | "ls" => manifest.latest.snapshot.clone(),
65        other => other.to_string(),
66    }
67}
68
69/// Runtime check: are we on Linux ARM?
70///
71/// Uses `std::env::consts` so that a cross-compiled binary running on ARM
72/// still detects its actual execution environment.
73fn is_linux_arm() -> bool {
74    std::env::consts::OS == "linux" && matches!(std::env::consts::ARCH, "aarch64" | "arm")
75}
76
77// ── Tests ─────────────────────────────────────────────────────────────────────
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82    use crate::models::minecraft::{LatestVersions, MojangVersionManifest, VersionEntry};
83
84    fn manifest(release: &str, snapshot: &str) -> MojangVersionManifest {
85        MojangVersionManifest {
86            latest: LatestVersions {
87                release: release.to_string(),
88                snapshot: snapshot.to_string(),
89            },
90            versions: vec![make_entry("1.20.4"), make_entry("24w14a")],
91        }
92    }
93
94    fn make_entry(id: &str) -> VersionEntry {
95        VersionEntry {
96            id: id.to_string(),
97            version_type: "release".to_string(),
98            url: format!("https://example.com/{id}.json"),
99            time: String::new(),
100            release_time: String::new(),
101        }
102    }
103
104    #[test]
105    fn alias_latest_release() {
106        let m = manifest("1.20.4", "24w14a");
107        assert_eq!(resolve_alias("latest_release", &m), "1.20.4");
108        assert_eq!(resolve_alias("r", &m), "1.20.4");
109        assert_eq!(resolve_alias("lr", &m), "1.20.4");
110    }
111
112    #[test]
113    fn alias_latest_snapshot() {
114        let m = manifest("1.20.4", "24w14a");
115        assert_eq!(resolve_alias("latest_snapshot", &m), "24w14a");
116        assert_eq!(resolve_alias("s", &m), "24w14a");
117        assert_eq!(resolve_alias("ls", &m), "24w14a");
118    }
119
120    #[test]
121    fn concrete_version_passes_through() {
122        let m = manifest("1.20.4", "24w14a");
123        assert_eq!(resolve_alias("1.19.4", &m), "1.19.4");
124    }
125
126    // ── Mock HTTP tests (wiremock) ────────────────────────────────────────────
127
128    /// Minimal manifest JSON template; `VERSION_URL` is replaced at test time
129    /// with the actual mock server URL.
130    const MOCK_MANIFEST_TEMPLATE: &str = r#"{
131        "latest": { "release": "1.20.4", "snapshot": "1.20.4" },
132        "versions": [
133            {
134                "id": "1.20.4",
135                "type": "release",
136                "url": "VERSION_URL",
137                "time": "2024-01-22T10:00:00+00:00",
138                "releaseTime": "2024-01-22T10:00:00+00:00"
139            }
140        ]
141    }"#;
142
143    const MOCK_VERSION_JSON: &str = r#"{
144        "id": "1.20.4",
145        "type": "release",
146        "assets": "16",
147        "libraries": [],
148        "mainClass": "net.minecraft.client.main.Main",
149        "assetIndex": {
150            "id": "16",
151            "sha1": "abc123",
152            "size": 100,
153            "url": "https://resources.example.com/16.json"
154        },
155        "downloads": {
156            "client": {
157                "sha1": "def456",
158                "size": 1000,
159                "url": "https://resources.example.com/client.jar"
160            }
161        }
162    }"#;
163
164    fn mock_options(version: &str) -> LaunchOptions {
165        use crate::launcher::options::{JavaOptions, LoaderConfig, MemoryConfig, ScreenConfig};
166        use crate::models::minecraft::Authenticator;
167        LaunchOptions {
168            path: std::path::PathBuf::from("/tmp/mc-test"),
169            version: version.to_string(),
170            authenticator: Authenticator {
171                access_token: String::new(),
172                name: "TestUser".into(),
173                uuid: "test-uuid".into(),
174                xbox_account: None,
175                user_properties: None,
176                client_id: None,
177                client_token: None,
178            },
179            timeout_secs: 5,
180            download_concurrency: 1,
181            verify_concurrency: 4,
182            memory: MemoryConfig::default(),
183            java: JavaOptions::default(),
184            loader: LoaderConfig::default(),
185            screen: ScreenConfig::default(),
186            verify: false,
187            game_args: vec![],
188            jvm_args: vec![],
189            instance: None,
190            url: None,
191            mcp: None,
192            intel_enabled_mac: false,
193            bypass_offline: false,
194            skip_bundle_check: false,
195            force_ipv4: false,
196        }
197    }
198
199    #[tokio::test]
200    async fn fetch_version_json_from_mock_server_concrete_version() {
201        use wiremock::matchers::{method, path};
202        use wiremock::{Mock, MockServer, ResponseTemplate};
203
204        let server = MockServer::start().await;
205
206        // Register manifest mock.
207        let version_url = format!("{}/versions/1.20.4.json", server.uri());
208        let manifest_body = MOCK_MANIFEST_TEMPLATE.replace("VERSION_URL", &version_url);
209        Mock::given(method("GET"))
210            .and(path("/manifest.json"))
211            .respond_with(
212                ResponseTemplate::new(200)
213                    .insert_header("content-type", "application/json")
214                    .set_body_string(manifest_body),
215            )
216            .mount(&server)
217            .await;
218
219        // Register version JSON mock.
220        Mock::given(method("GET"))
221            .and(path("/versions/1.20.4.json"))
222            .respond_with(
223                ResponseTemplate::new(200)
224                    .insert_header("content-type", "application/json")
225                    .set_body_string(MOCK_VERSION_JSON),
226            )
227            .mount(&server)
228            .await;
229
230        let manifest_url = format!("{}/manifest.json", server.uri());
231        let client = reqwest::Client::new();
232        let options = mock_options("1.20.4");
233
234        let result = fetch_version_json(&manifest_url, &options, &client)
235            .await
236            .unwrap();
237
238        assert_eq!(result.id, "1.20.4");
239        assert_eq!(result.version_type, "release");
240        assert_eq!(
241            result.main_class.as_deref(),
242            Some("net.minecraft.client.main.Main")
243        );
244        assert!(result.libraries.is_empty());
245    }
246
247    #[tokio::test]
248    async fn fetch_version_json_resolves_latest_release_alias() {
249        use wiremock::matchers::{method, path};
250        use wiremock::{Mock, MockServer, ResponseTemplate};
251
252        let server = MockServer::start().await;
253
254        let version_url = format!("{}/versions/1.20.4.json", server.uri());
255        let manifest_body = MOCK_MANIFEST_TEMPLATE.replace("VERSION_URL", &version_url);
256        Mock::given(method("GET"))
257            .and(path("/manifest.json"))
258            .respond_with(
259                ResponseTemplate::new(200)
260                    .insert_header("content-type", "application/json")
261                    .set_body_string(manifest_body),
262            )
263            .mount(&server)
264            .await;
265        Mock::given(method("GET"))
266            .and(path("/versions/1.20.4.json"))
267            .respond_with(
268                ResponseTemplate::new(200)
269                    .insert_header("content-type", "application/json")
270                    .set_body_string(MOCK_VERSION_JSON),
271            )
272            .mount(&server)
273            .await;
274
275        let manifest_url = format!("{}/manifest.json", server.uri());
276        let client = reqwest::Client::new();
277        // "latest_release" should resolve to "1.20.4" via the mock manifest.
278        let options = mock_options("latest_release");
279
280        let result = fetch_version_json(&manifest_url, &options, &client)
281            .await
282            .unwrap();
283
284        assert_eq!(result.id, "1.20.4");
285    }
286
287    #[tokio::test]
288    async fn fetch_version_json_returns_error_for_missing_version() {
289        use wiremock::matchers::{method, path};
290        use wiremock::{Mock, MockServer, ResponseTemplate};
291
292        let server = MockServer::start().await;
293
294        let version_url = format!("{}/versions/1.20.4.json", server.uri());
295        let manifest_body = MOCK_MANIFEST_TEMPLATE.replace("VERSION_URL", &version_url);
296        Mock::given(method("GET"))
297            .and(path("/manifest.json"))
298            .respond_with(
299                ResponseTemplate::new(200)
300                    .insert_header("content-type", "application/json")
301                    .set_body_string(manifest_body),
302            )
303            .mount(&server)
304            .await;
305
306        let manifest_url = format!("{}/manifest.json", server.uri());
307        let client = reqwest::Client::new();
308        let options = mock_options("99.99.99");
309
310        let result = fetch_version_json(&manifest_url, &options, &client).await;
311
312        assert!(matches!(result, Err(LaunchError::VersionNotFound(_))));
313    }
314
315    #[tokio::test]
316    async fn fetch_version_json_returns_http_error_on_500() {
317        use wiremock::matchers::{method, path};
318        use wiremock::{Mock, MockServer, ResponseTemplate};
319
320        let server = MockServer::start().await;
321        Mock::given(method("GET"))
322            .and(path("/manifest.json"))
323            .respond_with(ResponseTemplate::new(500))
324            .mount(&server)
325            .await;
326
327        let manifest_url = format!("{}/manifest.json", server.uri());
328        let client = reqwest::Client::new();
329        let options = mock_options("1.20.4");
330
331        let result = fetch_version_json(&manifest_url, &options, &client).await;
332        assert!(result.is_err());
333    }
334}