piston_mc/
java.rs

1#![doc = include_str!("../.wiki/Java.md")]
2
3use simple_download_utility::{FileDownloadArguments, MultiDownloadProgress, download_multiple_files};
4use anyhow::{Result, anyhow};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use std::fmt::Display;
8use std::path::Path;
9
10const PISTON_URL: &str = "https://piston-meta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json";
11
12#[derive(Debug, Serialize, Deserialize)]
13pub struct JavaManifest {
14    pub linux: Runtimes,
15    #[serde(rename = "linux-i386")]
16    pub linux_i386: Runtimes,
17    #[serde(rename = "mac-os")]
18    pub macos: Runtimes,
19    #[serde(rename = "mac-os-arm64")]
20    pub macos_arm64: Runtimes,
21    #[serde(rename = "windows-arm64")]
22    pub windows_arm64: Runtimes,
23    #[serde(rename = "windows-x64")]
24    pub windows_x64: Runtimes,
25    #[serde(rename = "windows-x86")]
26    pub windows_x86: Runtimes,
27}
28
29#[derive(Debug, Serialize, Deserialize)]
30pub struct Runtimes {
31    #[serde(rename = "java-runtime-alpha")]
32    pub alpha: Vec<JavaRuntime>,
33    #[serde(rename = "java-runtime-beta")]
34    pub beta: Vec<JavaRuntime>,
35    #[serde(rename = "java-runtime-gamma")]
36    pub gamma: Vec<JavaRuntime>,
37    #[serde(rename = "java-runtime-delta")]
38    pub delta: Vec<JavaRuntime>,
39    #[serde(rename = "java-runtime-gamma-snapshot")]
40    pub gamma_snapshot: Vec<JavaRuntime>,
41    #[serde(rename = "java-runtime-epsilon")]
42    pub epsilon: Vec<JavaRuntime>,
43    #[serde(rename = "jre-legacy")]
44    pub legacy: Vec<JavaRuntime>,
45    #[serde(rename = "minecraft-java-exe")]
46    pub minecraft_java_exe: serde_json::Value,
47}
48
49#[derive(Debug, Serialize, Deserialize)]
50pub struct JavaRuntime {
51    version: Version,
52    manifest: Manifest,
53    availability: Availability,
54}
55
56#[derive(Debug, Serialize, Deserialize)]
57pub struct Manifest {
58    pub sha1: String,
59    pub size: usize,
60    pub url: String,
61}
62
63#[derive(Debug, Serialize, Deserialize)]
64pub struct Version {
65    pub name: String,
66    pub released: chrono::DateTime<chrono::Utc>,
67}
68
69#[derive(Debug, Serialize, Deserialize)]
70pub struct Availability {
71    pub group: u32,
72    pub progress: u32,
73}
74
75#[derive(Debug, Serialize, Deserialize)]
76pub struct JavaInstallationFile {
77    #[serde(skip)]
78    pub name: String,
79    #[serde(rename = "type")]
80    pub file_type: Option<FileType>,
81    pub executable: Option<bool>,
82    pub downloads: Option<Downloads>,
83}
84
85#[derive(Debug, Serialize, Deserialize)]
86pub struct Downloads {
87    pub lzma: Option<DownloadItem>,
88    pub raw: DownloadItem,
89}
90#[derive(Debug, Serialize, Deserialize)]
91pub struct DownloadItem {
92    pub sha1: String,
93    pub size: usize,
94    pub url: String,
95}
96
97#[derive(Debug, Serialize, Deserialize)]
98pub enum FileType {
99    #[serde(rename = "file")]
100    File,
101    #[serde(rename = "directory")]
102    Directory,
103    #[serde(rename = "link")]
104    Link,
105}
106
107impl JavaManifest {
108    pub async fn fetch() -> Result<Self> {
109        let response = reqwest::get(PISTON_URL).await?;
110        let text = response.text().await?;
111        let json_result = serde_json::from_str::<Self>(&text);
112        #[cfg(feature = "log")]
113        if let Err(ref e) = json_result {
114            let line = e.line();
115            let column = e.column();
116            error!("Failed to deserialize VersionManifest from {}: {}", PISTON_URL, e);
117            error!("Error at line {}, column {}", line, column);
118
119            // Show context around the error (60 chars before and after)
120            let error_offset = text.lines().take(line - 1).map(|l| l.len() + 1).sum::<usize>() + column - 1;
121            let start = error_offset.saturating_sub(60);
122            let end = (error_offset + 60).min(text.len());
123            let context = &text[start..end];
124
125            error!("Context around error: {}", context);
126        }
127        Ok(json_result?)
128    }
129}
130
131impl JavaRuntime {
132    pub async fn get_installation_files(&self) -> Result<Vec<JavaInstallationFile>> {
133        let url = self.manifest.url.clone();
134        let response = reqwest::get(&url).await?;
135        let files: serde_json::Value = response.json().await?;
136        let files = files.get("files").ok_or_else(|| anyhow!("Missing 'files' field in response"))?;
137        let json_result = serde_json::from_value::<HashMap<String, JavaInstallationFile>>(files.clone());
138        #[cfg(feature = "log")]
139        if let Err(ref e) = json_result {
140            let line = e.line();
141            let column = e.column();
142            error!("Failed to deserialize VersionManifest from {}: {}", url, e);
143            error!("Error at line {}, column {}", line, column);
144        }
145        let map = json_result?;
146        Ok(map
147            .into_iter()
148            .map(|(name, mut file)| {
149                file.name = name;
150                file
151            })
152            .collect())
153    }
154
155    pub async fn install(
156        &self,
157        directory: impl AsRef<Path>,
158        parallel: u16,
159        sender: Option<tokio::sync::mpsc::Sender<MultiDownloadProgress>>,
160    ) -> Result<()> {
161        let directory = directory.as_ref();
162        let installation_files = self.get_installation_files().await?;
163
164        let args: Vec<FileDownloadArguments> = installation_files
165            .iter()
166            .filter_map(|item| {
167                item.downloads.as_ref().map(|download| FileDownloadArguments {
168                    url: download.raw.url.clone(),
169                    path: directory.join(&item.name).to_string_lossy().to_string(),
170                    sender: None,
171                    sha1: Some(download.raw.sha1.clone()),
172                })
173            })
174            .collect();
175
176        #[cfg(feature = "log")]
177        info!("Downloading files: {:?}", installation_files);
178
179        download_multiple_files(args, parallel, sender).await?;
180
181        Ok(())
182    }
183}
184
185impl Display for JavaRuntime {
186    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187        write!(f, "{}", self.version.name)
188    }
189}
190
191#[cfg(test)]
192mod test {
193    use crate::java::JavaManifest;
194    #[cfg(feature = "log")]
195    use crate::setup_logging;
196    use futures_util::{StreamExt, stream};
197
198    #[tokio::test]
199    async fn fetch() {
200        #[cfg(feature = "log")]
201        setup_logging();
202        let manifest = JavaManifest::fetch().await.unwrap();
203        info!("{:?}", manifest);
204    }
205    #[tokio::test]
206    async fn get_installation_files() {
207        #[cfg(feature = "log")]
208        setup_logging();
209        let manifest = JavaManifest::fetch().await.unwrap();
210        let runtimes = [
211            &manifest.linux.alpha,
212            &manifest.linux.beta,
213            &manifest.linux.gamma,
214            &manifest.linux.delta,
215            &manifest.linux.gamma_snapshot,
216            &manifest.linux.epsilon,
217            &manifest.linux.legacy,
218            &manifest.linux_i386.alpha,
219            &manifest.linux_i386.beta,
220            &manifest.linux_i386.gamma,
221            &manifest.linux_i386.delta,
222            &manifest.linux_i386.gamma_snapshot,
223            &manifest.linux_i386.epsilon,
224            &manifest.linux_i386.legacy,
225            &manifest.macos.alpha,
226            &manifest.macos.beta,
227            &manifest.macos.gamma,
228            &manifest.macos.delta,
229            &manifest.macos.gamma_snapshot,
230            &manifest.macos.epsilon,
231            &manifest.macos.legacy,
232            &manifest.macos_arm64.alpha,
233            &manifest.macos_arm64.beta,
234            &manifest.macos_arm64.gamma,
235            &manifest.macos_arm64.delta,
236            &manifest.macos_arm64.gamma_snapshot,
237            &manifest.macos_arm64.epsilon,
238            &manifest.macos_arm64.legacy,
239            &manifest.windows_arm64.alpha,
240            &manifest.windows_arm64.beta,
241            &manifest.windows_arm64.gamma,
242            &manifest.windows_arm64.delta,
243            &manifest.windows_arm64.gamma_snapshot,
244            &manifest.windows_arm64.epsilon,
245            &manifest.windows_arm64.legacy,
246            &manifest.windows_x64.alpha,
247            &manifest.windows_x64.beta,
248            &manifest.windows_x64.gamma,
249            &manifest.windows_x64.delta,
250            &manifest.windows_x64.gamma_snapshot,
251            &manifest.windows_x64.epsilon,
252            &manifest.windows_x64.legacy,
253            &manifest.windows_x86.alpha,
254            &manifest.windows_x86.beta,
255            &manifest.windows_x86.gamma,
256            &manifest.windows_x86.delta,
257            &manifest.windows_x86.gamma_snapshot,
258            &manifest.windows_x86.epsilon,
259            &manifest.windows_x86.legacy,
260        ];
261
262        let results: Vec<_> = stream::iter(runtimes)
263            .enumerate()
264            .map(|(idx, runtime_vec)| {
265                let runtime = runtime_vec.first();
266                async move {
267                    if let Some(runtime) = runtime {
268                        let files_result = runtime.get_installation_files().await;
269                        #[cfg(feature = "log")]
270                        if let Ok(ref files) = files_result {
271                            info!("Runtime {}: {} - found {} installation files", idx, runtime.version.name, files.len());
272                        }
273                        files_result
274                    } else {
275                        #[cfg(feature = "log")]
276                        info!("Runtime {}: empty runtime vector", idx);
277                        Ok(vec![])
278                    }
279                }
280            })
281            .buffer_unordered(10usize)
282            .collect()
283            .await;
284
285        for result in &results {
286            if let Err(e) = result {
287                #[cfg(feature = "log")]
288                error!("Failed to get installation files: {}", e);
289            }
290            assert!(result.is_ok());
291        }
292    }
293
294    #[tokio::test]
295    async fn install() {
296        #[cfg(feature = "log")]
297        setup_logging();
298        let manifest = JavaManifest::fetch().await.unwrap();
299        let directory = "target/test/";
300        let runtimes = [
301            &manifest.linux.alpha,
302            &manifest.linux.beta,
303            &manifest.linux.gamma,
304            &manifest.linux.delta,
305            &manifest.linux.gamma_snapshot,
306            &manifest.linux.epsilon,
307            &manifest.linux.legacy,
308            &manifest.linux_i386.alpha,
309            &manifest.linux_i386.beta,
310            &manifest.linux_i386.gamma,
311            &manifest.linux_i386.delta,
312            &manifest.linux_i386.gamma_snapshot,
313            &manifest.linux_i386.epsilon,
314            &manifest.linux_i386.legacy,
315            &manifest.macos.alpha,
316            &manifest.macos.beta,
317            &manifest.macos.gamma,
318            &manifest.macos.delta,
319            &manifest.macos.gamma_snapshot,
320            &manifest.macos.epsilon,
321            &manifest.macos.legacy,
322            &manifest.macos_arm64.alpha,
323            &manifest.macos_arm64.beta,
324            &manifest.macos_arm64.gamma,
325            &manifest.macos_arm64.delta,
326            &manifest.macos_arm64.gamma_snapshot,
327            &manifest.macos_arm64.epsilon,
328            &manifest.macos_arm64.legacy,
329            &manifest.windows_arm64.alpha,
330            &manifest.windows_arm64.beta,
331            &manifest.windows_arm64.gamma,
332            &manifest.windows_arm64.delta,
333            &manifest.windows_arm64.gamma_snapshot,
334            &manifest.windows_arm64.epsilon,
335            &manifest.windows_arm64.legacy,
336            &manifest.windows_x64.alpha,
337            &manifest.windows_x64.beta,
338            &manifest.windows_x64.gamma,
339            &manifest.windows_x64.delta,
340            &manifest.windows_x64.gamma_snapshot,
341            &manifest.windows_x64.epsilon,
342            &manifest.windows_x64.legacy,
343            &manifest.windows_x86.alpha,
344            &manifest.windows_x86.beta,
345            &manifest.windows_x86.gamma,
346            &manifest.windows_x86.delta,
347            &manifest.windows_x86.gamma_snapshot,
348            &manifest.windows_x86.epsilon,
349            &manifest.windows_x86.legacy,
350        ];
351
352        let results: Vec<_> = stream::iter(runtimes)
353            .map(|runtime| async move {
354                if let Some(runtime) = runtime.first() {
355                    let directory = std::path::Path::new(directory).join(format!("{}-{}", runtime, runtime.manifest.sha1));
356                    info!("Installing java {} to {}...", runtime, directory.display());
357                    runtime.install(&directory, 20, None).await
358                } else {
359                    warn!("No runtime specified");
360                    Ok(())
361                }
362            })
363            .buffer_unordered(27)
364            .collect()
365            .await;
366
367        for result in results {
368            result.unwrap();
369        }
370    }
371}