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            dns: None,
197        }
198    }
199
200    #[tokio::test]
201    async fn fetch_version_json_from_mock_server_concrete_version() {
202        use wiremock::matchers::{method, path};
203        use wiremock::{Mock, MockServer, ResponseTemplate};
204
205        let server = MockServer::start().await;
206
207        // Register manifest mock.
208        let version_url = format!("{}/versions/1.20.4.json", server.uri());
209        let manifest_body = MOCK_MANIFEST_TEMPLATE.replace("VERSION_URL", &version_url);
210        Mock::given(method("GET"))
211            .and(path("/manifest.json"))
212            .respond_with(
213                ResponseTemplate::new(200)
214                    .insert_header("content-type", "application/json")
215                    .set_body_string(manifest_body),
216            )
217            .mount(&server)
218            .await;
219
220        // Register version JSON mock.
221        Mock::given(method("GET"))
222            .and(path("/versions/1.20.4.json"))
223            .respond_with(
224                ResponseTemplate::new(200)
225                    .insert_header("content-type", "application/json")
226                    .set_body_string(MOCK_VERSION_JSON),
227            )
228            .mount(&server)
229            .await;
230
231        let manifest_url = format!("{}/manifest.json", server.uri());
232        let client = reqwest::Client::new();
233        let options = mock_options("1.20.4");
234
235        let result = fetch_version_json(&manifest_url, &options, &client)
236            .await
237            .unwrap();
238
239        assert_eq!(result.id, "1.20.4");
240        assert_eq!(result.version_type, "release");
241        assert_eq!(
242            result.main_class.as_deref(),
243            Some("net.minecraft.client.main.Main")
244        );
245        assert!(result.libraries.is_empty());
246    }
247
248    #[tokio::test]
249    async fn fetch_version_json_resolves_latest_release_alias() {
250        use wiremock::matchers::{method, path};
251        use wiremock::{Mock, MockServer, ResponseTemplate};
252
253        let server = MockServer::start().await;
254
255        let version_url = format!("{}/versions/1.20.4.json", server.uri());
256        let manifest_body = MOCK_MANIFEST_TEMPLATE.replace("VERSION_URL", &version_url);
257        Mock::given(method("GET"))
258            .and(path("/manifest.json"))
259            .respond_with(
260                ResponseTemplate::new(200)
261                    .insert_header("content-type", "application/json")
262                    .set_body_string(manifest_body),
263            )
264            .mount(&server)
265            .await;
266        Mock::given(method("GET"))
267            .and(path("/versions/1.20.4.json"))
268            .respond_with(
269                ResponseTemplate::new(200)
270                    .insert_header("content-type", "application/json")
271                    .set_body_string(MOCK_VERSION_JSON),
272            )
273            .mount(&server)
274            .await;
275
276        let manifest_url = format!("{}/manifest.json", server.uri());
277        let client = reqwest::Client::new();
278        // "latest_release" should resolve to "1.20.4" via the mock manifest.
279        let options = mock_options("latest_release");
280
281        let result = fetch_version_json(&manifest_url, &options, &client)
282            .await
283            .unwrap();
284
285        assert_eq!(result.id, "1.20.4");
286    }
287
288    #[tokio::test]
289    async fn fetch_version_json_returns_error_for_missing_version() {
290        use wiremock::matchers::{method, path};
291        use wiremock::{Mock, MockServer, ResponseTemplate};
292
293        let server = MockServer::start().await;
294
295        let version_url = format!("{}/versions/1.20.4.json", server.uri());
296        let manifest_body = MOCK_MANIFEST_TEMPLATE.replace("VERSION_URL", &version_url);
297        Mock::given(method("GET"))
298            .and(path("/manifest.json"))
299            .respond_with(
300                ResponseTemplate::new(200)
301                    .insert_header("content-type", "application/json")
302                    .set_body_string(manifest_body),
303            )
304            .mount(&server)
305            .await;
306
307        let manifest_url = format!("{}/manifest.json", server.uri());
308        let client = reqwest::Client::new();
309        let options = mock_options("99.99.99");
310
311        let result = fetch_version_json(&manifest_url, &options, &client).await;
312
313        assert!(matches!(result, Err(LaunchError::VersionNotFound(_))));
314    }
315
316    #[tokio::test]
317    async fn fetch_version_json_returns_http_error_on_500() {
318        use wiremock::matchers::{method, path};
319        use wiremock::{Mock, MockServer, ResponseTemplate};
320
321        let server = MockServer::start().await;
322        Mock::given(method("GET"))
323            .and(path("/manifest.json"))
324            .respond_with(ResponseTemplate::new(500))
325            .mount(&server)
326            .await;
327
328        let manifest_url = format!("{}/manifest.json", server.uri());
329        let client = reqwest::Client::new();
330        let options = mock_options("1.20.4");
331
332        let result = fetch_version_json(&manifest_url, &options, &client).await;
333        assert!(result.is_err());
334    }
335}