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