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            skip_bundle_check: false,
200        }
201    }
202
203    #[tokio::test]
204    async fn fetch_version_json_from_mock_server_concrete_version() {
205        use wiremock::matchers::{method, path};
206        use wiremock::{Mock, MockServer, ResponseTemplate};
207
208        let server = MockServer::start().await;
209
210        // Register manifest mock.
211        let version_url = format!("{}/versions/1.20.4.json", server.uri());
212        let manifest_body = MOCK_MANIFEST_TEMPLATE.replace("VERSION_URL", &version_url);
213        Mock::given(method("GET"))
214            .and(path("/manifest.json"))
215            .respond_with(
216                ResponseTemplate::new(200)
217                    .insert_header("content-type", "application/json")
218                    .set_body_string(manifest_body),
219            )
220            .mount(&server)
221            .await;
222
223        // Register version JSON mock.
224        Mock::given(method("GET"))
225            .and(path("/versions/1.20.4.json"))
226            .respond_with(
227                ResponseTemplate::new(200)
228                    .insert_header("content-type", "application/json")
229                    .set_body_string(MOCK_VERSION_JSON),
230            )
231            .mount(&server)
232            .await;
233
234        let manifest_url = format!("{}/manifest.json", server.uri());
235        let client = reqwest::Client::new();
236        let options = mock_options("1.20.4");
237
238        let result = fetch_version_json(&manifest_url, &options, &client)
239            .await
240            .unwrap();
241
242        assert_eq!(result.id, "1.20.4");
243        assert_eq!(result.version_type, "release");
244        assert_eq!(result.main_class.as_deref(), Some("net.minecraft.client.main.Main"));
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}