Skip to main content

hexomc_lib/version/
manifest.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4use crate::error::Result;
5
6const VERSION_MANIFEST_URL: &str =
7    "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json";
8
9#[derive(Debug, Deserialize, Serialize, Clone)]
10pub struct VersionManifest {
11    pub latest: Latest,
12    pub versions: Vec<VersionEntry>,
13}
14
15#[derive(Debug, Deserialize, Serialize, Clone)]
16pub struct Latest {
17    pub release: String,
18    pub snapshot: String,
19}
20
21#[derive(Debug, Deserialize, Serialize, Clone)]
22#[serde(rename_all = "camelCase")]
23pub struct VersionEntry {
24    pub id: String,
25    #[serde(rename = "type")]
26    pub version_type: String,
27    pub url: String,
28    pub sha1: String,
29    pub release_time: String,
30}
31
32#[derive(Debug, Deserialize, Serialize, Clone)]
33#[serde(rename_all = "camelCase")]
34pub struct VersionJson {
35    pub id: String,
36    pub main_class: String,
37    pub asset_index: AssetIndex,
38    pub assets: String,
39    pub java_version: JavaVersionInfo,
40    pub libraries: Vec<Library>,
41    pub arguments: Option<Arguments>,
42    /// Used by old versions (pre-1.13).
43    pub minecraft_arguments: Option<String>,
44    pub downloads: ClientDownloads,
45}
46
47#[derive(Debug, Deserialize, Serialize, Clone)]
48pub struct AssetIndex {
49    pub id: String,
50    pub sha1: String,
51    pub url: String,
52    pub size: u64,
53}
54
55#[derive(Debug, Deserialize, Serialize, Clone)]
56#[serde(rename_all = "camelCase")]
57pub struct JavaVersionInfo {
58    pub major_version: u32,
59}
60
61#[derive(Debug, Deserialize, Serialize, Clone)]
62pub struct Arguments {
63    pub jvm: Vec<Argument>,
64    pub game: Vec<Argument>,
65}
66
67/// A launch arg is either a plain string or an object with rules.
68#[derive(Debug, Deserialize, Serialize, Clone)]
69#[serde(untagged)]
70pub enum Argument {
71    Simple(String),
72    Conditional {
73        rules: Vec<Rule>,
74        value: ArgumentValue,
75    },
76}
77
78#[derive(Debug, Deserialize, Serialize, Clone)]
79#[serde(untagged)]
80pub enum ArgumentValue {
81    Single(String),
82    Multiple(Vec<String>),
83}
84
85#[derive(Debug, Deserialize, Serialize, Clone)]
86pub struct Rule {
87    pub action: String,
88    pub os: Option<OsRule>,
89    pub features: Option<HashMap<String, bool>>,
90}
91
92#[derive(Debug, Deserialize, Serialize, Clone)]
93pub struct OsRule {
94    pub name: Option<String>,
95    pub arch: Option<String>,
96    pub version: Option<String>,
97}
98
99#[derive(Debug, Deserialize, Serialize, Clone)]
100pub struct Library {
101    pub name: String,
102    pub downloads: Option<LibraryDownloads>,
103    pub rules: Option<Vec<Rule>>,
104    pub natives: Option<HashMap<String, String>>,
105    pub url: Option<String>,
106}
107
108#[derive(Debug, Deserialize, Serialize, Clone)]
109pub struct LibraryDownloads {
110    pub artifact: Option<LibraryArtifact>,
111    pub classifiers: Option<HashMap<String, LibraryArtifact>>,
112}
113
114#[derive(Debug, Deserialize, Serialize, Clone)]
115pub struct LibraryArtifact {
116    pub path: String,
117    pub sha1: String,
118    pub url: String,
119    pub size: u64,
120}
121
122#[derive(Debug, Deserialize, Serialize, Clone)]
123pub struct ClientDownloads {
124    pub client: DownloadEntry,
125    pub server: Option<DownloadEntry>,
126}
127
128#[derive(Debug, Deserialize, Serialize, Clone)]
129pub struct DownloadEntry {
130    pub sha1: String,
131    pub size: u64,
132    pub url: String,
133}
134
135#[derive(Debug, Deserialize, Serialize, Clone)]
136pub struct AssetIndexData {
137    pub objects: HashMap<String, AssetObject>,
138}
139
140#[derive(Debug, Deserialize, Serialize, Clone)]
141pub struct AssetObject {
142    pub hash: String,
143    pub size: u64,
144}
145
146/// Whether the current platform satisfies a library rule.
147pub fn check_library_rule(rules: &[Rule]) -> bool {
148    let current_os = current_os_name();
149    let mut allowed = false;
150
151    for rule in rules {
152        // Feature rules (has_custom_resolution / is_demo_user) are unsupported;
153        // skip the whole rule.
154        if rule.features.is_some() {
155            continue;
156        }
157
158        let matches = if let Some(os) = &rule.os {
159            os.name.as_deref().map_or(true, |n| n == current_os)
160        } else {
161            true
162        };
163
164        if matches {
165            allowed = rule.action == "allow";
166        }
167    }
168    allowed
169}
170
171/// Whether the current platform satisfies a JVM argument rule.
172pub fn check_jvm_rule(rules: &[Rule]) -> bool {
173    let current_os = current_os_name();
174    let current_arch = current_arch();
175    let mut allowed = false;
176
177    for rule in rules {
178        // Feature rules skipped, as above.
179        if rule.features.is_some() {
180            continue;
181        }
182
183        let os_matches = if let Some(os) = &rule.os {
184            let name_ok = os.name.as_deref().map_or(true, |n| n == current_os);
185            let arch_ok = os.arch.as_deref().map_or(true, |a| a == current_arch);
186            name_ok && arch_ok
187        } else {
188            true
189        };
190
191        if os_matches {
192            allowed = rule.action == "allow";
193        }
194    }
195    allowed
196}
197
198fn current_os_name() -> &'static str {
199    if cfg!(target_os = "windows") {
200        "windows"
201    } else if cfg!(target_os = "macos") {
202        "osx"
203    } else {
204        "linux"
205    }
206}
207
208fn current_arch() -> &'static str {
209    if cfg!(target_arch = "x86_64") {
210        "x64"
211    } else if cfg!(target_arch = "aarch64") {
212        "arm64"
213    } else {
214        "x86"
215    }
216}
217
218pub async fn fetch_version_manifest() -> Result<VersionManifest> {
219    let client = reqwest::Client::new();
220    let manifest = client
221        .get(VERSION_MANIFEST_URL)
222        .send()
223        .await?
224        .json::<VersionManifest>()
225        .await?;
226    Ok(manifest)
227}
228
229pub async fn fetch_version_json(url: &str) -> Result<VersionJson> {
230    let client = reqwest::Client::new();
231    let json = client
232        .get(url)
233        .send()
234        .await?
235        .json::<VersionJson>()
236        .await?;
237    Ok(json)
238}
239
240pub async fn fetch_asset_index(url: &str) -> Result<AssetIndexData> {
241    let client = reqwest::Client::new();
242    let data = client
243        .get(url)
244        .send()
245        .await?
246        .json::<AssetIndexData>()
247        .await?;
248    Ok(data)
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254
255    fn allow_rule(os_name: &str) -> Rule {
256        Rule {
257            action: "allow".to_string(),
258            os: Some(OsRule {
259                name: Some(os_name.to_string()),
260                arch: None,
261                version: None,
262            }),
263            features: None,
264        }
265    }
266
267    fn disallow_rule(os_name: &str) -> Rule {
268        Rule {
269            action: "disallow".to_string(),
270            os: Some(OsRule {
271                name: Some(os_name.to_string()),
272                arch: None,
273                version: None,
274            }),
275            features: None,
276        }
277    }
278
279    fn allow_all_rule() -> Rule {
280        Rule {
281            action: "allow".to_string(),
282            os: None,
283            features: None,
284        }
285    }
286
287    #[test]
288    fn allow_rule_matches_current_os() {
289        let os = if cfg!(target_os = "windows") { "windows" }
290                 else if cfg!(target_os = "macos") { "osx" }
291                 else { "linux" };
292        assert!(check_library_rule(&[allow_rule(os)]));
293    }
294
295    #[test]
296    fn allow_rule_does_not_match_other_os() {
297        // Can't be both windows and osx at once.
298        let other = if cfg!(target_os = "windows") { "osx" } else { "windows" };
299        // allow rule for another OS -> false (no matching rule).
300        assert!(!check_library_rule(&[allow_rule(other)]));
301    }
302
303    #[test]
304    fn allow_all_then_disallow_current() {
305        let os = if cfg!(target_os = "windows") { "windows" }
306                 else if cfg!(target_os = "macos") { "osx" }
307                 else { "linux" };
308        // allow all, then disallow current OS -> should be false
309        let rules = vec![allow_all_rule(), disallow_rule(os)];
310        assert!(!check_library_rule(&rules));
311    }
312
313    #[test]
314    fn empty_rules_not_allowed() {
315        // No rules -> false (at least one allow is required).
316        assert!(!check_library_rule(&[]));
317    }
318
319    #[tokio::test]
320    #[ignore = "需要網路"]
321    async fn fetch_manifest_returns_versions() {
322        let manifest = fetch_version_manifest().await.unwrap();
323        assert!(!manifest.versions.is_empty());
324        assert!(!manifest.latest.release.is_empty());
325    }
326
327    #[tokio::test]
328    #[ignore = "需要網路"]
329    async fn fetch_known_version_json() {
330        let manifest = fetch_version_manifest().await.unwrap();
331        let entry = manifest.versions.iter().find(|v| v.id == "1.21.4").unwrap();
332        let json = fetch_version_json(&entry.url).await.unwrap();
333        assert_eq!(json.id, "1.21.4");
334        assert!(!json.libraries.is_empty());
335        assert!(json.java_version.major_version >= 21);
336    }
337}